<div id="display"></div>
html, body { background: black; }
#display { position: fixed; top: 0; left: 0; width: 100%; height: 100%; }
svg circle { fill: white; opacity: 0.5; }
svg line { stroke: white; stroke-width: 2px; }
// modelling functions
var step = (a, x) => x >= a ? 1 : 0,
    clamp = (a, b, x) => Math.max(a, Math.min(b, x)),
    map = (a, b, c, d, x) => (x - a) / (b - a) * (d - c) + c,
    smoothstep = (a, b, x) => {
      let t = clamp(0, 1, map(a, b, 0, 1, x));
      return t * t * t * ((6 * t - 15) * t + 10);
    };

class Vec2d {
  constructor(x, y) { this.reset(x, y); }
  get mag() { return Math.sqrt(this.dot(this)); }
  clone() { return new Vec2d(this.x, this.y); }
  reset(x, y) { this.x = x; this.y = y; return this; }
  neg() { return this.scale(-1); }
  norm() {
    let m = this.mag;
    if (m > 0) return this.scale(1 / m);
    return this;
  }
  add(a, b) {
    let x, y;
    if (a instanceof Vec2d) {
      x = a.x; y = a.y;
    } else {
      x = a; y = b;
    }
    this.x += x;
    this.y += y;
    return this;
  }
  sub(a, b) {
    let x, y;
    if (a instanceof Vec2d) {
      x = a.x; y = a.y;
    } else {
      x = a; y = b;
    }
    this.x -= x;
    this.y -= y;
    return this;
  }
  scale(a) {
    this.x *= a;
    this.y *= a;
    return this;
  }
  dot(a, b) {
    let x, y;
    if (a instanceof Vec2d) {
      x = a.x; y = a.y;
    } else {
      x = a; y = b;
    }
    return this.x * x + this.y * y;
  }
  static add(a, b) { return a.clone().add(b); }
  static sub(a, b) { return a.clone().sub(b); }
  static dot(a, b) { return a.dot(b); }
}

class Particle {
  constructor(position) {
    this.position = position;
    this.force = new Vec2d(0, 0);
  }
}

class Simulation {
  constructor(particles) {
    this.particles = particles;
  }
  
  static get NODE_DISTANCE() { return 2; }
  static get DISTANCE_CUTOFF() { return this.NODE_DISTANCE * 8; }
  static get SPRING_COEFFICIENT() { return 0.02; }
  static get REPULSION_FORCE() { return 4; }
  
  static getRepulsionForce(a, b) {
    let f = Vec2d.sub(a.position, b.position),
        m = f.mag;
    return m > this.DISTANCE_CUTOFF ? f.reset(0, 0) : f.norm().scale(this.REPULSION_FORCE / (m * m));
  }
  static getSpringForce(a, b) {
    let f = Vec2d.sub(a.position, b.position),
        m = f.mag;
    return f.norm().scale((this.NODE_DISTANCE - m) * this.SPRING_COEFFICIENT);
  }
  
  step(dT) {
    let t = dT / 4;
    // process mutual repulsion
    for (let i = 0; i < this.particles.length - 1; i++) {
      for (let j = i + 1; j < this.particles.length; j++) {
        // mutual deflection force
        let a = this.particles[i], b = this.particles[j];
        let f = Simulation.getRepulsionForce(a, b);
        a.force.add(f);
        b.force.sub(f);
      }
    }
    // process linkage
    for (let i = 0; i < this.particles.length; i++) {
      let a = this.particles[i], b = this.particles[(i + 1) % this.particles.length];
      let f = Simulation.getSpringForce(a, b);
      a.force.add(f);
      b.force.sub(f);
    }
    // apply movement
    this.particles.forEach(particle => {
      let f = particle.force.clone().scale(t);
      particle.position.add(f);
      particle.force.reset(0, 0);
    });
  }
}

// D3 stuff

var container = document.querySelector('#display'),
    canvas = d3.select(container)
      .append('svg')
      .attr({ width: container.offsetWidth, height: container.offsetHeight });

var update = data => {
  let w2 = container.offsetWidth / 2, h2 = container.offsetHeight / 2;
  let links = canvas.selectAll('line').data(data);
  let particles = canvas.selectAll('circle').data(data);
  
  // update the links
  links.enter().append('line');
  
  links.attr({
    x1: d => d.position.x + w2,
    y1: d => d.position.y + h2,
    x2: (d, idx) => data[(idx + 1) % data.length].position.x + w2,
    y2: (d, idx) => data[(idx + 1) % data.length].position.y + h2,
  });
  
  links.exit().remove();
  
  // update the particles
  particles.enter().append('circle');
  
  particles.attr({
    cx: d => d.position.x + w2,
    cy: d => d.position.y + h2,
    r: 3
  });
  
  particles.exit().remove();
};

// set up the initial world, just start with a circle of 40 particles
var simulation = (() => {
  let n = 40, particles = [], r = 40;
  for (let a = 0; a < 2 * Math.PI; a += 2 * Math.PI / n) {
    particles.push(
      new Particle(
        new Vec2d(
          Math.cos(a) * r,
          Math.sin(a) * r
        )
      )
    );
  }
  return new Simulation(particles);
})();


// loop stuff
var lastTime = null, lastGenerated = null;

var tick = timestamp => {
  if (!lastTime) lastTime = timestamp;
  let elapsed = clamp(0, 16, timestamp - lastTime);
  lastTime = timestamp;
  
  simulation.step(elapsed);
  update(simulation.particles);
  
  if (!lastGenerated) lastGenerated = timestamp;
  // generate a new particle every 60ms
  if (simulation.particles.length < 800 && timestamp - lastGenerated > 60) {
    let idx = Math.floor(Math.random() * simulation.particles.length),
        nextIdx = (idx + 1) % simulation.particles.length,
        a = simulation.particles[idx],
        b = simulation.particles[nextIdx];
    simulation.particles.splice(
      idx,
      0,
      new Particle(
        a.position.clone().sub(b.position).scale(0.5).add(a.position)
      )
    );
    lastGenerated = timestamp;
  }
  
  requestAnimationFrame(tick);
};

requestAnimationFrame(tick);
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js