canvas {
  border: 1px solid black;
}
class Vec2 {
    constructor(readonly x: number = 0, readonly y: number = 0) { }
    plus(other: Vec2): Vec2 { return new Vec2(this.x + other.x, this.y + other.y); }
    times(scalar: number): Vec2 { return new Vec2(this.x * scalar, this.y * scalar); }
    minus(other: Vec2): Vec2 { return new Vec2(this.x - other.x, this.y - other.y); }
    length2(): number { return (this.x * this.x + this.y * this.y); }
    length(): number { return Math.sqrt(this.length2()) }
}

interface Numeric<T> {
    plus(other: T): T
    times(scalar: number): T
}

function runSimulation<T extends Numeric<T>>(
  y0: T,
  f: (t: number, y: T) => T,
  render: (y: T) => void
) {
    const h = (24 * 60 * 60) * 1 / 60.0

    function simulationStep(yi: T, ti: number) {
        render(yi)
        requestAnimationFrame(function() {
            // t_{i+1} = t_i + h
            const tNext = ti + h
            // y_{i+1} = y_i + h f(t_i, y_i)
            const yNext = yi.plus(f(ti, yi).times(h))
            simulationStep(yNext, tNext)
        })
    }
    simulationStep(y0, 0.0)
}

class TwoParticles implements Numeric<TwoParticles> {
    constructor(
        readonly x1: Vec2,
        readonly v1: Vec2,
        readonly x2: Vec2,
        readonly v2: Vec2
    ) { }

    plus(other: TwoParticles) {
        return new TwoParticles(
            this.x1.plus(other.x1),
            this.v1.plus(other.v1),
            this.x2.plus(other.x2),
            this.v2.plus(other.v2)
        );
    }

    times(scalar: number) {
        return new TwoParticles(
            this.x1.times(scalar),
            this.v1.times(scalar),
            this.x2.times(scalar),
            this.v2.times(scalar)
        )
    }
}

const canvas = document.createElement("canvas")
canvas.width = 400;
canvas.height = 400;
const ctx = canvas.getContext("2d")!;
document.body.appendChild(canvas);
ctx.fillStyle = "rgba(0, 0, 0, 0, 1)"
ctx.fillRect(0, 0, 400, 400);

function render(y: TwoParticles) {
    const { x1, x2 } = y;
    ctx.fillStyle = "rgba(0, 0, 0, 0.05)";
    ctx.fillRect(0, 0, 400, 400);

    const rEarth = 6.371e6/1e9;
    const rMoon = 1.73e6/1e9;
    ctx.fillStyle = "rgba(45, 66, 143, 1)";
    ctx.beginPath();
    ctx.ellipse((x1.x/1e9)*400 + 200, (x1.y/1e9)*400 + 200, rEarth*400*10, rEarth*400*10, 0, 0, 2 * Math.PI);
    ctx.fill();

    ctx.fillStyle = "rgba(189, 189, 189, 1)";
    ctx.beginPath();
    ctx.ellipse((x2.x/1e9)*400 + 200, (x2.y/1e9)*400 + 200, rMoon*400*10, rMoon*400*10, 0, 0, 2 * Math.PI);
    ctx.fill();
}


const G = 6.67e-11;
const m1 = 5.972e24;
const m2 = 7.34e22;

function f(t: number, y: TwoParticles) {
    const { x1, v1, x2, v2 } = y;
    return new TwoParticles(
        // dx1/dt = v1
        v1,
        // dv1/dt = G*m2*(x2-x1)/|x2-x1|^3
        x2.minus(x1).times(G * m2 / Math.pow(x2.minus(x1).length(), 3)),
        // dx2/dt = v2
        v2,
        // dv2/dt = G*m1*(x1-x1)/|x1-x2|^3
        x1.minus(x2).times(G * m1 / Math.pow(x1.minus(x2).length(), 3))
    )
}

const y0 = new TwoParticles(
    /* x1 */ new Vec2(0, 0),
    /* v1 */ new Vec2(0, -13.22),
    /* x2 */ new Vec2(3.6e8, 0),
    /* v2 */ new Vec2(0, 1.076e3)
)

runSimulation(y0, f, render)
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.