/************************ Ordinal Three Body Orbits ************************/ class Graphics { constructor(effects, masses) { this.metersPerPixel = 100; this.previousBodyPositions = Array(masses.length).fill(null).map(() => ({ x: null, y: null })); this.bodyPositions = Array(masses.length).fill(null).map(() => []); this.middleX = 1; this.middleY = 1; this.effects = effects; this.masses = masses; this.frameTime = 1 / effects.currentFPS; this.frameLostTolerance = effects.settings.frameLostTolerance; this.nextDrawTime = performance.now() / 1000 + this.frameTime; } calculateNewPositions(statePositions) { for (var iBody = 0; iBody < statePositions.length / 4; iBody++) { var bodyStart = iBody * 4; var x = statePositions[bodyStart + 0]; var y = statePositions[bodyStart + 1]; var position = { x: x / this.metersPerPixel + this.middleX, y: -y / this.metersPerPixel + this.middleY }; this.bodyPositions[iBody].push(position); } } clearPositions() { this.bodyPositions.forEach(positions => positions.length = 0); } clearPreviousPositions() { this.previousBodyPositions = Array(masses.length).fill(null).map(() => ({ x: null, y: null })); } drawOrbitalLines() { this.effects.onEachFrame() this.bodyPositions.forEach((positions, iBody) => { if (positions.length == 0) return const newPosition = positions[positions.length - 1]; let previousPosition = this.previousBodyPositions[iBody]; if (previousPosition.x === null || previousPosition.y === null) { previousPosition.x = newPosition.x; previousPosition.y = newPosition.y; return; } this.effects.draw(previousPosition, positions, iBody); previousPosition.x = newPosition.x; previousPosition.y = newPosition.y; }); } fitToContainer() { let actualSize = Math.min(window.innerHeight, window.innerWidth); this.effects.fitCanvas(actualSize); this.canvasHeight = actualSize * this.effects.dpr; this.middleX = Math.floor(this.canvasHeight / 2); this.middleY = Math.floor(this.canvasHeight / 2); this.frameTime = 1 / this.effects.currentFPS; this.nextDrawTime = performance.now() / 1000 + this.frameTime; } init() { if (this.effects.canvasNotSupported()) { return; } this.fitToContainer(); } clearScene(largestDistanceMeters) { this.clearPositions(); this.previousBodyPositions = Array(this.masses.length).fill(null).map(() => ({ x: null, y: null })); this.metersPerPixel = (this.effects.zoomOut * 2.3 * largestDistanceMeters) / this.canvasHeight; } } class Simulation { constructor(effects, masses, positions, velocities, period, maxDist, expansionRatio, isRelativelyPeriodic) { [positions, velocities, period, maxDist] = this.expansion(positions, velocities, period, maxDist, expansionRatio); this.maxCalculationTime = 5000000; this.period = period; this.periodLeft = period; this.maxDist = maxDist; this.isRelativelyPeriodic = isRelativelyPeriodic; this.graphics = new Graphics(effects, masses); this.physics = new Physics(); this.setCurrentModel(masses, period, positions, velocities); effects.simulation = this; } expansion(positions, velocities, period, maxDist, expansionRatio) { return [ positions.map(x => x.map(y => y * expansionRatio)), velocities.map(x => x.map(y => y * Math.sqrt(1 / expansionRatio))), period * expansionRatio / Math.sqrt(1 / expansionRatio), maxDist * expansionRatio ] } checkPeriod() { if (this.periodLeft <= 0) { this.periodLeft = this.period; this.graphics.effects.onEachPeriod(); if (this.graphics.effects.shouldResetEachPeriod) { this.graphics.clearPreviousPositions(); if (this.isRelativelyPeriodic) { this.physics.shiftConditions(); } else { this.physics.resetStateToInitialConditions(); } this.graphics.calculateNewPositions(this.physics.state.u); this.graphics.drawOrbitalLines(); } } } showErrorMsg() { this.graphics.effects.showMessage('Computation Resource Over Limit', 'The current simulation can
\ put the browser
\ environment under substantial
\ computational load.
\ This strain may lead
\ to the algorithm becoming
\ stuck in a local optimum,
\ inhibiting the effective
\ pursuit of the correct solution.
\ To ensure smooth performance
\ across various browsers and to
\ prevent freezes, simulations
\ requiring extensive computational
\ resources are disabled.
\ Truth is so rare; our arithmetic is
\ but a child\'s play, far from\ adequate to grasp it.'); } animate() { let gapTime = this.graphics.frameTime; let currentTime = performance.now() / 1000; if (currentTime < this.graphics.nextDrawTime) { // Skip this frame if the previous frame hasn't finished drawing yet window.requestAnimationFrame(this.animate.bind(this)); return; } else if (currentTime > this.graphics.nextDrawTime + this.graphics.frameTime * this.graphics.frameLostTolerance) { // if user switched tabs or paused execution, reset graphics.nextDrawTime this.graphics.nextDrawTime = currentTime + this.graphics.frameTime; } else { // cacluate gap time which is how many frameTime's have passed since last draw gapTime = Math.ceil((currentTime - this.graphics.nextDrawTime) / this.graphics.frameTime) * this.graphics.frameTime; } let accumulatedTime = 0; let dt; for (var i = 0; i < this.maxCalculationTime && accumulatedTime < gapTime && this.periodLeft > 0; i++) { dt = this.physics.updatePosition(Math.min(this.periodLeft, (gapTime - accumulatedTime))); if (isNaN(dt) || dt < 1e-12) { console.log('invalid timestep:', dt); this.showErrorMsg(); return; } this.periodLeft -= dt; this.graphics.calculateNewPositions(this.physics.state.u); accumulatedTime += dt; } this.graphics.nextDrawTime += accumulatedTime; currentTime = performance.now() / 1000; let delay = (this.graphics.nextDrawTime - currentTime) * 1000; setTimeout(() => { this.graphics.drawOrbitalLines(); this.graphics.clearPositions(); this.checkPeriod(dt); window.requestAnimationFrame(this.animate.bind(this)) }, delay); } start() { this.graphics.init(); this.physics.resetStateToInitialConditions(); this.graphics.clearScene(this.maxDist); window.addEventListener('resize', (event) => { this.graphics.fitToContainer(); this.graphics.clearScene(this.maxDist); this.graphics.calculateNewPositions(this.physics.state.u); this.graphics.drawOrbitalLines(); }); this.animate(); } setCurrentModel(masses, period, positions, velocities) { this.currentModel = { masses: masses, period: period, positions: positions.map(function (item) { return { x: item[0], y: item[1] } }), velocities: velocities.map(function (item) { return { x: item[0], y: item[1] } }), }; console.log(this.currentModel); this.physics.changeInitialConditions(this.currentModel); } } async function fetchBlockHeight() { try { const response = await fetch('/blockheight'); const data = await response.text(); let currentBlock = parseInt(data, 10); return currentBlock; } catch (error) { console.error('Error:', error); } } async function runSimulation() { let currentBlock = await fetchBlockHeight(); if (isNaN(currentBlock)) { var selectColor = 5; // currentBlock = 210000 * selectColor + 1; currentBlock = 2016 * selectColor + 2; console.log("Fall back to local testing, currentBlock: ", currentBlock); } else { console.log("currentBlock: ", currentBlock); } let effects = new Effect(effectsSettings, currentBlock); window.effects = effects; if (typeof expansionRatio === 'undefined') { expansionRatio = 1; } if (typeof isRelativelyPeriodic === 'undefined') { isRelativelyPeriodic = false; } let simulation = new Simulation(effects, masses, positions, velocities, period, maxDist, expansionRatio, isRelativelyPeriodic); simulation.start(); } runSimulation();