|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<body> |
|
<script src="//d3js.org/d3.v4.js"></script> |
|
<script> |
|
|
|
var width = 960, |
|
height = 300, |
|
τ = 2 * Math.PI; |
|
|
|
var nodes = d3.range(4000).map(function () { |
|
var angle = Math.random() * 2 * Math.PI, radius = 60 * Math.sqrt(Math.random()); |
|
return { |
|
x: width/4 + radius * Math.sin(angle), |
|
y: height/2 + radius * Math.cos(angle), |
|
vx: Math.sin(angle) * Math.random(), |
|
vy: Math.cos(angle) * Math.random(), |
|
}; |
|
}); |
|
|
|
var force = d3.forceSimulation() |
|
.nodes(nodes) |
|
.force("collide", d3.forceCollide(3).strength(0.7)) |
|
.force("bounce-on-container", function () { |
|
for (var i = 0, n = nodes.length, node; i < n; ++i) { |
|
node = nodes[i]; |
|
var dx = (node.x + node.vx) / width - 1 / 2, |
|
dy = (node.y + node.vy) / height - 1 / 2; |
|
if (dx*dx > 0.16) { |
|
node.vx *= -1; |
|
} |
|
if (dy*dy > 0.16) { |
|
node.vy *= -1; |
|
} |
|
|
|
} |
|
}) |
|
|
|
.force("internal-barrier", function (alpha) { |
|
for (var i = 0, n = nodes.length, node; i < n; ++i) { |
|
node = nodes[i]; |
|
var dx0 = (node.x) / width - 1 / 2, |
|
dy0 = (node.y) / height - 1 / 2; |
|
var dx1 = (node.x + node.vx) / width - 1 / 2; |
|
|
|
if (dx0 * dx1 < 0 && Math.abs(dy0) > aperture(alpha) ) { |
|
node.vx *= -1; |
|
} |
|
} |
|
}) |
|
.force("limit-speed", function (alpha) { |
|
|
|
for (var i = 0, n = nodes.length, node; i < n; ++i) { |
|
node = nodes[i]; |
|
var E = 1 + node.vx*node.vx + node.vy*node.vy, |
|
mult = 1 + 0.03 * Math.log(6/E); |
|
node.vx *= mult; |
|
node.vy *= mult; |
|
} |
|
}) |
|
.on("tick", ticked) |
|
.alphaDecay(0.01) |
|
.alphaMin(0) |
|
.velocityDecay(0); |
|
|
|
function aperture(alpha) { |
|
return 0.2 / (1-Math.log(alpha)); |
|
} |
|
|
|
var canvas = d3.select("body").append("canvas") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.on('click', click); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", 100); |
|
|
|
var context = canvas.node().getContext("2d"); |
|
|
|
function click() { |
|
force.alpha(1).restart(); |
|
} |
|
|
|
|
|
var speeds1 = [], speeds2 = [], ct = 0; |
|
|
|
var y = d3.scaleLinear() |
|
.range([100,0]); |
|
|
|
var line = d3.line(), |
|
speedline1 = svg.append('path') |
|
.attr('fill','none') |
|
.attr('stroke', 'red'), |
|
speedline2 = svg.append('path') |
|
.attr('fill','none') |
|
.attr('stroke', 'green'); |
|
|
|
|
|
function ticked() { |
|
|
|
var speed1 = d3.mean(nodes |
|
.filter(function(n) { |
|
return n.x> width/2; |
|
}) |
|
.map(function(n){ |
|
return n.vx*n.vx + n.vy*n.vy; |
|
})), |
|
speed2 = d3.mean(nodes |
|
.filter(function(n) { |
|
return n.x < width/2; |
|
}) |
|
.map(function(n){ |
|
return n.vx*n.vx + n.vy*n.vy; |
|
})); |
|
if (ct++ > width) {speeds1.shift();speeds2.shift();} |
|
speeds1.push(speed1 || 0); |
|
speeds2.push(speed2 || 0) |
|
y.domain([0,d3.max(d3.merge([speeds1,speeds2]))]); |
|
speedline1.attr('d', line(speeds1.map(function(d,i){ |
|
return [i,y(d)]; |
|
}))); |
|
speedline2.attr('d', line(speeds2.map(function(d,i){ |
|
return [i,y(d)]; |
|
}))); |
|
|
|
|
|
context.clearRect(0, 0, width, height); |
|
|
|
context.strokeStyle = "#444"; |
|
context.beginPath(); |
|
// rectangle |
|
context.moveTo(width * 0.1, height * 0.1); |
|
context.lineTo(width * 0.1, height * 0.9); |
|
context.lineTo(width * 0.9, height * 0.9); |
|
context.lineTo(width * 0.9, height * 0.1); |
|
context.lineTo(width * 0.1, height * 0.1); |
|
context.moveTo(width * 0.5, height * 0.1); |
|
// internal barrier |
|
context.lineTo(width * 0.5, height * (0.5 - aperture(force.alpha()))); |
|
context.moveTo(width * 0.5, height * 0.9); |
|
context.lineTo(width * 0.5, height * (0.5 + aperture(force.alpha()))); |
|
context.lineWidth = 1; |
|
context.stroke() |
|
|
|
context.beginPath(); |
|
for (var i = 0, n = nodes.length; i < n; ++i) { |
|
var node = nodes[i]; |
|
context.moveTo(node.x, node.y); |
|
context.arc(node.x, node.y, 1.5, 0, τ); |
|
} |
|
context.fillStyle = "#644"; |
|
context.fill(); |
|
|
|
} |
|
|
|
</script> |