roto: video -> take file builder with interactive tuning
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>
This commit is contained in:
commit
7a6bdea157
13 changed files with 1277 additions and 0 deletions
41
js/take.js
Normal file
41
js/take.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// 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');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue