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>
42 lines
2 KiB
JavaScript
42 lines
2 KiB
JavaScript
// 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');
|
|
}
|