The test renderer turned out to be the product. Everything that decides how the work looks - stabilisation, reduction, timing, frame removal, palette - already happens here, and the flat indexed output already reads the way it should. The reason to leave is in the original design's own rule: never make a timing decision that requires a full render to evaluate. Honouring that moved every judgement out of Animator Pro, which left the host doing nothing but writing a file, in exchange for modal UI, minutes-long renders, one-level undo, FLX delta invariants, a single tween state and a cel singleton. What does NOT change is the constraint. 320x200, indexed palette, flat fills, no antialiasing - inherited, but load-bearing rather than accidental. The rasteriser writes palette indices and expands to RGBA only at the end precisely so nothing can soften an edge. Modern conveniences belong in the workflow. Adds docs/design.md: the principles, carried over without the Poco/FLX/cel machinery, plus architecture and an honest list of what is missing - the largest gap being that plates still have nowhere to be drawn. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
2.3 KiB
JavaScript
54 lines
2.3 KiB
JavaScript
// MediaPipe FaceLandmarker index tables.
|
|
// Ring arrays are ORDERED traversals, not raw connection sets: vertex position
|
|
// within a ring is the vertex's identity, and every downstream stage depends on
|
|
// that ordering being stable. See docs/design.md, "Fixed topology".
|
|
|
|
// Rigid landmarks for the similarity fit. Eye corners, nose bridge, nose tip.
|
|
// Nothing here may be a feature that moves under performance: including the
|
|
// mouth or brows bleeds performance into the stabilization.
|
|
export const RIGID = [33, 133, 362, 263, 168, 6, 1];
|
|
|
|
// Outer lip ring, clockwise from the right corner over the top.
|
|
// index 0 = right corner, 5 = top centre, 10 = left corner, 15 = bottom centre.
|
|
export const LIPS_OUTER = [
|
|
61, 185, 40, 39, 37, 0, 267, 269, 270, 409,
|
|
291, 375, 321, 405, 314, 17, 84, 181, 91, 146,
|
|
];
|
|
|
|
// Inner lip ring, same orientation and the same four cardinal positions.
|
|
export const LIPS_INNER = [
|
|
78, 191, 80, 81, 82, 13, 312, 311, 310, 415,
|
|
308, 324, 318, 402, 317, 14, 87, 178, 88, 95,
|
|
];
|
|
|
|
// Inner upper / lower lip centres. Their separation is the aperture signal that
|
|
// decides whether the mouth interior is present at all.
|
|
export const APERTURE = [13, 14];
|
|
|
|
// Face oval, used only to derive the placeholder plate in v1.
|
|
export const FACE_OVAL = [
|
|
10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288,
|
|
397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136,
|
|
172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109,
|
|
];
|
|
|
|
// Eye corners, for the calibration box and for reporting fit residual.
|
|
export const EYE_INNER = [133, 362];
|
|
|
|
// Pick `n` slots from a ring of `len` by even spacing. Returns RING POSITIONS,
|
|
// not landmark ids: positions are the vertex identity downstream, and mapping ids
|
|
// back to positions with indexOf would silently pick the wrong slot if a table
|
|
// ever repeated an id.
|
|
//
|
|
// For even n this naturally lands on the cardinal positions (corners and lip
|
|
// centres) of a 20-point ring. Fixed indices, never adaptive decimation: the
|
|
// vertex at slot k means the same thing on every frame of the shot.
|
|
export function subsampleSlots(len, n) {
|
|
const out = [];
|
|
for (let k = 0; k < n; k++) out.push(Math.round((k * len) / n) % len);
|
|
return out;
|
|
}
|
|
|
|
export function subsampleRing(ring, n) {
|
|
return subsampleSlots(ring.length, n).map((s) => ring[s]);
|
|
}
|