<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
This Pen doesn't use any external CSS resources.