interface Numeric { plus(other: T): T times(scalar: number): T } function runSimulation>( y0: T, f: (t: number, y: T) => T, applyConstraints: (y: T) => T, iterationsPerFrame: number, render: (y: T) => void ) { const frameTime = 1 / 60.0 const h = frameTime / iterationsPerFrame function simulationStep(yi: T, ti: number) { render(yi) requestAnimationFrame(function () { for (let i = 0; i < iterationsPerFrame; i++) { yi = yi.plus(f(ti, yi).times(h)) yi = applyConstraints(yi) ti = ti + h } simulationStep(yi, ti) }) } simulationStep(y0, 0.0) } 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); const g = -9.8; // m / s^2 const r = 0.2; // m class Ball implements Numeric { constructor(readonly x: number, readonly v: number) { } plus(other: Ball) { return new Ball(this.x + other.x, this.v + other.v) } times(scalar: number) { return new Ball(this.x * scalar, this.v * scalar) } } function f(t: number, y: Ball) { const { x, v } = y return new Ball(v, g) } function applyConstraints(y: Ball): Ball { const { x, v } = y if (x <= 0 && v < 0) { return new Ball(x, -v) } return y } const y0 = new Ball( /* x */ 0.8, /* v */ 0 ) function render(y: Ball) { ctx.clearRect(0, 0, 400, 400) ctx.fillStyle = '#EB5757' ctx.beginPath() ctx.ellipse(200, 400 - ((y.x + r) * 300), r * 300, r * 300, 0, 0, 2 * Math.PI) ctx.fill() } runSimulation(y0, f, applyConstraints, 30, render)