spline: add catmull-rom demo
This commit is contained in:
@@ -22,9 +22,12 @@ export function lerpPoint(a: Point, b: Point, p: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function lerpRgb(a: number, b: number, t: number) {
|
export function lerpRgb(a: number, b: number, t: number) {
|
||||||
const red = lerp(a >> 16, b >> 16, t)
|
// extract rgb components from a and b
|
||||||
const green = lerp((a >> 8) & 0xff, (b >> 8) & 0xff, t)
|
const [ra, ga, ba] = [a >> 16, (a >> 8) & 0xff, a & 0xff];
|
||||||
const blue = lerp(a & 0xff, b & 0xff, t)
|
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
|
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;
|
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) {
|
export function resizeCanvas(ctx: CanvasRenderingContext2D) {
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
ctx.canvas.width = ctx.canvas.clientWidth;
|
ctx.canvas.width = ctx.canvas.clientWidth;
|
||||||
|
|||||||
+2
-1
@@ -17,13 +17,14 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="bezier-1.html">Bezier demo #1</a></li>
|
<li><a href="bezier-1.html">Bezier demo #1</a></li>
|
||||||
<li><a href="bezier-2.html">Bezier demo #2</a></li>
|
<li><a href="bezier-2.html">Bezier demo #2</a></li>
|
||||||
<li><a href="spline.html">Bezier spline demo</a></li>
|
<li><a href="spline.html">Splines demo</a></li>
|
||||||
<li><a href="metaballs.html">Metaballs</a></li>
|
<li><a href="metaballs.html">Metaballs</a></li>
|
||||||
<li><a href="image.html">Image demo</a></li>
|
<li><a href="image.html">Image demo</a></li>
|
||||||
<li><a href="move.html">Move demo</a></li>
|
<li><a href="move.html">Move demo</a></li>
|
||||||
<li><a href="life.html">Game of life</a></li>
|
<li><a href="life.html">Game of life</a></li>
|
||||||
<li><a href="ray.html">Ray</a></li>
|
<li><a href="ray.html">Ray</a></li>
|
||||||
<li><a href="snake.html">Snake</a></li>
|
<li><a href="snake.html">Snake</a></li>
|
||||||
|
<li><a href="snake2.html">Snake (trail)</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ function update(ctx: CanvasRenderingContext2D, time: number) {
|
|||||||
// convert snake_head coords to screen space
|
// convert snake_head coords to screen space
|
||||||
const head = snake[0].wrap(bounds)
|
const head = snake[0].wrap(bounds)
|
||||||
if (head.distance(apple) < SNAKE_RADIUS * 2) {
|
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];
|
const { x: lastX, y: lastY } = snake[snake.length - 1];
|
||||||
snake.push(new Vec2d(lastX, lastY));
|
snake.push(new Vec2d(lastX, lastY));
|
||||||
speed += 10;
|
speed += 10;
|
||||||
|
|||||||
+7
-3
@@ -3,10 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<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>Document</title>
|
<title>Spline demo</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" width="800" height="600"></canvas>
|
<div style="color: white; padding: 1rem;" id="status-line"></div>
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
</body>
|
</body>
|
||||||
<script type="module" src="spline.js"></script>
|
<script type="module" src="spline.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
Point, resizeCanvas,
|
Point, resizeCanvas,
|
||||||
cubicBezier, quadraticBezier, drawCurve, drawPoints
|
cubicBezier, quadraticBezier, drawCurve, drawPoints,
|
||||||
|
catmullRom
|
||||||
} from "./common.js"
|
} from "./common.js"
|
||||||
|
|
||||||
|
let mode: "catmull" | "bezier" = "bezier";
|
||||||
|
let looped = false;
|
||||||
|
|
||||||
|
|
||||||
function draw(ctx: CanvasRenderingContext2D, points: Point[]) {
|
function draw(ctx: CanvasRenderingContext2D, points: Point[]) {
|
||||||
let start = 0;
|
let start = 0;
|
||||||
|
if (mode === "bezier") {
|
||||||
while (true) {
|
while (true) {
|
||||||
const sl = points.slice(start, start + 4);
|
const sl = points.slice(start, start + 4);
|
||||||
if (sl.length === 4) {
|
if (sl.length === 4) {
|
||||||
@@ -20,15 +25,21 @@ function draw(ctx: CanvasRenderingContext2D, points: Point[]) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (mode === "catmull") {
|
||||||
|
// Draw Catmull-Rom Spline.
|
||||||
|
const catmull = catmullRom(points, { looped: looped });
|
||||||
|
drawCurve(ctx, catmull);
|
||||||
|
}
|
||||||
drawPoints(ctx, points);
|
drawPoints(ctx, points);
|
||||||
|
setStatusLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement | null;
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement | null;
|
||||||
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");
|
||||||
|
resizeCanvas(ctx);
|
||||||
|
|
||||||
let selection: number | undefined = undefined;
|
let selection: number | undefined = undefined;
|
||||||
let points = [
|
let points = [
|
||||||
@@ -64,7 +75,7 @@ function init() {
|
|||||||
// redraw
|
// redraw
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
draw(ctx, points);
|
draw(ctx, points);
|
||||||
|
setStatusLine();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,7 +92,6 @@ function init() {
|
|||||||
// redraw
|
// redraw
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
draw(ctx, points);
|
draw(ctx, points);
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,6 +102,7 @@ function init() {
|
|||||||
// redraw
|
// redraw
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
draw(ctx, points);
|
draw(ctx, points);
|
||||||
|
setStatusLine();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,6 +115,7 @@ function init() {
|
|||||||
// redraw
|
// redraw
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
draw(ctx, points);
|
draw(ctx, points);
|
||||||
|
setStatusLine();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,6 +126,40 @@ function init() {
|
|||||||
canvas.ontouchend = () => {
|
canvas.ontouchend = () => {
|
||||||
selection = undefined;
|
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();
|
init();
|
||||||
|
|||||||
Reference in New Issue
Block a user