Skip to content

Instantly share code, notes, and snippets.

@joar
Last active September 7, 2015 07:37
Show Gist options
  • Select an option

  • Save joar/5f47e3cefc8daa6fcdf8 to your computer and use it in GitHub Desktop.

Select an option

Save joar/5f47e3cefc8daa6fcdf8 to your computer and use it in GitHub Desktop.
D3 rectangular collisions
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 <= )
}
<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