SOURCE / PINNED RELEASE
Made of little things.
Big Rig Rush
- Release
- ec9b6549f752…
- Author-recorded commit
- 74b729258b1a…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-0a99f0cae40
Archive hash verified: 900e22810340ccb1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
export interface Vec {
x: number;
y: number;
}
export interface Pose {
x: number;
y: number;
angle: number;
}
/** A polyline with cumulative lengths and a bounding box. */
export interface Path {
pts: Vec[];
cum: number[];
len: number;
x0: number;
y0: number;
x1: number;
y1: number;
}
export const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v));
export function makePath(pts: Vec[]): Path {
const cum = [0];
let x0 = Infinity;
let y0 = Infinity;
let x1 = -Infinity;
let y1 = -Infinity;
for (let i = 0; i < pts.length; i++) {
const p = pts[i];
if (i > 0) cum.push(cum[i - 1] + Math.hypot(p.x - pts[i - 1].x, p.y - pts[i - 1].y));
x0 = Math.min(x0, p.x);
y0 = Math.min(y0, p.y);
x1 = Math.max(x1, p.x);
y1 = Math.max(y1, p.y);
}
return { pts, cum, len: cum[cum.length - 1], x0, y0, x1, y1 };
}
export function pointAt(path: Path, s: number): Vec {
const { pts, cum } = path;
if (pts.length === 1) return pts[0];
const d = clamp(s, 0, path.len);
let lo = 0;
let hi = cum.length - 1;
while (hi - lo > 1) {
const mid = (lo + hi) >> 1;
if (cum[mid] <= d) lo = mid;
else hi = mid;
}
const a = pts[lo];
const b = pts[hi];
const t = (d - cum[lo]) / (cum[hi] - cum[lo] || 1);
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
}
/** Position and a smoothed heading (looks a little ahead and behind). */
export function poseAt(path: Path, s: number): Pose {
const p = pointAt(path, s);
const a = pointAt(path, s - 2);
const b = pointAt(path, s + 2);
return { x: p.x, y: p.y, angle: Math.atan2(b.y - a.y, b.x - a.x) };
}
export function cubicPoints(p0: Vec, p1: Vec, p2: Vec, p3: Vec, n: number): Vec[] {
const out: Vec[] = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const u = 1 - t;
out.push({
x: u * u * u * p0.x + 3 * u * u * t * p1.x + 3 * u * t * t * p2.x + t * t * t * p3.x,
y: u * u * u * p0.y + 3 * u * u * t * p1.y + 3 * u * t * t * p2.y + t * t * t * p3.y,
});
}
return out;
}
export function quadPoints(p0: Vec, p1: Vec, p2: Vec, n: number): Vec[] {
const out: Vec[] = [];
for (let i = 0; i <= n; i++) {
const t = i / n;
const u = 1 - t;
out.push({ x: u * u * p0.x + 2 * u * t * p1.x + t * t * p2.x, y: u * u * p0.y + 2 * u * t * p1.y + t * t * p2.y });
}
return out;
}
/** Offset a polyline to the right of its travel direction (canvas y points down). */
export function offsetPoints(pts: Vec[], off: number, closed = false): Vec[] {
const n = pts.length;
return pts.map((p, i) => {
const prev = i > 0 ? pts[i - 1] : closed ? pts[n - 1] : p;
const next = i < n - 1 ? pts[i + 1] : closed ? pts[0] : p;
const tx = next.x - prev.x;
const ty = next.y - prev.y;
const l = Math.hypot(tx, ty) || 1;
return { x: p.x - (ty / l) * off, y: p.y + (tx / l) * off };
});
}
export function tracePath(ctx: CanvasRenderingContext2D, pts: Vec[], closed = false): void {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
if (closed) ctx.closePath();
}
export const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
export function angleDiff(a: number, b: number): number {
let d = a - b;
while (d > Math.PI) d -= Math.PI * 2;
while (d < -Math.PI) d += Math.PI * 2;
return d;
}
/** Smooth curve through control points (Catmull-Rom), resampled about every `step` units. */
export function smoothCurve(ctrl: Vec[], step: number): Vec[] {
if (ctrl.length < 3) return resample(ctrl, step);
const dense: Vec[] = [];
for (let i = 0; i < ctrl.length - 1; i++) {
const p0 = ctrl[Math.max(0, i - 1)];
const p1 = ctrl[i];
const p2 = ctrl[i + 1];
const p3 = ctrl[Math.min(ctrl.length - 1, i + 2)];
const n = Math.max(4, Math.ceil(Math.hypot(p2.x - p1.x, p2.y - p1.y) / (step * 0.5)));
for (let k = 0; k < n; k++) {
const t = k / n;
const t2 = t * t;
const t3 = t2 * t;
dense.push({
x: 0.5 * (2 * p1.x + (-p0.x + p2.x) * t + (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 + (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3),
y: 0.5 * (2 * p1.y + (-p0.y + p2.y) * t + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3),
});
}
}
dense.push(ctrl[ctrl.length - 1]);
return resample(dense, step);
}
/** Evenly respace a polyline (keeps both ends). */
export function resample(pts: Vec[], step: number): Vec[] {
const path = makePath(pts);
const n = Math.max(1, Math.round(path.len / step));
const out: Vec[] = [];
for (let i = 0; i <= n; i++) out.push(pointAt(path, (path.len * i) / n));
return out;
}
/** The part of a polyline between distances s0 and s1 along it. */
export function slicePath(pts: Vec[], s0: number, s1: number): Vec[] {
const path = makePath(pts);
const a = clamp(s0, 0, path.len);
const b = clamp(s1, a, path.len);
const out: Vec[] = [pointAt(path, a)];
for (let i = 1; i < pts.length - 1; i++) if (path.cum[i] > a && path.cum[i] < b) out.push(pts[i]);
out.push(pointAt(path, b));
return out;
}
export function convexHull(points: Vec[]): Vec[] {
const pts = [...points].sort((a, b) => a.x - b.x || a.y - b.y);
if (pts.length < 3) return pts;
const cross = (o: Vec, a: Vec, b: Vec) => (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
const lower: Vec[] = [];
for (const p of pts) {
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();
lower.push(p);
}
const upper: Vec[] = [];
for (let i = pts.length - 1; i >= 0; i--) {
const p = pts[i];
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();
upper.push(p);
}
upper.pop();
lower.pop();
return lower.concat(upper);
}
/** Corners of a rectangle centred at (x, y), rotated by `angle`. */
export function boxCorners(x: number, y: number, angle: number, halfL: number, halfW: number): Vec[] {
const c = Math.cos(angle);
const s = Math.sin(angle);
return [
{ x: x + c * halfL - s * halfW, y: y + s * halfL + c * halfW },
{ x: x + c * halfL + s * halfW, y: y + s * halfL - c * halfW },
{ x: x - c * halfL + s * halfW, y: y - s * halfL - c * halfW },
{ x: x - c * halfL - s * halfW, y: y - s * halfL + c * halfW },
];
}
