UNPKG

d3-force-surface

Version:

A multi-surface elastic collision force type for the d3-force simulation engine.

173 lines (159 loc) 5.43 kB
function constant (x) { return function () { return x; }; } function surface () { var nodes, surfaces = [], elasticity = 1, // 0 <= number <= 1 radius = function radius(node) { return 1; }, // accessor: number > 0 from = function from(surface) { return surface.from; }, // accessor: { x, y } to = function to(surface) { return surface.to; }, // accessor: { x, y } oneWay = function oneWay(surface) { return !!surface.oneWay; }, // accessor: boolean onImpact; // (node, surface) callback function force() { nodes.forEach(function (node) { var nodeRadius = radius(node), velVect = cart2Polar(node.vx, node.vy), lineToFuture = { from: { x: node.x, y: node.y }, to: { x: node.x + node.vx, y: node.y + node.vy } }; surfaces.forEach(function (surface) { var surfLine = { from: from(surface), to: to(surface) }, surfVect = cart2Polar(surfLine.to.x - surfLine.from.x, surfLine.to.y - surfLine.from.y), inReverse = normalizeAngle(surfVect.a - velVect.a) < Math.PI; // true if the node is travelling in a direction of S>N relative to the from(W)>to(E) axis if (inReverse && oneWay(surface)) return; // Don't collide in reverse direction var nodeLine = translateLn(lineToFuture, polar2Cart(nodeRadius, surfVect.a + Math.PI / 2 * (inReverse ? -1 : 1)) // node radius offset facing the surface ), nodeBisect = { from: translatePnt(node, polar2Cart(nodeRadius, surfVect.a - Math.PI / 2)), to: translatePnt(node, polar2Cart(nodeRadius, surfVect.a + Math.PI / 2)) }, overlaps = intersect(surfLine, nodeBisect); // Whether the node is already overlapping with the surface if (!overlaps && !intersect(surfLine, nodeLine)) return; // No collision var velRel = polar2Cart(velVect.d, velVect.a - surfVect.a); // x is the velocity parallel to the surface, y is perpendicular // Invert velocity and absorb shock (elasticity) along the surface perpendicular velRel.y *= -elasticity; // Convert back to original plane var _rotatePnt = rotatePnt(velRel, surfVect.a); node.vx = _rotatePnt.x; node.vy = _rotatePnt.y; if (overlaps && !oneWay(surface)) { // In two-way mode, need to move node out of the way to prevent trapping var nudgeOffset = polar2Cart(rotatePnt(translatePnt(nodeLine.from, { x: -surfLine.from.x, y: -surfLine.from.y }), -surfVect.a).y, // How much overlap along the surface perpendicular surfVect.a - Math.PI / 2 // Backwards from surface, along its perpendicular ); node.x += nudgeOffset.x; node.y += nudgeOffset.y; } onImpact && onImpact(node, surface); }); }); // function intersect(la, lb) { return turn(la.from, lb.from, lb.to) != turn(la.to, lb.from, lb.to) && turn(la.from, la.to, lb.from) != turn(la.from, la.to, lb.to); function turn(p1, p2, p3) { var a = (p3.y - p1.y) * (p2.x - p1.x), b = (p2.y - p1.y) * (p3.x - p1.x); return a > b + Number.EPSILON ? 1 : a + Number.EPSILON < b ? -1 : 0; } } function translatePnt(_ref, _ref2) { var x = _ref.x, y = _ref.y; var trX = _ref2.x, trY = _ref2.y; return { x: x + trX, y: y + trY }; } function translateLn(_ref3, tr) { var from = _ref3.from, to = _ref3.to; return { from: translatePnt(from, tr), to: translatePnt(to, tr) }; } function rotatePnt(_ref4, a) { var x = _ref4.x, y = _ref4.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) }; } function normalizeAngle(a) { var PI2 = Math.PI * 2; while (a < 0) a += PI2; while (a > PI2) a -= PI2; return a; } } force.initialize = function (_) { nodes = _; }; force.surfaces = function (_) { return arguments.length ? (surfaces = _, force) : surfaces; }; force.elasticity = function (_) { return arguments.length ? (elasticity = _, force) : elasticity; }; force.radius = function (_) { return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), force) : radius; }; force.from = function (_) { return arguments.length ? (from = _, force) : from; }; force.to = function (_) { return arguments.length ? (to = _, force) : to; }; force.oneWay = function (_) { return arguments.length ? (oneWay = typeof _ === "function" ? _ : constant(!!_), force) : oneWay; }; force.onImpact = function (_) { return arguments.length ? (onImpact = _, force) : onImpact; }; return force; } export { surface as default };