Skip to content

Instantly share code, notes, and snippets.

@vasturiano
Last active March 10, 2018 10:37
Show Gist options
  • Save vasturiano/773c84f393afc98de3e99d551566481d to your computer and use it in GitHub Desktop.
Save vasturiano/773c84f393afc98de3e99d551566481d to your computer and use it in GitHub Desktop.
Binary Star System

A simulation of orbital trajectories using the d3-force simulation engine with the gravity-like d3-force-magnetic attraction force.

By default the layout represents a binary system. All the stars (red nodes) have the same mass and their attraction influence on the satellite (blue node) is proportional to the inverse-square of the distance. The stars are statically positioned and remain unaffected by either the satellite or other stars. Stars can be added and removed by double-clicking on canvas/nodes. All nodes can also be repositioned by dragging.

The initial velocity (and direction) of the satellite can be changed using the controls on the top-right. The length of the pre-estimated trajectory can be manipulated by changing the number of samples (represented as dots).

See also Single Orbital Trajectory.

<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<script src="//unpkg.com/d3-force-magnetic"></script>
<script src="//unpkg.com/kapsule"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.5/dat.gui.min.js"></script>
<script src="orbital-canvas.js"></script>
<link rel="stylesheet" href="orbital-canvas.css">
<style>body { margin: 0; }</style>
</head>
<body>
<div id="orbital-canvas"></div>
<script src="index.js"></script>
</body>
const orbitalComp = OrbitalCanvas()
(document.getElementById('orbital-canvas'));
const initV = orbitalComp.initialV();
// Controls
const controls = { 'px/frame': initV, vertical: true, samples: 5000 };
const gui = new dat.GUI();
const vGui = gui.addFolder('Initial Velocity');
vGui.open();
vGui.add(controls, 'px/frame', 0, initV*2).step(0.0001).onChange(orbitalComp.initialV);
vGui.add(controls, 'vertical').onChange(v => orbitalComp.initialVAngle(v ? -90 : 0));
gui.add(controls, 'samples', 0, 50000).step(100).onChange(orbitalComp.numSamples);
gui.close();
.scaffold {
position: absolute;
top: 0;
left: 0;
}
.satellite, .ghost {
r: 5;
fill: rgba(0, 0, 255, .6);
}
.star {
r: 10;
fill: red;
}
.ghost, .star {
cursor: grab;
cursor: -webkit-grab;
}
.ghost:active, .star:active {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.info {
position: absolute;
bottom: 5px;
left: 50%;
transform: translate(-50%, 0);
pointer-events: none;
user-select: none;
font-family: Sans-serif;
font-size: 12px;
color: darkgray;
opacity: .8;
}
const OrbitalCanvas = Kapsule({
props: {
initialV: {},
initialVAngle: { default: -90 }, // 0: right, 90 down
numSamples: { default: 5000 }
},
init(domElem, state, { width = window.innerWidth, height = window.innerHeight}) {
state.width = width;
state.height = height;
const orbitalD = state.width/3;
state.satelliteInit = { x: -orbitalD, y: 0 };
state.stars = [{ x: -state.width*.2, y: 0}, { x: state.width*.2, y: 0}];
// Proportional to cube of satellite distance to maintain behavior over different widths
const G = 5e-4 * Math.pow(orbitalD, 3); // Determines motion speed
state.totalMass = 1;
if (state.initialV === null) {
// Generate default
const magicRatio = 1.131; // Sync for approx H (1.131) or eight-shaped (1.308) orbit in default layout
state.initialV = Math.sqrt(magicRatio * G * state.totalMass / orbitalD);
}
state.forceSim = d3.forceSimulation()
.alphaDecay(0)
.velocityDecay(0)
.stop()
.force('gravity', d3.forceMagnetic()
.strength(G)
.charge(d => d.mass)
)
.on('tick', () => {
state.elSatellite
.attr('cx', d => d.x)
.attr('cy', d => d.y);
});
// Dom init
state.trails = d3.select(domElem)
.append('canvas')
.attr('class', 'trails');
state.canvas = d3.select(domElem)
.append('svg')
.attr('class', 'scaffold')
.attr('width', state.width)
.attr('height', state.height)
.attr('viewBox', `${-state.width/2} ${-state.height/2} ${state.width} ${state.height}`)
.on('dblclick', () => {
d3.event.stopPropagation();
state.stars.push({
x: d3.event.x-state.width/2,
y: d3.event.y-state.height/2
});
state._rerender();
});
state.elSatellite = state.canvas.append('circle').attr('class', 'satellite');
state.canvas.append('circle').attr('class', 'ghost')
.call(d3.drag().on('drag', () => {
state.satelliteInit.x = d3.event.x;
state.satelliteInit.y = d3.event.y;
state._rerender();
}));
d3.select(domElem).append('div').attr('class', 'info')
.text('double-click to add/remove | click-drag to reposition');
},
update(state) {
const satellite = { mass: 0 };
state.forceSim.stop();
resetNodes();
satelliteAnchorDigest();
starDigest();
// Predict orbit trajectories
// Clear canvas
const ctx = state.trails
.attr('width', state.width)
.attr('height', state.height)
.node()
.getContext('2d');
ctx.translate(state.width/2, state.height/2);
ctx.fillStyle = 'rgba(0, 0, 75, .6)';
d3.range(state.numSamples).forEach(() => {
state.forceSim.tick();
ctx.beginPath();
ctx.fillRect(satellite.x, satellite.y, 1.4, 1.4);
ctx.fill();
});
// Animate satellite
resetNodes();
state.elSatellite.datum(satellite);
state.forceSim.restart();
//
function resetNodes() {
satellite.x = state.satelliteInit.x;
satellite.y = state.satelliteInit.y;
satellite.vx = state.initialV * Math.cos(state.initialVAngle*Math.PI/180);
satellite.vy = state.initialV * Math.sin(state.initialVAngle*Math.PI/180);
state.forceSim.nodes([
satellite,
...state.stars.map(star => ({
x: star.x,
y: star.y,
fx: star.x,
fy: star.y,
mass: state.totalMass/state.stars.length
}))
]);
}
function satelliteAnchorDigest() {
state.canvas.select('.ghost').datum(state.satelliteInit)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
function starDigest() {
const star = state.canvas.selectAll('.star').data(state.stars);
star.exit().remove();
star.merge(
star.enter()
.append('circle').attr('class', 'star')
.call(d3.drag().on('drag', d => {
d.x = d3.event.x;
d.y = d3.event.y;
state._rerender();
}))
.on('dblclick', d => {
d3.event.stopPropagation();
state.stars.splice(state.stars.indexOf(d), 1);
state._rerender();
})
)
.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment