Skip to content

Instantly share code, notes, and snippets.

@vasturiano
Last active March 11, 2023 10:10
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save vasturiano/2992bcb530bc2d64519c5b25201492fd to your computer and use it in GitHub Desktop.
Save vasturiano/2992bcb530bc2d64519c5b25201492fd to your computer and use it in GitHub Desktop.
Entropy

Simulation of collision between moving particles with different masses inside a container, using the d3-force physics engine.

Uses two elastic collision forces, d3.forceBounce to handle elastic collisions between nodes, and d3.forceSurface for collisions with the container walls.

You can reduce the elasticity (coefficient of restitution) of the particles using the slider on the upper-left, causing kinetic energy to be lost at every collision. Elasticity of 1 indicates a pure elastic collision, while a value of 0 will cause particles to stick to each other along the collision axis.

<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js"></script>
<script src="//unpkg.com/d3-force-bounce"></script>
<script src="//unpkg.com/d3-force-surface"></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.002" step="0.00001" oninput="onDensityChange(this.value)">
<span id="numparticles-val"></span>
<br>
Elasticity:
<input id="elasticity-control" class="slider-control" type="range" min="0" max="1" step="0.1" value="1" oninput="onElasticityChange(this.value)">
<span id="elasticity-val">1</span>
</div>
<svg id="canvas"></svg>
<script src="index.js"></script>
</body>
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;
}
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