|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title>Ball drop with force-directed layout and collision detection</title> |
|
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> |
|
<style type="text/css"> |
|
circle { |
|
stroke-width: 1.5px; |
|
} |
|
|
|
line { |
|
stroke: #999; |
|
} |
|
|
|
.border-box { |
|
fill: hsla(0, 100%, 100%, 0); |
|
stroke: black; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<script type="text/javascript"> |
|
|
|
var w = 440, |
|
h = 500, |
|
r = 16, |
|
fill = d3.scale.category20(); |
|
|
|
var force = d3.layout.force() |
|
.friction(0.4) |
|
.size([w, h]); |
|
|
|
var svg = d3.select('body').append('svg:svg') |
|
.attr('width', w) |
|
.attr('height', h); |
|
|
|
var borderbox = svg.append('rect').classed('border-box', true) |
|
.attr({ |
|
width: w, |
|
height: h |
|
}); |
|
|
|
d3.json('readme_formatted.json', function(json) { |
|
var nodes = json.nodes; |
|
nodes.forEach(setRandomPositionAtTopOfBounds); |
|
var magnetNodes = nodes.map(magnetNodeForNode); |
|
|
|
var linkData = magnetNodes.map(function makeLink(magnetNode, i) { |
|
return { |
|
source: magnetNode, |
|
target: nodes[i], |
|
value: 1 |
|
}; |
|
}); |
|
|
|
nodes = nodes.concat(magnetNodes); |
|
nodes.forEach(function setRadius(node) { node.radius = r; }); |
|
|
|
var node = svg.selectAll('circle') |
|
.data(nodes) |
|
.enter().append('svg:circle') |
|
.attr('r', r - .75) |
|
.style('fill', function(d) { return fill(d.group); }) |
|
.style('stroke', function(d) { return d3.rgb(fill(d.group)).darker(); }) |
|
|
|
force |
|
.nodes(nodes) |
|
.links(linkData) |
|
.on('tick', tick) |
|
.start(); |
|
|
|
function tick() { |
|
var q = d3.geom.quadtree(nodes), |
|
i = 0, |
|
n = nodes.length; |
|
|
|
while (++i < n) q.visit(collide(nodes[i])); |
|
|
|
node.attr('cx', updateCx) |
|
.attr('cy', updateCy); |
|
} |
|
|
|
function collide(node) { |
|
var r = node.radius, |
|
nx1 = node.x - r, |
|
nx2 = node.x + r, |
|
ny1 = node.y - r, |
|
ny2 = node.y + r; |
|
return function(quad, x1, y1, x2, y2) { |
|
if (quad.point && (quad.point !== node)) { |
|
var x = node.x - quad.point.x, |
|
y = node.y - quad.point.y, |
|
l = Math.sqrt(x * x + y * y), |
|
r = node.radius + quad.point.radius; |
|
if (!node.fixed && l < r) { |
|
// The node and the node described by the quad are too close. |
|
// Push them away from each other. |
|
l = (l - r) / l * 0.5; |
|
if (!isFinite(l)) { |
|
l = 0; |
|
} |
|
x *= l; |
|
y *= l; |
|
node.x -= x; |
|
node.y -= y; |
|
quad.point.x += x; |
|
quad.point.y += y; |
|
} |
|
} |
|
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1; |
|
} |
|
} |
|
|
|
}); |
|
|
|
// The magnet nodes do not move. |
|
// It stays under the bottom of the bounding box. |
|
|
|
function magnetNodeForNode(node) { |
|
return { |
|
name: 'magnet', |
|
group: 100, |
|
fixed: true, |
|
// isAMagnet: true, |
|
x: node.x, |
|
y: h + r + 20 |
|
}; |
|
} |
|
|
|
function updateCx(d) { |
|
if (!d.fixed) { |
|
d.x = Math.max(r, Math.min(w - r, d.x)); |
|
} |
|
return d.x; |
|
} |
|
|
|
function updateCy(d) { |
|
if (!d.fixed) { |
|
d.y = Math.max(r, Math.min(h - r, d.y)); |
|
} |
|
return d.y; |
|
} |
|
|
|
function linkNodesToMagnetNode(nodes, magnetNode) { |
|
return nodes.map(function linkToNode(node) { |
|
return { |
|
source: magnetNode, |
|
target: node, |
|
value: 1 |
|
}; |
|
}); |
|
} |
|
|
|
function setRandomPositionAtTopOfBounds(node) { |
|
node.x = ~~(Math.random() * w); |
|
node.y = 0; |
|
} |
|
|
|
</script> |
|
</body> |
|
</html> |