class Vec2d { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } toString() { return `[${this.x}, ${this.y}]`; } scale(scalar: number): Vec2d { return new Vec2d(this.x * scalar, this.y * scalar); } add(other: Vec2d): Vec2d { return new Vec2d(this.x + other.x, this.y + other.y) } lerp(other: Vec2d, t: number): Vec2d { // (1-t)*A + B*t return this.scale(1-t).add(other.scale(t)); } } // state let target: Vec2d | undefined = undefined; let pos = new Vec2d(200, 200); let velocity = new Vec2d(500, 500); let pause = false; let mode: "follow" | "bounce" = "bounce"; let start: number | undefined = undefined; function drawCircle(ctx: CanvasRenderingContext2D, center: Vec2d, radius: number, color: number) { ctx.save(); ctx.beginPath(); ctx.arc(center.x, center.y, radius, 0, 2*Math.PI); ctx.strokeStyle = `#${color.toString(16)}` ctx.lineWidth = 5; ctx.stroke(); ctx.restore(); } function resizeCanvas(ctx: CanvasRenderingContext2D) { ctx.canvas.width = window.innerWidth; ctx.canvas.height = window.innerHeight; ctx.clearRect(0, 0, ctx.canvas.height, ctx. canvas.width); } function update(ctx: CanvasRenderingContext2D, timestamp: number) { switch (mode) { case "bounce": updateBounce(ctx, timestamp); break; case "follow": updateFollow(ctx); break; default: throw new Error(`Unknown mode: ${mode}`); } if (!pause) window.requestAnimationFrame(t => update(ctx, t)); } function updateFollow(ctx: CanvasRenderingContext2D) { if (target) pos = pos.lerp(target, 0.01); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawCircle(ctx, pos, 100, 0xFF00FF); } function updateBounce(ctx: CanvasRenderingContext2D, timestamp: number) { if (!start) start = timestamp; const dt = timestamp - start; // P_t+1 = P_t + V * t const newPos = pos.add(velocity.scale(0.001*dt)); if (newPos.x > ctx.canvas.width - 100) { velocity.x *= -1; newPos.x = ctx.canvas.width - 100; } if (newPos.y > ctx.canvas.height - 100) { velocity.y *= -1; newPos.y = ctx.canvas.height - 100; } if (newPos.x < 100) { velocity.x *= -1; newPos.x = 100; } if (newPos.y < 100) { velocity.y *= -1; newPos.y = 100; } //console.log(velocity); pos = newPos; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); drawCircle(ctx, pos, 100, 0xFF00FF); start = timestamp; } function init() { const canvas = document.getElementById("canvas") as HTMLCanvasElement | null; if (!canvas) throw new Error("unable to get canvas HTML element"); const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; if (!ctx) throw new Error("unable to get canvas 2D context"); ctx.canvas.width = window.innerWidth; ctx.canvas.height = window.innerHeight; canvas.onmousemove = (evt) => { const {clientX, clientY} = evt; target = new Vec2d(clientX, clientY); } canvas.onclick = () => { if (pause) { window.requestAnimationFrame(t => update(ctx, t)); } pause = !pause; } window.onkeydown = (evt) => { console.log("key down", evt); if (mode === "follow") mode = "bounce"; else mode = "follow" console.log("mode", mode); } window.addEventListener('resize', () => resizeCanvas(ctx)); window.requestAnimationFrame(t => update(ctx, t)); } init();