|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<body> |
|
<script src="https://d3js.org/d3.v4.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.js"></script> |
|
|
|
<script> |
|
|
|
|
|
// Controls |
|
const gui = new dat.GUI(); |
|
const controls = { |
|
N: 100, |
|
J: 1, |
|
K: -0.1, |
|
temperature: false, |
|
}; |
|
for (i in controls) gui.add(controls, i) |
|
|
|
const width = 960, |
|
height = 500, |
|
margin = 10, |
|
tau = 2 * Math.PI; |
|
|
|
const M = width - height; |
|
|
|
const X = d3.scaleLinear().domain([-2,2]).range([margin + M/2, width - margin - M/2]), |
|
Y = d3.scaleLinear().domain([-2,2]).range([margin, height - margin]), |
|
F = (h) => d3.hsl(h * 360 / tau, 0.9, 0.5, 1), |
|
G = d3.interpolateInferno; |
|
|
|
var nodes = d3.range(2).map(function() { |
|
return { |
|
x: 1 - 2 * Math.random(), |
|
y: 1 - 2 * Math.random(), |
|
f: Math.random() * tau, |
|
}; |
|
}); |
|
|
|
|
|
const swarmalator = function() { |
|
|
|
return function (alpha) { |
|
nodes.forEach((d ,i )=> { |
|
d.vx = d.vy = 0; |
|
}); |
|
var n = nodes.length; |
|
nodes.forEach((d, i) => { |
|
for (var j = i+1; j < n; j++) { |
|
var e = nodes[j]; |
|
var dx = d.x - e.x, |
|
dy = d.y - e.y, |
|
df = d.f - e.f, |
|
dist2 = dx * dx + dy * dy, |
|
dist = Math.sqrt(dist2); |
|
|
|
var mu = ((1 + controls.J * Math.cos(df)) * dist - 1) / dist2 / n; |
|
|
|
d.vx -= dx * mu; |
|
d.vy -= dy * mu; |
|
e.vx += dx * mu; |
|
e.vy += dy * mu; |
|
d.f -= Math.sin(df) / dist * controls.K / n; |
|
e.f += Math.sin(df) / dist * controls.K / n; |
|
|
|
} |
|
}); |
|
}; |
|
} |
|
|
|
const simulation = d3.forceSimulation() |
|
.alphaDecay(1e-12) |
|
.nodes(nodes) |
|
.on("tick", ticked); |
|
|
|
simulation.force('swarm', swarmalator()); |
|
|
|
|
|
const canvas = d3.select("body").append("canvas") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.on('mousemove click', function() { |
|
simulation.alpha(0.1).restart(); |
|
}); |
|
|
|
const context = canvas.node().getContext("2d"); |
|
|
|
|
|
|
|
|
|
function ticked() { |
|
if (nodes.length < controls.N) nodes.push({ |
|
x: 1 - 2 * Math.random(), |
|
y: 1 - 2 * Math.random(), |
|
f: Math.random() * tau, |
|
}); |
|
|
|
if (nodes.length > controls.N) nodes = nodes.slice(1, nodes.length); |
|
|
|
simulation.nodes(nodes); |
|
|
|
|
|
const r = 0.25 * height / Math.sqrt(nodes.length); |
|
context.clearRect(0, 0, width, height); |
|
for (var i = 0, n = nodes.length; i < n; ++i) { |
|
var node = nodes[i]; |
|
context.beginPath(); |
|
context.moveTo(X(node.x), Y(node.y)); |
|
context.arc(X(node.x), Y(node.y), r, 0, tau); |
|
if (controls.temperature) { |
|
context.fillStyle = G(4 * Math.sqrt(Math.sqrt(node.vx * node.vx + node.vy * node.vx))) ; |
|
} else { |
|
context.fillStyle = F(node.f); |
|
} |
|
context.fill(); |
|
} |
|
|
|
} |
|
|
|
</script> |