@pinepaper.studio/mcp-server 1.6.7 → 1.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +9 -5
- package/README.es.md +9 -5
- package/README.fr.md +9 -5
- package/README.hi.md +8 -5
- package/README.ja.md +8 -5
- package/README.ko.md +8 -4
- package/README.md +256 -17
- package/README.pt-BR.md +9 -5
- package/README.zh-CN.md +6 -4
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +86 -2
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +143 -60
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/character/data/concepts.json +1 -0
- package/dist/character/data/depictions.json +1 -0
- package/dist/character/data/styles.json +1 -0
- package/dist/character/duration-scale.d.ts +13 -0
- package/dist/character/duration-scale.d.ts.map +1 -0
- package/dist/character/duration-scale.js +13 -0
- package/dist/character/duration-scale.js.map +1 -0
- package/dist/character/path-bbox.d.ts +82 -0
- package/dist/character/path-bbox.d.ts.map +1 -0
- package/dist/character/path-bbox.js +496 -0
- package/dist/character/path-bbox.js.map +1 -0
- package/dist/character/perform.d.ts +149 -0
- package/dist/character/perform.d.ts.map +1 -0
- package/dist/character/perform.js +626 -0
- package/dist/character/perform.js.map +1 -0
- package/dist/character/resolve.d.ts +51 -0
- package/dist/character/resolve.d.ts.map +1 -0
- package/dist/character/resolve.js +239 -0
- package/dist/character/resolve.js.map +1 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +224 -0
- package/dist/design/design-systems.d.ts.map +1 -0
- package/dist/design/design-systems.js +434 -0
- package/dist/design/design-systems.js.map +1 -0
- package/dist/execution/canvas-state.d.ts +7 -0
- package/dist/execution/canvas-state.d.ts.map +1 -1
- package/dist/execution/canvas-state.js +20 -8
- package/dist/execution/canvas-state.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.d.ts +9 -0
- package/dist/ontology/design-graph.d.ts.map +1 -1
- package/dist/ontology/design-graph.js +98 -3
- package/dist/ontology/design-graph.js.map +1 -1
- package/dist/ontology/types.d.ts +42 -0
- package/dist/ontology/types.d.ts.map +1 -1
- package/dist/ontology/vocabulary.d.ts.map +1 -1
- package/dist/ontology/vocabulary.js +20 -0
- package/dist/ontology/vocabulary.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +25 -0
- package/dist/tools/compact-descriptions.js.map +1 -1
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/definitions.d.ts.map +1 -1
- package/dist/tools/definitions.js +404 -25
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-methods.d.ts +18 -0
- package/dist/tools/engine-methods.d.ts.map +1 -0
- package/dist/tools/engine-methods.js +241 -0
- package/dist/tools/engine-methods.js.map +1 -0
- package/dist/tools/handlers/character.d.ts +121 -0
- package/dist/tools/handlers/character.d.ts.map +1 -0
- package/dist/tools/handlers/character.js +315 -0
- package/dist/tools/handlers/character.js.map +1 -0
- package/dist/tools/handlers.d.ts +2 -0
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +522 -18
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +15 -3
- package/dist/tools/minimal-descriptions.js.map +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +8 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +106 -1
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1273 -18
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +6805 -124
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +627 -17
- package/dist/types/schemas.js.map +1 -1
- package/dist/vendor/design/art-deco-generator.d.ts +52 -0
- package/dist/vendor/design/art-deco-generator.d.ts.map +1 -0
- package/dist/vendor/design/art-deco-generator.js +144 -0
- package/dist/vendor/design/art-deco-generator.js.map +1 -0
- package/dist/vendor/design/art-nouveau-generator.d.ts +46 -0
- package/dist/vendor/design/art-nouveau-generator.d.ts.map +1 -0
- package/dist/vendor/design/art-nouveau-generator.js +134 -0
- package/dist/vendor/design/art-nouveau-generator.js.map +1 -0
- package/dist/vendor/design/bauhaus-generator.d.ts +53 -0
- package/dist/vendor/design/bauhaus-generator.d.ts.map +1 -0
- package/dist/vendor/design/bauhaus-generator.js +126 -0
- package/dist/vendor/design/bauhaus-generator.js.map +1 -0
- package/dist/vendor/design/brutalist-generator.d.ts +53 -0
- package/dist/vendor/design/brutalist-generator.d.ts.map +1 -0
- package/dist/vendor/design/brutalist-generator.js +148 -0
- package/dist/vendor/design/brutalist-generator.js.map +1 -0
- package/dist/vendor/design/claymorphism-generator.d.ts +46 -0
- package/dist/vendor/design/claymorphism-generator.d.ts.map +1 -0
- package/dist/vendor/design/claymorphism-generator.js +135 -0
- package/dist/vendor/design/claymorphism-generator.js.map +1 -0
- package/dist/vendor/design/corporate-memphis-generator.d.ts +47 -0
- package/dist/vendor/design/corporate-memphis-generator.d.ts.map +1 -0
- package/dist/vendor/design/corporate-memphis-generator.js +144 -0
- package/dist/vendor/design/corporate-memphis-generator.js.map +1 -0
- package/dist/vendor/design/cyberpunk-generator.d.ts +52 -0
- package/dist/vendor/design/cyberpunk-generator.d.ts.map +1 -0
- package/dist/vendor/design/cyberpunk-generator.js +162 -0
- package/dist/vendor/design/cyberpunk-generator.js.map +1 -0
- package/dist/vendor/design/de-stijl-generator.d.ts +50 -0
- package/dist/vendor/design/de-stijl-generator.d.ts.map +1 -0
- package/dist/vendor/design/de-stijl-generator.js +154 -0
- package/dist/vendor/design/de-stijl-generator.js.map +1 -0
- package/dist/vendor/design/design-style.d.ts +3 -0
- package/dist/vendor/design/design-style.d.ts.map +1 -0
- package/dist/vendor/design/design-style.js +12 -0
- package/dist/vendor/design/design-style.js.map +1 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts +59 -0
- package/dist/vendor/design/design-systems-dtcg.d.ts.map +1 -0
- package/dist/vendor/design/design-systems-dtcg.js +571 -0
- package/dist/vendor/design/design-systems-dtcg.js.map +1 -0
- package/dist/vendor/design/frutiger-aero-generator.d.ts +46 -0
- package/dist/vendor/design/frutiger-aero-generator.d.ts.map +1 -0
- package/dist/vendor/design/frutiger-aero-generator.js +133 -0
- package/dist/vendor/design/frutiger-aero-generator.js.map +1 -0
- package/dist/vendor/design/memphis-generator.d.ts +56 -0
- package/dist/vendor/design/memphis-generator.d.ts.map +1 -0
- package/dist/vendor/design/memphis-generator.js +158 -0
- package/dist/vendor/design/memphis-generator.js.map +1 -0
- package/dist/vendor/design/mid-century-upa-generator.d.ts +46 -0
- package/dist/vendor/design/mid-century-upa-generator.d.ts.map +1 -0
- package/dist/vendor/design/mid-century-upa-generator.js +137 -0
- package/dist/vendor/design/mid-century-upa-generator.js.map +1 -0
- package/dist/vendor/design/neumorphic-generator.d.ts +53 -0
- package/dist/vendor/design/neumorphic-generator.d.ts.map +1 -0
- package/dist/vendor/design/neumorphic-generator.js +182 -0
- package/dist/vendor/design/neumorphic-generator.js.map +1 -0
- package/dist/vendor/design/op-art-generator.d.ts +45 -0
- package/dist/vendor/design/op-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/op-art-generator.js +116 -0
- package/dist/vendor/design/op-art-generator.js.map +1 -0
- package/dist/vendor/design/pixel-art-generator.d.ts +45 -0
- package/dist/vendor/design/pixel-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/pixel-art-generator.js +143 -0
- package/dist/vendor/design/pixel-art-generator.js.map +1 -0
- package/dist/vendor/design/pop-art-generator.d.ts +53 -0
- package/dist/vendor/design/pop-art-generator.d.ts.map +1 -0
- package/dist/vendor/design/pop-art-generator.js +164 -0
- package/dist/vendor/design/pop-art-generator.js.map +1 -0
- package/dist/vendor/design/risograph-generator.d.ts +53 -0
- package/dist/vendor/design/risograph-generator.d.ts.map +1 -0
- package/dist/vendor/design/risograph-generator.js +170 -0
- package/dist/vendor/design/risograph-generator.js.map +1 -0
- package/dist/vendor/design/swiss-style-generator.d.ts +55 -0
- package/dist/vendor/design/swiss-style-generator.d.ts.map +1 -0
- package/dist/vendor/design/swiss-style-generator.js +147 -0
- package/dist/vendor/design/swiss-style-generator.js.map +1 -0
- package/dist/vendor/design/vaporwave-generator.d.ts +51 -0
- package/dist/vendor/design/vaporwave-generator.d.ts.map +1 -0
- package/dist/vendor/design/vaporwave-generator.js +139 -0
- package/dist/vendor/design/vaporwave-generator.js.map +1 -0
- package/manifest.json +33 -1
- package/package.json +13 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"perform.d.ts","sourceRoot":"","sources":["../../src/character/perform.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/* VENDORED from mcp-cloud/src/services/character-perform.ts.
|
|
2
|
+
* sha256: 91737fa6410fbe24e7b969c9d1a97ca4c6694c117854ce3182606efa41e98db7
|
|
3
|
+
* Generator: scripts/sync-character-modules.mjs
|
|
4
|
+
*
|
|
5
|
+
* Do NOT edit here — change it in mcp-cloud and re-run the generator, or the
|
|
6
|
+
* two implementations drift and a figure performs differently depending on
|
|
7
|
+
* which surface a caller reached. Guarded by character.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* A CHARACTER IS DIRECTED, NOT DRAWN.
|
|
11
|
+
*
|
|
12
|
+
* This is the translation step the declarative graph exists for. A model says
|
|
13
|
+
* WHAT happens — this character, here, this tall, blinks at 1.2s and speaks
|
|
14
|
+
* from 2.0 to 4.5 — and the knowledge of HOW lives in the graph: which parts
|
|
15
|
+
* the figure has, which of them are alternates of one another, how long a blink
|
|
16
|
+
* lasts, how fast a mouth moves. Nothing here asks a model for geometry.
|
|
17
|
+
*
|
|
18
|
+
* WHY IT HAS TO BE THIS WAY. The alternative is what exists today: a model
|
|
19
|
+
* writes thirty-odd correct calls — create the skeleton, add each bone with the
|
|
20
|
+
* right parent and angle, create each shape, attach each to the right bone,
|
|
21
|
+
* then author poses — and one mistake anywhere leaves a broken figure. That is
|
|
22
|
+
* a lot of exactly-right output to ask of a language model, and it is the
|
|
23
|
+
* reported reason characters do not work. A symbol and a list of beats is
|
|
24
|
+
* something a small model can get right, and a small model getting it right is
|
|
25
|
+
* the difference between this running in the cloud and running on someone's
|
|
26
|
+
* laptop.
|
|
27
|
+
*
|
|
28
|
+
* THE THREE MOTIONS ARE THE ONES STORYTELLERS ASKED FOR — blink, speak, turn
|
|
29
|
+
* the head — and each is expressed with machinery both renderers were measured
|
|
30
|
+
* on this week: opacity keyframes and position keyframes. No shape keys, no IK,
|
|
31
|
+
* no deformers. A channel that needs a subsystem the container cannot draw is a
|
|
32
|
+
* channel that works in a preview and fails in the delivered video, which is
|
|
33
|
+
* the failure this whole layer is a response to.
|
|
34
|
+
*
|
|
35
|
+
* NOT LIP SYNC. `say` opens and closes the mouth for the length of a line; it
|
|
36
|
+
* does not match phonemes, because matching phonemes needs the word timings a
|
|
37
|
+
* voice track carries and no voice track exists yet. Called `say` rather than
|
|
38
|
+
* `lipSync` so nobody reads more into it than it does.
|
|
39
|
+
*/
|
|
40
|
+
import { DURATION_SCALE_MS } from "./duration-scale.js";
|
|
41
|
+
import { pathBBox, scalePath, shiftPath } from "./path-bbox.js";
|
|
42
|
+
/** Nearest duration token to `ms` — the same rounding the token repair uses. */
|
|
43
|
+
const nearestOnScale = (ms) => DURATION_SCALE_MS.reduce((best, d) => (Math.abs(d - ms) < Math.abs(best - ms) ? d : best), DURATION_SCALE_MS[0]);
|
|
44
|
+
/** Sorted, deduplicated keyframes — a track with two values at one instant is
|
|
45
|
+
* ambiguous and the renderers resolve it differently. */
|
|
46
|
+
function tidy(kfs) {
|
|
47
|
+
const byTime = new Map();
|
|
48
|
+
// LATER WINS AT THE SAME INSTANT, and the zero anchor is pushed last — so it
|
|
49
|
+
// is merged in first here, letting a real beat at t=0 override it.
|
|
50
|
+
for (const k of [...kfs].reverse()) {
|
|
51
|
+
const t = Math.round(k.time * 1000) / 1000;
|
|
52
|
+
byTime.set(t, { ...(byTime.get(t) ?? {}), ...k, time: t });
|
|
53
|
+
}
|
|
54
|
+
return [...byTime.values()].sort((a, b) => a.time - b.time);
|
|
55
|
+
}
|
|
56
|
+
/** Relative luminance of a #rgb/#rrggbb colour, 0..1. Unknown formats read as
|
|
57
|
+
* light, so an unrecognised colour keeps the old stroked behaviour. */
|
|
58
|
+
function luminance(colour) {
|
|
59
|
+
const h = colour.trim().replace(/^#/, "");
|
|
60
|
+
const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
|
|
61
|
+
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
62
|
+
return 1;
|
|
63
|
+
const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
|
|
64
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
65
|
+
}
|
|
66
|
+
/** Is this fill already the drawing's own ink? Such a part must not be
|
|
67
|
+
* outlined — see the note at the stroke below. */
|
|
68
|
+
function isInkFill(colour) {
|
|
69
|
+
return luminance(colour) < 0.22;
|
|
70
|
+
}
|
|
71
|
+
export function performCharacter(input) {
|
|
72
|
+
const { id, depiction: dep, at, height, channels, beats, durationSeconds } = input;
|
|
73
|
+
const k = height / dep.height;
|
|
74
|
+
const byName = new Map(channels.map((c) => [c.name, c]));
|
|
75
|
+
/** Beats naming a channel this concept has no definition for — reported, not hidden. */
|
|
76
|
+
const unknownChannels = [];
|
|
77
|
+
const ops = [];
|
|
78
|
+
/**
|
|
79
|
+
* ONE CONTINUOUS CONTOUR, WHICH IS WHAT MAKES A FIGURE READ AS ONE OBJECT.
|
|
80
|
+
*
|
|
81
|
+
* Stroking each part separately draws the seams between primitives at exactly
|
|
82
|
+
* the weight that separates the figure from the sky. Rendered at 0.9s of
|
|
83
|
+
* `ledge-at-dusk`, the pigeon is a circle-with-an-outline sitting on an
|
|
84
|
+
* ellipse-with-an-outline and the head's contour cuts straight across the
|
|
85
|
+
* body — which is the "the head is not connected" complaint, drawn.
|
|
86
|
+
*
|
|
87
|
+
* The fix is the one flat-vector artwork has always used, and both Rive and
|
|
88
|
+
* Lottie files are built this way: lay the ink down FIRST, as an unfilled
|
|
89
|
+
* double-width stroke of every silhouette part, then paint the fills over it.
|
|
90
|
+
* A stroke is centred on its path, so each fill covers the inner half of its
|
|
91
|
+
* own ink and the inner half of every ink it overlaps. What survives is the
|
|
92
|
+
* outer boundary of the UNION — one contour, no internal seams — without any
|
|
93
|
+
* boolean path arithmetic.
|
|
94
|
+
*
|
|
95
|
+
* The ink copy is `carriedBy` its source, so it inherits that part's motion by
|
|
96
|
+
* construction. There is no second track to drift.
|
|
97
|
+
*
|
|
98
|
+
* Opt-in per style: a style that wants separate outlines keeps them.
|
|
99
|
+
*/
|
|
100
|
+
const UNION_INK = dep.ink?.union === true;
|
|
101
|
+
if (UNION_INK) {
|
|
102
|
+
for (const [pid, part] of Object.entries({ ...dep.parts })) {
|
|
103
|
+
// Only the silhouette. Parts that are already ink, or carry reduced
|
|
104
|
+
// interior linework, are detail drawn ON the form and are not part of
|
|
105
|
+
// its boundary.
|
|
106
|
+
if ((part.inkWeight ?? 1) < 1)
|
|
107
|
+
continue;
|
|
108
|
+
if (dep.palette[part.fill ?? ""] ?? part.fill) {
|
|
109
|
+
const fill = dep.palette[part.fill ?? ""] ?? part.fill ?? "";
|
|
110
|
+
if (isInkFill(fill))
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
dep.parts[`${pid}__ink`] = { at: part.at, path: part.path, carriedBy: pid, inkWeight: 2 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Ink copies first: everything painted after them covers their inner half. */
|
|
117
|
+
const partIds = Object.keys(dep.parts).sort((a, b) => (a.endsWith("__ink") ? 0 : 1) - (b.endsWith("__ink") ? 0 : 1));
|
|
118
|
+
/**
|
|
119
|
+
* Every part this one carries, at any depth. A chain (skull → beak → nostril)
|
|
120
|
+
* has to propagate the whole way down or the deepest part is the one that
|
|
121
|
+
* tears off, which is the hardest kind to notice.
|
|
122
|
+
*
|
|
123
|
+
* Cycles in the data are ignored rather than thrown on: a malformed depiction
|
|
124
|
+
* should draw a stiff figure, not fail the render.
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* A BONE'S MOTION, EXPRESSED FOR THE PART IT CARRIES.
|
|
128
|
+
*
|
|
129
|
+
* Translation is the easy half and the only half the first version did: the
|
|
130
|
+
* carrier's dx is the child's dx, so the same offsets are handed straight on.
|
|
131
|
+
*
|
|
132
|
+
* ROTATION IS NOT. A bone that turns swings everything on it AROUND ITS OWN
|
|
133
|
+
* PIVOT — that is what distinguishes a head turning from a head being slid
|
|
134
|
+
* sideways, and it is the difference the eye reads as a figure being solid.
|
|
135
|
+
* Copying the carrier's rotation onto the child instead makes the child spin
|
|
136
|
+
* about its own centre: the beak would rotate on the spot while the skull
|
|
137
|
+
* turns beneath it, which is a worse artefact than the one the bone was
|
|
138
|
+
* built to remove.
|
|
139
|
+
*
|
|
140
|
+
* So a carried part gets BOTH — the same angle, and the displacement its rest
|
|
141
|
+
* position travels along the arc:
|
|
142
|
+
*
|
|
143
|
+
* v = childRest - pivot
|
|
144
|
+
* v' = R(theta) * v
|
|
145
|
+
* offset = v' - v
|
|
146
|
+
*
|
|
147
|
+
* Positions here are the part's placed centre in scene units, which is what
|
|
148
|
+
* `at` resolves to at the emission below, so the arc is computed in the same
|
|
149
|
+
* space it will be drawn in.
|
|
150
|
+
*/
|
|
151
|
+
const carry = (acc, carrier, child, property) => {
|
|
152
|
+
if (property !== "rotation")
|
|
153
|
+
return acc;
|
|
154
|
+
const cp = dep.parts[carrier], ch = dep.parts[child];
|
|
155
|
+
if (!cp || !ch)
|
|
156
|
+
return acc;
|
|
157
|
+
const vx = (ch.at[0] - cp.at[0]) * k;
|
|
158
|
+
const vy = (ch.at[1] - cp.at[1]) * k;
|
|
159
|
+
return acc.map((kf) => {
|
|
160
|
+
const deg = typeof kf.rotation === "number" ? kf.rotation : 0;
|
|
161
|
+
const r = (deg * Math.PI) / 180;
|
|
162
|
+
const cos = Math.cos(r), sin = Math.sin(r);
|
|
163
|
+
return {
|
|
164
|
+
...kf,
|
|
165
|
+
rotation: deg,
|
|
166
|
+
x: vx * cos - vy * sin - vx,
|
|
167
|
+
y: vx * sin + vy * cos - vy,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
const carriedTransitively = (carrier) => {
|
|
172
|
+
const out = [];
|
|
173
|
+
const seen = new Set([carrier]);
|
|
174
|
+
let front = [carrier];
|
|
175
|
+
while (front.length) {
|
|
176
|
+
const next = [];
|
|
177
|
+
for (const id of front) {
|
|
178
|
+
for (const pid of partIds) {
|
|
179
|
+
if (dep.parts[pid]?.carriedBy !== id || seen.has(pid))
|
|
180
|
+
continue;
|
|
181
|
+
seen.add(pid);
|
|
182
|
+
out.push(pid);
|
|
183
|
+
next.push(pid);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
front = next;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
};
|
|
190
|
+
// Which drawn parts belong to which alternate group. A part named as the
|
|
191
|
+
// `alternate` of any channel starts hidden; the base starts visible.
|
|
192
|
+
const alternates = new Set();
|
|
193
|
+
for (const c of channels) {
|
|
194
|
+
if (c.alternate)
|
|
195
|
+
alternates.add(c.alternate);
|
|
196
|
+
// Every speech state except the first is an alternate of it: the closed
|
|
197
|
+
// mouth is what the face wears at rest.
|
|
198
|
+
for (const st of (c.states ?? []).slice(1))
|
|
199
|
+
alternates.add(st);
|
|
200
|
+
}
|
|
201
|
+
const partFill = (pid) => {
|
|
202
|
+
const part = dep.parts[pid];
|
|
203
|
+
return dep.palette[part?.fill ?? ""] ?? part?.fill ?? "#1f2937";
|
|
204
|
+
};
|
|
205
|
+
const hidden = (p) => [...alternates].some((a) => p === a || p.startsWith(`${a}_`));
|
|
206
|
+
/**
|
|
207
|
+
* A CONTOUR CANNOT BE WIDER THAN THE THING IT CONTAINS.
|
|
208
|
+
*
|
|
209
|
+
* The union layer strokes at double weight so that each fill covers the inner
|
|
210
|
+
* half and the outer half survives as the silhouette. On a large part that is
|
|
211
|
+
* exactly right. On a SMALL one the two halves meet in the middle and the
|
|
212
|
+
* stroke floods the shape: the pigeon's tail rendered as a black smear, having
|
|
213
|
+
* been a clean wedge before.
|
|
214
|
+
*
|
|
215
|
+
* So the weight is capped by the part's own short side. Half of the smaller
|
|
216
|
+
* dimension is the most that can be drawn before the strokes from opposite
|
|
217
|
+
* edges overlap, and a third leaves the shape still legible as a shape.
|
|
218
|
+
*/
|
|
219
|
+
const inkWidthFor = (pid) => {
|
|
220
|
+
const nominal = dep.ink.width * k * 0.6 * (dep.parts[pid]?.inkWeight ?? 1);
|
|
221
|
+
const b = pathBBox(dep.parts[pid]?.path ?? "");
|
|
222
|
+
if (!b)
|
|
223
|
+
return Math.max(0.6, nominal);
|
|
224
|
+
const short = Math.min(b.maxX - b.minX, b.maxY - b.minY) * k;
|
|
225
|
+
if (!Number.isFinite(short) || short <= 0)
|
|
226
|
+
return Math.max(0.6, nominal);
|
|
227
|
+
return Math.max(0.6, Math.min(nominal, short / 3));
|
|
228
|
+
};
|
|
229
|
+
// A CHANNEL MAY NAME SEVERAL PARTS. A vehicle's suspension lifts the body,
|
|
230
|
+
// the glazing and the trim as one thing; declaring that as three identical
|
|
231
|
+
// channels (the pigeon's four bob channels) says the same thing three times
|
|
232
|
+
// and lets them drift apart. A bare string still works and means what it
|
|
233
|
+
// always did.
|
|
234
|
+
const matching = (spec) => {
|
|
235
|
+
const wanted = Array.isArray(spec) ? spec : [spec];
|
|
236
|
+
return partIds.filter((p) => wanted.some((w) => p === w || p.startsWith(`${w}_`)));
|
|
237
|
+
};
|
|
238
|
+
/** The value each transform channel says this part sits at when idle. */
|
|
239
|
+
const restingOf = (pid) => {
|
|
240
|
+
const out = {};
|
|
241
|
+
for (const c of channels) {
|
|
242
|
+
if (c.kind !== "transform" || !c.part || !c.property)
|
|
243
|
+
continue;
|
|
244
|
+
if (!matching(c.part).includes(pid))
|
|
245
|
+
continue;
|
|
246
|
+
const rest = c.rest ?? 1;
|
|
247
|
+
if (rest !== 1)
|
|
248
|
+
out[c.property] = rest;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
};
|
|
252
|
+
// ── THE FIGURE ────────────────────────────────────────────────────────────
|
|
253
|
+
// Every part is a path placed in the frame's space. `at` is the part's centre
|
|
254
|
+
// in the figure's own space, so the figure moves as one by construction.
|
|
255
|
+
for (const pid of partIds) {
|
|
256
|
+
const p = dep.parts[pid];
|
|
257
|
+
ops.push({
|
|
258
|
+
tool: "pinepaper_create_item",
|
|
259
|
+
args: {
|
|
260
|
+
itemType: "path",
|
|
261
|
+
position: { x: 0, y: 0 },
|
|
262
|
+
properties: {
|
|
263
|
+
id: `${id}_${pid}`,
|
|
264
|
+
// ONE FIGURE, NOT EIGHT SHAPES. The parts are a single thing that
|
|
265
|
+
// moves and reads together — that is what a rig IS — and saying so
|
|
266
|
+
// is the graph's job, not the caller's. Without it every gate that
|
|
267
|
+
// counts units sees eight competing elements, and
|
|
268
|
+
// `compositionVerdict` (whose own comment says a rigged figure must
|
|
269
|
+
// not be punished for being properly assembled) fails the very
|
|
270
|
+
// piece the rig exists to make.
|
|
271
|
+
groupId: id,
|
|
272
|
+
pathData: shiftPath(scalePath(p.path, k), at.x + p.at[0] * k, at.y + p.at[1] * k),
|
|
273
|
+
closed: true,
|
|
274
|
+
fillColor: partFill(pid),
|
|
275
|
+
// Scaled with the figure: an outline that stays 4px while the drawing
|
|
276
|
+
// grows is a hairline on a large one and a smear on a small one.
|
|
277
|
+
//
|
|
278
|
+
// BUT INK IS NOT OUTLINED. The corpus is FLAT art: its linework is
|
|
279
|
+
// already drawn, as filled dark shapes. A face arrives as ONE
|
|
280
|
+
// compound path of six subpaths — two eyes, brows, nose, mouth — and
|
|
281
|
+
// stroking it adds a 6px outline around every one of those features
|
|
282
|
+
// at a 250px figure height, which merges them into dark smears. The
|
|
283
|
+
// figure has no eyes and its face reads as damage.
|
|
284
|
+
//
|
|
285
|
+
// Invisible at contact-sheet scale, unmissable at full size, which is
|
|
286
|
+
// how it survived review: the eyes were four pixels wide there.
|
|
287
|
+
//
|
|
288
|
+
// So the stroke goes on the SILHOUETTE — the skin and garment shapes
|
|
289
|
+
// an inked scene needs to sit around — and never on a part that is
|
|
290
|
+
// already ink.
|
|
291
|
+
// An ink copy carries NO fill — it exists to be half-covered. Its
|
|
292
|
+
// source loses its own stroke, because the union layer is now the
|
|
293
|
+
// only thing that draws the contour.
|
|
294
|
+
...(pid.endsWith("__ink") ? { fillColor: "transparent" } : {}),
|
|
295
|
+
// An ink copy is ALWAYS stroked. It declares no fill, so partFill
|
|
296
|
+
// falls back to the dark default and isInkFill reads it as linework
|
|
297
|
+
// and refuses to outline it — which left the union layer invisible
|
|
298
|
+
// and the bird with no contour at all.
|
|
299
|
+
...(dep.ink
|
|
300
|
+
&& (pid.endsWith("__ink") || !isInkFill(partFill(pid)))
|
|
301
|
+
&& (dep.parts[pid]?.inkWeight ?? 1) > 0
|
|
302
|
+
&& !(UNION_INK && !pid.endsWith("__ink")) ? {
|
|
303
|
+
strokeColor: dep.ink.color,
|
|
304
|
+
// The part's own share of the style's ink — see `inkWeight`. The
|
|
305
|
+
// floor stays, so a light interior line is thin and never absent.
|
|
306
|
+
strokeWidth: inkWidthFor(pid),
|
|
307
|
+
...(dep.ink.join ? { strokeJoin: dep.ink.join } : {}),
|
|
308
|
+
...(dep.ink.cap ? { strokeCap: dep.ink.cap } : {}),
|
|
309
|
+
} : {}),
|
|
310
|
+
...(hidden(pid) ? { opacity: 0 } : {}),
|
|
311
|
+
// A PART STARTS AT ITS RESTING VALUE WHETHER OR NOT IT IS ANIMATED.
|
|
312
|
+
//
|
|
313
|
+
// The resting transform was written only into the keyframe track, and
|
|
314
|
+
// a part with no beats gets no track — so a channel declaring a rest
|
|
315
|
+
// other than 1 was ignored entirely for any figure nobody asked to
|
|
316
|
+
// move. Measured on the Open Peeps cast: an eyelid whose rest is
|
|
317
|
+
// scaleY 0.02 rendered at full height and sat over the eye, so twelve
|
|
318
|
+
// characters had their eyes shut until someone thought to blink them.
|
|
319
|
+
...restingOf(pid),
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// ── THE PERFORMANCE ───────────────────────────────────────────────────────
|
|
325
|
+
// One track per part, built from every beat that touches it, then written
|
|
326
|
+
// once. Writing a track per beat would leave the last one winning and the
|
|
327
|
+
// rest silently discarded.
|
|
328
|
+
const opacity = new Map();
|
|
329
|
+
const shiftX = new Map();
|
|
330
|
+
const transform = new Map();
|
|
331
|
+
const cycle = new Map();
|
|
332
|
+
/** A part's drawn extent, so a rig amplitude given as a fraction means the
|
|
333
|
+
* same thing at every size the figure is drawn. */
|
|
334
|
+
const partSize = (pid) => {
|
|
335
|
+
const b = pathBBox(dep.parts[pid]?.path ?? "");
|
|
336
|
+
return b ? { w: (b.maxX - b.minX) * k, h: (b.maxY - b.minY) * k } : { w: 10 * k, h: 10 * k };
|
|
337
|
+
};
|
|
338
|
+
const swap = (base, alt, from, to) => {
|
|
339
|
+
// A hard cut, not a dissolve: two drawings of one face cross-fading reads
|
|
340
|
+
// as a double exposure, not as a mouth opening.
|
|
341
|
+
const e = 0.001;
|
|
342
|
+
for (const p of matching(base))
|
|
343
|
+
opacity.set(p, [...(opacity.get(p) ?? []), { time: Math.max(0, from - e), opacity: 1 }, { time: from, opacity: 0 }, { time: to, opacity: 0 }, { time: to + e, opacity: 1 }]);
|
|
344
|
+
for (const p of matching(alt))
|
|
345
|
+
opacity.set(p, [...(opacity.get(p) ?? []), { time: Math.max(0, from - e), opacity: 0 }, { time: from, opacity: 1 }, { time: to, opacity: 1 }, { time: to + e, opacity: 0 }]);
|
|
346
|
+
};
|
|
347
|
+
for (const b of beats) {
|
|
348
|
+
const c = byName.get(b.channel);
|
|
349
|
+
/**
|
|
350
|
+
* A BEAT ON A CHANNEL THIS CONCEPT DOES NOT DECLARE WAS DROPPED IN SILENCE.
|
|
351
|
+
*
|
|
352
|
+
* `continue` alone, with no record. Measured across the shipped corpus
|
|
353
|
+
* 2026-09-18 by mutation — changing the channel name left the scene
|
|
354
|
+
* byte-identical — 28 beats were discarded this way: 14 `blink`,
|
|
355
|
+
* 10 `headTurn`, 4 `say`. The SAME channel names work on other characters,
|
|
356
|
+
* which is what makes it so easy to miss: an author writes `blink` for a
|
|
357
|
+
* cast of four, it animates on three, and the fourth just never blinks.
|
|
358
|
+
*
|
|
359
|
+
* Still dropped, because a beat cannot drive a channel that does not
|
|
360
|
+
* exist — only the silence is removed. The available names go with it,
|
|
361
|
+
* since "blink is not a channel here" is useless without "these are".
|
|
362
|
+
*/
|
|
363
|
+
if (!c) {
|
|
364
|
+
unknownChannels.push(`character "${id}": beat at ${b.at}s names channel "${b.channel}", which this concept does not declare. ` +
|
|
365
|
+
`Available: ${channels.map((x) => x.name).join(", ") || "(none)"}.`);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (c.kind === "alternate" && c.base && c.alternate) {
|
|
369
|
+
swap(c.base, c.alternate, b.at, b.at + (c.holdMs ?? 120) / 1000);
|
|
370
|
+
}
|
|
371
|
+
else if (c.kind === "speak" && c.states && c.states.length >= 2) {
|
|
372
|
+
/**
|
|
373
|
+
* SPEECH IS NOT A METRONOME, AND IT IS NOT TWO FACES.
|
|
374
|
+
*
|
|
375
|
+
* The first version alternated two WHOLE expressions six times a second.
|
|
376
|
+
* Both of those are wrong and the result was reported, correctly, as
|
|
377
|
+
* flashing: swapping whole faces changes the eyes and brows on every
|
|
378
|
+
* syllable, so the character strobes between two people, and an even 6Hz
|
|
379
|
+
* toggle is a machine rather than a mouth.
|
|
380
|
+
*
|
|
381
|
+
* So the states share their eyes — only the mouth differs between them —
|
|
382
|
+
* and a syllable OPENS AND CLOSES THROUGH THE MIDDLE STATE rather than
|
|
383
|
+
* cutting between extremes. That middle is the transition that was
|
|
384
|
+
* missing; with two states there is nowhere for the mouth to be on the
|
|
385
|
+
* way.
|
|
386
|
+
*
|
|
387
|
+
* The rhythm varies because real speech does. The golden ratio spreads
|
|
388
|
+
* the variation evenly without repeating, and it is deterministic, which
|
|
389
|
+
* the content-addressed render cache requires.
|
|
390
|
+
*/
|
|
391
|
+
const until = b.until ?? b.at + 1;
|
|
392
|
+
const closed = c.states[0], open = c.states[c.states.length - 1];
|
|
393
|
+
const mid = c.states.length > 2 ? c.states[1] : open;
|
|
394
|
+
const rate = c.syllablesPerSecond ?? 3.6;
|
|
395
|
+
let t = b.at, k = 0;
|
|
396
|
+
while (t < until) {
|
|
397
|
+
// 0.72x to 1.28x of the nominal syllable, never the same twice running.
|
|
398
|
+
const jitter = 0.72 + 0.56 * (((k + 1) * 0.6180339887) % 1);
|
|
399
|
+
const dur = Math.min((1 / rate) * jitter, until - t);
|
|
400
|
+
if (dur < 0.05)
|
|
401
|
+
break;
|
|
402
|
+
// open on the way up, hold, close on the way down, then a beat shut.
|
|
403
|
+
swap(closed, mid, t, t + dur * 0.18);
|
|
404
|
+
swap(closed, open, t + dur * 0.18, t + dur * 0.58);
|
|
405
|
+
swap(closed, mid, t + dur * 0.58, t + dur * 0.76);
|
|
406
|
+
t += dur;
|
|
407
|
+
k++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else if (c.kind === "alternate-cycle" && c.base && c.alternate) {
|
|
411
|
+
const until = b.until ?? b.at + 1;
|
|
412
|
+
const period = 1 / (c.rateHz ?? 6);
|
|
413
|
+
// Open on the first half of each period, closed on the second — the
|
|
414
|
+
// mouth has to CLOSE between syllables or the character gapes.
|
|
415
|
+
for (let t = b.at; t < until; t += period)
|
|
416
|
+
swap(c.base, c.alternate, t, Math.min(until, t + period / 2));
|
|
417
|
+
}
|
|
418
|
+
else if (c.kind === "transform" && c.part && c.property) {
|
|
419
|
+
/**
|
|
420
|
+
* A GESTURE IS A DEFORMATION, NOT A DIFFERENT DRAWING.
|
|
421
|
+
*
|
|
422
|
+
* The swap kinds above cut between two shapes, which is right when the
|
|
423
|
+
* shapes genuinely differ — an open eye and a closed lid are drawn, not
|
|
424
|
+
* squashed. It is wrong for everything that merely MOVES: a mouth
|
|
425
|
+
* opening is one mouth changing, and expressing it as a cut throws away
|
|
426
|
+
* the whole middle of the gesture. That missing middle is what reads as
|
|
427
|
+
* flashing however well the two ends are chosen.
|
|
428
|
+
*
|
|
429
|
+
* As a transform it has an attack, a peak and a release, it eases, and
|
|
430
|
+
* the same channel works on any part of any concept — a wing, a sail, a
|
|
431
|
+
* lid. Nothing here is specific to a face.
|
|
432
|
+
*/
|
|
433
|
+
const rest = c.rest ?? 1, peak = c.peak ?? 1.6;
|
|
434
|
+
const attack = (c.attackMs ?? 90) / 1000, release = (c.releaseMs ?? 130) / 1000;
|
|
435
|
+
const hold = Math.max(0, (b.until ?? b.at + attack + release) - b.at - attack - release);
|
|
436
|
+
const ease = c.easing ?? "easeInOut";
|
|
437
|
+
const key = (t, v) => ({ time: Math.max(0, t), [c.property]: v, easing: ease });
|
|
438
|
+
for (const p of matching(c.part)) {
|
|
439
|
+
transform.set(p, [...(transform.get(p) ?? []),
|
|
440
|
+
key(b.at, rest), key(b.at + attack, peak),
|
|
441
|
+
key(b.at + attack + hold, peak), key(b.at + attack + hold + release, rest)]);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else if (c.kind === "cycle" && c.part && c.property) {
|
|
445
|
+
/**
|
|
446
|
+
* A CYCLE IS THE GRAPH'S OWN RIG, FINALLY CONSUMED.
|
|
447
|
+
*
|
|
448
|
+
* `concept_rig` has described exactly this since the class layer was
|
|
449
|
+
* built — a part, a property, an amplitude and a duration — and nothing
|
|
450
|
+
* has ever read it. It is not a gesture with an end: a pigeon bobs while
|
|
451
|
+
* it walks, a tail flicks the whole time it is on screen, so this repeats
|
|
452
|
+
* for as long as the beat asks rather than resolving.
|
|
453
|
+
*
|
|
454
|
+
* THE THROW IS ASYMMETRIC. A sine reads as a metronome and a pigeon does
|
|
455
|
+
* not bob like one: the head goes forward fast and the body catches up
|
|
456
|
+
* slowly. Forward in the first third, recover across the rest.
|
|
457
|
+
*
|
|
458
|
+
* Amplitude is a FRACTION OF THE PART for x and y, degrees for rotation,
|
|
459
|
+
* which is how the graph states it — a pixel amplitude would mean
|
|
460
|
+
* something different at every size the figure is drawn.
|
|
461
|
+
*/
|
|
462
|
+
const until = b.until ?? durationSeconds;
|
|
463
|
+
const period = (c.durationMs ?? 800) / 1000;
|
|
464
|
+
const ease = c.easing ?? "easeInOut";
|
|
465
|
+
/**
|
|
466
|
+
* THE THROW, STATED IN THE SYSTEM'S VOCABULARY.
|
|
467
|
+
*
|
|
468
|
+
* A cycle is asymmetric on purpose — a pigeon throws its head forward
|
|
469
|
+
* and the body catches up — and the asymmetry used to be a bare 0.33 of
|
|
470
|
+
* the period. That produced intervals belonging to no scale: an 800ms
|
|
471
|
+
* bob split into 264 and 536, and a piece made mostly of cycles came out
|
|
472
|
+
* at 20% on the duration scale, which is the definition of not having a
|
|
473
|
+
* timing system.
|
|
474
|
+
*
|
|
475
|
+
* Rounding the ATTACK onto the scale and giving the remainder to the
|
|
476
|
+
* release keeps the shape and puts both halves on it: 800 becomes
|
|
477
|
+
* 300/500, 1200 becomes 400/800. The asymmetry survives; the arbitrary
|
|
478
|
+
* number does not.
|
|
479
|
+
*/
|
|
480
|
+
const attack = nearestOnScale(period * 0.33 * 1000) / 1000;
|
|
481
|
+
for (const pid of matching(c.part)) {
|
|
482
|
+
// A carried part measures against what carries it — see `spanPart`.
|
|
483
|
+
const ref = c.spanPart ? (matching(c.spanPart)[0] ?? pid) : pid;
|
|
484
|
+
const span = partSize(ref);
|
|
485
|
+
const amp = c.property === "rotation" ? (c.amplitude ?? 10)
|
|
486
|
+
: (c.amplitude ?? 0.3) * (c.property === "x" ? span.w : span.h);
|
|
487
|
+
const acc = [];
|
|
488
|
+
for (let t = b.at; t < until; t += period) {
|
|
489
|
+
acc.push({ time: t, [c.property]: 0, easing: ease });
|
|
490
|
+
acc.push({ time: Math.min(until, t + attack), [c.property]: amp, easing: ease });
|
|
491
|
+
}
|
|
492
|
+
acc.push({ time: until, [c.property]: 0, easing: ease });
|
|
493
|
+
cycle.set(pid, [...(cycle.get(pid) ?? []), ...acc]);
|
|
494
|
+
/**
|
|
495
|
+
* CARRIED PARTS MOVE WITH THEIR CARRIER, not alongside it.
|
|
496
|
+
*
|
|
497
|
+
* The same offsets, the same object, propagated down the carrier chain
|
|
498
|
+
* — so there is no second computation that could round differently,
|
|
499
|
+
* ease differently, or start a beat a frame late. This is the whole
|
|
500
|
+
* fix: a figure cannot come apart at a joint it has no independent
|
|
501
|
+
* motion across.
|
|
502
|
+
*/
|
|
503
|
+
for (const child of carriedTransitively(pid)) {
|
|
504
|
+
cycle.set(child, [...(cycle.get(child) ?? []), ...carry(acc, pid, child, c.property)]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
else if (c.kind === "shift") {
|
|
509
|
+
/**
|
|
510
|
+
* A SHIFT MOVES WHAT IT NAMES — and this moved everything.
|
|
511
|
+
*
|
|
512
|
+
* `part` was read for every other kind of channel and ignored here, so a
|
|
513
|
+
* shift always displaced the ENTIRE figure. `pp:Character.headTurn` got
|
|
514
|
+
* away with it because that depiction is a portrait: head, face and
|
|
515
|
+
* features, no body, so "every part" and "the head" are the same set.
|
|
516
|
+
* `pp:Peep` has body_0..2, and the same channel would slide the whole
|
|
517
|
+
* person sideways to turn their head.
|
|
518
|
+
*
|
|
519
|
+
* Declaring a part now narrows it, and carried parts come along — so a
|
|
520
|
+
* head turn takes the face, the hair and the eyelids with it, which is
|
|
521
|
+
* the entire reason the bone exists. A channel that names nothing still
|
|
522
|
+
* moves everything, because that is what the portrait relies on.
|
|
523
|
+
*/
|
|
524
|
+
const dx = (b.value ?? 0) * (c.unitsPerValue ?? 9) * k;
|
|
525
|
+
const settle = b.until ?? b.at + 0.6;
|
|
526
|
+
const named = c.part ? matching(c.part) : partIds;
|
|
527
|
+
const moved = new Set(named);
|
|
528
|
+
for (const pid of named)
|
|
529
|
+
for (const child of carriedTransitively(pid))
|
|
530
|
+
moved.add(child);
|
|
531
|
+
for (const p of moved) {
|
|
532
|
+
shiftX.set(p, [...(shiftX.get(p) ?? []), { time: Math.max(0, b.at - 0.001), dx: 0 }, { time: settle, dx }]);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// Position tracks need ABSOLUTE x, and a part's x is its own placed centre.
|
|
537
|
+
let tracks = 0;
|
|
538
|
+
for (const pid of partIds) {
|
|
539
|
+
const p = dep.parts[pid];
|
|
540
|
+
const baseX = at.x + p.at[0] * k;
|
|
541
|
+
const kfs = [];
|
|
542
|
+
for (const s of shiftX.get(pid) ?? [])
|
|
543
|
+
kfs.push({ time: s.time, x: baseX + s.dx });
|
|
544
|
+
for (const o of opacity.get(pid) ?? [])
|
|
545
|
+
kfs.push(o);
|
|
546
|
+
for (const tr of transform.get(pid) ?? [])
|
|
547
|
+
kfs.push(tr);
|
|
548
|
+
// A cycle's x/y are OFFSETS from where the part sits; everything else in a
|
|
549
|
+
// keyframe track is absolute, so they are added here rather than earlier.
|
|
550
|
+
for (const cy of cycle.get(pid) ?? []) {
|
|
551
|
+
const out = { time: cy.time, ...(cy.easing ? { easing: cy.easing } : {}) };
|
|
552
|
+
if (typeof cy.x === "number")
|
|
553
|
+
out.x = baseX + cy.x;
|
|
554
|
+
if (typeof cy.y === "number")
|
|
555
|
+
out.y = at.y + p.at[1] * k + cy.y;
|
|
556
|
+
if (typeof cy.rotation === "number")
|
|
557
|
+
out.rotation = cy.rotation;
|
|
558
|
+
if (typeof cy.scaleY === "number")
|
|
559
|
+
out.scaleY = cy.scaleY;
|
|
560
|
+
kfs.push(out);
|
|
561
|
+
}
|
|
562
|
+
if (!kfs.length)
|
|
563
|
+
continue;
|
|
564
|
+
// EVERY TRACK STARTS AT ZERO.
|
|
565
|
+
//
|
|
566
|
+
// An item whose first keyframe is at t=5 is INVISIBLE for the five seconds
|
|
567
|
+
// before it — measured: the head appeared exactly when its shift track
|
|
568
|
+
// began and the face exactly when its blink track began, and the first
|
|
569
|
+
// second of the film was an empty frame. The engine's own player treats
|
|
570
|
+
// "before the track" as "not yet", and this clock runs before it, so the
|
|
571
|
+
// player has the last word.
|
|
572
|
+
//
|
|
573
|
+
// Anchoring at zero with the baseline removes the state entirely rather
|
|
574
|
+
// than arguing with it: there is no "before the track" if the track covers
|
|
575
|
+
// the whole piece.
|
|
576
|
+
const restingTransform = {};
|
|
577
|
+
for (const c of channels) {
|
|
578
|
+
if (c.kind !== "transform" || !c.part || !c.property)
|
|
579
|
+
continue;
|
|
580
|
+
if (matching(c.part).includes(pid))
|
|
581
|
+
restingTransform[c.property] = c.rest ?? 1;
|
|
582
|
+
}
|
|
583
|
+
kfs.push({ time: 0, x: baseX, opacity: hidden(pid) ? 0 : 1, ...restingTransform });
|
|
584
|
+
// NOTHING ORGANIC MOVES AT A CONSTANT SPEED, and a rig is organic by
|
|
585
|
+
// definition. The transform and cycle channels set their own easing; the
|
|
586
|
+
// head-turn shift, the resting anchor and the closing hold did not, so a
|
|
587
|
+
// turn slid across the frame at a dead constant rate — the one tell that
|
|
588
|
+
// reads as "animated by a machine" no matter how good the drawing is.
|
|
589
|
+
// An explicit easing always wins; this only fills the silence.
|
|
590
|
+
const track = tidy(kfs).map((kf) => (kf.easing ? kf : { ...kf, easing: "easeInOut" }));
|
|
591
|
+
/**
|
|
592
|
+
* A track that never returns leaves the figure mid-gesture at the cut — and
|
|
593
|
+
* the hold must carry EVERY property the track animates, not whichever one
|
|
594
|
+
* the last keyframe happened to set.
|
|
595
|
+
*
|
|
596
|
+
* Cloning the final keyframe looked right and was not: the eye's last
|
|
597
|
+
* keyframe is a blink, which sets scaleY alone, so the closing hold dropped
|
|
598
|
+
* x. Every other part of the head ended with an explicit x at the duration
|
|
599
|
+
* and the eye did not — one keyframe's difference between a bound part and
|
|
600
|
+
* its carrier, which is exactly the kind of divergence the bone exists to
|
|
601
|
+
* make impossible. It was harmless here only because the held value and the
|
|
602
|
+
* last value agreed; that is luck, not a guarantee.
|
|
603
|
+
*/
|
|
604
|
+
const last = track[track.length - 1];
|
|
605
|
+
if (last.time < durationSeconds) {
|
|
606
|
+
const held = { time: durationSeconds };
|
|
607
|
+
for (const kf of track) {
|
|
608
|
+
for (const [k, v] of Object.entries(kf)) {
|
|
609
|
+
if (k === "time" || k === "easing")
|
|
610
|
+
continue;
|
|
611
|
+
held[k] = v;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (last.easing)
|
|
615
|
+
held.easing = last.easing;
|
|
616
|
+
track.push(held);
|
|
617
|
+
}
|
|
618
|
+
ops.push({
|
|
619
|
+
tool: "pinepaper_keyframe_animate",
|
|
620
|
+
args: { itemId: `${id}_${pid}`, duration: durationSeconds, keyframes: track },
|
|
621
|
+
});
|
|
622
|
+
tracks++;
|
|
623
|
+
}
|
|
624
|
+
return { ops, parts: partIds.map((p) => `${id}_${p}`), tracks, unknownChannels };
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=perform.js.map
|