vendor m3e-canvas 5c83ac6, with the import rewrite scripted
upstream imports through a @/ alias, and this platform already binds @/* to src/apps/officer-web and @/components/* to its own shared components — so an unrewritten import resolves to the WRONG module rather than failing. the rewrite is mechanical and lives in scripts/vendor.sh so a re-vendor produces a readable diff. lib/ai.ts and AiPanel are vendored despite being unwanted: deleting them meant editing vendored code, and Canvas.tsx references them throughout. a faithful copy carrying an unused module beats a doctored one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Vendored
+99
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { DURATION_PER_SHAPE_MS, LoadingAnimator, SHAPE_COUNT, Spring, getShapes, morphedShape } from "./shapes";
|
||||
|
||||
describe("getShapes", () => {
|
||||
it("samples every shape in the sequence with the same point count", () => {
|
||||
const shapes = getShapes();
|
||||
expect(shapes).toHaveLength(SHAPE_COUNT);
|
||||
const n = shapes[0].length;
|
||||
expect(n).toBeGreaterThan(0);
|
||||
for (const s of shapes) expect(s).toHaveLength(n);
|
||||
});
|
||||
|
||||
it("normalizes every shape into the unit box, up to resampling drift", () => {
|
||||
for (const shape of getShapes()) {
|
||||
let extreme = 0;
|
||||
for (const [x, y] of shape) {
|
||||
expect(Math.abs(x)).toBeLessThanOrEqual(1 + 1e-9);
|
||||
expect(Math.abs(y)).toBeLessThanOrEqual(1 + 1e-9);
|
||||
extreme = Math.max(extreme, Math.abs(x), Math.abs(y));
|
||||
}
|
||||
/* resampling along the arc can step just past the exact extreme vertex */
|
||||
expect(extreme).toBeGreaterThan(0.99);
|
||||
}
|
||||
});
|
||||
|
||||
it("caches the sampled shapes", () => {
|
||||
expect(getShapes()).toBe(getShapes());
|
||||
});
|
||||
});
|
||||
|
||||
describe("morphedShape", () => {
|
||||
it("returns a shape exactly at every whole fraction", () => {
|
||||
const shapes = getShapes();
|
||||
for (let i = 0; i < SHAPE_COUNT; i++) expect(morphedShape(i)).toEqual(shapes[i]);
|
||||
});
|
||||
|
||||
it("interpolates halfway between neighbours at a half fraction", () => {
|
||||
const [a, b] = getShapes();
|
||||
const mid = morphedShape(0.5);
|
||||
expect(mid[0]).toEqual([(a[0][0] + b[0][0]) / 2, (a[0][1] + b[0][1]) / 2]);
|
||||
});
|
||||
|
||||
it("wraps around the sequence, past both ends", () => {
|
||||
expect(morphedShape(SHAPE_COUNT)).toEqual(getShapes()[0]);
|
||||
const shapes = getShapes();
|
||||
const [last, first] = [shapes[SHAPE_COUNT - 1], shapes[0]];
|
||||
const mid = morphedShape(-0.5);
|
||||
expect(mid[0]).toEqual([(last[0][0] + first[0][0]) / 2, (last[0][1] + first[0][1]) / 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Spring", () => {
|
||||
it("settles at its target", () => {
|
||||
const s = new Spring(200, 0.6);
|
||||
s.target = 1;
|
||||
for (let i = 0; i < 600; i++) s.step(1 / 60);
|
||||
expect(s.pos).toBeCloseTo(1, 2);
|
||||
expect(Math.abs(s.vel)).toBeLessThan(0.01);
|
||||
});
|
||||
|
||||
it("overshoots when underdamped, not when critically damped", () => {
|
||||
const peakAfter = (damping: number) => {
|
||||
const s = new Spring(200, damping);
|
||||
s.target = 1;
|
||||
let peak = 0;
|
||||
for (let i = 0; i < 120; i++) {
|
||||
s.step(1 / 60);
|
||||
peak = Math.max(peak, s.pos);
|
||||
}
|
||||
return peak;
|
||||
};
|
||||
expect(peakAfter(0.6)).toBeGreaterThan(1);
|
||||
expect(peakAfter(1)).toBeLessThanOrEqual(1.001);
|
||||
});
|
||||
});
|
||||
|
||||
describe("LoadingAnimator", () => {
|
||||
it("advances the morph with time while keeping rotation inside one turn", () => {
|
||||
const a = new LoadingAnimator();
|
||||
for (let ts = 0; ts <= 3 * DURATION_PER_SHAPE_MS; ts += 50) {
|
||||
a.update(ts);
|
||||
expect(a.rotation).toBeGreaterThanOrEqual(0);
|
||||
expect(a.rotation).toBeLessThan(360);
|
||||
}
|
||||
expect(a.morph).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("is deterministic for identical frame sequences", () => {
|
||||
const a = new LoadingAnimator();
|
||||
const b = new LoadingAnimator();
|
||||
for (let ts = 0; ts <= 2000; ts += 33) {
|
||||
a.update(ts);
|
||||
b.update(ts);
|
||||
}
|
||||
expect(a.morph).toBe(b.morph);
|
||||
expect(a.rotation).toBe(b.rotation);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user