move: implement circle movement

This commit is contained in:
2026-08-05 20:49:53 +02:00
parent 8032bdd31a
commit 0136981551
3 changed files with 83 additions and 48 deletions
+4 -1
View File
@@ -4,8 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello typescript</title> <title>Hello typescript</title>
<style>
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head> </head>
<body style="margin: 0; padding 0; overflow: hidden; background-color: #000000"> <body style="margin: 0; padding: 0; overflow: hidden; background-color: #000000">
<canvas id="canvas" tabindex="0"></canvas> <canvas id="canvas" tabindex="0"></canvas>
<script type="module" src="./move.js"></script> <script type="module" src="./move.js"></script>
</body> </body>
+61 -33
View File
@@ -4,7 +4,9 @@ import { drawCircle, resizeCanvas } from "./common.js";
const SEGMENT_SPACING = 100; const SEGMENT_SPACING = 100;
function initTrail(pos: Vec2d, len: number) { 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 // state
@@ -12,19 +14,35 @@ let target: Vec2d | undefined = undefined;
let trail = initTrail(new Vec2d(200, 200), 5); let trail = initTrail(new Vec2d(200, 200), 5);
let velocity = new Vec2d(500, 500); let velocity = new Vec2d(500, 500);
let pause = false; let pause = false;
let mode: "follow" | "bounce" = "bounce"; let mode: "follow" | "bounce" | "circle" = "follow";
let lastFrameTime: number | undefined = undefined; let lastFrameTime: number | undefined = undefined;
function update(ctx: CanvasRenderingContext2D, timestamp: number) { function update(ctx: CanvasRenderingContext2D, timestamp: number) {
if (!lastFrameTime) lastFrameTime = timestamp; if (!lastFrameTime) lastFrameTime = timestamp;
const dt = (timestamp - lastFrameTime) / 1000; // seconds const dt = (timestamp - lastFrameTime) / 1000; // seconds
lastFrameTime = timestamp; lastFrameTime = timestamp;
if (!pause) {
switch (mode) { switch (mode) {
case "bounce": updateBounce(ctx, dt); break; case "bounce":
case "follow": updateFollow(ctx, dt); break; updateBounce(ctx, dt);
default: throw new Error(`Unknown mode: ${mode}`); 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) { function updateFollow(ctx: CanvasRenderingContext2D, dt: number) {
@@ -34,32 +52,43 @@ function updateFollow(ctx: CanvasRenderingContext2D, dt: number) {
// frame, so this is framerate dependent: // frame, so this is framerate dependent:
// trail[0] = trail[0].lerp(target, 0.1); // trail[0] = trail[0].lerp(target, 0.1);
// use exponential decay instead, this is framerate independent: // 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); trail[0] = trail[0].lerp(target, alpha);
updateTrail(); 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) { function updateBounce(ctx: CanvasRenderingContext2D, dt: number) {
// update head pos // update head pos
// P_t+1 = P_t + V * t // P_t+1 = P_t + V * t
const newPos = trail[0].add(velocity.scale(dt)); 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.x > ctx.canvas.width - 100) {
if (newPos.y > ctx.canvas.height - 100) { velocity.y *= -1; newPos.y = ctx.canvas.height - 100; } velocity.x *= -1;
if (newPos.x < 100) { velocity.x *= -1; newPos.x = 100; } newPos.x = ctx.canvas.width - 100;
if (newPos.y < 100) { velocity.y *= -1; newPos.y = 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; trail[0] = newPos;
updateTrail(); updateTrail();
}
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); function updateCircle(ctx: CanvasRenderingContext2D, dt: number) {
for (const pos of trail) { const center = new Vec2d(ctx.canvas.width / 2, ctx.canvas.height / 2);
drawCircle(ctx, pos, 20, 0xFF00FF); 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() { function updateTrail() {
@@ -82,29 +111,28 @@ function init() {
if (!canvas) throw new Error("unable to get canvas HTML element"); if (!canvas) throw new Error("unable to get canvas HTML element");
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null; const ctx = canvas.getContext("2d") as CanvasRenderingContext2D | null;
if (!ctx) throw new Error("unable to get canvas 2D context"); if (!ctx) throw new Error("unable to get canvas 2D context");
ctx.canvas.width = window.innerWidth ctx.canvas.width = ctx.canvas.clientWidth;
ctx.canvas.height = window.innerHeight ctx.canvas.height = ctx.canvas.clientHeight;
canvas.onmousemove = (evt) => { canvas.onmousemove = (evt) => {
const {clientX, clientY} = evt; const { clientX, clientY } = evt;
target = new Vec2d(clientX, clientY); target = new Vec2d(clientX, clientY);
} };
canvas.onclick = () => { canvas.onclick = () => {
if (pause) {
window.requestAnimationFrame(t => update(ctx, t));
}
pause = !pause; pause = !pause;
} };
window.onkeydown = (evt) => { window.onkeydown = (evt) => {
if (mode === "follow") mode = "bounce"; if (mode === "bounce") mode = "follow";
else 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(); init();
+4
View File
@@ -13,6 +13,10 @@ export default class Vec2d implements Point {
this.y = y; 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) { static random(maxX: number, maxY: number) {
return new Vec2d(Math.random() * maxX, Math.random() * maxY); return new Vec2d(Math.random() * maxX, Math.random() * maxY);
} }