Skip to content

Instantly share code, notes, and snippets.

@vasturiano
Last active October 22, 2017 04:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vasturiano/8105c6b44c064e49a046ae1fb07c9f4f to your computer and use it in GitHub Desktop.
Save vasturiano/8105c6b44c064e49a046ae1fb07c9f4f to your computer and use it in GitHub Desktop.
Plasma

Simulation of attraction-of-opposites particles, similar to the interaction between protons and electrons.

Uses D3's force plugin forceMagnetic to simulate the asymmetrical attraction/repulsion of objects in an inverse-square relationship with distance. Further, particles are prevented from overlapping by applying a non-elastic collision force using forceBounce.

Using the slider controls you can add/remove particles to the system, and regulate the ratio of positive (+) vs negative (-) particles.

<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
<script src="//unpkg.com/d3-force-bounce"></script>
<script src="//unpkg.com/d3-force-surface"></script>
<script src="//unpkg.com/d3-force-magnetic"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="controls">
Particles:
<input id="density-control" class="slider-control" type="range" min="0" max="0.001" step="0.00001" oninput="onDensityChange(this.value)">
<span id="numparticles-val"></span>
<br>
<span style="color: darkslategrey">(-)</span>:
<input id="polarity-control" class="slider-control" type="range" min="0" max="1" step="0.01" value="0.03" oninput="onPolarityChange()">
<span style="color: crimson">(+)</span>
</div>
<svg id="canvas"></svg>
<script src="index.js"></script>
</body>
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;
}
body {
margin: 0;
text-align: center;
font-family: sans-serif;
font-size: 14px;
}
#controls {
text-align: left;
position: absolute;
margin: 8px;
padding: 1px 5px 5px 5px;
background: rgba(230, 230, 250, 0.7);
opacity: 0.5;
border-radius: 3px;
z-index: 1000;
}
#controls:hover {
opacity: 1;
}
.slider-control {
position: relative;
top: 3px;
cursor: grab;
cursor: -webkit-grab;
}
.slider-control:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment