block by tophtucker 16fbd7e7c6274ed329111cbe139a6bb6

Elastic collisions

Full Screen

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:

index.html

<!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>

forceCollideElastic.js

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;
}