const INIT_DENSITY = 0.0004, // particles per sq px PROTON_RADIUS = 9, ELECTRON_RADIUS = 2.5, PROTON_ELECTRON_CHARGE_RATIO = 10, ACCELERATION_K = 0.05; 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*(node.pole>0 ? PROTON_ELECTRON_CHARGE_RATIO : 1)) .strength(ACCELERATION_K) .polarity((q1,q2) => q1*q2 < 0) // Attraction of opposites ); // 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); onPolarityChange(); } function onPolarityChange() { const probPositive = +d3.select('#polarity-control').node().value; const negThreshold = Math.floor(probPositive * forceSim.nodes().length); forceSim.nodes().forEach((node, i) => { node.r = i < negThreshold ? PROTON_RADIUS : ELECTRON_RADIUS; 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: PROTON_RADIUS, 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('fill', d => d.pole<0 ? 'darkslategrey': 'crimson') .attr('r', d=>d.r) .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; }