const INIT_DENSITY = 0.00025, // particles per sq px PARTICLE_RADIUS_RANGE = [1, 18], PARTICLE_VELOCITY_RANGE = [0, 4]; 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) ) .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) ); // Init particles onDensityChange(INIT_DENSITY); // Event handlers function onDensityChange(density) { const newNodes = genNodes(density); d3.select('#numparticles-val').text(newNodes.length); d3.select('#density-control').attr('value', density); forceSim.nodes(newNodes); } function onElasticityChange(elasticity) { d3.select('#elasticity-val').text(elasticity); forceSim.force('bounce').elasticity(elasticity); forceSim.force('container').elasticity(elasticity); } // 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(() => { const angle = Math.random() * 2 * Math.PI, velocity = Math.random() * (PARTICLE_VELOCITY_RANGE[1] - PARTICLE_VELOCITY_RANGE[0]) + PARTICLE_VELOCITY_RANGE[0]; return { x: Math.random() * canvasWidth, y: Math.random() * canvasHeight, vx: Math.cos(angle) * velocity, vy: Math.sin(angle) * velocity, r: Math.round(Math.random() * (PARTICLE_RADIUS_RANGE[1] - PARTICLE_RADIUS_RANGE[0]) + PARTICLE_RADIUS_RANGE[0]) } })]; } 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', 'darkslategrey') ) .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; }