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; }