In the following p5.js code I'm trying to create 2 separate methods.
centerInWindow() is meant to keep the image centered in the canvas while it's being scaled down after the user clicks on the canvas.
centerToClick() is meant to keep the image centered on the point the user clicked on, while it's being scaled up.
None of them work and I'm having trouble getting the logic right.
function centerInWindow(img) {
let currentSize = img.width * currentScale
imgX = (windowWidth / 2) - (currentSize / 2)
imgY = (windowHeight / 2) - (currentSize / 2)
}
function centerToClick() {
imgX = clickX * currentScale
imgY = clickY * currentScale
}
let minScale = 1
let maxScale = 5
let targetScale = minScale
let currentScale = targetScale
let clickX, clickY, imgX, imgY
let idx = 0
function setup() {
pixelDensity(1)
createCanvas(windowWidth, windowHeight)
preload(IMG_PATHS, IMGS)
frameRate(12)
}
function draw() {
clear()
if (currentScale < targetScale) {
currentScale += 0.05
if (currentScale > targetScale) {
currentScale = targetScale
}
centerToClick()
} else if (currentScale > targetScale) {
currentScale -= 0.05
if (currentScale < targetScale) {
currentScale = targetScale
}
centerInWindow(IMGS[idx])
} else {
centerInWindow(IMGS[idx])
}
scale(currentScale)
image(IMGS[idx], imgX, imgY)
idx++
if (idx === IMGS.length) {
idx = 0
}
}
window.addEventListener('click', function({ clientX, clientY }) {
targetScale = targetScale === maxScale ? minScale : maxScale
clickX = clientX
clickY = clientY
})
See it in action here.
Any help would be appreciated.
There are probably multiple ways to solve this problem, but here's one:
Imaging your viewport is an NxM rectangle, and you are drawing some portion of a scene in within that viewport. In order to zoom in and out you can shift the origin at which you draw that scene and increase or decrease the scale. The tricky part is to make it possible to zoom in and out centered on an arbitrary point within the currently visible portion of the scene, keeping that point in the scene locked to the current point in the viewport.
Given some center point, and a desired scale factor, it is possible to determine the necessary change in the offset of the scene to preserve the position of the center point after scaling.
There's probably some complicated trigonometric proof for how to calculate this, but conveniently it is a simple calculation based on the ratio of the offset of the mouse from the current top left of the scene, to the scaled height of the scene.
x_offset -= (x_center - x_offset) / (N * current_scale) * (N * new_scale - N * current_scale)
y_offset -= (y_center - y_offset) / (M * current_scale) * (M * new_scale - M * current_scale)
Conveniently it is possible to apply this repeatedly as your scale changes and regardless of whether scale is increasing or decreasing.
Here's a sample sketch demonstrating this:
const viewport = { width: 400, height: 300 };
let scaledView = { ...viewport, x: 0, y: 0 };
function setup() {
createCanvas(windowWidth, windowHeight);
viewport.x = (width - viewport.width) / 2;
viewport.y = (height - viewport.height) / 2;
}
function draw() {
background(255);
translate(viewport.x, viewport.y);
push();
translate(scaledView.x, scaledView.y);
scale(scaledView.width / viewport.width, scaledView.height / viewport.height);
// Draw scene
ellipseMode(CENTER);
noStroke();
fill(200);
rect(0, 0, viewport.width, viewport.height);
stroke('blue');
noFill();
strokeWeight(1);
translate(viewport.width / 2, viewport.height / 2);
circle(0, 0, 200);
arc(0, 0, 120, 120, PI * 0.25, PI * 0.75);
strokeWeight(4)
point(-40, -40);
point(40, -40);
pop();
noFill();
stroke(0);
rect(0, 0, viewport.width, viewport.height);
// viewport relative mouse position
let mousePos = { x: mouseX - viewport.x, y: mouseY - viewport.y };
if (mousePos.x >= 0 && mousePos.x <= viewport.width &&
mousePos.y >= 0 && mousePos.y <= viewport.height) {
line(scaledView.x, scaledView.y, mousePos.x, mousePos.y);
let updatedView = keyIsDown(SHIFT) ? getUnZoomedView(mousePos) : getZoomedView(mousePos);
line(scaledView.x, scaledView.y, updatedView.x, updatedView.y);
stroke('red');
rect(updatedView.x, updatedView.y, updatedView.width, updatedView.height);
}
}
function getZoomedView(center) {
return getScaledView(center, 1.1);
}
function getUnZoomedView(center) {
return getScaledView(center, 0.9);
}
function getScaledView(center, factor) {
// the center position relative to the scaled/shifted scene
let viewCenterPos = {
x: center.x - scaledView.x,
y: center.y - scaledView.y
};
// determine how much we will have to shift to keep the position centered
let shift = {
x: map(viewCenterPos.x, 0, scaledView.width, 0, 1),
y: map(viewCenterPos.y, 0, scaledView.height, 0, 1)
};
// calculate the new view dimensions
let updatedView = {
width: scaledView.width * factor,
height: scaledView.height * factor
};
// adjust the x and y offsets according to the shift
updatedView.x = scaledView.x + (updatedView.width - scaledView.width) * -shift.x;
updatedView.y = scaledView.y + (updatedView.height - scaledView.height) * -shift.y;
return updatedView;
}
function mouseClicked() {
// viewport relative mouse position
let mousePos = { x: mouseX - viewport.x, y: mouseY - viewport.y };
if (mousePos.x >= 0 && mousePos.x <= viewport.width &&
mousePos.y >= 0 && mousePos.y <= viewport.height) {
scaledView = keyIsDown(SHIFT) ? getUnZoomedView(mousePos) : getZoomedView(mousePos);
}
}
html, body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
We can actually simplify these equations because N and M can be factored out. The previous pseudo code becomes:
x_offset -= ((x_center - x_offset) * (new_scale - current_scale) * N) / (current_scale * N)
And because both the top and bottom are multiplied by N this becomes:
x_offset -= ((x_center - x_offset) * (new_scale - current_scale)) / (current_scale)
Here is an example using your image drawing code:
const IMG_PATHS = [
'https://storage.googleapis.com/www.paulwheeler.us/files/windows-95-desktop-background.jpg'
];
let IMGS = [];
const minScale = 1;
const maxScale = 5;
let targetScale = minScale;
let currentScale = targetScale;
let targetOrigin = {
x: 0,
y: 0
};
let currentOrigin = {
x: 0,
y: 0
};
let idx = 0;
function preloadHelper(pathsToImgs, imgs) {
for (let pathToImg of pathsToImgs) {
loadImage(pathToImg, img => {
imgs.push(img);
})
}
}
// Make sure load images happen from the actual preload() function. p5.js has special logic when these calls happen here to have the sketch wait to start until all the loadXXX class are complete
function preload() {
preloadHelper(IMG_PATHS, IMGS);
}
function setup() {
pixelDensity(1)
createCanvas(windowWidth, windowHeight)
frameRate(12)
}
function draw() {
clear();
/*
if (currentScale < targetScale) {
currentScale += 0.01
} else if (currentScale > targetScale) {
currentScale -= 0.01
} */
// By making all of the changing components part of a vector and normalizing it we can ensure that the we reach our target origin and scale at the same point
let offset = createVector(
targetOrigin.x - currentOrigin.x,
targetOrigin.y - currentOrigin.y,
// Give the change in scale more weight so that it happens at a similar rate to the translation. This is especially noticable when there is little to no offset required
(targetScale - currentScale) * 500
);
if (offset.magSq() > 0.01) {
// Multiplying by a larger number will move faster
offset.normalize().mult(8);
currentOrigin.x += offset.x;
currentOrigin.y += offset.y;
currentScale += offset.z / 500;
// We need to make sure we do not over shoot or targets
if (offset.x > 0 && currentOrigin.x > targetOrigin.x) {
currentOrigin.x = targetOrigin.x;
}
if (offset.x < 0 && currentOrigin.x < targetOrigin.x) {
currentOrigin.x = targetOrigin.x;
}
if (offset.y > 0 && currentOrigin.y > targetOrigin.y) {
currentOrigin.y = targetOrigin.y;
}
if (offset.y < 0 && currentOrigin.y < targetOrigin.y) {
currentOrigin.y = targetOrigin.y;
}
if (offset.z > 0 && currentScale > targetScale) {
currentScale = targetScale;
}
if (offset.z < 0 && currentScale < targetScale) {
currentScale = targetScale;
}
}
translate(currentOrigin.x, currentOrigin.y);
scale(currentScale);
image(IMGS[idx], 0, 0);
}
function mouseClicked() {
targetScale = constrain(
keyIsDown(SHIFT) ? currentScale * 0.9 : currentScale * 1.1,
minScale,
maxScale
);
targetOrigin = getScaledOrigin({
x: mouseX,
y: mouseY
},
currentScale,
targetScale
);
}
function getScaledOrigin(center, currentScale, newScale) {
// the center position relative to the scaled/shifted scene
let viewCenterPos = {
x: center.x - currentOrigin.x,
y: center.y - currentOrigin.y
};
// determine the new origin
let originShift = {
x: viewCenterPos.x / currentScale * (newScale - currentScale),
y: viewCenterPos.y / currentScale * (newScale - currentScale)
};
return {
x: currentOrigin.x - originShift.x,
y: currentOrigin.y - originShift.y
};
}
html,
body {
margin: 0;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
Related
I'm working on a project where I simulate physics with balls.
Here is the link to the p5 editor of the project.
My problem is the following, when I add a lot of ball (like 200), balls are stacking but some of them will eventually collapse and I don't know why.
Can somebody explain why it does this and how to solve the problem ?
Thanks.
Here is the code of the sketch.
document.oncontextmenu = function () {
return false;
}
let isFlushing = false;
let isBallDiameterRandom = false;
let displayInfos = true;
let displayWeight = false;
let clickOnce = false;
let FRAME_RATE = 60;
let SPEED_FLUSH = 3;
let Y_GROUND;
let lastFR;
let balls = [];
function setup() {
frameRate(FRAME_RATE);
createCanvas(window.innerWidth, window.innerHeight);
Y_GROUND = height / 20 * 19;
lastFR = FRAME_RATE;
}
function draw() {
background(255);
if (isFlushing) {
for (let i = 0; i < SPEED_FLUSH; i++) {
balls.pop();
}
if (balls.length === 0) {
isFlushing = false;
}
}
balls.forEach(ball => {
ball.collide();
ball.move();
ball.display(displayWeight);
ball.checkCollisions();
});
if (mouseIsPressed) {
let ballDiameter;
if (isBallDiameterRandom) {
ballDiameter = random(15, 101);
} else {
ballDiameter = 25;
}
if (canAddBall(mouseX, mouseY, ballDiameter)) {
isFlushing = false;
let newBall = new Ball(mouseX, mouseY, ballDiameter, balls);
if (mouseButton === LEFT && !clickOnce) {
balls.push(newBall);
clickOnce = true;
}
if (mouseButton === RIGHT) {
balls.push(newBall);
}
}
}
drawGround();
if (displayInfos) {
displayShortcuts();
displayFrameRate();
displayBallCount();
}
}
function mouseReleased() {
if (mouseButton === LEFT) {
clickOnce = false;
}
}
function keyPressed() {
if (keyCode === 32) {//SPACE
displayInfos = !displayInfos;
}
if (keyCode === 70) {//F
isFlushing = true;
}
if (keyCode === 71) {//G
isBallDiameterRandom = !isBallDiameterRandom;
}
if (keyCode === 72) {//H
displayWeight = !displayWeight;
}
}
function canAddBall(x, y, d) {
let isInScreen =
y + d / 2 < Y_GROUND &&
y - d / 2 > 0 &&
x + d / 2 < width &&
x - d / 2 > 0;
let isInAnotherBall = false;
for (let i = 0; i < balls.length; i++) {
let d = dist(x, y, balls[i].position.x, balls[i].position.y);
if (d < balls[i].w) {
isInAnotherBall = true;
break;
}
}
return isInScreen && !isInAnotherBall;
}
function drawGround() {
strokeWeight(0);
fill('rgba(200,200,200, 0.25)');
rect(0, height / 10 * 9, width, height / 10);
}
function displayFrameRate() {
if (frameCount % 30 === 0) {
lastFR = round(frameRate());
}
textSize(50);
fill(255, 0, 0);
let lastFRWidth = textWidth(lastFR);
text(lastFR, width - lastFRWidth - 25, 50);
textSize(10);
text('fps', width - 20, 50);
}
function displayBallCount() {
textSize(50);
fill(255, 0, 0);
text(balls.length, 10, 50);
let twBalls = textWidth(balls.length);
textSize(10);
text('balls', 15 + twBalls, 50);
}
function displayShortcuts() {
let hStart = 30;
let steps = 15;
let maxTW = 0;
let controlTexts = [
'LEFT CLICK : add 1 ball',
'RIGHT CLICK : add 1 ball continuously',
'SPACE : display infos',
'F : flush balls',
'G : set random ball diameter (' + isBallDiameterRandom + ')',
'H : display weight of balls (' + displayWeight + ')'
];
textSize(11);
fill(0);
for (let i = 0; i < controlTexts.length; i++) {
let currentTW = textWidth(controlTexts[i]);
if (currentTW > maxTW) {
maxTW = currentTW;
}
}
for (let i = 0; i < controlTexts.length; i++) {
text(controlTexts[i], width / 2 - maxTW / 2 + 5, hStart);
hStart += steps;
}
fill(200, 200, 200, 100);
rect(width / 2 - maxTW / 2,
hStart - (controlTexts.length + 1) * steps,
maxTW + steps,
(controlTexts.length + 1) * steps - steps / 2
);
}
Here is the code of the Ball class.
class Ball {
constructor(x, y, w, e) {
this.id = e.length;
this.w = w;
this.e = e;
this.progressiveWidth = 0;
this.rgb = [
floor(random(0, 256)),
floor(random(0, 256)),
floor(random(0, 256))
];
this.mass = w;
this.position = createVector(x + random(-1, 1), y);
this.velocity = createVector(0, 0);
this.acceleration = createVector(0, 0);
this.gravity = 0.2;
this.friction = 0.5;
}
collide() {
for (let i = this.id + 1; i < this.e.length; i++) {
let dx = this.e[i].position.x - this.position.x;
let dy = this.e[i].position.y - this.position.y;
let distance = sqrt(dx * dx + dy * dy);
let minDist = this.e[i].w / 2 + this.w / 2;
if (distance < minDist) {
let angle = atan2(dy, dx);
let targetX = this.position.x + cos(angle) * minDist;
let targetY = this.position.y + sin(angle) * minDist;
this.acceleration.set(
targetX - this.e[i].position.x,
targetY - this.e[i].position.y
);
this.velocity.sub(this.acceleration);
this.e[i].velocity.add(this.acceleration);
//TODO : Effets bizarre quand on empile les boules (chevauchement)
this.velocity.mult(this.friction);
}
}
}
move() {
this.velocity.add(createVector(0, this.gravity));
this.position.add(this.velocity);
}
display(displayMass) {
if (this.progressiveWidth < this.w) {
this.progressiveWidth += this.w / 10;
}
stroke(0);
strokeWeight(2);
fill(this.rgb[0], this.rgb[1], this.rgb[2], 100);
ellipse(this.position.x, this.position.y, this.progressiveWidth);
if (displayMass) {
strokeWeight(1);
textSize(10);
let tempTW = textWidth(int(this.w));
text(int(this.w), this.position.x - tempTW / 2, this.position.y + 4);
}
}
checkCollisions() {
if (this.position.x > width - this.w / 2) {
this.velocity.x *= -this.friction;
this.position.x = width - this.w / 2;
} else if (this.position.x < this.w / 2) {
this.velocity.x *= -this.friction;
this.position.x = this.w / 2;
}
if (this.position.y > Y_GROUND - this.w / 2) {
this.velocity.x -= this.velocity.x / 100;
this.velocity.y *= -this.friction;
this.position.y = Y_GROUND - this.w / 2;
} else if (this.position.y < this.w / 2) {
this.velocity.y *= -this.friction;
this.position.y = this.w / 2;
}
}
}
I see this overlapping happen when the sum of ball masses gets bigger than the elasticity of the balls. At least it seems so. I made a copy with a smaller pool so it doesn't take so much time to reproduce the problem.
In the following example, with 6 balls (a mass of 150 units) pressing on the base row, we see that the 13 balls in the base row overlap. The base row has a width of ca. 300 pixels, which is only enough space for 12 balls of diameter 25. I think this is showing the limitation of the model: the balls are displayed circular but indeed have an amount of elasticity that they should display deformed instead. It's hard to say how this can be fixed without implementing drawing complicated shapes. Maybe less friction?
BTW: great physics engine you built there :-)
Meanwhile I was able to make another screenshot with even fewer balls. The weight of three of them (eq. 75 units) is sufficient to create overlapping in the base row.
I doubled the size of the balls and changed the pool dimensions as to detedt that there is a more serious error in the engine. I see that the balls are pressed so heavily under pressure that they have not enough space for their "volume" (area). Either they have to implode or it's elastic counter force must have greater impact of the whole scene. If you pay close attention to the pendulum movements made by the balls at the bottom, which have the least space, you will see that they are very violent, but apparently have no chance of reaching the outside.
Could it be that your evaluation order
balls.forEach(ball => {
ball.collide();
ball.move();
ball.display(displayWeight);
ball.checkCollisions();
});
is not able to propagate the collisions in a realistic way?
I'm trying to add many flying dots to the canvas that connect to each other with a line when the distance between them is between 0 and 300 and have the line's opacity change based on the distance between them. As you can see, I've done all that completely with two dots. However, I'm facing two problems now.
Problem number one - If I wanted to add 15 ellipses, I'd more or less have to copy the code for each new ellipse. I'm fairly certain this is not the way to do it.
The second problem is that I'm hard-coding which dots should connect to each other. I want each dot to connect to each nearby dot, however, I'm not sure how to do that exactly.
var el1 = {
x: 100,
y: 100,
width: 5,
height: 5
};
var el2 = {
x: 300,
y: 300,
width: 5,
height: 5
};
var speed = 2;
var opacity = 0;
var xdirection = 1;
var ydirection = 1;
function setup() {
createCanvas(windowWidth, windowHeight - 4);
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight - 4);
}
function draw() {
background(51, 51, 51);
fill(55, 90, 80);
noStroke();
ellipse(el1.x, el1.y, el1.width, el1.height);
ellipse(el2.x, el2.y, el2.width, el2.height);
el1.x = el1.x + speed * xdirection;
el1.y = el1.y + speed * ydirection;
if (el1.x > windowWidth || el1.x < 5) {
xdirection *= -1;
}
if (el1.y > windowHeight || el1.y < 5) {
ydirection *= -1;
}
if (dist(el1.x,el1.y,el2.x,el2.y) < 300) {
var opacity = map(dist(el1.x,el1.y,el2.x,el2.y),0, 300, 255, 0);
stroke(55, 90, 80, opacity);
line(el1.x,el1.y,el2.x,el2.y);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.2/p5.js"></script>
Define your ellipse as an object, create them as many as you want (15), put into array and start drawing, constantly detecting the distance between all of them.
Object himself:
var noOfEllipses = 15;
var opacity = 0;
var arrOfEllipses = [];
class Ellips {
constructor(x, y, w, h, s=2, dx=1, dy=1){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.s = s;
this.dx = dx;
this.dy = dy;
};
}
Next, initial drawing. I gave dots random dimensions and locations, you can add any individual property (speed/direction/color/etc).
function randInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
function setup() {
createCanvas(windowWidth, windowHeight - 4);
for(i=0; i<noOfEllipses;i++){
var el = new Ellips(
randInt(3, windowWidth), //x
randInt(3, windowHeight), //y
randInt(3, 8), //width
randInt(3, 8), //height
randInt(10, 50)/10 //speed
);
arrOfEllipses.push(el); //put them into array
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight - 4);
}
Animation. Iterate through array of dots, location/speed/etc. are inner properties. Second for{} is needed to calculate distance and draw lines.
function draw() {
background(51, 51, 51);
fill(200, 200, 80);
noStroke();
for(var i=0; i<arrOfEllipses.length; i++){
el = arrOfEllipses[i];
el.x = el.x + el.s * el.dx;
el.y = el.y + el.s * el.dy;
if (el.x > windowWidth || el.x < 5) {
el.dx *= -1;
}
if (el.y > windowHeight || el.y < 5) {
el.dy *= -1;
}
for(var j=i+1; j<arrOfEllipses.length; j++){
el2 = arrOfEllipses[j];
if (dist(el.x,el.y,el2.x,el2.y) < 300) {
var opacity = map(dist(el.x,el.y,el2.x,el2.y),0, 300, 255, 0);
stroke(200, 200, 80, opacity);
line(el.x,el.y,el2.x,el2.y);
}
}
ellipse(el.x, el.y, el.w, el.h);
}
}
Am very new to canvas and javascript. I found a snippet for a starfield animation effect but it loops indefinitely.
How do I get the animation to stop after say, 30 seconds? I believe it has something to do with clearInterval or setTimeout but I have no idea where in the code this should be implemented.
Any help would be greatly appreciated.
window.onload = function() {
var starfieldCanvasId = "starfieldCanvas",
framerate = 60,
numberOfStarsModifier = 1.0,
flightSpeed = 0.02;
var canvas = document.getElementById(starfieldCanvasId),
context = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height,
numberOfStars = width * height / 1000 * numberOfStarsModifier,
dirX = width / 2,
dirY = height / 4,
stars = [],
TWO_PI = Math.PI * 2;
for(var x = 0; x < numberOfStars; x++) {
stars[x] = {
x: range(0, width),
y: range(0, height),
size: range(0, 1)
};
}
window.setInterval(tick, Math.floor(1000 / framerate));
function tick() {
var oldX,
oldY;
context.clearRect(0, 0, width, height);
for(var x = 0; x < numberOfStars; x++) {
oldX = stars[x].x;
oldY = stars[x].y;
stars[x].x += (stars[x].x - dirX) * stars[x].size * flightSpeed ;
stars[x].y += (stars[x].y - dirY) * stars[x].size * flightSpeed ;
stars[x].size += flightSpeed;
if(stars[x].x < 0 || stars[x].x > width || stars[x].y < 0 || stars[x].y > height) {
stars[x] = {
x: range(0, width),
y: range(0, height),
size: 0
};
}
context.strokeStyle = "rgba(160, 160, 230, " + Math.min(stars[x].size, 2) + ")";
context.lineWidth = stars[x].size;
context.beginPath();
context.moveTo(oldX, oldY);
context.lineTo(stars[x].x, stars[x].y);
context.stroke();
}
}
function range(start, end) {
return Math.random() * (end - start) + start;
}
};
It appears that tick() is the animation loop, so change the line;
window.setInterval(tick, Math.floor(1000 / framerate));
to
window.animLoop = window.setInterval(tick, Math.floor(1000 / framerate));
window.setTimeout( function() { window.clearInterval( window.animLoop ) }, 30000 );
where 30000 is the time to end in milliseconds.
This will stop the animation from repeating by ending the interval from looping.
I'm trying to zoom at mouse position, like say on google maps. It kind of works but it shifts the point i want to zoom in on wherever it matches up with the original.Then when i zoom at that point it works fine. I think I need to translate the point back to the mouse, but I'm not sure how to do it exactly.
This is the code before i draw:
translate(zoomLocation.x, zoomLocation.y);
scale(zoom);
translate(-zoomLocation.x, -zoomLocation.y);
drawGrid();
And this is when I zoom:
event.preventDefault();
zoomLocation = {
x: zoomLocation.x + (mouseX - zoomLocation.x) / zoom,
y: zoomLocation.y + (mouseY - zoomLocation.y) / zoom
};
zoom -= zoomSensitivity * event.delta;
let colors = {
background: 0,
gridLines: "white"
};
let nVariables = 4;
let zoom = 1;
let zoomLocation = {
x: 0,
y: 0
};
let zoomSensitivity = 0.0002;
function draw() {
translate(zoomLocation.x, zoomLocation.y);
scale(zoom);
translate(-zoomLocation.x, -zoomLocation.y);
drawGrid();
stroke("blue");
ellipse(zoomLocation.x + (mouseX - zoomLocation.x) / zoom, zoomLocation.y + (mouseY - zoomLocation.y) / zoom, 10, 10);
stroke("red");
ellipse(zoomLocation.x, zoomLocation.y, 10, 10);
}
function setup() {
zoomLocation = {
x: 0,
y: windowHeight / 2
}
createCanvas(windowWidth, windowHeight);
}
function mouseWheel(event) {
event.preventDefault();
let oldZoom = zoom;
zoomLocation = {
x: zoomLocation.x + (mouseX - zoomLocation.x) / zoom,
y: zoomLocation.y + (mouseY - zoomLocation.y) / zoom
};
zoom -= zoomSensitivity * event.delta;
}
function drawGrid() {
let nCells = 2 ** nVariables;
if (nCells > 2048) {
if (!window.confirm(`You are about to create ${nCells} cells. This might lag your browser. Are you sure?`)) {
return;
}
}
background(colors.background);
let gridWidth = windowWidth - 2;
let gridHeight = min(gridWidth / nCells, windowHeight / 2 - 2);
let gridY = windowHeight / 2;
stroke(colors.gridLines);
line(0, gridY, gridWidth, gridY);
line(0, gridY + gridHeight, gridWidth, gridY + gridHeight);
for (let i = 0; i < nCells + 1; i++) {
line(i * (gridWidth / nCells), gridY, i * (gridWidth / nCells), gridY + gridHeight)
}
let curveHeight = 2;
let drawVariable = (n) => {
let p1 = {
x: 1 / (2 ** (n + 1)) * gridWidth,
y: gridY
};
let c1 = {
x: p1.x,
y: p1.y + gridWidth / (2 ** n) * curveHeight
};
let p2 = {
y: p1.y
};
let c2 = {
y: c1.y
};;
noFill();
stroke("red");
if (n == 0) {
p2.x = gridWidth;
c2.x = p2.x;
c1.y = c2.y = p1.y + gridWidth / 2 * curveHeight;
curve(c1.x, c1.y, p1.x, p1.y, p2.x, p2.y, c2.x, c2.y);
return;
}
for (let i = 3; i < 2 ** (n + 1); i += 2) {
p2.x = i / (2 ** (n + 1)) * gridWidth
c2.x = p2.x;
if ((i - 3) % 4 == 0) {
curve(c1.x, c1.y, p1.x, p1.y, p2.x, p2.y, c2.x, c2.y);
} else {
p1.x = p2.x;
c1.x = c2.x;
}
}
};
for (let i = 0; i < nVariables; i++) {
drawVariable(i);
}
}
body {
margin: 0
}
button {
outline: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.11/addons/p5.dom.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.js" integrity="sha256-DZAnKJ/6XZ9si04Hgrsxu/8s717jcIzLy3oi35EouyE=" crossorigin="anonymous"></script>
What you're looking for is an application of Affine Transformation. Here's an example where I'm applying transformations incrementally to transform + scale at the mouse pointer location for a Google Maps type zoom effect: Zoom Effect in p5.js Web Editor.
This is a good medium article that explains why this works: Zooming at the Mouse Coordinates with Affine Transformations
This is another good article that has some mathematical explanations on how it achieves the effects: Affine Transformations — Pan, Zoom, Skew
I have been creating a clone of agar.io and I don't understand why the circles start vibrating when they touch each other. Below is my code:
var
canvas,
ctx,
width = innerWidth,
height = innerHeight,
mouseX = 0,
mouseY = 0;
var
camera = {
x: 0,
y: 0,
update: function(obj) {
this.x = obj.x - width / 2;
this.y = obj.y - height / 2;
}
},
player = {
defaultMass: 54,
x: 0,
y: 0,
blobs: [],
update: function() {
for (var i = 0; i < this.blobs.length; i++) {
var x = mouseX + camera.x - this.blobs[i].x;
var y = mouseY + camera.y - this.blobs[i].y;
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var speed = 54 / this.blobs[i].mass;
this.blobs[i].velX = x / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].velY = y / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].x += this.blobs[i].velX;
this.blobs[i].y += this.blobs[i].velY;
for (var j = 0; j < this.blobs.length; j++) {
if (j != i && this.blobs[i] !== undefined) {
var blob1 = this.blobs[i];
var blob2 = this.blobs[j];
var dist = Math.sqrt(Math.pow(blob2.x - blob1.x, 2) + Math.pow(blob2.y - blob1.y, 2));
if (dist < blob1.mass + blob2.mass) {
if (this.blobs[i].x < this.blobs[j].x) {
this.blobs[i].x--;
} else if (this.blobs[i].x > this.blobs[j].x) {
this.blobs[i].x++;
}
if (this.blobs[i].y < this.blobs[j].y) {
this.blobs[i].y--;
} else if ((this.blobs[i].y > this.blobs[j].y)) {
this.blobs[i].y++;
}
}
}
}
}
this.x += (mouseX - width / 2) / (width / 2) * 1;
this.y += (mouseY - height / 2) / (height / 2) * 1
},
split: function(cell) {
cell.mass /= 2;
this.blobs.push({
x: cell.x,
y: cell.y,
mass: cell.mass
});
},
draw: function() {
for (var i = 0; i < this.blobs.length; i++) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(-camera.x + this.blobs[i].x, -camera.y + this.blobs[i].y, this.blobs[i].mass, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
function handleMouseMove(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
function setup() {
canvas = document.getElementById("game");
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
addEventListener("mousemove", handleMouseMove);
player.blobs.push({
x: 0,
y: 0,
mass: player.defaultMass
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass / 2
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass * 2
});
var loop = function() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function update() {
camera.update(player.blobs[0]);
player.update();
}
function draw() {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
player.draw();
}
setup();
body {
margin: 0;
padding: 0;
}
<canvas id="game">kindly update your browser.</canvas>
Separating circles
Your separation code was not correct. Use the vector between them to get the new pos.
The vector between them
To find if two circles are intercepting find the length of the vector from one to the next
The two circles.
var cir1 = {x : 100, y : 100, r : 120}; // r is the radius
var cir2 = {x : 250, y : 280, r : 150}; // r is the radius
The vector from cir2 to cir1
var vx = cir2.x - cir1.x;
var vy = cir2.y - cir1.y;
The length of the vector
var len = Math.sqrt(x * x + y * y);
// or use the ES6 Math.hypot function
/* var len = Math.hypot(x,y); */
The circles overlap if the sum of the radii is greater than the length of the vector between them
if(cir1.r + cir2.r > len){ // circles overlap
Normalise the vector
If they overlap you need to move one away from the other. There are many ways to do this, the simplest way is to move one circle along the line between them.
First normalise the vector from cir1 to cir2 by dividing by its (vector) length.
vx \= len;
vy \= len;
Note that the length could be zero. If this happens then you will get NaN in further calculations. If you suspect you may get one circle at the same location as another the easiest way to deal with the zero move one circle a little.
// replace the two lines above with
if(len === 0){ // circles are on top of each other
vx = 1; // move the circle (abstracted into the vector)
}else{
vx \= len; // normalise the vector
vy \= len;
}
Move circle/s to just touch
Now you have the normalised vector which is 1 unit long you can make it any length you need by multiplying the two scalars vx, vy with the desired length which in this case is the sum of the two circles radii.
var mx = vx * (cir1.r + cir2.r); // move distance
var my = vy * (cir1.r + cir2.r);
.Only use one of the following methods.
You can now position one of the circles the correct distance so that they just touch
// move cir1
cir1.x = cir2.x - mx;
cir1.y = cir2.y - my;
Or move the second circle
cir2.x = cir1.x + mx;
cir2.y = cir1.y + my;
Or move both circles but you will have to first find the proportional center between the two
var pLen = cir1.r / (cir1.r + cir2.r); // find the ratio of the radii
var cx = cir1.x + pLen * vx * len; // find the proportional center between
var cy = cir1.y + pLen * vy * len; // the two circles
Then move both circles away from that point by their radii
cir1.x = cx - vx * cir1.r; // move circle 1 away from the shared center
cir1.y = cy - vy * cir1.r;
cir2.x = cx + vx * cir2.r; // move circle 2 away from the shared center
cir2.y = cy + vy * cir2.r;
DEMO
Copy of OP's snippet with mods to fix problem by moving the the first circle blob1 away from the second blob2 and assuming they will never be at the same spot (no divide by zero)
var
canvas,
ctx,
width = innerWidth,
height = innerHeight,
mouseX = 0,
mouseY = 0;
var
camera = {
x: 0,
y: 0,
update: function(obj) {
this.x = obj.x - width / 2;
this.y = obj.y - height / 2;
}
},
player = {
defaultMass: 54,
x: 0,
y: 0,
blobs: [],
update: function() {
for (var i = 0; i < this.blobs.length; i++) {
var x = mouseX + camera.x - this.blobs[i].x;
var y = mouseY + camera.y - this.blobs[i].y;
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var speed = 54 / this.blobs[i].mass;
this.blobs[i].velX = x / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].velY = y / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].x += this.blobs[i].velX;
this.blobs[i].y += this.blobs[i].velY;
for (var j = 0; j < this.blobs.length; j++) {
if (j != i && this.blobs[i] !== undefined) {
var blob1 = this.blobs[i];
var blob2 = this.blobs[j];
var x = blob2.x - blob1.x; // get the vector from blob1 to blob2
var y = blob2.y - blob1.y; //
var dist = Math.sqrt(x * x + y * y); // get the distance between the two blobs
if (dist < blob1.mass + blob2.mass) { // if the distance is less than the 2 radius
// if there is overlap move blob one along the line between the two the distance of the two radius
x /= dist; // normalize the vector. This makes the vector 1 unit long
y /= dist;
// multiplying the normalised vector by the correct distance between the two
// and subtracting that distance from the blob 2 give the new pos of
// blob 1
blob1.x = blob2.x - x * (blob1.mass + blob2.mass);
blob1.y = blob2.y - y * (blob1.mass + blob2.mass);
}
}
}
}
this.x += (mouseX - width / 2) / (width / 2) * 1;
this.y += (mouseY - height / 2) / (height / 2) * 1
},
split: function(cell) {
cell.mass /= 2;
this.blobs.push({
x: cell.x,
y: cell.y,
mass: cell.mass
});
},
draw: function() {
for (var i = 0; i < this.blobs.length; i++) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(-camera.x + this.blobs[i].x, -camera.y + this.blobs[i].y, this.blobs[i].mass, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
function handleMouseMove(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
function setup() {
canvas = document.getElementById("game");
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
addEventListener("mousemove", handleMouseMove);
player.blobs.push({
x: 0,
y: 0,
mass: player.defaultMass
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass / 2
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass * 2
});
var loop = function() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function update() {
camera.update(player.blobs[0]);
player.update();
}
function draw() {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
player.draw();
}
setup();
body {
margin: 0;
padding: 0;
}
<canvas id="game">kindly update your browser.</canvas>
var
canvas,
ctx,
width = innerWidth,
height = innerHeight,
mouseX = 0,
mouseY = 0;
var
camera = {
x: 0,
y: 0,
update: function(obj) {
this.x = obj.x - width / 2;
this.y = obj.y - height / 2;
}
},
player = {
defaultMass: 54,
x: 0,
y: 0,
blobs: [],
update: function() {
for (var i = 0; i < this.blobs.length; i++) {
var x = mouseX + camera.x - this.blobs[i].x;
var y = mouseY + camera.y - this.blobs[i].y;
var length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var speed = 54 / this.blobs[i].mass;
this.blobs[i].velX = x / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].velY = y / length * speed * Math.min(1, Math.pow(x / this.blobs[i].mass, 2));
this.blobs[i].x += this.blobs[i].velX;
this.blobs[i].y += this.blobs[i].velY;
for (var j = 0; j < this.blobs.length; j++) {
if (j != i && this.blobs[i] !== undefined) {
var blob1 = this.blobs[i];
var blob2 = this.blobs[j];
var dist = Math.sqrt(Math.pow(blob2.x - blob1.x, 2) + Math.pow(blob2.y - blob1.y, 2));
if (dist < blob1.mass + blob2.mass) {
if (this.blobs[i].x < this.blobs[j].x) {
this.blobs[i].x--;
} else if (this.blobs[i].x > this.blobs[j].x) {
this.blobs[i].x++;
}
if (this.blobs[i].y < this.blobs[j].y) {
this.blobs[i].y--;
} else if ((this.blobs[i].y > this.blobs[j].y)) {
this.blobs[i].y++;
}
}
}
}
}
this.x += (mouseX - width / 2) / (width / 2) * 1;
this.y += (mouseY - height / 2) / (height / 2) * 1
},
split: function(cell) {
cell.mass /= 2;
this.blobs.push({
x: cell.x,
y: cell.y,
mass: cell.mass
});
},
draw: function() {
for (var i = 0; i < this.blobs.length; i++) {
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(-camera.x + this.blobs[i].x, -camera.y + this.blobs[i].y, this.blobs[i].mass, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
}
};
function handleMouseMove(e) {
mouseX = e.clientX;
mouseY = e.clientY;
}
function setup() {
canvas = document.getElementById("game");
ctx = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
addEventListener("mousemove", handleMouseMove);
player.blobs.push({
x: 0,
y: 0,
mass: player.defaultMass
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass / 2
});
player.blobs.push({
x: 100,
y: 100,
mass: player.defaultMass * 2
});
var loop = function() {
update();
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function update() {
camera.update(player.blobs[0]);
player.update();
}
function draw() {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
player.draw();
}
setup();
body {
margin: 0;
padding: 0;
}
<canvas id="game">kindly update your browser.</canvas>