d3-force-bounce
Version:
An elastic collision force type for the d3-force simulation engine.
146 lines (133 loc) • 4.76 kB
JavaScript
import { quadtree } from 'd3-quadtree';
function constant (x) {
return function () {
return x;
};
}
function bounce () {
var nodes,
elasticity = 1,
// 0 <= number <= 1
radius = function radius(node) {
return 1;
},
// accessor: number > 0
mass = function mass(node) {
return Math.pow(radius(node), 2);
},
// accessor: number > 0 (Mass proportional to area by default)
onImpact; // (node, node) callback
function force() {
var tree = quadtree(nodes, function (d) {
return d.x;
}, function (d) {
return d.y;
}).visitAfter(function (quad) {
if (quad.data) return quad.r = radius(quad.data);
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r; // Store largest radius per tree node
}
}
});
nodes.forEach(function (a) {
var ra = radius(a);
tree.visit(function (quad, x0, y0, x1, y1) {
var b = quad.data,
rb = quad.r,
minDistance = ra + rb;
if (b) {
// Leaf tree node
if (b.index > a.index) {
// Prevent visiting same node pair more than once
if (a.x === b.x && a.y === b.y) {
// Totally overlap > jiggle b
var jiggleVect = polar2Cart(1e-6, Math.random() * 2 * Math.PI);
b.x += jiggleVect.x;
b.y += jiggleVect.y;
}
var impactVect = cart2Polar(b.x - a.x, b.y - a.y),
// Impact vector from a > b
overlap = Math.max(0, minDistance - impactVect.d);
if (!overlap) return; // No collision
var vaRel = rotatePnt({
x: a.vx,
y: a.vy
}, -impactVect.a),
// x is the velocity along the impact line, y is tangential
vbRel = rotatePnt({
x: b.vx,
y: b.vy
}, -impactVect.a);
// Transfer velocities along the direct line of impact (tangential remain the same)
var _getAfterImpactVeloci = getAfterImpactVelocities(mass(a), mass(b), vaRel.x, vbRel.x, elasticity);
vaRel.x = _getAfterImpactVeloci.a;
vbRel.x = _getAfterImpactVeloci.b;
var _rotatePnt = rotatePnt(vaRel, impactVect.a);
a.vx = _rotatePnt.x;
a.vy = _rotatePnt.y;
var _rotatePnt2 = rotatePnt(vbRel, impactVect.a);
b.vx = _rotatePnt2.x;
b.vy = _rotatePnt2.y;
// Split them apart
var nudge = polar2Cart(overlap, impactVect.a),
nudgeBias = ra / (ra + rb); // Weight of nudge to apply to B
a.x -= nudge.x * (1 - nudgeBias);
a.y -= nudge.y * (1 - nudgeBias);
b.x += nudge.x * nudgeBias;
b.y += nudge.y * nudgeBias;
onImpact && onImpact(a, b);
}
return;
}
// Only keep traversing sub-tree quadrants if radius overlaps
return x0 > a.x + minDistance || x1 < a.x - minDistance || y0 > a.y + minDistance || y1 < a.y - minDistance;
});
});
//
function getAfterImpactVelocities(ma, mb, va, vb) {
var elasticity = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1;
// Apply momentum conservation equation with coefficient of restitution (elasticity)
return {
a: (elasticity * mb * (vb - va) + ma * va + mb * vb) / (ma + mb),
b: (elasticity * ma * (va - vb) + ma * va + mb * vb) / (ma + mb)
};
}
function rotatePnt(_ref, a) {
var x = _ref.x,
y = _ref.y;
var vect = cart2Polar(x, y);
return polar2Cart(vect.d, vect.a + a);
}
function cart2Polar(x, y) {
x = x || 0; // Normalize -0 to 0 to avoid -Infinity issues in atan
return {
d: Math.sqrt(x * x + y * y),
a: x === 0 && y === 0 ? 0 : Math.atan(y / x) + (x < 0 ? Math.PI : 0) // Add PI for coords in 2nd & 3rd quadrants
};
}
function polar2Cart(d, a) {
return {
x: d * Math.cos(a),
y: d * Math.sin(a)
};
}
}
force.initialize = function (_) {
nodes = _;
};
force.elasticity = function (_) {
return arguments.length ? (elasticity = _, force) : elasticity;
};
force.radius = function (_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), force) : radius;
};
force.mass = function (_) {
return arguments.length ? (mass = typeof _ === "function" ? _ : constant(+_), force) : mass;
};
force.onImpact = function (_) {
return arguments.length ? (onImpact = _, force) : onImpact;
};
return force;
}
export { bounce as default };