block by fil 2cec616f64cda2cdb0e7302c9b1be3a5

Swarmalator

Full Screen

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

index.html

<!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>