|
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); |
|
} |
|
|
|
} |
|
}); |