diff --git a/common.ts b/common.ts index 7fd27cf..1de809f 100644 --- a/common.ts +++ b/common.ts @@ -22,9 +22,12 @@ export function lerpPoint(a: Point, b: Point, p: number) { } export function lerpRgb(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) + // extract rgb components from a and b + const [ra, ga, ba] = [a >> 16, (a >> 8) & 0xff, a & 0xff]; + const [rb, gb, bb] = [b >> 16, (b >> 8) & 0xff, b & 0xff]; + const red = lerp(ra, rb, t); + const green = lerp(ga, gb, t); + const blue = lerp(ba, bb, t); return (red << 16) | (green << 8) | blue } @@ -59,6 +62,58 @@ export function cubicBezier(a: Point, b: Point, c: Point, d: Point, res=0.05) { return curve; } +export function catmullRom(points: Point[], { res = 0.05, looped = false }: { res?: number; looped?: boolean } = {}) { + let curve: Point[] = []; + // In non-looped mode, the curve goes from `points[1]` to `points[points.length - 2]`: the first + // and last points act as control points only. + // In looped mode, just treat the array as a circular array. + const tmax = looped ? points.length : points.length - 3; + for (let t = 0; t < tmax; t += res) { + const indices = looped ? getPointIndicesLooped(points, t) : getPointIndices(points, t); + const pos = getSplinePoint(...indices, t); + curve.push(pos); + } + return curve; + + function getPointIndices(points: Point[], t: number): [number, number, number, number] { + // Select 4 consecutive control points in the points array according to t. + // t = 1 => first point, t = 2 => second point, etc. + const p1 = Math.floor(t) + 1; + const p2 = p1 + 1; + const p3 = p2 + 1; + const p0 = p1 - 1; + return [p0, p1, p2, p3]; + } + + function getPointIndicesLooped(points: Point[], t: number): [number, number, number, number] { + const p1 = Math.floor(t); + const p2 = (p1 + 1) % points.length; + const p3 = (p2 + 1) % points.length; + const p0 = (p1 - 1 + points.length) % points.length; + return [p0, p1, p2, p3]; + } + + function getSplinePoint(p0: number, p1: number, p2: number, p3: number, t: number): Point { + // Normalize t to [0, 1] + t = t - Math.floor(t); + + const tt = t * t; + const ttt = tt * t; + + // Catmull-Rom basis functions, derived from the matrix form + const q1 = -ttt + 2.0 * tt - t; + const q2 = 3.0 * ttt - 5.0 * tt + 2.0; + const q3 = -3.0 * ttt + 4.0 * tt + t; + const q4 = ttt - tt; + + // Interpolate x and y coordinates + const tx = 0.5 * (points[p0].x * q1 + points[p1].x * q2 + points[p2].x * q3 + points[p3].x * q4); + const ty = 0.5 * (points[p0].y * q1 + points[p1].y * q2 + points[p2].y * q3 + points[p3].y * q4); + + return { x: tx, y: ty }; + } +} + export function resizeCanvas(ctx: CanvasRenderingContext2D) { ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.canvas.width = ctx.canvas.clientWidth; diff --git a/index.html b/index.html index 1ecd3d5..d542743 100644 --- a/index.html +++ b/index.html @@ -17,13 +17,14 @@ diff --git a/move.ts b/move.ts index b22b1d0..dbcd4b9 100644 --- a/move.ts +++ b/move.ts @@ -91,8 +91,8 @@ function init() { 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; + ctx.canvas.width = ctx.canvas.clientWidth; + ctx.canvas.height = ctx.canvas.clientHeight; canvas.onmousemove = (evt) => { const {clientX, clientY} = evt; diff --git a/snake.ts b/snake.ts index ff62214..d4ff318 100644 --- a/snake.ts +++ b/snake.ts @@ -89,7 +89,7 @@ function update(ctx: CanvasRenderingContext2D, time: number) { // convert snake_head coords to screen space const head = snake[0].wrap(bounds) if (head.distance(apple) < SNAKE_RADIUS * 2) { - apple = Vec2d.random(bounds.x, bounds.y) + apple = randomApple(bounds); const { x: lastX, y: lastY } = snake[snake.length - 1]; snake.push(new Vec2d(lastX, lastY)); speed += 10; diff --git a/spline.html b/spline.html index 233b7d9..ecd8cb4 100644 --- a/spline.html +++ b/spline.html @@ -3,10 +3,14 @@ - Document + Spline demo + - - + +
+ diff --git a/spline.ts b/spline.ts index d9cde2c..ea568b8 100644 --- a/spline.ts +++ b/spline.ts @@ -1,34 +1,45 @@ import { Point, resizeCanvas, - cubicBezier, quadraticBezier, drawCurve, drawPoints + cubicBezier, quadraticBezier, drawCurve, drawPoints, + catmullRom } from "./common.js" +let mode: "catmull" | "bezier" = "bezier"; +let looped = false; + function draw(ctx: CanvasRenderingContext2D, points: Point[]) { - let start = 0; + let start = 0; + if (mode === "bezier") { while (true) { - const sl = points.slice(start, start + 4); - if (sl.length === 4) { - const bezier = cubicBezier(...(sl as [Point, Point, Point, Point])); - drawCurve(ctx, bezier); - start += 3; - } else if (sl.length === 3) { - const bezier = quadraticBezier(...(sl as [Point, Point, Point])); - drawCurve(ctx, bezier); - start += 2; - } else { - break; - } + const sl = points.slice(start, start + 4); + if (sl.length === 4) { + const bezier = cubicBezier(...(sl as [Point, Point, Point, Point])); + drawCurve(ctx, bezier); + start += 3; + } else if (sl.length === 3) { + const bezier = quadraticBezier(...(sl as [Point, Point, Point])); + drawCurve(ctx, bezier); + start += 2; + } else { + break; + } } - drawPoints(ctx, points); + } else if (mode === "catmull") { + // Draw Catmull-Rom Spline. + const catmull = catmullRom(points, { looped: looped }); + drawCurve(ctx, catmull); + } + drawPoints(ctx, points); + setStatusLine(); } 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"); + resizeCanvas(ctx); let selection: number | undefined = undefined; let points = [ @@ -64,7 +75,7 @@ function init() { // redraw ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); draw(ctx, points); - + setStatusLine(); } }; @@ -81,7 +92,6 @@ function init() { // redraw ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); draw(ctx, points); - } }; @@ -92,6 +102,7 @@ function init() { // redraw ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); draw(ctx, points); + setStatusLine(); } }; @@ -104,6 +115,7 @@ function init() { // redraw ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); draw(ctx, points); + setStatusLine(); } }; @@ -114,6 +126,40 @@ function init() { canvas.ontouchend = () => { selection = undefined; }; + + window.onresize = () => { + resizeCanvas(ctx); + draw(ctx, points); + setStatusLine(); + }; + + window.onkeydown = (evt) => { + if (evt.key === " ") mode = mode === "bezier" ? "catmull" : "bezier"; + if (evt.key === "l") looped = !looped; + + // redraw + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + draw(ctx, points); + setStatusLine(); + }; +} + +function setStatusLine() { + const statusLine = document.getElementById("status-line"); + if (!statusLine) throw new Error("Could not find status line"); + + let msg = ""; + switch (mode) { + case "bezier": { + msg = "Bezier spline demo"; + break; + } + case "catmull": { + msg = looped ? "Catmull-Rom spline demo (looped)" : "Catmull-Rom spline demo"; + break; + } + } + statusLine.innerText = msg; } init(); diff --git a/vec.ts b/vec.ts index 4e121b7..cb81bdd 100644 --- a/vec.ts +++ b/vec.ts @@ -50,7 +50,7 @@ export default class Vec2d implements Point { } length(): number { - return Math.sqrt(this.x * this.x + this.y * this.y); + return Math.hypot(this.x, this.y); } distance(other: Vec2d): number {