// Take-file writer. Format is specified in docs/design.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}${take.fps ? ` fps=${take.fps}` : ''}`); 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)}` + (take.eyeSlots ? ` slot_eye_r=${r(take.eyeSlots.r.cx)},${r(take.eyeSlots.r.cy)},${r(take.eyeSlots.r.w)}` + ` slot_eye_l=${r(take.eyeSlots.l.cx)},${r(take.eyeSlots.l.cy)},${r(take.eyeSlots.l.w)}` : '') + ` 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}`); // `clip` names a part this one is stencilled by, not merely drawn after. // The iris needs it: at an extreme gaze the disc reaches past the lid, and // ordering alone would put it on the cheek. if (part.clip) bits.push(`clip=${part.clip}`); 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; } // A disc is three numbers, so it gets its own key shape rather than being // pre-tessellated into a polygon here: the renderer draws a real circle // with hard edges, and a five-pixel iris approximated by a polygon would // lose a pixel off its silhouette on some frames and not others. // A square, in whole pixels, at an integer centre. Same argument as the // disc: the renderer is told the shape, not a polygon approximating it. if (part.kind === 'rect') { L.push(`key ${part.name.padEnd(9)} f=${k.f} src=${k.src} rect=${r(k.c.x)},${r(k.c.y)},${k.size}`); continue; } if (part.kind === 'disc') { L.push(`key ${part.name.padEnd(9)} f=${k.f} src=${k.src} disc=${r(k.c.x)},${r(k.c.y)},${r(k.r)}`); 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'); }