Analysis half of the pipeline in docs/roto-puppet.md. Stabilises a face out of a clip via a similarity fit on rigid landmarks, reduces the lip contour to a fixed vertex budget, selects sparse keys on velocity minima, and previews the result as flat indexed fills so timing can be judged without an Animator Pro render. - landmarks.js ordered lip/oval rings; slot position is vertex identity - mathutil.js closed-form 2D similarity, Procrustes mean, transform smoothing - pipeline.js stabilise -> subsample -> key-select - raster.js indexed scanline fill, no antialiasing - take.js take-file writer - selftest.js 29 assertions, incl. ring simplicity at every vertex budget Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
41 lines
2 KiB
JavaScript
41 lines
2 KiB
JavaScript
// Take-file writer. Format is specified in docs/roto-puppet.md, "The take
|
|
// format". Line-oriented text on purpose: Poco can parse it with fopen/fgets
|
|
// from poco/src/safefile.c and strtok/atoi/atof from poco/src/strlib.c, so the
|
|
// Animator Pro render script needs no new native code.
|
|
|
|
const r = (v) => Math.round(v);
|
|
|
|
export function writeTake(take) {
|
|
const L = [];
|
|
L.push(`take name=${take.name} frames=${take.frames} width=${take.width} height=${take.height} exposure=${take.exposure}`);
|
|
L.push('');
|
|
take.palette.forEach((p, i) => L.push(`pal ${i} ${p.name.padEnd(11)} ${i}`));
|
|
L.push('');
|
|
// v1 emits a single frozen plate derived from the face oval. A real project
|
|
// replaces this with hand-drawn angles referenced by cel frame; the record
|
|
// shape is the same either way.
|
|
L.push(`plate 0 kind=poly slot_mouth=${r(take.slot.x)},${r(take.slot.y)} scale=1.00 rot=0 squash=1.00`);
|
|
L.push('');
|
|
for (const part of take.parts) {
|
|
const bits = [`part ${part.name.padEnd(9)} kind=${part.kind} z=${part.z}`];
|
|
if (part.color !== undefined) bits.push(`color=${part.color}`);
|
|
if (part.kind === 'poly') bits.push('closed=1 fill=1');
|
|
bits.push(`interp=${part.interp}`);
|
|
if (part.parent) bits.push(`parent=${part.parent}`);
|
|
L.push(bits.join(' '));
|
|
}
|
|
L.push('');
|
|
for (const part of take.parts) {
|
|
for (const k of part.keys) {
|
|
if (k.hidden) { L.push(`key ${part.name.padEnd(9)} f=${k.f} hidden`); continue; }
|
|
if (part.kind === 'plate') { L.push(`key ${part.name.padEnd(9)} f=${k.f} plate=${k.plate}`); continue; }
|
|
const pts = k.pts.map((p) => `${r(p.x)},${r(p.y)}`).join(' ');
|
|
// f is authoritative (what renders); src is the pre-snap extreme frame,
|
|
// kept only as a tuning signal - a key dragged far means the minimum-hold
|
|
// and the exposure grid are fighting.
|
|
L.push(`key ${part.name.padEnd(9)} f=${k.f} src=${k.src} n=${k.pts.length} ${pts}`);
|
|
}
|
|
}
|
|
L.push('');
|
|
return L.join('\n');
|
|
}
|