Related
I am trying to write aa small physics demo using Javascript. I have multiple balls that bounce off each other just fine, but things go wrong when I try to add gravity.
I am trying to conserve the momentum once they hit, but when I add constant gravity to each one, the physics start to break down.
Here is what I have in terms of code:
class Ball {
constructor ({
x,
y,
vx,
vy,
radius,
color = 'red',
}) {
this.x = x
this.y = y
this.vx = vx
this.vy = vy
this.radius = radius
this.color = color
this.mass = 1
}
render (ctx) {
ctx.save()
ctx.fillStyle = this.color
ctx.strokeStyle = this.color
ctx.translate(this.x, this.y)
ctx.strokeRect(-this.radius, -this.radius, this.radius * 2, this.radius * 2)
ctx.beginPath()
ctx.arc(0, 0, this.radius, Math.PI * 2, false)
ctx.closePath()
ctx.fill()
ctx.restore()
return this
}
getBounds () {
return {
x: this.x - this.radius,
y: this.y - this.radius,
width: this.radius * 2,
height: this.radius * 2
}
}
}
const intersects = (rectA, rectB) => {
return !(rectA.x + rectA.width < rectB.x ||
rectB.x + rectB.width < rectA.x ||
rectA.y + rectA.height < rectB.y ||
rectB.y + rectB.height < rectA.y)
}
const checkWall = (ball) => {
const bounceFactor = 0.5
if (ball.x + ball.radius > canvas.width) {
ball.x = canvas.width - ball.radius
ball.vx *= -bounceFactor
}
if (ball.x - ball.radius < 0) {
ball.x = ball.radius
ball.vx *= -bounceFactor
}
if (ball.y + ball.radius > canvas.height) {
ball.y = canvas.height - ball.radius
ball.vy *= -1
}
if (ball.y - ball.radius < 0) {
ball.y = ball.radius
ball.vy *= -bounceFactor
}
}
const rotate = (x, y, sin, cos, reverse) => {
return {
x: reverse ? x * cos + y * sin : x * cos - y * sin,
y: reverse ? y * cos - x * sin : y * cos + x * sin
}
}
const checkCollision = (ball0, ball1, dt) => {
const dx = ball1.x - ball0.x
const dy = ball1.y - ball0.y
const dist = Math.sqrt(dx * dx + dy * dy)
const minDist = ball0.radius + ball1.radius
if (dist < minDist) {
//calculate angle, sine, and cosine
const angle = Math.atan2(dy, dx)
const sin = Math.sin(angle)
const cos = Math.cos(angle)
//rotate ball0's position
const pos0 = {x: 0, y: 0}
//rotate ball1's position
const pos1 = rotate(dx, dy, sin, cos, true)
//rotate ball0's velocity
const vel0 = rotate(ball0.vx, ball0.vy, sin, cos, true)
//rotate ball1's velocity
const vel1 = rotate(ball1.vx, ball1.vy, sin, cos, true)
//collision reaction
const vxTotal = (vel0.x - vel1.x)
vel0.x = ((ball0.mass - ball1.mass) * vel0.x + 2 * ball1.mass * vel1.x) /
(ball0.mass + ball1.mass)
vel1.x = vxTotal + vel0.x
const absV = Math.abs(vel0.x) + Math.abs(vel1.x)
const overlap = (ball0.radius + ball1.radius) - Math.abs(pos0.x - pos1.x)
pos0.x += vel0.x / absV * overlap
pos1.x += vel1.x / absV * overlap
//rotate positions back
const pos0F = rotate(pos0.x, pos0.y, sin, cos, false)
const pos1F = rotate(pos1.x, pos1.y, sin, cos, false)
//adjust positions to actual screen positions
ball1.x = ball0.x + pos1F.x
ball1.y = ball0.y + pos1F.y
ball0.x = ball0.x + pos0F.x
ball0.y = ball0.y + pos0F.y
//rotate velocities back
const vel0F = rotate(vel0.x, vel0.y, sin, cos, false)
const vel1F = rotate(vel1.x, vel1.y, sin, cos, false)
ball0.vx = vel0F.x
ball0.vy = vel0F.y
ball1.vx = vel1F.x
ball1.vy = vel1F.y
}
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let oldTime = 0
canvas.width = innerWidth
canvas.height = innerHeight
document.body.appendChild(canvas)
const log = document.getElementById('log')
const balls = new Array(36).fill(null).map(_ => new Ball({
x: Math.random() * innerWidth,
y: Math.random() * innerHeight,
vx: (Math.random() * 2 - 1) * 5,
vy: (Math.random() * 2 - 1) * 5,
radius: 20,
}))
requestAnimationFrame(updateFrame)
function updateFrame (ts) {
const dt = ts - oldTime
oldTime = ts
ctx.clearRect(0, 0, innerWidth, innerHeight)
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// ADD GRAVITY HERE
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// CHECK FOR COLLISIONS HERE
checkCollision(ball0, ball1, dt)
}
}
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
// const dist = ball2.x - ball1.x
// if (Math.abs(dist) < ball1.radius + ball2.radius) {
// const vxTotal = ball1.vx - ball2.vx
// ball1.vx = ((ball1.mass - ball2.mass) * ball1.vx + 2 * ball2.mass * ball2.vx) / (ball1.mass + ball2.mass)
// ball2.vx = vxTotal + ball1.vx
// ball1.x += ball1.vx
// ball2.x += ball2.vx
// }
// ball.vy += 0.5
// ball.x += ball.vx
// ball.y += ball.vy
//
// ball.render(ctx)
requestAnimationFrame(updateFrame)
}
* { margin: 0; padding: 0; }
As you can see, I have checkCollision helper method, which calculates the kinetic energy and new velocities of a ball once it has collided with another ball. My update loop looks like this:
// add velocities to balls position
// check if its hitting any wall and bounce it back
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// Add constant gravity to the vertical velocity
// When balls stack up on each other at the bottom, the gravity is still applied and my
// "checkCollision" method freaks out and the physics start to explode
ball.vy += 0.8
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
for (let i = 0; i < balls.length; i++) {
const ball0 = balls[i]
for (let j = i + 1; j < balls.length; j++) {
const ball1 = balls[j]
// Check collisions between two balls
checkCollision(ball0, ball1, dt)
}
}
// Finally render the ball on-screen
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
ball.render(ctx)
}
How do I calculate aa gravity, while preventing the physics from exploding when the balls start stacking on top of each other?
It seems that the gravity force is colliding with the "checkCollision" method. The checkCollision method tries to move them back in place, but the constant gravity overwrites it and continues pulling them down.
EDIT: After some reading I understand some Verlet integration is in order, but am having difficulties with wrapping my head around it.
for (let i = 0; i < balls.length; i++) {
const ball = balls[i]
// This line needs to be converted to verlet motion?
ball.vy += 2
ball.x += ball.vx * (dt * 0.005)
ball.y += ball.vy * (dt * 0.005)
checkWall(ball)
}
Balls do not overlap
There is a fundamental flaw in the collision testing due to the fact that the collisions are calculated only when 2 balls overlap. In the real world this never happens.
The result of "collide on overlap" when many balls are interacting, will result in behavior that does not conserve the total energy of the system.
Resolve by order of collision
You can resolve collisions such that balls never overlap however the amount of processing is indeterminate per frame, growing exponentially as the density of balls increases.
The approach is to locate the first collision between balls in the time between frames. Resolve that collision and then with the new position of that collision find the next collision closest in time forward from the last. Do that until there are no pending collisions for that frame. (There is more to it than that) The result is that the simulation will never be in the impossible state where balls overlap.
Check out my Pool simulator on CodePen that uses this method to simulate pool balls. The balls can have any speed and always resolve correctly.
Verlet integration.
However you can reduce the noise using the overlapping collisions by using verlet integration which will keep the total energy of the balls at a more stable level.
To do that we introduce 2 new properties of the ball, px, py that hold the previous position of the ball.
Each frame we calculate the balls velocity as the difference between the current position and the new position. That velocity is used to do the calculations for the frame.
When a ball changes direction (hits wall or another ball) we also need to change the balls previous position to match where it would have been on the new trajectory.
Use constant time steps.
Using time steps based on time since last frame will also introduce noise and should not be used in the overlap collision method.
Reduce time, increase iteration
To further combat the noise you need to slow the overall speed of the balls to reduce the amount they overlay and thus more closely behave as if they collided at the point ballA.radius + ballB.radius apart. Also you should test every ball against every other ball, not just ball against balls above it in the balls array.
To keep the animation speed up you solve the ball V ball V wall collisions a few times per frame. The example does 5. The best value depends on the total energy of the balls, the level of noise that is acceptable, and the CPU power of the device its running on.
Accuracy matters
Your collision function is also way out there. I had a quick look and it did not look right. I added an alternative in the example.
When a ball hits a wall it does so at some time between the frames. You must move the ball away from the wall by the correct distance. Not doing so is like simulating a ball that sticks to a wall a tiny bit each time it hits, further diverging from what really happens.
Example
This is a rewrite of your original code. Click canvas to add some energy.
const ctx = canvas.getContext("2d");
const BOUNCE = 0.75;
const resolveSteps = 5;
var oldTime = 0;
const $setOf = (count, fn = (i) => i) => {var a = [], i = 0; while (i < count) { a.push(fn(i++)) } return a };
const $rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const $randP = (min = 1, max = min + (min = 0), p = 2) => Math.random() ** p * (max - min) + min;
var W = canvas.width, H = canvas.height;
const BALL_COUNT = 80;
const BALL_RADIUS = 15, BALL_MIN_RADIUS = 6;
const GRAV = 0.5 / resolveSteps;
requestAnimationFrame(updateFrame);
canvas.addEventListener("click", () => {
balls.forEach(b => {
b.px = b.x + (Math.random() * 18 - 9);
b.py = b.y + (Math.random() * -18);
})
});
class Ball {
constructor({x, y, vx, vy, radius}) {
this.x = x;
this.y = y;
this.px = x - vx;
this.py = y - vy;
this.vx = vx;
this.vy = vy;
this.radius = radius;
this.mass = radius * radius * Math.PI * (4 / 3); // use sphere volume as mass
}
render(ctx) {
ctx.moveTo(this.x + this.radius, this.y);
ctx.arc(this.x, this.y, this.radius, Math.PI * 2, false);
}
move() {
this.vx = this.x - this.px;
this.vy = this.y - this.py;
this.vy += GRAV;
this.px = this.x;
this.py = this.y;
this.x += this.vx;
this.y += this.vy;
this.checkWall();
}
checkWall() {
const ball = this;
const top = ball.radius;
const left = ball.radius;
const bottom = H - ball.radius;
const right = W - ball.radius;
if (ball.x > right) {
const away = (ball.x - right) * BOUNCE;
ball.x = right - away;
ball.vx = -Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
} else if (ball.x < left) {
const away = (ball.x - left) * BOUNCE;
ball.x = left + away;
ball.vx = Math.abs(ball.vx) * BOUNCE;
ball.px = ball.x - ball.vx;
}
if (ball.y > bottom) {
const away = (ball.y - bottom) * BOUNCE;
ball.y = bottom - away;
ball.vy = -Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
} else if (ball.y < top) {
const away = (ball.y - top) * BOUNCE;
ball.y = top + away;
ball.vy = Math.abs(ball.vy) * BOUNCE;
ball.py = ball.y - ball.vy;
}
}
collisions() {
var b, dx, dy, nx, ny, cpx, cpy, p, d, i = 0;
var {x, y, vx, vy, px, py, radius: r, mass: m} = this;
while (i < balls.length) {
b = balls[i++];
if (this !== b) {
const rr = r + b.radius;
if (x + rr > b.x && x < b.x + rr && y + rr > b.y && y < b.y + rr) {
dx = x - b.x;
dy = y - b.y;
d = (dx * dx + dy * dy) ** 0.5;
if (d < rr) {
nx = (b.x - x) / d;
ny = (b.y - y) / d;
p = 2 * (vx * nx + vy * ny - b.vx * nx - b.vy * ny) / (m + b.mass);
cpx = (x * b.radius + b.x * r) / rr;
cpy = (y * b.radius + b.y * r) / rr;
x = cpx + r * (x - b.x) / d;
y = cpy + r * (y - b.y) / d;
b.x = cpx + b.radius * (b.x - x) / d;
b.y = cpy + b.radius * (b.y - y) / d;
px = x - (vx -= p * b.mass * nx);
py = y - (vy -= p * b.mass * ny);
b.px = b.x - (b.vx += p * m * nx);
b.py = b.y - (b.vy += p * m * ny);
}
}
}
}
this.x = x;
this.y = y;
this.px = px;
this.py = py;
this.vx = vx;
this.vy = vy;
this.checkWall();
}
}
const balls = (() => {
return $setOf(BALL_COUNT, () => new Ball({
x: $rand(BALL_RADIUS, W - BALL_RADIUS),
y: $rand(BALL_RADIUS, H - BALL_RADIUS),
vx: $rand(-2, 2),
vy: $rand(-2, 2),
radius: $randP(BALL_MIN_RADIUS, BALL_RADIUS, 4),
}));
})();
function updateFrame(ts) {
var i = 0, j = resolveSteps;
ctx.clearRect(0, 0, W, H);
while (i < balls.length) { balls[i++].move() }
while (j--) {
i = 0;
while (i < balls.length) { balls[i++].collisions(balls) }
}
ctx.fillStyle = "#0F0";
ctx.beginPath();
i = 0;
while (i < balls.length) { balls[i++].render(ctx) }
ctx.fill();
requestAnimationFrame(updateFrame)
}
<canvas id="canvas" width="400" height="180" style="border:1px solid black;"></canvas>
<div style="position: absolute; top: 10px; left: 10px;">Click to stir</div>
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 create a hyperdrive effect, like from Star Wars, where the stars have a motion trail. I've gotten as far as creating the motion trail on a single circle, it still looks like the trail is going down in the y direction and not forwards or positive in the z direction.
Also, how could I do this with (many) randomly placed circles as if they were stars?
My code is on jsfiddle (https://jsfiddle.net/5m7x5zxu/) and below:
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
var xPos = 180;
var yPos = 100;
var motionTrailLength = 16;
var positions = [];
function storeLastPosition(xPos, yPos) {
// push an item
positions.push({
x: xPos,
y: yPos
});
//get rid of first item
if (positions.length > motionTrailLength) {
positions.pop();
}
}
function update() {
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i = positions.length-1; i > 0; i--) {
var ratio = (i - 1) / positions.length;
drawCircle(positions[i].x, positions[i].y, ratio);
}
drawCircle(xPos, yPos, "source");
var k=2;
storeLastPosition(xPos, yPos);
// update position
if (yPos > 125) {
positions.pop();
}
else{
yPos += k*1.1;
}
requestAnimationFrame(update);
}
update();
function drawCircle(x, y, r) {
if (r == "source") {
r = 1;
} else {
r*=1.1;
}
context.beginPath();
context.arc(x, y, 3, 0, 2 * Math.PI, true);
context.fillStyle = "rgba(255, 255, 255, " + parseFloat(1-r) + ")";
context.fill();
}
Canvas feedback and particles.
This type of FX can be done many ways.
You could just use a particle systems and draw stars (as lines) moving away from a central point, as the speed increase you increase the line length. When at low speed the line becomes a circle if you set ctx.lineWidth > 1 and ctx.lineCap = "round"
To add to the FX you can use render feedback as I think you have done by rendering the canvas over its self. If you render it slightly larger you get a zoom FX. If you use ctx.globalCompositeOperation = "lighter" you can increase the stars intensity as you speed up to make up for the overall loss of brightness as stars move faster.
Example
I got carried away so you will have to sift through the code to find what you need.
The particle system uses the Point object and a special array called bubbleArray to stop GC hits from janking the animation.
You can use just an ordinary array if you want. The particles are independent of the bubble array. When they have moved outside the screen they are move to a pool and used again when a new particle is needed. The update function moves them and the draw Function draws them I guess LOL
The function loop is the main loop and adds and draws particles (I have set the particle count to 400 but should handle many more)
The hyper drive is operated via the mouse button. Press for on, let go for off. (It will distort the text if it's being displayed)
The canvas feedback is set via that hyperSpeed variable, the math is a little complex. The sCurce function just limits the value to 0,1 in this case to stop alpha from going over or under 1,0. The hyperZero is just the sCurve return for 1 which is the hyper drives slowest speed.
I have pushed the feedback very close to the limit. In the first few lines of the loop function you can set the top speed if(mouse.button){ if(hyperSpeed < 1.75){ Over this value 1.75 and you will start to get bad FX, at about 2 the whole screen will just go white (I think that was where)
Just play with it and if you have questions ask in the comments.
const ctx = canvas.getContext("2d");
// very simple mouse
const mouse = {x : 0, y : 0, button : false}
function mouseEvents(e){
mouse.x = e.pageX;
mouse.y = e.pageY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents));
// High performance array pool using buubleArray to separate pool objects and active object.
// This is designed to eliminate GC hits involved with particle systems and
// objects that have short lifetimes but used often.
// Warning this code is not well tested.
const bubbleArray = () => {
const items = [];
var count = 0;
return {
clear(){ // warning this dereferences all locally held references and can incur Big GC hit. Use it wisely.
this.items.length = 0;
count = 0;
},
update() {
var head, tail;
head = tail = 0;
while(head < count){
if(items[head].update() === false) {head += 1 }
else{
if(tail < head){
const temp = items[head];
items[head] = items[tail];
items[tail] = temp;
}
head += 1;
tail += 1;
}
}
return count = tail;
},
createCallFunction(name, earlyExit = false){
name = name.split(" ")[0];
const keys = Object.keys(this);
if(Object.keys(this).indexOf(name) > -1){ throw new Error(`Can not create function name '${name}' as it already exists.`) }
if(!/\W/g.test(name)){
let func;
if(earlyExit){
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ if (items[i++].${name}() === true) { break } }`;
}else{
func = `var items = this.items; var count = this.getCount(); var i = 0;\nwhile(i < count){ items[i++].${name}() }`;
}
!this.items && (this.items = items);
this[name] = new Function(func);
}else{ throw new Error(`Function name '${name}' contains illegal characters. Use alpha numeric characters.`) }
},
callEach(name){var i = 0; while(i < count){ if (items[i++][name]() === true) { break } } },
each(cb) { var i = 0; while(i < count){ if (cb(items[i], i++) === true) { break } } },
next() { if (count < items.length) { return items[count ++] } },
add(item) {
if(count === items.length){
items.push(item);
count ++;
}else{
items.push(items[count]);
items[count++] = item;
}
return item;
},
getCount() { return count },
}
}
// Helpers rand float, randI random Int
// doFor iterator
// sCurve curve input -Infinity to Infinity out -1 to 1
// randHSLA creates random colour
// CImage, CImageCtx create image and image with context attached
const randI = (min, max = min + (min = 0)) => (Math.random() * (max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const doFor = (count, cb) => { var i = 0; while (i < count && cb(i++) !== true); }; // the ; after while loop is important don't remove
const sCurve = (v,p) => (2 / (1 + Math.pow(p,-v))) -1;
const randHSLA = (h, h1, s = 100, s1 = 100, l = 50, l1 = 50, a = 1, a1 = 1) => { return `hsla(${randI(h,h1) % 360},${randI(s,s1)}%,${randI(l,l1)}%,${rand(a,a1)})` }
const CImage = (w = 128, h = w) => (c = document.createElement("canvas"),c.width = w,c.height = h, c);
const CImageCtx = (w = 128, h = w) => (c = CImage(w,h), c.ctx = c.getContext("2d"), c);
// create image to hold text
var textImage = CImageCtx(1024, 1024);
var c = textImage.ctx;
c.fillStyle = "#FF0";
c.font = "64px arial black";
c.textAlign = "center";
c.textBaseline = "middle";
const text = "HYPER,SPEED FX,VII,,Battle of Jank,,Hold the mouse,button to increase,speed.".split(",");
text.forEach((line,i) => { c.fillText(line,512,i * 68 + 68) });
const maxLines = text.length * 68 + 68;
function starWarIntro(image,x1,y1,x2,y2,pos){
var iw = image.width;
var ih = image.height;
var hh = (x2 - x1) / (y2 - y1); // Slope of left edge
var w2 = iw / 2; // half width
var z1 = w2 - x1; // Distance (z) to first line
var z2 = (z1 / (w2 - x2)) * z1 - z1; // distance (z) between first and last line
var sk,t3,t3a,z3a,lines, z3, dd = 0, a = 0, as = 2 / (y2 - y1);
for (var y = y1; y < y2 && dd < maxLines; y++) { // for each line
t3 = ((y - y1) * hh) + x1; // get scan line top left edge
t3a = (((y+1) - y1) * hh) + x1; // get scan line bottom left edge
z3 = (z1 / (w2 - t3)) * z1; // get Z distance to top of this line
z3a = (z1 / (w2 - t3a)) * z1; // get Z distance to bottom of this line
dd = ((z3 - z1) / z2) * ih; // get y bitmap coord
a += as;
ctx.globalAlpha = a < 1 ? a : 1;
dd += pos; // kludge for this answer to make text move
// does not move text correctly
lines = ((z3a - z1) / z2) * ih-dd; // get number of lines to copy
ctx.drawImage(image, 0, dd , iw, lines, t3, y, w - t3 * 2, 1.5);
}
}
// canvas settings
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
// diagonal distance used to set point alpha (see point update)
var diag = Math.sqrt(w * w + h * h);
// If window size is changed this is called to resize the canvas
// It is not called via the resize event as that can fire to often and
// debounce makes it feel sluggish so is called from main loop.
function resizeCanvas(){
points.clear();
canvas.width = innerWidth;
canvas.height = innerHeight;
w = canvas.width;
h = canvas.height;
cw = w / 2; // center
ch = h / 2;
diag = Math.sqrt(w * w + h * h);
}
// create array of points
const points = bubbleArray();
// create optimised draw function itterator
points.createCallFunction("draw",false);
// spawns a new star
function spawnPoint(pos){
var p = points.next();
p = points.add(new Point())
if (p === undefined) { p = points.add(new Point()) }
p.reset(pos);
}
// point object represents a single star
function Point(pos){ // this function is duplicated as reset
if(pos){
this.x = pos.x;
this.y = pos.y;
this.dead = false;
}else{
this.x = 0;
this.y = 0;
this.dead = true;
}
this.alpha = 0;
var x = this.x - cw;
var y = this.y - ch;
this.dir = Math.atan2(y,x);
this.distStart = Math.sqrt(x * x + y * y);
this.speed = rand(0.01,1);
this.col = randHSLA(220,280,100,100,50,100);
this.dx = Math.cos(this.dir) * this.speed;
this.dy = Math.sin(this.dir) * this.speed;
}
Point.prototype = {
reset : Point, // resets the point
update(){ // moves point and returns false when outside
this.speed *= hyperSpeed; // increase speed the more it has moved
this.x += Math.cos(this.dir) * this.speed;
this.y += Math.sin(this.dir) * this.speed;
var x = this.x - cw;
var y = this.y - ch;
this.alpha = (Math.sqrt(x * x + y * y) - this.distStart) / (diag * 0.5 - this.distStart);
if(this.alpha > 1 || this.x < 0 || this.y < 0 || this.x > w || this.h > h){
this.dead = true;
}
return !this.dead;
},
draw(){ // draws the point
ctx.strokeStyle = this.col;
ctx.globalAlpha = 0.25 + this.alpha *0.75;
ctx.beginPath();
ctx.lineTo(this.x - this.dx * this.speed, this.y - this.dy * this.speed);
ctx.lineTo(this.x, this.y);
ctx.stroke();
}
}
const maxStarCount = 400;
const p = {x : 0, y : 0};
var hyperSpeed = 1.001;
const alphaZero = sCurve(1,2);
var startTime;
function loop(time){
if(startTime === undefined){
startTime = time;
}
if(w !== innerWidth || h !== innerHeight){
resizeCanvas();
}
// if mouse down then go to hyper speed
if(mouse.button){
if(hyperSpeed < 1.75){
hyperSpeed += 0.01;
}
}else{
if(hyperSpeed > 1.01){
hyperSpeed -= 0.01;
}else if(hyperSpeed > 1.001){
hyperSpeed -= 0.001;
}
}
var hs = sCurve(hyperSpeed,2);
ctx.globalAlpha = 1;
ctx.setTransform(1,0,0,1,0,0); // reset transform
//==============================================================
// UPDATE the line below could be the problem. Remove it and try
// what is under that
//==============================================================
//ctx.fillStyle = `rgba(0,0,0,${1-(hs-alphaZero)*2})`;
// next two lines are the replacement
ctx.fillStyle = "Black";
ctx.globalAlpha = 1-(hs-alphaZero) * 2;
//==============================================================
ctx.fillRect(0,0,w,h);
// the amount to expand canvas feedback
var sx = (hyperSpeed-1) * cw * 0.1;
var sy = (hyperSpeed-1) * ch * 0.1;
// increase alpha as speed increases
ctx.globalAlpha = (hs-alphaZero)*2;
ctx.globalCompositeOperation = "lighter";
// draws feedback twice
ctx.drawImage(canvas,-sx, -sy, w + sx*2 , h + sy*2)
ctx.drawImage(canvas,-sx/2, -sy/2, w + sx , h + sy)
ctx.globalCompositeOperation = "source-over";
// add stars if count < maxStarCount
if(points.getCount() < maxStarCount){
var cent = (hyperSpeed - 1) *0.5; // pulls stars to center as speed increases
doFor(10,()=>{
p.x = rand(cw * cent ,w - cw * cent); // random screen position
p.y = rand(ch * cent,h - ch * cent);
spawnPoint(p)
})
}
// as speed increases make lines thicker
ctx.lineWidth = 2 + hs*2;
ctx.lineCap = "round";
points.update(); // update points
points.draw(); // draw points
ctx.globalAlpha = 1;
// scroll the perspective star wars text FX
var scrollTime = (time - startTime) / 5 - 2312;
if(scrollTime < 1024){
starWarIntro(textImage,cw - h * 0.5, h * 0.2, cw - h * 3, h , scrollTime );
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas { position : absolute; top : 0px; left : 0px; }
<canvas id="canvas"></canvas>
Here's another simple example, based mainly on the same idea as Blindman67, concetric lines moving away from center at different velocities (the farther from center, the faster it moves..) also no recycling pool here.
"use strict"
var c = document.createElement("canvas");
document.body.append(c);
var ctx = c.getContext("2d");
var w = window.innerWidth;
var h = window.innerHeight;
var ox = w / 2;
var oy = h / 2;
c.width = w; c.height = h;
const stars = 120;
const speed = 0.5;
const trailLength = 90;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "#fff"
ctx.fillRect(ox, oy, 1, 1);
init();
function init() {
var X = [];
var Y = [];
for(var i = 0; i < stars; i++) {
var x = Math.random() * w;
var y = Math.random() * h;
X.push( translateX(x) );
Y.push( translateY(y) );
}
drawTrails(X, Y)
}
function translateX(x) {
return x - ox;
}
function translateY(y) {
return oy - y;
}
function getDistance(x, y) {
return Math.sqrt(x * x + y * y);
}
function getLineEquation(x, y) {
return function(n) {
return y / x * n;
}
}
function drawTrails(X, Y) {
var count = 1;
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, w, h);
function anim() {
for(var i = 0; i < X.length; i++) {
var x = X[i];
var y = Y[i];
drawNextPoint(x, y, count);
}
count+= speed;
if(count < trailLength) {
window.requestAnimationFrame(anim);
}
else {
init();
}
}
anim();
}
function drawNextPoint(x, y, step) {
ctx.fillStyle = "#fff";
var f = getLineEquation(x, y);
var coef = Math.abs(x) / 100;
var dist = getDistance( x, y);
var sp = speed * dist / 100;
for(var i = 0; i < sp; i++) {
var newX = x + Math.sign(x) * (step + i) * coef;
var newY = translateY( f(newX) );
ctx.fillRect(newX + ox, newY, 1, 1);
}
}
body {
overflow: hidden;
}
canvas {
position: absolute;
left: 0;
top: 0;
}
Im currently working on a small HTML5 game. Now I am busy with making the small car shrink in size when it reaches the horizon (on the road) and go back to original size when it drives back towards 'us'.
I am pretty new to javascript, that is why I started making this small game, I figured what better way to learn, right?
So this is what I have (within a canvas):
var render = function () {
if (bgReady) {
ctx.drawImage(bgImage, 0, 0);
}
if (carReady) {
//ctx.drawImage(carImage, car.x, car.y);
ctx.drawImage(carImage, car.x, car.y, (carImage.width /100 * 50),(carImage.height / 100 * 50));
}
};
What I want to say is, that the size only should be altered when car.y gets smaller. However I am not really sure about where to place it, in order to not get a chronological-error.
What I am trying to get is:
ctx.drawImage(carImage, car.x, car.y, (carImage.width /100 * 50),(carImage.height / 100 * 50));
if(car.y++){
car.Image.width /i * 50 && carImage.height /i * 50}
With the variable 'i' increasing as car.y increases.
I hope someone can point me into the right direction.
Mieer
EDIT:
// Handle keyboard controls
var keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
addEventListener("keyup", function (e) {
delete keysDown[e.keyCode];
}, false);
// Reset the game when the player catches a fuel
var reset = function () {
car.x = canvas.width / 3;
car.y = canvas.height / 1.2;
// Throw the fuel somewhere on the screen randomly [lengte(math random) keer breedte voor een random plaat in de oppervlakte)]
//fuel.x = 300 + (Math.random() * (canvas.width - 700)); -old
fuel.y = 500 + (Math.random() * (canvas.height - 550));
//The left-most x coordinate for each y coordinate,
//The road-width for each y coordinate.
var left_most_x = 10 + ((500-10)/1000) * fuel.y
var road_width = 500 - ((500-30)/1000) * fuel.y
fuel.x = left_most_x + (Math.random() * road_width)
};
// Update game objects
var update = function (modifier) {
if (38 in keysDown) { // Player holding up
car.y -= car.speed * modifier;
}
if (40 in keysDown) { // Player holding down
car.y += car.speed * modifier;
}
if (37 in keysDown) { // Player holding left
car.x -= car.speed * modifier;
}
if (39 in keysDown) { // Player holding right
car.x += car.speed * modifier;
}
// Are they touching?
if (
car.x <= (fuel.x + 32)
&& fuel.x <= (car.x + 32)
&& car.y <= (fuel.y + 32)
&& fuel.y <= (car.y + 32)
) {
++fuelsCaught;
reset();
}
};
Try keeping track of the last y value:
var lastY = 0;
var render = function () {
if (bgReady)
ctx.drawImage(bgImage, 0, 0);
if (carReady)
ctx.drawImage(carImage, car.x, car.y, (carImage.width / 100 * 50), (carImage.height / 100 * 50));
var deltaY = car.y - lastY;
carImage.width = carImage.width - deltaY;
carImage.height = carImage.height - deltaY;
lastY = car.y;
};
I need som help on moving the elements in the array I created call the "Predator" array. Now I have all the images appearing on the SVG box as well as stored in the array.
I have difficulties calling them in a "for" loop and to move them randomly. Further that, I would want to animate the images more closely to real life. Example pls take a look on this website.
<script>
var startPredator = 20; //number of fish to start with
var Predator = []; //array of fish
var predatorW = 307; //fish width
var predatorH = 313; //fish height
var velocity = 100; //base velocity
var imageStrip; //predator image strip
//var backgroundImage; //background image
//var backgroundImageW = 981; //background image width
//var backgroundImageH = 767; //background image height
//var WIDTH = document.body.offsetWidth;
//var HEIGHT = document.body.offsetHeight;
function animate(){
document.animation.src = arraylist[imgNum].src
imgNum++
for (var i=arraylist[].length; i--;){
arraylist[i].move();
}
setInterval(function () { draw(); }, 16.7);
}
function start() {
for (var i = 0; i < arraylist.length; i++) {
var image = document.createElement("img");
image.alt = images[i][0];
image.width = "150";
image.height = "150";
image.src = images[i][1];
var j = i;
if (j > 2) j = i - 3;
document.getElementsByTagName("div")[j].appendChild(image);
}
}
function Predator() {
var angle = Math.PI * 2 * Math.random(); //set the x,y direction this predator swims
var xAngle = Math.cos(angle); //set the x value of the angle
var yAngle = Math.sin(angle); //set the y value of the angle
var zAngle = 1+-2*Math.round(Math.random()); //set if the predator is swimming toward us or away. 1 = toward us; -1 = away from us
var x = Math.floor(Math.random() * (WIDTH - predatorW) + predatorW / 2); //set the starting x location
var y = Math.floor(Math.random() * (HEIGHT - predatorH) + predatorH / 2); //set the starting y location
var zFar = 100; //set how far away can a predator go
var zFarFactor = 1; //set the max size the predator can be. 1=100%
var zClose = 0; //set how near a predator can come
var z = Math.floor(Math.random() * ((zFar - zClose))); //set the starting z location
var scale = .1; //set the rate of scaling each frame
var flip = 1; //set the direction of the fish. 1=right; -1=left
var cellCount = 16; //set the number of cells (columns) in the image strip animation
var cell = Math.floor(Math.random() * (cellCount-1)); //set the first cell (columns) of the image strip animation
var cellReverse = -1; //set which direction we go through the image strip
var species = Math.floor(Math.random() * 3); //set which species of predator this predator is. each species is a row in the image strip
// stop predator from swimming straight up or down
if (angle > Math.PI * 4 / 3 && angle < Math.PI * 5 / 3 || angle > Math.PI * 1 / 3 && angle < Math.PI * 2 / 3) {
angle = Math.PI * 1 / 3 * Math.random();
xAngle = Math.cos(angle);
yAngle = Math.sin(angle);
}
// face the predator the right way if angle is between 6 o'clock and 12 o'clock
if (angle > Math.PI / 2 && angle < Math.PI / 2 * 3) {
flip = -1;
}
function swim() {
// Calculate next position of species
var nextX = x + xAngle * velocity * fpsMeter.timeDeltaS;
var nextY = y + yAngle * velocity * fpsMeter.timeDeltaS;
var nextZ = z + zAngle * .1 * velocity * fpsMeter.timeDeltaS;
var nextScale = Math.abs(nextZ) / (zFar - zClose);
// If species is going to move off right side of screen
if (nextX + fishW / 2 * scale > WIDTH) {
// If angle is between 3 o'clock and 6 o'clock
if ((angle >= 0 && angle < Math.PI / 2)) {
angle = Math.PI - angle;
xAngle = Math.cos(angle);
yAngle = Math.sin(angle) * Math.random();
flip = -flip;
}
// If angle is between 12 o'clock and 3 o'clock
else if (angle > Math.PI / 2 * 3) {
angle = angle - (angle - Math.PI / 2 * 3) * 2
xAngle = Math.cos(angle);
yAngle = Math.sin(angle) * Math.random();
flip = -flip;
}
}
// If fish is going to move off left side of screen
if (nextX - fishW / 2 * scale < 0) {
// If angle is between 6 o'clock and 9 o'clock
if ((angle > Math.PI / 2 && angle < Math.PI)) {
angle = Math.PI - angle;
xAngle = Math.cos(angle);
yAngle = Math.sin(angle) * Math.random();
flip = -flip;
}
// If angle is between 9 o'clock and 12 o'clock
else if (angle > Math.PI && angle < Math.PI / 2 * 3) {
angle = angle + (Math.PI / 2 * 3 - angle) * 2
xAngle = Math.cos(angle);
yAngle = Math.sin(angle) * Math.random();
flip = -flip;
}
}
// If fish is going to move off bottom side of screen
if (nextY + fishH / 2 * scale > HEIGHT) {
// If angle is between 3 o'clock and 9 o'clock
if ((angle > 0 && angle < Math.PI)) {
angle = Math.PI * 2 - angle;
xAngle = Math.cos(angle);
yAngle = Math.sin(angle) * Math.random();
}
}
// If fish is going to move off top side of screen
if (nextY - fishH / 2 * scale < 0) {
// If angle is between 9 o'clock and 3 o'clock
if ((angle > Math.PI && angle < Math.PI * 2)) {
angle = angle - (angle - Math.PI) * 2;
xAngle = Math.cos(angle);
yAngle = Math.sin(angle);
}
}
// If fish is going too far (getting too small)
if (nextZ <= zClose && zAngle < 0) {
zAngle = -zAngle;
}
// If fish is getting to close (getting too large)
if (((WIDTH / fishW) * 10) < ((fishW * fish.length) / WIDTH)) {
zFarFactor = .3
}
else if (((WIDTH / fishW) * 2) < ((fishW * fish.length) / WIDTH)) {
zFarFactor = .5
}
else { zFarFactor = 1 }
if (nextZ >= zFar * zFarFactor && zAngle > 0) {
zAngle = -zAngle;
}
if (scale < .1) { scale = .1 }; //don't let fish get too tiny
//draw the fish
//locate the fish
ctx.save();
ctx.translate(x, y);
ctx.scale(scale, scale); // make the fish bigger or smaller depending on how far away it is.
ctx.transform(flip, 0, 0, 1, 0, 0); //make the fish face the way he's swimming.
ctx.drawImage(imageStrip, fishW * cell, fishH * species, fishW, fishH, -fishW / 2, -fishH / 2, fishW, fishH); //draw the fish
ctx.save();
scale = nextScale // increment scale for next time
ctx.restore();
ctx.restore();
//increment to next state
x = nextX;
y = nextY;
z = nextZ;
if (cell >= cellCount-1 || cell <= 0) { cellReverse = cellReverse * -1; } //go through each cell in the animation
cell = cell + 1 * cellReverse; //go back down once we hit the end of the animation
}
return {
swim: swim
}
}
</script>
My concern is more on the animate and start function. Am I calling the images from the array to move?
Please offer a helping hand. Thanks