/************************
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();