Last active
September 7, 2015 07:37
-
-
Save joar/5f47e3cefc8daa6fcdf8 to your computer and use it in GitHub Desktop.
D3 rectangular collisions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const RIGHT = 0, | |
| DOWN = 1.5707963267948966, | |
| LEFT = Math.PI, | |
| UP = -1.5707963267948966 | |
| let DIRECTION_MAP = {} | |
| DIRECTION_MAP[UP] = 'UP' | |
| DIRECTION_MAP[DOWN] = 'DOWN' | |
| DIRECTION_MAP[LEFT] = 'LEFT' | |
| DIRECTION_MAP[RIGHT] = 'RIGHT' | |
| // Visualization elements | |
| var svg, | |
| node, | |
| point, | |
| nodeCenter, | |
| pointCenter, | |
| nd, | |
| pd, | |
| triangle, | |
| collisionIndicator | |
| var drag; | |
| var firstTick = true, | |
| DRAGGING = false; | |
| let colorIndex = 0, | |
| colorStep = 40 | |
| // Rendering state | |
| var on, op, // Last tick node and point positions | |
| collision | |
| let n = { | |
| id: 0, | |
| x: 400 + 10, | |
| y: 200 + 5, | |
| width: 200, | |
| height: 40 | |
| }, | |
| p = { | |
| id: 1, | |
| x: 400 + 0, | |
| y: 200 + 0, | |
| width: 200, | |
| height: 40 | |
| } | |
| function init() { | |
| drag = d3.behavior.drag() | |
| drag.on('dragstart', (d, x, y) => { | |
| }) | |
| drag.on('drag', function (d) { | |
| d.x += d3.event.dx | |
| d.y += d3.event.dy | |
| }) | |
| svg = d3.select('#collide') | |
| .append('svg') | |
| .attr('viewBox', '0 0 1000 1000') | |
| .attr('preserveAspectRatio', 'xMidYMid meet') | |
| node = svg.append('rect') | |
| .attr('id', 'node') | |
| .style('fill', c()) | |
| node.datum(n) | |
| .call(drag) | |
| point = svg.append('rect') | |
| .attr('id', 'point') | |
| .style('fill', c()) | |
| point.datum(p) | |
| .call(drag) | |
| let cr = 1, | |
| sw = .5 | |
| pointCenter = svg.append('circle') | |
| .attr('fill', c()) | |
| .attr('stroke', 'black') | |
| .attr('stroke-width', sw) | |
| .attr('r', cr) | |
| nodeCenter = svg.append('circle') | |
| .attr('fill', c()) | |
| .attr('stroke', 'black') | |
| .attr('stroke-width', sw) | |
| .attr('r', cr) | |
| // Point indicator | |
| let pic = c() | |
| pd = svg.append('line') | |
| .attr('marker-end', makeMarker('point', pic)) | |
| .attr('stroke', pic) | |
| .attr('stroke-width', 7) | |
| // Node indicator | |
| let nic = c() | |
| nd = svg.append('line') | |
| .attr('marker-end', makeMarker('point', nic)) | |
| .attr('stroke', nic) | |
| .attr('stroke-width', 7) | |
| collisionIndicator = svg.append('line') | |
| .attr('stroke-width', 2) | |
| .attr('stroke', c()) | |
| triangle = svg.append('path') | |
| .attr('stroke', c()) | |
| .attr('fill', 'none') | |
| d3.timer(render) | |
| window.addEventListener('keydown', e => { | |
| switch (String.fromCharCode(e.keyCode)) { | |
| case ' ': | |
| e.preventDefault(); | |
| tick(); | |
| break; | |
| } | |
| }) | |
| } | |
| function tick() { | |
| on = cloneCoords(n) | |
| op = cloneCoords(p) | |
| collide2(n, p) | |
| render() | |
| } | |
| function render() { | |
| renderNode(node, n) | |
| renderNode(point, p) | |
| renderCenter(nodeCenter, n) | |
| renderCenter(pointCenter, p) | |
| renderTriangle(n, p) | |
| renderCollisionDirection(p) | |
| renderDelta(on, op) | |
| } | |
| function renderCollisionDirection(d) { | |
| if (collision !== undefined) { | |
| let isHoriz = [DOWN, UP].indexOf(collision) != -1, | |
| x1, y1, | |
| x2, y2 | |
| let yOffset = collision == DOWN ? d.height : 0 | |
| let xOffset = collision == RIGHT ? d.width : 0 | |
| let x2Offset = isHoriz ? d.width : 0 | |
| let y2Offset = !isHoriz ? d.height : 0 | |
| x1 = d.x + xOffset | |
| y1 = d.y + yOffset | |
| x2 = d.x + xOffset + x2Offset | |
| y2 = d.y + yOffset + y2Offset | |
| collisionIndicator.attr({x1, y1, x2, y2}) | |
| show(collisionIndicator) | |
| } else { | |
| hide(collisionIndicator) | |
| } | |
| } | |
| function renderTriangle(n, p) { | |
| let {tx, ty, th} = getCollisionTriangle(n, p) | |
| let s = getCenter(n) | |
| //console.log('tx, ty, th', tx, ty, th) | |
| triangle.attr('d', `M ${s.x} ${s.y} | |
| l ${tx} ${ty} | |
| l ${-tx} 0 | |
| z`) | |
| } | |
| function renderNode(em, d) { | |
| em.attr({ | |
| x: d.x, | |
| y: d.y, | |
| width: d.width, | |
| height: d.height | |
| }) | |
| } | |
| function renderCenter(em, d) { | |
| let c = getCenter(d) | |
| em.attr('cx', c.x) | |
| em.attr('cy', c.y) | |
| } | |
| function renderDelta(on, op) { | |
| let pc = getCenter(p), | |
| nc = getCenter(n) | |
| if (op && !hasSamePosition(op, p)) { | |
| pd.attr({ | |
| x1: pc.x, | |
| y1: pc.y, | |
| x2: pc.x + (p.x - op.x), | |
| y2: pc.y + (p.y - op.y) | |
| }) | |
| show(pd) | |
| } else { | |
| hide(pd) | |
| } | |
| if (op && !hasSamePosition(on, n)) { | |
| nd.attr({ | |
| x1: nc.x, | |
| y1: nc.y, | |
| x2: nc.x + (n.x - on.x), | |
| y2: nc.y + (n.y - on.y) | |
| }) | |
| show(nd) | |
| } else { | |
| hide(nd) | |
| } | |
| } | |
| /** | |
| * Check if two rectangles overlap and return the side of the rectangle that | |
| * overlaps. | |
| * @param a | |
| * @param b | |
| * @returns {*} The face of {a} that is hitting {b} in radians. 0 = right | |
| */ | |
| function overlap2(a, b) { | |
| let bCenter = getCenter(a), | |
| aCenter = getCenter(b), | |
| averangeWidth = (a.width + b.width) / 2, | |
| averageHeight = (a.height + b.height) / 2, | |
| deltaX = bCenter.x - aCenter.x, | |
| deltaY = bCenter.y - aCenter.y | |
| if (Math.abs(deltaX) <= averangeWidth && Math.abs(deltaY) <= averageHeight) { | |
| // Collision! | |
| let wy = averangeWidth * deltaY, | |
| hx = averageHeight * deltaX | |
| if (wy > hx) { | |
| if (wy > -hx) { | |
| return DOWN | |
| } else { | |
| return RIGHT | |
| } | |
| } else { | |
| if (wy > -hx) { | |
| return LEFT | |
| } else { | |
| return UP | |
| } | |
| } | |
| } | |
| } | |
| function getCollisionTriangle(n, p) { | |
| let nc = getCenter(n), | |
| pc = getCenter(p), | |
| tx, ty, th | |
| // Difference between the center of node, point. | |
| tx = pc.x - nc.x | |
| ty = pc.y - nc.y | |
| // The hypotenuse of the triangle, i.e. the distance between the center | |
| // of node, point. | |
| th = Math.hypot(tx, ty) | |
| return {tx, ty, th} | |
| } | |
| /** | |
| * New collision handler | |
| * @param n | |
| * @param p | |
| */ | |
| function collide2(n, p) { | |
| collision = overlap2(n, p) | |
| let averageWidth = (n.width + p.width) / 2, | |
| averageHeight = (n.height + p.height) / 2 | |
| console.log('Collision', DIRECTION_MAP[collision], collision) | |
| if (collision !== undefined) { | |
| let {tx, ty, th} = getCollisionTriangle(n, p) | |
| let toNode = angle(getCenter(p), getCenter(n)), | |
| toPoint = toNode + Math.PI / 2 | |
| let overlap | |
| if (collision == UP || collision == DOWN) { | |
| overlap = averageHeight - Math.abs(ty) | |
| } else { | |
| overlap = averageWidth - Math.abs(tx) | |
| } | |
| let nodePos = getPoint( | |
| n, | |
| collision, | |
| overlap / 2 | |
| ) | |
| let pointPos = getPoint( | |
| p, | |
| collision + Math.PI, | |
| overlap / 2 | |
| ) | |
| n.x = nodePos.x | |
| n.y = nodePos.y | |
| p.x = pointPos.x | |
| p.y = pointPos.y | |
| } | |
| } | |
| /** | |
| * Old collision handler | |
| * @param node | |
| * @param point | |
| * @returns {boolean} | |
| */ | |
| function collide(node, point) { | |
| let updated = false; | |
| let xDifference = node.x - point.x, | |
| yDifference = node.y - point.y, | |
| avgWidth = (point.width + node.width) / 2, | |
| avgHeight = (point.height + node.height) / 2, | |
| xAbsolute = Math.abs(xDifference), | |
| yAbsolute = Math.abs(yDifference) | |
| if (xAbsolute < avgWidth && yAbsolute < avgHeight) { | |
| let distance = Math.hypot(xDifference, yDifference), | |
| xDistance = (xAbsolute - avgWidth) / distance, | |
| yDistance = (yAbsolute - avgHeight) / distance | |
| // TODO: Why reset tha max one? | |
| //if (Math.abs(xDistance) > Math.abs(yDistance)) { | |
| // xDistance = 0; | |
| //} else { | |
| // yDistance = 0; | |
| //} | |
| node.x -= xDifference * xDistance / 2; | |
| node.y -= yDifference * yDistance / 2; | |
| point.x += xDifference * xDistance / 2; | |
| point.y += yDifference * yDistance / 2; | |
| updated = true; | |
| } | |
| return updated; | |
| } | |
| /** | |
| * Get the center coordinates of an object. | |
| * @param d | |
| * @returns {{x: number, y: number}} | |
| */ | |
| function getCenter(d) { | |
| return { | |
| x: d.x + d.width / 2, | |
| y: d.y + d.height / 2 | |
| } | |
| } | |
| function getColor(hue) { | |
| return `hsl(${hue}, 60%, 60%)` | |
| } | |
| function c() { | |
| let color = getColor(colorIndex) | |
| colorIndex += colorStep | |
| return color | |
| } | |
| function makeMarker(name, fill) { | |
| let id = `${name}-${Math.round(Math.random() * 1000)}` | |
| let defs = d3.select('#marker-defs') | |
| if (defs.empty()) { | |
| defs = d3.select('body').append('svg') | |
| .append('defs') | |
| .attr('id', 'marker-defs') | |
| } | |
| /* | |
| <marker id="triangle" | |
| viewBox="0 0 10 10" refX="0" refY="5" | |
| markerUnits="strokeWidth" | |
| markerWidth="4" markerHeight="3" | |
| orient="auto"> | |
| <path d="M 0 0 L 10 5 L 0 10 z" /> | |
| </marker> | |
| */ | |
| defs.append('marker') | |
| .attr('id', id) | |
| .attr('stroke', 'none') | |
| .attr('stroke-width', 0) | |
| .attr('fill', fill) | |
| .attr('viewBox', '0 0 10 10') | |
| .attr({ | |
| markerWidth: 4, | |
| markerHeight: 3, | |
| refX: 4, | |
| refY: 5, | |
| orient: 'auto' | |
| }) | |
| .append('path') | |
| .attr('d', 'M 0 0 L 10 5 L0 8 z') | |
| return `url(#${id})` | |
| } | |
| /** | |
| * Clone the {x} and {y} attributes of an object. | |
| * | |
| * @param d | |
| * @returns {{x: *, y: *}} | |
| */ | |
| function cloneCoords(d) { | |
| let {x, y} = d | |
| return {x, y} | |
| } | |
| /** | |
| * Get the point at {angle}, {distance} from point {start}. | |
| * @param start Point | |
| * @param angle Angle in radians | |
| * @param distance {Number} | |
| * @returns {{x: *, y: *}} | |
| */ | |
| function getPoint(start, angle, distance) { | |
| let x = Math.cos(angle) * distance + start.x, | |
| y = Math.sin(angle) * distance + start.y; | |
| return {x, y}; | |
| } | |
| /** | |
| * Calculate the angle from point A to point B. | |
| * @param a Point | |
| * @param b Point | |
| * @returns {number} Angle in radians | |
| */ | |
| function angle(a, b) { | |
| return Math.atan2(b.y - a.y, b.x - a.x); | |
| } | |
| function hide(em) { | |
| em.style('opacity', 0) | |
| } | |
| function show(em) { | |
| em.style('opacity', 1) | |
| } | |
| function hasSamePosition(a, b) { | |
| return a.x == b.x && a.y == b.y | |
| } | |
| /** | |
| * Radians to degrees | |
| * @param rad | |
| * @returns {number} | |
| */ | |
| function rad2deg(rad) { | |
| return rad * (180 / Math.PI) | |
| } | |
| /** | |
| * Degrees to radians | |
| * @param deg | |
| * @returns {number} | |
| */ | |
| function deg2rad(deg) { | |
| return deg * (Math.PI / 180) | |
| } | |
| function normalizeAngle(r) { | |
| //while (r <= ) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <html> | |
| <head> | |
| <title>Collision test</title> | |
| <meta http-equiv="content-type" content="text/html; charset=utf8" /> | |
| <style> | |
| body { | |
| background: hsl(0, 0, 60%); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="collide"></div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.js"></script> | |
| <script type="text/babel" src="collision.js"></script> | |
| <script> | |
| init(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment