From 0136981551e7978ccbbd0977f4e6972651ec86aa Mon Sep 17 00:00:00 2001 From: Thibaud Date: Wed, 5 Aug 2026 20:49:53 +0200 Subject: [PATCH] move: implement circle movement --- move.html | 5 ++- move.ts | 122 +++++++++++++++++++++++++++++++++--------------------- vec.ts | 4 ++ 3 files changed, 83 insertions(+), 48 deletions(-) diff --git a/move.html b/move.html index 83c6e2c..6d21d8f 100644 --- a/move.html +++ b/move.html @@ -4,8 +4,11 @@ Hello typescript + - + diff --git a/move.ts b/move.ts index cba0444..9c77de7 100644 --- a/move.ts +++ b/move.ts @@ -4,7 +4,9 @@ import { drawCircle, resizeCanvas } from "./common.js"; const SEGMENT_SPACING = 100; function initTrail(pos: Vec2d, len: number) { - return Array.from({ length: len }, (_, i) => pos.add(new Vec2d(0, -1).scale(i * SEGMENT_SPACING))); + return Array.from({ length: len }, (_, i) => + pos.add(new Vec2d(0, -1).scale(i * SEGMENT_SPACING)), + ); } // state @@ -12,19 +14,35 @@ let target: Vec2d | undefined = undefined; let trail = initTrail(new Vec2d(200, 200), 5); let velocity = new Vec2d(500, 500); let pause = false; -let mode: "follow" | "bounce" = "bounce"; +let mode: "follow" | "bounce" | "circle" = "follow"; let lastFrameTime: number | undefined = undefined; function update(ctx: CanvasRenderingContext2D, timestamp: number) { if (!lastFrameTime) lastFrameTime = timestamp; const dt = (timestamp - lastFrameTime) / 1000; // seconds lastFrameTime = timestamp; + if (!pause) { switch (mode) { - case "bounce": updateBounce(ctx, dt); break; - case "follow": updateFollow(ctx, dt); break; - default: throw new Error(`Unknown mode: ${mode}`); + case "bounce": + updateBounce(ctx, dt); + break; + case "follow": + updateFollow(ctx, dt); + break; + case "circle": + updateCircle(ctx, dt); + break; + default: + throw new Error(`Unknown mode: ${mode}`); } - if (!pause) window.requestAnimationFrame(t => update(ctx, t)); + } + + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + for (const pos of trail) { + drawCircle(ctx, pos, 20, 0xff00ff); + } + + window.requestAnimationFrame((t) => update(ctx, t)); } function updateFollow(ctx: CanvasRenderingContext2D, dt: number) { @@ -34,32 +52,43 @@ function updateFollow(ctx: CanvasRenderingContext2D, dt: number) { // frame, so this is framerate dependent: // trail[0] = trail[0].lerp(target, 0.1); // use exponential decay instead, this is framerate independent: - const alpha = Math.exp(-10 * dt) + const alpha = Math.exp(-10 * dt); trail[0] = trail[0].lerp(target, alpha); updateTrail(); } - - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - for (const pos of trail) { - drawCircle(ctx, pos, 20, 0xFF00FF); - } } function updateBounce(ctx: CanvasRenderingContext2D, dt: number) { - // update head pos - // P_t+1 = P_t + V * t - const newPos = trail[0].add(velocity.scale(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; } - trail[0] = newPos; - updateTrail(); + // update head pos + // P_t+1 = P_t + V * t + const newPos = trail[0].add(velocity.scale(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; + } + trail[0] = newPos; + updateTrail(); +} - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - for (const pos of trail) { - drawCircle(ctx, pos, 20, 0xFF00FF); - } +function updateCircle(ctx: CanvasRenderingContext2D, dt: number) { + const center = new Vec2d(ctx.canvas.width / 2, ctx.canvas.height / 2); + const rel = trail[0].sub(center); + const radius = rel.length(); + const theta = Math.atan2(rel.y, rel.x) + dt; + trail[0] = center.add(Vec2d.fromPolar(radius, theta)); + updateTrail(); } function updateTrail() { @@ -78,33 +107,32 @@ function updateTrail() { } 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 + 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 = ctx.canvas.clientWidth; + ctx.canvas.height = ctx.canvas.clientHeight; - canvas.onmousemove = (evt) => { - const {clientX, clientY} = evt; - target = new Vec2d(clientX, clientY); - } + canvas.onmousemove = (evt) => { + const { clientX, clientY } = evt; + target = new Vec2d(clientX, clientY); + }; - canvas.onclick = () => { - if (pause) { - window.requestAnimationFrame(t => update(ctx, t)); - } - pause = !pause; - } + canvas.onclick = () => { + pause = !pause; + }; - window.onkeydown = (evt) => { - if (mode === "follow") mode = "bounce"; - else mode = "follow" - } + window.onkeydown = (evt) => { + if (mode === "bounce") mode = "follow"; + else if (mode === "follow") mode = "circle"; + else if (mode === "circle") mode = "bounce"; + console.log(mode); + }; - window.addEventListener('resize', () => resizeCanvas(ctx)); + window.addEventListener("resize", () => resizeCanvas(ctx)); - window.requestAnimationFrame(t => update(ctx, t)); + window.requestAnimationFrame((t) => update(ctx, t)); } init(); diff --git a/vec.ts b/vec.ts index 042d7d7..cb1dee8 100644 --- a/vec.ts +++ b/vec.ts @@ -13,6 +13,10 @@ export default class Vec2d implements Point { this.y = y; } + static fromPolar(r: number, theta: number) { + return new Vec2d(r * Math.cos(theta), r * Math.sin(theta)); + } + static random(maxX: number, maxY: number) { return new Vec2d(Math.random() * maxX, Math.random() * maxY); }