Skip to content

Instantly share code, notes, and snippets.

@vasturiano
Last active October 22, 2017 04:20
Show Gist options
  • Save vasturiano/54dd054d22be863da5afe2db02e033e2 to your computer and use it in GitHub Desktop.
Save vasturiano/54dd054d22be863da5afe2db02e033e2 to your computer and use it in GitHub Desktop.
Force-simulated Solar System

Don't see anything?... Exactly! Increase the Size distortion slider to reveal the planetary bodies.

This shows a force-simulated version of the solar system. The orbital trajectories are derived purely from the configuration of distances and masses of all the bodies. It is rendered true-to-scale, which shows how vast the empty space really is in between the bodies.

The simulation is ran using the d3-force simulation engine with the gravity-like d3-force-magnetic (inverse square law) attraction force. Each body is given an initial tangential velocity, equal to its orbital speed around the central sun, calculated as √(GM/d). See also force-simulated orbit trajectories.

Use the Lock on radio buttons to select the central pivoting body. The Size distortion slider lets you exaggerate the size of the bodies linearly, so they can be visible. These options are for rendering purposes only and do not affect the mechanics of motion.

Zoom-in/out using the scroll-wheel. Zooming in reduces the simulation speed for the visualization purposes, as closer bodies orbit faster.

<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.9.1/d3.min.js"></script>
<script src="//unpkg.com/d3-force-magnetic"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/chroma-js/1.3.4/chroma.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="canvas"></canvas>
<div id="controls">
<div id="bodylock">
Lock on:
</div>
<div style="margin-top: 10px;"></div>
Size distortion:
<br/>
<input id="size-control" class="slider-control" type="range" min="1" max="2000" value="1" step="1" oninput="onBodyDistortionChange(this.value)">
<span id="bodydistortion-val">1</span>x
</div>
</div>
<div id="info">
<span id="au-100px-scale"></span>&nbsp;AU
<svg width="100px" height="6px">
<path d="M0,0V6H100V0" stroke="darkgrey" stroke-width="1.5" fill="transparent"></path>
</svg>
(scroll to zoom)
</div>
<script src="index.js"></script>
<script>
load('solar-system.json');
</script>
</body>
const G = 1.5e-3, // Controls overall simulation speed
TRAIL_LENGTH = 250, // # simulation samples
TRAIL_MAX_ALPHA = 0.5, // trail opacity upper limit
TRAIL_THICKNESS = 0.08; // relative to body's diameter
const width = window.innerWidth, height = window.innerHeight;
let bodyDistortion = 1,
lockOn,
zoomLevel = 1,
au = d3.scaleLinear() // Astronomical unit
.range([0, Math.min(width, height)]);
// Size canvas
d3.select('#canvas')
.attr('width', width)
.attr('height', height)
.call(d3.zoom()
.scaleExtent([1, 1e6])
.on('zoom', function() {
zoomed(d3.event.transform.k);
})
);
const forceSim = d3.forceSimulation()
.alphaDecay(0)
.velocityDecay(0)
.force('gravity', d3.forceMagnetic()
.id(d => d.id)
.charge(node => node.mass)
)
.on('tick', ticked);
//
function ticked() {
const TAU = 2*Math.PI,
F = 1e8; // Scale factor, to prevent bug of (scaled) arcs with r<0.002 from disappearing
const ctx = d3.select('#canvas')
.attr('width', width) // Wipe it
.attr('height', height)
.node().getContext('2d');
// 0,0 at canvas center
ctx.translate(width/2, height/2);
// Apply zoom
if (zoomLevel) {
ctx.scale(zoomLevel/F, zoomLevel/F);
}
// Lock on body
if (lockOn) {
ctx.translate(-lockOn.x*F, -lockOn.y*F);
}
const nodes = forceSim.nodes();
for (let i=0; i<nodes.length; i++) {
const node = nodes[i],
r = Math.min(node.name==='sun'?2:Infinity, node.r * bodyDistortion),
color = chroma(node.color);
ctx.fillStyle = color.css();
ctx.beginPath();
ctx.arc(node.x*F, node.y*F, r*F, 0, TAU);
ctx.fill();
// Add orbit trails
const relAlpha = TRAIL_MAX_ALPHA/node.trail.length,
trailR = r*TRAIL_THICKNESS*F,
rgb = color.rgb().join(',');
for (let idx=0, len=node.trail.length; idx<len; idx++) {
const pnt = node.trail[idx];
ctx.fillStyle = `rgba(${rgb},${(idx+1)*relAlpha})`;
ctx.beginPath();
ctx.arc(pnt[0]*F, pnt[1]*F, trailR, 0, TAU);
//ctx.fillRect(pnt[0], pnt[1], trailR, trailR); // rects have better performance than arcs
ctx.fill();
}
// Push current coords to trail buffer
node.trail.push([node.x, node.y]);
if (node.trail.length > TRAIL_LENGTH) node.trail.shift();
}
}
function zoomed(newZoomLevel=zoomLevel) {
const changeRatio = zoomLevel/newZoomLevel,
sqrtChangeRatio = Math.sqrt(changeRatio);
zoomLevel = newZoomLevel;
d3.select('#au-100px-scale').text(Math.round(au.invert(100) / zoomLevel * 1000) / 1000);
// Slow down motion on zoom-in
forceSim.stop();
forceSim.force('gravity').strength(forceSim.force('gravity').strength()()*changeRatio);
forceSim.nodes().forEach(d => {
d.vx *= sqrtChangeRatio;
d.vy *= sqrtChangeRatio;
});
forceSim.restart();
}
function load(jsonFile) {
d3.json(jsonFile, (error, bodies) => {
const maxDistance = d3.max(bodies.map(d => d3.max(d.satellites.map(s => d.distance + s.distance))));
au.domain([0, maxDistance * 2.1]);
zoomed(); // Display scale
const pxG = G * Math.pow(au(1), 3); // in cube of AUs
forceSim.nodes(parseBodies(bodies))
.force('gravity').strength(pxG);
// Add lock radio buttons
const bodyLock = d3.select('#bodylock').selectAll('div')
.data(forceSim.nodes()).enter().append('div');
lockOn = forceSim.nodes()[0]; // Lock on first body
bodyLock.append('input')
.attr('type', 'radio')
.attr('name', 'bodylock')
.attr('id', d => `bodylock-${d.name}`)
.attr('value', d => d.name)
.attr('checked', d => d.name === 'sun' ? true : null)
.on("change", function() {
forceSim.nodes().some(d => {
if (d.name === this.value) {
lockOn = d;
return true;
}
});
});
bodyLock.append('label')
.attr('for', d => `bodylock-${d.name}`)
.style('color', d => d.color)
.text(d => `${d.symbol?`${d.symbol} `:''}${d.name}`);
//
function parseBodies(bodies, parentMass = 0, posOffset = [0,0], velocityOffset = [0,0]) {
return [].concat(...bodies.map(body => {
const ang = (body.phase || (Math.random() * 360)) * Math.PI/180, // Random init angle if not specified (to prevent aligned init forces from distorting orbits)
x = posOffset[0] + au(body.distance) * Math.sin(ang),
y = posOffset[1] - au(body.distance) * Math.cos(ang),
relVelocity = (body.distance ? Math.sqrt(pxG * parentMass / au(body.distance)): 0) * (body.factorV || 1), // orbital velocity: sqrt(GM/d)
vx = velocityOffset[0] + relVelocity * Math.cos(ang),
vy = velocityOffset[1] + relVelocity * Math.sin(ang);
return [{
name: body.name,
symbol: body.symbol || null,
color: body.color || 'darkgrey',
r: au(body.r || Math.cbrt(body.mass)),
mass: body.mass, // mass in solar masses
x: x, // radius, distance & velocity in AUs
y: y,
vx: vx,
vy: vy,
trail: [] // Store previous positions
},
...parseBodies(body.satellites || [], body.mass, [x,y], [vx,vy])
]
}));
}
});
}
// Event handlers
function onBodyDistortionChange(dist) {
bodyDistortion = dist;
d3.select('#bodydistortion-val').text(dist);
}
[
{
"name": "sun",
"symbol": "☉",
"color": "#FFFF55",
"mass": 1,
"r": 4.7e-3,
"distance": 0,
"satellites": [
{ "name": "mercury", "symbol": "☿", "color": "#D4CCC5", "mass": 0.17e-6, "r": 16e-6, "distance": 0.39 },
{ "name": "venus", "symbol": "♀", "color": "#99CC32", "mass": 2.45e-6, "r": 40e-6, "distance": 0.723 },
{
"name": "earth",
"symbol": "♁",
"color": "#007FFF",
"mass": 3e-6,
"r": 43e-6,
"distance": 1,
"satellites": [
{ "name": "moon", "symbol": "☽", "color": "#A8A8A8", "mass": 0.037e-6, "r": 12e-6, "distance": 0.0026 }
]
},
{ "name": "mars", "symbol": "♂", "color": "#FF7700", "mass": 0.32e-6, "r": 23e-6, "distance": 1.524 },
{ "name": "jupiter", "symbol": "♃", "color": "#D98719", "mass": 955e-6, "r": 477e-6, "distance": 5.203 },
{ "name": "saturn", "symbol": "♄", "color": "#B5A642", "mass": 286e-6, "r": 402e-6, "distance": 9.539 },
{ "name": "uranus", "symbol": "⛢", "color": "#7093DB", "mass": 44e-6, "r": 170e-6, "distance": 19.18 },
{ "name": "neptune", "symbol": "♆", "color": "#7093DB", "mass": 52e-6, "r": 165e-6, "distance": 30.06 }
]
}
]
body {
text-align: center;
font-family: Sans-serif;
margin: 0;
background: #000016;
}
#info {
position: absolute;
bottom: 0;
right: 0;
color: darkgrey;
font-size: 12px;
opacity: 0.7;
}
#controls {
font-size: 14px;
text-align: left;
position: absolute;
top: 0;
left: 0;
margin: 8px;
padding: 1px 5px 5px 5px;
color: darkgrey;
background: #000033;
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