const width = window.innerWidth, height = window.innerHeight; const orbitDistance = height / 4, G = 1e-3 * Math.pow(orbitDistance, 3), // Proportional to cube of orbit distance to maintain behavior over different heights centralMass = 1, orbitalV = Math.sqrt(G * centralMass / orbitDistance); let initialV = orbitalV, numPnts = 5000; // Draw scaffold canvas d3.select('#scaffold') .attr('width', width) .attr('height', height) .attr('viewBox', `${-width/2} ${-height/2} ${width} ${height}`); d3.select('#ghost').attr('cy', -orbitDistance); simulateTrajectory(orbitalV, numPnts); // function simulateTrajectory(initV, numTicks) { const satellite = { mass: 0, x: 0, y: -orbitDistance, vx: initV, vy: 0 }, forceSim = d3.forceSimulation() .alphaDecay(0) .velocityDecay(0) .stop() .force('gravity', d3.forceMagnetic() .strength(G) .charge(d => d.mass) ) .nodes([ { mass: centralMass }, satellite ]); // Clear canvas const ctx = d3.select('canvas#trails') .attr('width', width) .attr('height', height) .node() .getContext('2d'); ctx.translate(width/2, height/2); ctx.fillStyle = 'rgba(0, 0, 75, .35)'; d3.range(numTicks).forEach(() => { forceSim.tick(); ctx.beginPath(); ctx.fillRect(satellite.x, satellite.y, 1, 1); ctx.fill(); }); // Animate satellite const elSatellite = d3.select('#satellite').datum(satellite); satellite.x = 0; satellite.y = -orbitDistance; satellite.vx = initV; satellite.vy = 0; forceSim.restart() .on('tick', () => { elSatellite.attr('cx', d => d.x) .attr('cy', d => d.y); }); } // Event handlers function onVelocityChange(relV) { d3.select('#velocity-val').text(relV); initialV = relV * orbitalV; simulateTrajectory(initialV, numPnts); } function onNumSamplesChange(num) { d3.select('#samples-val').text(num); numPnts = num; simulateTrajectory(initialV, numPnts); }