// 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)} 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'); }