124 lines
3.5 KiB
TypeScript
124 lines
3.5 KiB
TypeScript
|
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();
|