|
const INIT_DENSITY = 0.0002, // particles per sq px |
|
PARTICLE_RADIUS_RANGE = [2, 10], |
|
ACCELERATION_K = 0.3; |
|
|
|
const canvasWidth = window.innerWidth, |
|
canvasHeight = window.innerHeight, |
|
svgCanvas = d3.select('svg#canvas') |
|
.attr('width', canvasWidth) |
|
.attr('height', canvasHeight); |
|
|
|
const forceSim = d3.forceSimulation() |
|
.alphaDecay(0) |
|
.velocityDecay(0) |
|
.on('tick', particleDigest) |
|
.force('bounce', d3.forceBounce() |
|
.radius(d => d.r) |
|
.elasticity(0) |
|
) |
|
.force('container', d3.forceSurface() |
|
.surfaces([ |
|
{from: {x:0,y:0}, to: {x:0,y:canvasHeight}}, |
|
{from: {x:0,y:canvasHeight}, to: {x:canvasWidth,y:canvasHeight}}, |
|
{from: {x:canvasWidth,y:canvasHeight}, to: {x:canvasWidth,y:0}}, |
|
{from: {x:canvasWidth,y:0}, to: {x:0,y:0}} |
|
]) |
|
.oneWay(true) |
|
.radius(d => d.r) |
|
.elasticity(0) |
|
) |
|
.force('magnetic', d3.forceMagnetic() |
|
.charge(node => node.r*node.r*node.pole) |
|
.strength(ACCELERATION_K) |
|
); |
|
|
|
// Init particles |
|
onDensityChange(INIT_DENSITY); |
|
|
|
// Event handlers |
|
function onDensityChange(density) { |
|
d3.select('#density-control').attr('value', density); |
|
forceSim.nodes(genNodes(density)); |
|
d3.select('#numparticles-val').text(forceSim.nodes().length); |
|
onAttractionChange(); |
|
} |
|
|
|
function onAttractionChange() { |
|
const probAttraction = +d3.select('#attraction-control').node().value; |
|
const negThreshold = Math.floor(probAttraction * forceSim.nodes().length); |
|
forceSim.nodes().forEach((node, i) => { |
|
node.pole = i < negThreshold ? 1 : -1; |
|
}); |
|
} |
|
|
|
// |
|
|
|
function genNodes(density) { |
|
const numParticles = Math.round(canvasWidth * canvasHeight * density), |
|
existingParticles = forceSim.nodes(); |
|
|
|
// Trim |
|
if (numParticles < existingParticles.length) { |
|
return existingParticles.slice(0, numParticles); |
|
} |
|
|
|
// Append |
|
return [...existingParticles, ...d3.range(numParticles - existingParticles.length).map(() => { |
|
return { |
|
x: Math.random() * canvasWidth, |
|
y: Math.random() * canvasHeight, |
|
r: Math.round(Math.random() * (PARTICLE_RADIUS_RANGE[1] - PARTICLE_RADIUS_RANGE[0]) + PARTICLE_RADIUS_RANGE[0]), |
|
pole: 1 |
|
} |
|
})]; |
|
} |
|
|
|
function particleDigest() { |
|
let particle = svgCanvas.selectAll('circle.particle').data(forceSim.nodes().map(hardLimit)); |
|
|
|
particle.exit().remove(); |
|
|
|
particle.merge( |
|
particle.enter().append('circle') |
|
.classed('particle', true) |
|
.attr('r', d=>d.r) |
|
) |
|
.attr('fill', d => d.pole>0 ? 'darkslategrey': 'crimson') |
|
.attr('cx', d => d.x) |
|
.attr('cy', d => d.y); |
|
} |
|
|
|
function hardLimit(node) { |
|
// Keep in canvas |
|
node.x = Math.max(node.r, Math.min(canvasWidth-node.r, node.x)); |
|
node.y = Math.max(node.r, Math.min(canvasHeight-node.r, node.y)); |
|
|
|
return node; |
|
} |