Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active September 6, 2016 04:56
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tophtucker/16fbd7e7c6274ed329111cbe139a6bb6 to your computer and use it in GitHub Desktop.
Elastic collisions
license: mit

Adapting d3.forceCollide to do elastic collisions (like billiards). You can click and drag the balls.

High school physics refresher from Wikipedia. Inspired by chatting with Robert Monfera and Chris Given's riffing on d3.forceCollide.

To-do:

  • Make it a proper es6 module
  • Log net momentum and kinetic energy for debugging
  • Balls can tunnel through each other or spawn atop each other, which somehow leads to non-conservation of momentum
  • In any case, momentum won't be conserved here because of the wall-bounces & dragging. A momentum-conserving finite-area alternative to walls would be to make the window a torus, I think, right?
  • Generalize to other shapes?
  • Relativistic! Lol.
  • Actual billiards game; then add gravitational forces and other twists...
var constant = function(x) {
return function() {
return x;
};
}
var jiggle = function() {
return (Math.random() - 0.5) * 1e-6;
}
function x(d) {
return d.x + d.vx;
}
function y(d) {
return d.y + d.vy;
}
function forceCollideElastic(radius) {
var nodes,
radii,
masses,
strength = 1,
iterations = 1;
if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);
function force() {
var i, n = nodes.length,
tree,
node,
xi,
yi,
ri,
ri2;
for (var k = 0; k < iterations; ++k) {
tree = d3.quadtree(nodes, x, y).visitAfter(prepare);
for (i = 0; i < n; ++i) {
node = nodes[i];
ri = radii[i], ri2 = ri * ri;
xi = node.x + node.vx;
yi = node.y + node.vy;
tree.visit(apply);
}
}
function apply(quad, x0, y0, x1, y1) {
var data = quad.data, rj = quad.r, r = ri + rj;
if (data) {
if (data.index > i) {
var x = xi - data.x - data.vx,
y = yi - data.y - data.vy,
l = x * x + y * y;
if (l < r * r) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
console.log('Collide!');
var π = Math.PI,
x1 = node.x,
y1 = node.y,
x2 = data.x,
y2 = data.y,
m1 = masses[i],
m2 = masses[data.index],
v1x = node.vx,
v1y = node.vy,
v2x = data.vx,
v2y = data.vy,
v1 = Math.sqrt(Math.pow(v1x,2) + Math.pow(v1y,2)),
v2 = Math.sqrt(Math.pow(v2x,2) + Math.pow(v2y,2));
// get contact angle
var φ = Math.atan2(y2-y1, x2-x1);
// get movement angles
var θ1 = Math.atan2(v1y, v1x);
var θ2 = Math.atan2(v2y, v2x);
var v1x_new =
( v1 * Math.cos(θ1 - φ) * (m1 - m2) +
2 * m2 * v2 * Math.cos(θ2 - φ) ) /
( m1 + m2 ) *
Math.cos(φ) +
v1 * Math.sin(θ1 - φ) * Math.cos(φ + π/2);
var v1y_new =
( v1 * Math.cos(θ1 - φ) * (m1 - m2) +
2 * m2 * v2 * Math.cos(θ2 - φ) ) /
( m1 + m2 ) *
Math.sin(φ) +
v1 * Math.sin(θ1 - φ) * Math.cos(φ + π/2);
var v2x_new =
( v2 * Math.cos(θ2 - φ) * (m2 - m1) +
2 * m1 * v1 * Math.cos(θ1 - φ) ) /
( m2 + m1 ) *
Math.cos(φ) +
v2 * Math.sin(θ2 - φ) * Math.cos(φ + π/2);
var v2y_new =
( v2 * Math.cos(θ2 - φ) * (m2 - m1) +
2 * m1 * v1 * Math.cos(θ1 - φ) ) /
( m2 + m1 ) *
Math.sin(φ) +
v2 * Math.sin(θ2 - φ) * Math.cos(φ + π/2);
node.vx = v1x_new;
node.vy = v1y_new;
data.vx = v2x_new;
data.vy = v2y_new;
// l = (r - (l = Math.sqrt(l))) / l * strength;
// node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj));
// node.vy += (y *= l) * r;
// data.vx -= x * (r = 1 - r);
// data.vy -= y * r;
}
}
return;
}
return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
}
}
function prepare(quad) {
if (quad.data) return quad.r = radii[quad.data.index];
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r;
}
}
}
force.initialize = function(_) {
var i, n = (nodes = _).length; radii = new Array(n); masses = new Array(n);
for (i = 0; i < n; ++i) {
radii[i] = +radius(nodes[i], i, nodes);
masses[i] = Math.PI * Math.pow(radii[i],2);
}
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};
force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), force) : radius;
};
return force;
}
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="forceCollideElastic.js"></script>
<script>
var width = 960
var height = 500
var numParticles = 4
var color = function() { return '#'+Math.floor(Math.random()*16777215).toString(16); }
var nodes = Array.apply(null, Array(numParticles)).map(function (_, i) {
var r = Math.random() * 60 + 20
var velocity = Math.random() * 2 + 1
var angle = Math.random() * 360
return {
x: Math.random() * (width - r),
y: Math.random() * (height - r),
vx: velocity * Math.cos(angle * Math.PI / 180),
vy: velocity * Math.sin(angle * Math.PI / 180),
r: r,
fill: color(i)
}
})
var drag = d3.drag()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
var svg = d3.select('body').append('svg')
var ball = svg
.attr('width', width)
.attr('height', height)
.selectAll('circle')
.data(nodes)
.enter().append('circle')
.style('fill', function (d) { return d.fill })
.attr('r', function (d) { return d.r })
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
.call(drag)
var collisionForce = forceCollideElastic()
.radius(function (d) { return d.r })
var boxForce = boundedBox()
.bounds([[0, 0], [width, height]])
.size(function (d) { return d.r })
d3.forceSimulation()
.velocityDecay(0)
.alphaTarget(1)
.on('tick', ticked)
.force('box', boxForce)
.force('collision', collisionForce)
.nodes(nodes)
function boundedBox() {
var nodes, sizes
var bounds
var size = constant(0)
function force() {
var node, size
var xi, x0, x1, yi, y0, y1
var i = -1
while (++i < nodes.length) {
node = nodes[i]
size = sizes[i]
xi = node.x + node.vx
x0 = bounds[0][0] - (xi - size)
x1 = bounds[1][0] - (xi + size)
yi = node.y + node.vy
y0 = bounds[0][1] - (yi - size)
y1 = bounds[1][1] - (yi + size)
if (x0 > 0 || x1 < 0) {
node.x += node.vx
node.vx = -node.vx
if (node.vx < x0) { node.x += x0 - node.vx }
if (node.vx > x1) { node.x += x1 - node.vx }
}
if (y0 > 0 || y1 < 0) {
node.y += node.vy
node.vy = -node.vy
if (node.vy < y0) { node.vy += y0 - node.vy }
if (node.vy > y1) { node.vy += y1 - node.vy }
}
}
}
force.initialize = function (_) {
sizes = (nodes = _).map(size)
}
force.bounds = function (_) {
return (arguments.length ? (bounds = _, force) : bounds)
}
force.size = function (_) {
return (arguments.length
? (size = typeof _ === 'function' ? _ : constant(_), force)
: size)
}
return force
}
function ticked() {
ball
.attr('cx', function (d) { return d.x })
.attr('cy', function (d) { return d.y })
}
var px, py, vx, vy, offsetX, offsetY
function dragStarted(d) {
vx = 0
vy = 0
offsetX = (px = d3.event.x) - (d.fx = d.x)
offsetY = (py = d3.event.y) - (d.fy = d.y)
}
function dragged(d) {
d.vx = vx = d3.event.x - px
d.vy = vy = d3.event.y - py
d.fx = Math.max(Math.min((px = d3.event.x) - offsetX, width - d.r), d.r)
d.fy = Math.max(Math.min((py = d3.event.y) - offsetY, height - d.r), d.r)
}
function dragEnded(d) {
d.fx = null
d.fy = null
d.vx = vx;
d.vy = vy;
}
function constant(_) {
return function () { return _ }
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment