Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

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

Simulation of accretion of particles attracted by electrostatic or gravitational forces, analogous to the interaction within dust clouds found in the early stages of star formation.

Uses D3's force plugin forceMagnetic to simulate the attraction/repulsion of objects of varying mass 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 attractive (green attracts all other) vs repellent (red repels all other).

<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>
<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: crimson">Repellent</span>:
<input id="attraction-control" class="slider-control" type="range" min="0" max="1" step="0.1" value="1" oninput="onAttractionChange()">
<span style="color: darkslategrey">Attractive</span>
</div>
<svg id="canvas"></svg>
<script src="index.js"></script>
</body>
const INIT_DENSITY = 0.0002, // particles per sq px
PARTICLE_RADIUS_RANGE = [2, 10],
ACCELERATION_K = 0.3;
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)
.strength(ACCELERATION_K)
);
// 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);
onAttractionChange();
}
function onAttractionChange() {
const probAttraction = +d3.select('#attraction-control').node().value;
const negThreshold = Math.floor(probAttraction * forceSim.nodes().length);
forceSim.nodes().forEach((node, i) => {
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: Math.round(Math.random() * (PARTICLE_RADIUS_RANGE[1] - PARTICLE_RADIUS_RANGE[0]) + PARTICLE_RADIUS_RANGE[0]),
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('r', d=>d.r)
)
.attr('fill', d => d.pole>0 ? 'darkslategrey': 'crimson')
.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