Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196import { describe, expect, it } from "vitest";import { bezier, join, lengthAtY, polyline, ribbon, sample, straight, trace} from "#lib/components/landing/thread/geometry.ts";
describe("sample", () => { it("walks a straight line at an even step", () => { const line = sample([ [0, 0], [0, 10] ]); expect(line.total).toBeCloseTo(10); expect(line.pts).toHaveLength(6); expect(line.len).toEqual([0, 2, 4, 6, 8, 10]); });
it("passes through every point it was given", () => { const line = sample([ [0, 0], [40, 30], [0, 80] ]); const near = (x: number, y: number) => line.pts.some(([px, py]) => Math.hypot(px - x, py - y) < 1.5); expect(near(40, 30)).toBe(true); expect(near(0, 80)).toBe(true); });});
describe("trace", () => { it("walks a drawn-out curve at an even step", () => { const line = trace( bezier( [ [0, 0], [0, 10], [10, 10], [10, 20] ], 40 ), 2 ); expect(line.pts[0]).toEqual([0, 0]); expect(line.pts[line.pts.length - 1]).toEqual([10, 20]); for (let i = 1; i < line.len.length - 1; i++) expect(line.len[i] - line.len[i - 1]).toBeCloseTo(2); expect(line.total).toBeGreaterThan(Math.hypot(10, 20)); });});
describe("bezier", () => { it("runs from the start to the end through the hull", () => { const pts = bezier( [ [0, 0], [0, 10], [10, 10], [10, 0] ], 4 ); expect(pts).toHaveLength(5); expect(pts[0]).toEqual([0, 0]); expect(pts[4]).toEqual([10, 0]); expect(pts[2][0]).toBeCloseTo(5); expect(pts[2][1]).toBeCloseTo(7.5); });});
describe("join", () => { it("runs the lengths on and drops the shared point", () => { const a = straight([0, 0], [0, 4]); const b = straight([0, 4], [0, 10]); const line = join(a, b); expect(line.total).toBeCloseTo(10); expect(line.len.at(-1)).toBeCloseTo(10); expect(new Set(line.pts.map(([, y]) => y)).size).toBe(line.pts.length); });});
describe("lengthAtY", () => { it("maps a height on a vertical run to its length", () => { const line = straight([0, 100], [0, 300]); expect(lengthAtY(line, 50)).toBe(0); expect(lengthAtY(line, 200)).toBeCloseTo(100); expect(lengthAtY(line, 400)).toBeCloseTo(200); });
it("only ever draws more as the page scrolls past a curl", () => { // down, back up, and down again const line = sample([ [0, 0], [0, 60], [30, 40], [60, 80], [60, 160] ]); let last = 0; for (let y = 0; y <= 170; y += 5) { const len = lengthAtY(line, y); expect(len).toBeGreaterThanOrEqual(last); last = len; } expect(last).toBeCloseTo(line.total); });});
describe("ribbon", () => { const line = straight([0, 0], [0, 200]);
it("is a closed outline, out along one edge and back along the other", () => { const d = ribbon(line)(0, line.total); expect(d.startsWith("M")).toBe(true); expect(d.endsWith("Z")).toBe(true); expect(d.split("L")).toHaveLength(line.pts.length * 2); });
it("stays within its width plus the swell and fray", () => { const xs = [ ...ribbon(line, { width: 2 })(0, line.total).matchAll(/(-?[\d.]+) -?[\d.]+/g) ].map((m) => Math.abs(Number(m[1]))); expect(Math.max(...xs)).toBeLessThan(1 * 1.16 + 0.08 + 1e-6); });
it("is the same brush for the same seed, and another for another", () => { expect(ribbon(line, { seed: 3 })(0, line.total)).toBe( ribbon(line, { seed: 3 })(0, line.total) ); expect(ribbon(line, { seed: 3 })(0, line.total)).not.toBe( ribbon(line, { seed: 4 })(0, line.total) ); });
it("slices the stretch between two lengths, a sample past each end", () => { const d = ribbon(line)(50, 100); expect(d.split("L")).toHaveLength((100 / 2 - 50 / 2 + 3) * 2); });
it("weighs the brush along the line by its profile", () => { const widths = (d: string) => { const xs = [...d.matchAll(/(-?[\d.]+) (-?[\d.]+)/g)].map((m) => [ Number(m[1]), Number(m[2]) ]); const n = xs.length / 2; // Out along one edge, back along the other: pair each sample with its twin. return xs.slice(0, n).map(([x], i) => Math.abs(x - xs[xs.length - 1 - i][0])); }; const plain = widths(ribbon(line, { swell: 0, fray: 0, taper: [0, 0] })(0, line.total)); const heavyToLight = widths( ribbon(line, { swell: 0, fray: 0, taper: [0, 0], profile: (s, total) => (s < total / 2 ? 2 : 0.5) })(0, line.total) ); const n = plain.length; expect(heavyToLight[2]).toBeCloseTo(plain[2] * 2); expect(heavyToLight[n - 3]).toBeCloseTo(plain[n - 3] * 0.5); });});
describe("polyline", () => { const span = polyline(straight([0, 0], [0, 4]));
it("draws the samples as one path", () => { expect(span(0, 4)).toBe("M0 0L0 2L0 4"); expect(span(-2, 10)).toBe("M0 0L0 2L0 4"); });
it("cuts off at the end, between samples", () => { expect(span(0, 3)).toBe("M0 0L0 2L0 3"); expect(span(0, 2)).toBe("M0 0L0 2"); expect(span(0, 0.5)).toBe("M0 0L0 0.5"); });
it("starts at the start, between samples", () => { expect(span(1, 3)).toBe("M0 1L0 2L0 3"); expect(span(2.5, 3.5)).toBe("M0 2.5L0 3.5"); });
it("draws nothing for an empty span", () => { expect(span(0, 0)).toBe(""); expect(span(3, 3)).toBe(""); });});