d3.forceSimulation for https://www.nature.com/articles/s41467-017-01190-3
Observable version: https://beta.observablehq.com/@fil/swarmalator
(See also http://usediscretion.blogspot.fi/2017/01/the-swarmalator.html )
Built with blockbuilder.org
license: mit |
d3.forceSimulation for https://www.nature.com/articles/s41467-017-01190-3
Observable version: https://beta.observablehq.com/@fil/swarmalator
(See also http://usediscretion.blogspot.fi/2017/01/the-swarmalator.html )
Built with blockbuilder.org
<!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> |