diff --git a/common.ts b/common.ts index 9163e83..8d9c35a 100644 --- a/common.ts +++ b/common.ts @@ -21,6 +21,14 @@ export function lerp(a: Point, b: Point, p: number) { }; } +export function lerpRgb(a: number, b: number, t: number) { + const _lerp = (a: number, b: number, t: number) => (1-t)*a + b*t; + const red = _lerp(a >> 16, b >> 16, t) + const green = _lerp((a >> 8) & 0xff, (b >> 8) & 0xff, t) + const blue = _lerp(a & 0xff, b & 0xff, t) + return (red << 16) | (green << 8) | blue +} + export function quadraticBezier(a: Point, b: Point, c: Point, res=0.05) { const eps = 0.001; // to prevent issues with float comparaison (p <= 1) const curve = []; @@ -64,6 +72,15 @@ export function drawCircle(ctx: CanvasRenderingContext2D, center: Point, radius: ctx.restore(); } +export function fillCircle(ctx: CanvasRenderingContext2D, center: Point, radius: number, color: number) { + ctx.save(); + ctx.beginPath(); + ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); + ctx.fillStyle = `#${color.toString(16)}`; + ctx.fill(); + ctx.restore(); +} + export function drawLine(ctx: CanvasRenderingContext2D, start: Point, end: Point, color: number, dashed=false) { ctx.save(); ctx.beginPath(); diff --git a/move.ts b/move.ts index 68ba945..ef5a478 100644 --- a/move.ts +++ b/move.ts @@ -1,29 +1,5 @@ -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)); - } -} +import Vec2d from "./vec.js"; +import { drawCircle, resizeCanvas } from "./common.js"; // state let target: Vec2d | undefined = undefined; @@ -33,28 +9,12 @@ 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": + case "follow": updateFollow(ctx); break; default: @@ -112,7 +72,7 @@ function init() { console.log("key down", evt); if (mode === "follow") mode = "bounce"; else mode = "follow" - console.log("mode", mode); + console.log("mode", mode); } window.addEventListener('resize', () => resizeCanvas(ctx)); diff --git a/ray.ts b/ray.ts index 2e3e2c4..c2f360e 100644 --- a/ray.ts +++ b/ray.ts @@ -1,7 +1,4 @@ -import { - Point, resizeCanvas, drawCircle, drawLine, - cubicBezier, quadraticBezier, drawCurve, drawPoints -} from "./common.js" +import { Point, resizeCanvas, drawCircle } from "./common.js" const GRID_SIZE = 20; @@ -62,4 +59,3 @@ function drawGrid(ctx: CanvasRenderingContext2D) { } init(); - diff --git a/snake.ts b/snake.ts index edf4018..9505b24 100644 --- a/snake.ts +++ b/snake.ts @@ -1,52 +1,5 @@ -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); - } - - sub(other: Vec2d): Vec2d { - return new Vec2d(this.x - other.x, this.y - other.y); - } - - length(): number { - return Math.sqrt(this.x * this.x + this.y * this.y); - } - - distance(other: Vec2d): number { - return this.sub(other).length(); - } - - lerp(other: Vec2d, t: number): Vec2d { - // (1-t)*A + B*t - return this.scale(1 - t).add(other.scale(t)); - } - - clamp(min: Vec2d, max: Vec2d): Vec2d { - return new Vec2d( - Math.min(Math.max(this.x, min.x), max.x), - Math.min(Math.max(this.y, min.y), max.y) - ); - } - - wrap(bounds: Vec2d) { - return new Vec2d(trueMod(this.x, bounds.x), trueMod(this.y, bounds.y)); - } -} +import Vec2d from "./vec.js"; +import { fillCircle, lerpRgb } from "./common.js"; // constants const SNAKE_SIZE = 20; @@ -71,41 +24,12 @@ let speed = 200; // px/s let interpolation_factor = 10; let paused = false; -// utility functions -function trueMod(v: number, max: number): number { - return ((v % max) + max) % max; -} - function resizeCanvas(ctx: CanvasRenderingContext2D) { ctx.canvas.width = ctx.canvas.clientWidth; ctx.canvas.height = ctx.canvas.clientHeight; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); } -function drawCircle(ctx: CanvasRenderingContext2D, center: Vec2d, radius: number, color: string) { - ctx.save(); - ctx.beginPath(); - ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); - ctx.fillStyle = color; - ctx.fill(); - ctx.restore(); -} - -function getRandomPos(maxX: number, maxY: number): Vec2d { - return new Vec2d(Math.random() * maxX, Math.random() * maxY) -} - -function lerpColor(a: number, b: number, t: number) { - const red = lerp(a >> 16, b >> 16, t) - const green = lerp((a >> 8) & 0xff, (b >> 8) & 0xff, t) - const blue = lerp(a & 0xff, b & 0xff, t) - return (red << 16) | (green << 8) | blue -} - -function lerp(a: number, b: number, t: number) { - return (1-t)*a + b*t -} - let lastTime: number | undefined = undefined; function update(ctx: CanvasRenderingContext2D, t: number) { @@ -135,7 +59,7 @@ function update(ctx: CanvasRenderingContext2D, t: number) { const head = snake[0].wrap(bounds) // check if snake head is overlapping apple if (head.distance(apple) < SNAKE_SIZE) { - apple = getRandomPos(ctx.canvas.width, ctx.canvas.height); + apple = Vec2d.random(bounds.x, bounds.y) const { x: lastX, y: lastY } = snake[snake.length - 1]; snake.push(new Vec2d(lastX, lastY)); speed += 10; @@ -145,19 +69,19 @@ function update(ctx: CanvasRenderingContext2D, t: number) { // draw apple ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - drawCircle(ctx, apple, SNAKE_SIZE / 2, `#${APPLE_COLOR.toString(16)}`) + fillCircle(ctx, apple, SNAKE_SIZE / 2, APPLE_COLOR); // draw snake for (let i = 0; i < snake.length - 1; ++i) { // draw a trail of overlapping circles by interpolating between the body positions for (let j = 0; j < interpolation_factor; ++j) { const pos = snake[i].lerp(snake[i + 1], j / interpolation_factor).wrap(bounds); - const color = lerpColor(HEAD_COLOR, TAIL_COLOR, (i + j / interpolation_factor) / snake.length) - drawCircle(ctx, pos, SNAKE_SIZE / 2, `#${color.toString(16)}`) + const color = lerpRgb(HEAD_COLOR, TAIL_COLOR, (i + j / interpolation_factor) / snake.length) + fillCircle(ctx, pos, SNAKE_SIZE / 2, color) } } const tail = snake[snake.length - 1].wrap(bounds) - drawCircle(ctx, tail, SNAKE_SIZE / 2, `#${TAIL_COLOR.toString(16)}`) + fillCircle(ctx, tail, SNAKE_SIZE / 2, TAIL_COLOR) window.requestAnimationFrame((t) => update(ctx, t)); } @@ -170,7 +94,7 @@ function init() { if (!ctx) throw new Error("unable to get canvas 2D context"); resizeCanvas(ctx); - apple = getRandomPos(canvas.width, canvas.height) + apple = Vec2d.random(canvas.width, canvas.height) canvas.onmousemove = () => {}; diff --git a/spline.ts b/spline.ts index 7b6ad32..d9cde2c 100644 --- a/spline.ts +++ b/spline.ts @@ -1,5 +1,5 @@ -import { - Point, resizeCanvas, drawCircle, drawLine, +import { + Point, resizeCanvas, cubicBezier, quadraticBezier, drawCurve, drawPoints } from "./common.js" @@ -40,7 +40,7 @@ function init() { { x: 850, y: 450 }, { x: 900, y: 550 }, ]; - + resizeCanvas(ctx); // Init canvas draw(ctx, points); diff --git a/vec.ts b/vec.ts new file mode 100644 index 0000000..97f9345 --- /dev/null +++ b/vec.ts @@ -0,0 +1,59 @@ +import { Point } from "./common"; + +function trueMod(v: number, max: number): number { + return ((v % max) + max) % max; +} + +export default class Vec2d implements Point { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + static random(maxX: number, maxY: number) { + return new Vec2d(Math.random() * maxX, Math.random() * maxY); + } + + 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); + } + + sub(other: Vec2d): Vec2d { + return new Vec2d(this.x - other.x, this.y - other.y); + } + + length(): number { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + distance(other: Vec2d): number { + return this.sub(other).length(); + } + + lerp(other: Vec2d, t: number): Vec2d { + // (1-t)*A + B*t + return this.scale(1 - t).add(other.scale(t)); + } + + clamp(min: Vec2d, max: Vec2d): Vec2d { + return new Vec2d( + Math.min(Math.max(this.x, min.x), max.x), + Math.min(Math.max(this.y, min.y), max.y) + ); + } + + wrap(bounds: Vec2d) { + return new Vec2d(trueMod(this.x, bounds.x), trueMod(this.y, bounds.y)); + } +}