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