Public
Edited
Jun 10, 2021
4 forks
6 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
simulation = {
reset;
const simulation = new Simulation(width, 400);

let initial = true;

// do ... while is cool as hell: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/do...while
do {
if (addTracker && initial) {
simulation.addBall(radius, "red");
initial = false;
} else {
simulation.addBall(radius);
}
} while (simulation.balls.length < count);

return simulation;
}
Insert cell
class Simulation {
constructor(width, height) {
this.width = width;
this.height = height;
this.balls = [];
}

addBall(radius, fill = "#444444") {
const diameter = radius * 2;

const minX = diameter;
const minY = diameter;
const maxX = this.width - diameter;
const maxY = this.height - diameter;

const x = Math.random() * (maxX - minX) + minX;
const y = Math.random() * (maxY - minY) + minY;
const direction = Math.random() * Math.PI * 2;

this.balls.push(new Ball(x, y, radius, direction, ballSpeed, fill));
// }
}

update() {
for (const ball of this.balls) {
ball.move();
}
}

testForWalls() {
for (const ball of this.balls) {
// too far right
if (ball.x + ball.radius > this.width) {
ball.dx *= -1;
ball.x = this.width - ball.radius;
continue;
}

// too far left
if (ball.x - ball.radius < 0) {
ball.dx *= -1;
ball.x = ball.radius;
continue;
}

// too far down
if (ball.y + ball.radius > this.height) {
ball.dy *= -1;
ball.y = this.height - ball.radius;
continue;
}

// too far up
if (ball.y - ball.radius < 0) {
ball.dy *= -1;
ball.y = ball.radius;
}
}
}

checkForCollisionsIndexed() {
// create our new index
const index = new Flatbush(this.balls.length);

// add them
for (const ball of this.balls) {
index.add(...ball.bbox);
}

// prep the index
index.finish();

for (const first of this.balls) {
const collisions = index.search(...first.bbox);

for (const idx of collisions) {
const second = this.balls[idx];

if (first === second) continue;

if (first.isCollidingWith(second)) {
this.collide(first, second);
}
}
}
}

checkForCollisions() {
// this could be much more efficient, but you need ~1,000+ balls for it
// to start to matter, so I kept it simple
for (const first of this.balls) {
for (const second of this.balls) {
// skip if same ball
if (first === second) continue;
if (first.isCollidingWith(second)) {
this.collide(first, second);
}
}
}
}

collide(first, second) {
// first we determine if they're moving toward each other
// this allows overlaps going in opposite directions to "resolve"
// and not stick to each other
const dx = first.x - second.x;
const dy = first.y - second.y;
const vx = second.dx - first.dx;
const vy = second.dy - first.dy;
const dot = dx * vx + dy * vy;

// if going toward each other, bounce 'em
if (dot > 0) {
const cr = first.mass + second.mass;
const dr = first.mass - second.mass;

// determine our new velocities
const vx1 = (first.dx * dr + 2 * second.mass * second.dx) / cr;
const vy1 = (first.dy * dr + 2 * second.mass * second.dy) / cr;
const vx2 = (second.dx * -dr + 2 * first.mass * first.dx) / cr;
const vy2 = (second.dy * -dr + 2 * first.mass * first.dy) / cr;

// set them
first.dx = vx1;
first.dy = vy1;
second.dx = vx2;
second.dy = vy2;

// trigger a move for both so they stop overlapping immediately
first.move();
second.move();
}
}

render(context) {
for (const ball of this.balls) {
context.beginPath();
context.fillStyle = ball.fill;
context.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
context.fill();
context.closePath();
}
}
}
Insert cell
class Ball {
constructor(x, y, radius, direction, speed, fill, state = {}) {
this.x = x;
this.y = y;
this.radius = radius;
this.mass = radius;
this.direction = direction;
this.speed = speed;
this.dx = this.speed * Math.cos(this.direction);
this.dy = this.speed * Math.sin(this.direction);
this.fill = fill;
this.state = state;
}

move() {
this.x += this.dx;
this.y += this.dy;
}

isCollidingWith(ball) {
const dx = this.x - ball.x;
const dy = this.y - ball.y;
const dr = this.radius + ball.radius;
// don't use the square root because it's 1) slower, and 2) not needed for collision detection
const squaredDistance = Math.pow(dx, 2) + Math.pow(dy, 2);

return squaredDistance < Math.pow(dr, 2);
}

get bbox() {
return [
this.x - this.radius,
this.y - this.radius,
this.x + this.radius,
this.y + this.radius
];
}
}
Insert cell
Flatbush = require("flatbush@3")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more