@pixerate/editor 0.53.0 → 0.55.0
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/dist/agent/index.cjs +20 -12
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +4 -3
- package/dist/agent/index.d.ts +4 -3
- package/dist/agent/index.js +18 -10
- package/dist/agent/index.js.map +1 -1
- package/dist/animation/index.cjs +16 -2
- package/dist/animation/index.cjs.map +1 -1
- package/dist/animation/index.d.cts +4 -226
- package/dist/animation/index.d.ts +4 -226
- package/dist/animation/index.js +15 -1
- package/dist/animation-editor/index.cjs +2221 -0
- package/dist/animation-editor/index.cjs.map +1 -0
- package/dist/animation-editor/index.d.cts +1117 -0
- package/dist/animation-editor/index.d.ts +1117 -0
- package/dist/animation-editor/index.js +2221 -0
- package/dist/animation-editor/index.js.map +1 -0
- package/dist/bezier-BfwIbVTl.d.cts +548 -0
- package/dist/bezier-DYBvLgUH.d.ts +548 -0
- package/dist/bridge/cli.cjs +36 -0
- package/dist/bridge/cli.cjs.map +1 -0
- package/dist/bridge/cli.d.cts +1 -0
- package/dist/bridge/cli.d.ts +1 -0
- package/dist/bridge/cli.js +36 -0
- package/dist/bridge/cli.js.map +1 -0
- package/dist/bridge/index.cjs +16 -0
- package/dist/bridge/index.cjs.map +1 -0
- package/dist/bridge/index.d.cts +65 -0
- package/dist/bridge/index.d.ts +65 -0
- package/dist/bridge/index.js +16 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge-client-C5OziCRK.d.cts +148 -0
- package/dist/bridge-client-C5dRjrz5.d.ts +148 -0
- package/dist/canvas/index.cjs +1564 -557
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +411 -7
- package/dist/canvas/index.d.ts +411 -7
- package/dist/canvas/index.js +1559 -552
- package/dist/canvas/index.js.map +1 -1
- package/dist/chart/index.cjs +34 -76
- package/dist/chart/index.cjs.map +1 -1
- package/dist/chart/index.d.cts +9 -38
- package/dist/chart/index.d.ts +9 -38
- package/dist/chart/index.js +23 -65
- package/dist/chart/index.js.map +1 -1
- package/dist/chunk-AWKPSOSX.js +401 -0
- package/dist/chunk-AWKPSOSX.js.map +1 -0
- package/dist/chunk-B6DSIEXA.js +96 -0
- package/dist/chunk-B6DSIEXA.js.map +1 -0
- package/dist/chunk-EGFUVXKK.cjs +96 -0
- package/dist/chunk-EGFUVXKK.cjs.map +1 -0
- package/dist/{chunk-AMWXSYCI.js → chunk-GPWNYQ7R.js} +2 -370
- package/dist/chunk-GPWNYQ7R.js.map +1 -0
- package/dist/{chunk-TO22LX34.js → chunk-GTMJ4Y3S.js} +2208 -2920
- package/dist/chunk-GTMJ4Y3S.js.map +1 -0
- package/dist/chunk-H5K77C7Y.cjs +3848 -0
- package/dist/chunk-H5K77C7Y.cjs.map +1 -0
- package/dist/chunk-K73CKYUM.cjs +458 -0
- package/dist/chunk-K73CKYUM.cjs.map +1 -0
- package/dist/chunk-MLREO6J3.cjs +737 -0
- package/dist/chunk-MLREO6J3.cjs.map +1 -0
- package/dist/chunk-NONDTTH7.js +5979 -0
- package/dist/chunk-NONDTTH7.js.map +1 -0
- package/dist/chunk-NV22EMOZ.cjs +401 -0
- package/dist/chunk-NV22EMOZ.cjs.map +1 -0
- package/dist/{chunk-YNSK4MGT.cjs → chunk-O2UH7LZE.cjs} +1549 -2261
- package/dist/chunk-O2UH7LZE.cjs.map +1 -0
- package/dist/chunk-PTMIGXZ2.js +467 -0
- package/dist/chunk-PTMIGXZ2.js.map +1 -0
- package/dist/chunk-QIWFEEWG.cjs +467 -0
- package/dist/chunk-QIWFEEWG.cjs.map +1 -0
- package/dist/chunk-VWZB6BCY.js +737 -0
- package/dist/chunk-VWZB6BCY.js.map +1 -0
- package/dist/chunk-WWJNC7SX.js +3848 -0
- package/dist/chunk-WWJNC7SX.js.map +1 -0
- package/dist/chunk-XCXQDPID.cjs +5979 -0
- package/dist/chunk-XCXQDPID.cjs.map +1 -0
- package/dist/color-E9pwm--w.d.cts +33 -0
- package/dist/color-E9pwm--w.d.ts +33 -0
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +3 -3
- package/dist/diagram/index.cjs +1167 -0
- package/dist/diagram/index.cjs.map +1 -0
- package/dist/diagram/index.d.cts +810 -0
- package/dist/diagram/index.d.ts +810 -0
- package/dist/diagram/index.js +1167 -0
- package/dist/diagram/index.js.map +1 -0
- package/dist/{easing-DTmX99QG.d.cts → easing-CNGk-W6d.d.cts} +1 -1
- package/dist/{easing-DTmX99QG.d.ts → easing-CNGk-W6d.d.ts} +1 -1
- package/dist/generation/index.d.cts +2 -2
- package/dist/generation/index.d.ts +2 -2
- package/dist/image-editor/index.d.cts +1 -1
- package/dist/image-editor/index.d.ts +1 -1
- package/dist/parameters/index.d.cts +1 -1
- package/dist/parameters/index.d.ts +1 -1
- package/dist/pathfinder-BZL8aSu0.d.cts +469 -0
- package/dist/pathfinder-CNtsbCRK.d.ts +469 -0
- package/dist/player-Dg0FT0UI.d.cts +193 -0
- package/dist/player-W-5DX3Go.d.ts +193 -0
- package/dist/presence-Cb6WhcrQ.d.cts +77 -0
- package/dist/presence-Cb6WhcrQ.d.ts +77 -0
- package/dist/{registry-CSlH4iUn.d.ts → registry-7AZFuand.d.ts} +1 -1
- package/dist/{registry-B4l-lYts.d.cts → registry-DIpIaCkH.d.cts} +1 -1
- package/dist/svg-CLg2zqZA.d.cts +847 -0
- package/dist/svg-CPK82TtU.d.ts +847 -0
- package/dist/tools/index.cjs +10 -2
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.d.cts +3 -74
- package/dist/tools/index.d.ts +3 -74
- package/dist/tools/index.js +12 -4
- package/dist/types-BULrFyuQ.d.cts +316 -0
- package/dist/types-BULrFyuQ.d.ts +316 -0
- package/dist/vector/index.cjs +1328 -6752
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +7 -847
- package/dist/vector/index.d.ts +7 -847
- package/dist/vector/index.js +645 -6069
- package/dist/vector/index.js.map +1 -1
- package/package.json +19 -1
- package/dist/chunk-AMWXSYCI.js.map +0 -1
- package/dist/chunk-TO22LX34.js.map +0 -1
- package/dist/chunk-XFYUFVAX.cjs +0 -826
- package/dist/chunk-XFYUFVAX.cjs.map +0 -1
- package/dist/chunk-YNSK4MGT.cjs.map +0 -1
- package/dist/pathfinder-B9xh_pCZ.d.ts +0 -1013
- package/dist/pathfinder-QdG70sr9.d.cts +0 -1013
|
@@ -0,0 +1,2221 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
var _chunkXCXQDPIDcjs = require('../chunk-XCXQDPID.cjs');
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
var _chunkO2UH7LZEcjs = require('../chunk-O2UH7LZE.cjs');
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
var _chunkQKLRAA4Tcjs = require('../chunk-QKLRAA4T.cjs');
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
var _chunkO4ONBPZWcjs = require('../chunk-O4ONBPZW.cjs');
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
var _chunkMR3Z4OQIcjs = require('../chunk-MR3Z4OQI.cjs');
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
var _chunkA6PHCAGXcjs = require('../chunk-A6PHCAGX.cjs');
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
var _chunkXZ4L3R4Kcjs = require('../chunk-XZ4L3R4K.cjs');
|
|
46
|
+
|
|
47
|
+
// src/animation-editor/types.ts
|
|
48
|
+
var MOTION_COMMAND_TYPES = [
|
|
49
|
+
"enter",
|
|
50
|
+
"exit",
|
|
51
|
+
"draw",
|
|
52
|
+
"move",
|
|
53
|
+
"rotate",
|
|
54
|
+
"scale",
|
|
55
|
+
"fade",
|
|
56
|
+
"style",
|
|
57
|
+
"morph",
|
|
58
|
+
"follow",
|
|
59
|
+
"pulse",
|
|
60
|
+
"shake",
|
|
61
|
+
"effect",
|
|
62
|
+
"wait"
|
|
63
|
+
];
|
|
64
|
+
var MOTION_ENTER_STYLES = ["fade", "pop", "grow", "slide", "draw"];
|
|
65
|
+
var MOTION_EXIT_STYLES = ["fade", "shrink", "slide", "undraw"];
|
|
66
|
+
var MOTION_DIRECTIONS = ["up", "down", "left", "right"];
|
|
67
|
+
var MOTION_DEFAULT_DURATIONS = {
|
|
68
|
+
enter: 600,
|
|
69
|
+
exit: 450,
|
|
70
|
+
move: 700,
|
|
71
|
+
rotate: 700,
|
|
72
|
+
scale: 500,
|
|
73
|
+
fade: 400,
|
|
74
|
+
style: 500,
|
|
75
|
+
morph: 900,
|
|
76
|
+
follow: 1400,
|
|
77
|
+
pulse: 600,
|
|
78
|
+
shake: 500,
|
|
79
|
+
effect: 900
|
|
80
|
+
};
|
|
81
|
+
var MOTION_DEFAULT_HOLD = 600;
|
|
82
|
+
var MOTION_CAMERA_MS = 1e3;
|
|
83
|
+
var MOTION_DRAW_MS_PER_UNIT = 1.5;
|
|
84
|
+
var MOTION_DRAW_RANGE = [500, 2500];
|
|
85
|
+
|
|
86
|
+
// src/animation-editor/issues.ts
|
|
87
|
+
var MOTION_ISSUE_CODES = {
|
|
88
|
+
"no-shots": { severity: "note", wording: "The scene has no shots yet." },
|
|
89
|
+
"duplicate-shot-id": { severity: "error", wording: "Two shots share an id." },
|
|
90
|
+
"unknown-target": { severity: "error", wording: "A command names a node the artwork does not have." },
|
|
91
|
+
"unknown-path": { severity: "error", wording: "A follow command names a path the artwork does not have." },
|
|
92
|
+
"not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
|
|
93
|
+
"unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
|
|
94
|
+
"unknown-effect": { severity: "error", wording: "An effect command names an effect the node does not have." },
|
|
95
|
+
"unknown-effect-param": { severity: "error", wording: "An effect command names a parameter that is not a number of that effect." },
|
|
96
|
+
"unknown-frame": { severity: "error", wording: "A camera frames a node the artwork does not have." },
|
|
97
|
+
"invalid-camera": { severity: "error", wording: "A camera rectangle has no area." },
|
|
98
|
+
"invalid-color": { severity: "error", wording: "A colour cannot be read." },
|
|
99
|
+
"paint-not-color": { severity: "warning", wording: "A style command recolours a gradient or empty paint, which only switches at the end." },
|
|
100
|
+
"overlap": { severity: "warning", wording: "Two commands change the same property of a node at the same time; the later one wins." },
|
|
101
|
+
"enter-visible": { severity: "warning", wording: "A node enters while it is already showing, so it blinks out first." },
|
|
102
|
+
"exit-hidden": { severity: "warning", wording: "A node exits or animates while it is hidden." },
|
|
103
|
+
"out-of-frame": { severity: "warning", wording: "A node a command animates ends up outside the camera." },
|
|
104
|
+
"too-fast": { severity: "warning", wording: "A change is too quick to follow." },
|
|
105
|
+
"scene-too-long": { severity: "warning", wording: "The scene runs past its target length." },
|
|
106
|
+
"scene-too-short": { severity: "warning", wording: "The scene ends before its target length." },
|
|
107
|
+
"unknown-control-color": { severity: "warning", wording: "A control stands for a colour the scene never uses." },
|
|
108
|
+
"empty-shot": { severity: "note", wording: "A shot neither moves the camera nor runs a command." },
|
|
109
|
+
"camera-aspect": { severity: "note", wording: "A camera rectangle has a different shape from the artwork, so it is letterboxed." }
|
|
110
|
+
};
|
|
111
|
+
function motionIssue(code, message, extra = {}) {
|
|
112
|
+
return { code, severity: MOTION_ISSUE_CODES[code].severity, message, ...extra };
|
|
113
|
+
}
|
|
114
|
+
function motionIssueKey(issue) {
|
|
115
|
+
if (_optionalChain([issue, 'access', _2 => _2.ids, 'optionalAccess', _3 => _3.length])) return `${issue.code}:${[...issue.ids].sort().join(",")}`;
|
|
116
|
+
return `${issue.code}@${_nullishCoalesce(issue.shot, () => ( ""))}#${_nullishCoalesce(issue.command, () => ( ""))}`;
|
|
117
|
+
}
|
|
118
|
+
function motionIssueLine(issue) {
|
|
119
|
+
const where = issue.shot !== void 0 ? `Shot "${issue.shot}"${issue.command !== void 0 ? `, command ${issue.command + 1}` : ""}: ` : "";
|
|
120
|
+
return `${where}${issue.message}`;
|
|
121
|
+
}
|
|
122
|
+
function editDistance(a, b) {
|
|
123
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
124
|
+
for (let j = 1; j <= b.length; j++) d[0][j] = j;
|
|
125
|
+
for (let i = 1; i <= a.length; i++) {
|
|
126
|
+
for (let j = 1; j <= b.length; j++) {
|
|
127
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
128
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
129
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return d[a.length][b.length];
|
|
133
|
+
}
|
|
134
|
+
function suggestName(word, candidates) {
|
|
135
|
+
const w = word.toLowerCase();
|
|
136
|
+
let best;
|
|
137
|
+
let bestDistance = Infinity;
|
|
138
|
+
for (const c of candidates) {
|
|
139
|
+
const distance = editDistance(w, c.toLowerCase());
|
|
140
|
+
if (distance < bestDistance) {
|
|
141
|
+
best = c;
|
|
142
|
+
bestDistance = distance;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return best !== void 0 && bestDistance <= Math.max(1, Math.floor(word.length / 3)) ? best : void 0;
|
|
146
|
+
}
|
|
147
|
+
function namedWithSuggestion(word, candidates) {
|
|
148
|
+
const s = suggestName(word, candidates);
|
|
149
|
+
return s !== void 0 && s !== word ? `"${word}" (did you mean "${s}"?)` : `"${word}"`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/animation-editor/parse.ts
|
|
153
|
+
var MotionSceneFormatError = class extends Error {
|
|
154
|
+
|
|
155
|
+
constructor(problems) {
|
|
156
|
+
super(`The scene is not well formed:
|
|
157
|
+
${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
158
|
+
this.name = "MotionSceneFormatError";
|
|
159
|
+
this.problems = problems;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "target", "controls", "presets"];
|
|
163
|
+
var SHOT_KEYS = ["id", "label", "camera", "run", "hold"];
|
|
164
|
+
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms"];
|
|
165
|
+
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note"];
|
|
166
|
+
var COMMAND_KEYS = {
|
|
167
|
+
enter: [...BASE_KEYS, "style", "direction", "distance"],
|
|
168
|
+
exit: [...BASE_KEYS, "style", "direction", "distance"],
|
|
169
|
+
draw: [...BASE_KEYS, "from", "to"],
|
|
170
|
+
move: [...BASE_KEYS, "by", "to"],
|
|
171
|
+
rotate: [...BASE_KEYS, "by", "to"],
|
|
172
|
+
scale: [...BASE_KEYS, "to"],
|
|
173
|
+
fade: [...BASE_KEYS, "to"],
|
|
174
|
+
style: [...BASE_KEYS, "fill", "stroke", "strokeWidth"],
|
|
175
|
+
morph: [...BASE_KEYS, "into"],
|
|
176
|
+
follow: [...BASE_KEYS, "path", "rotate", "from", "to"],
|
|
177
|
+
pulse: [...BASE_KEYS, "amount", "count"],
|
|
178
|
+
shake: [...BASE_KEYS, "amount", "count"],
|
|
179
|
+
effect: [...BASE_KEYS, "effect", "param", "to"],
|
|
180
|
+
wait: ["type", "duration", "with", "note"]
|
|
181
|
+
};
|
|
182
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
183
|
+
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
184
|
+
var isPoint = (v) => isObject(v) && isNum(v.x) && isNum(v.y);
|
|
185
|
+
function unknownKeys(value, known, where, problems) {
|
|
186
|
+
for (const key of Object.keys(value)) if (!known.includes(key)) problems.push(`${where}: unknown field ${namedWithSuggestion(key, known)}.`);
|
|
187
|
+
}
|
|
188
|
+
function commandShapeProblems(command2, where) {
|
|
189
|
+
const problems = [];
|
|
190
|
+
if (!isObject(command2)) return [`${where} must be an object.`];
|
|
191
|
+
const type = command2.type;
|
|
192
|
+
if (typeof type !== "string" || !MOTION_COMMAND_TYPES.includes(type)) {
|
|
193
|
+
return [`${where}: unknown type ${typeof type === "string" ? namedWithSuggestion(type, MOTION_COMMAND_TYPES) : String(type)}; use one of ${MOTION_COMMAND_TYPES.join(", ")}.`];
|
|
194
|
+
}
|
|
195
|
+
unknownKeys(command2, COMMAND_KEYS[type], `${where} (${type})`, problems);
|
|
196
|
+
const p = (msg) => problems.push(`${where} (${type}): ${msg}`);
|
|
197
|
+
const nonNegative = (key) => {
|
|
198
|
+
if (command2[key] !== void 0 && !(isNum(command2[key]) && command2[key] >= 0)) p(`"${key}" must be a number of milliseconds \u2265 0.`);
|
|
199
|
+
};
|
|
200
|
+
const fraction = (key) => {
|
|
201
|
+
if (command2[key] !== void 0 && !(isNum(command2[key]) && command2[key] >= 0 && command2[key] <= 1)) p(`"${key}" must be a fraction from 0 to 1.`);
|
|
202
|
+
};
|
|
203
|
+
const optionalNumber = (key) => {
|
|
204
|
+
if (command2[key] !== void 0 && !isNum(command2[key])) p(`"${key}" must be a number.`);
|
|
205
|
+
};
|
|
206
|
+
if (command2.with !== void 0 && command2.with !== "after" && command2.with !== "previous") p(`"with" must be "after" or "previous".`);
|
|
207
|
+
if (command2.note !== void 0 && typeof command2.note !== "string") p(`"note" must be a string.`);
|
|
208
|
+
if (type === "wait") {
|
|
209
|
+
if (!isNum(command2.duration) || command2.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
|
|
210
|
+
return problems;
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(command2.targets) || command2.targets.length === 0 || !command2.targets.every((t) => typeof t === "string" && t.length > 0)) p(`"targets" must be a non-empty list of node ids.`);
|
|
213
|
+
nonNegative("duration");
|
|
214
|
+
nonNegative("delay");
|
|
215
|
+
nonNegative("stagger");
|
|
216
|
+
if (command2.ease !== void 0 && !_chunkXZ4L3R4Kcjs.isAnimationEasing.call(void 0, command2.ease)) p(`"ease" is not an easing; use a preset such as "ease-in-out" or { "type": "cubic-bezier", \u2026 }.`);
|
|
217
|
+
switch (type) {
|
|
218
|
+
case "enter":
|
|
219
|
+
case "exit": {
|
|
220
|
+
const styles = type === "enter" ? MOTION_ENTER_STYLES : MOTION_EXIT_STYLES;
|
|
221
|
+
if (command2.style !== void 0 && !(typeof command2.style === "string" && styles.includes(command2.style)))
|
|
222
|
+
p(`"style" must be one of ${styles.join(", ")}${typeof command2.style === "string" ? ` (got ${namedWithSuggestion(command2.style, styles)})` : ""}.`);
|
|
223
|
+
if (command2.direction !== void 0 && !MOTION_DIRECTIONS.includes(command2.direction)) p(`"direction" must be one of ${MOTION_DIRECTIONS.join(", ")}.`);
|
|
224
|
+
optionalNumber("distance");
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case "draw":
|
|
228
|
+
fraction("from");
|
|
229
|
+
fraction("to");
|
|
230
|
+
break;
|
|
231
|
+
case "move":
|
|
232
|
+
if (command2.by === void 0 && command2.to === void 0) p(`give "by" (an offset) or "to" (a point).`);
|
|
233
|
+
if (command2.by !== void 0 && command2.to !== void 0) p(`give "by" or "to", not both.`);
|
|
234
|
+
if (command2.by !== void 0 && !isPoint(command2.by)) p(`"by" must be { "x": number, "y": number }.`);
|
|
235
|
+
if (command2.to !== void 0 && !isPoint(command2.to)) p(`"to" must be { "x": number, "y": number }.`);
|
|
236
|
+
break;
|
|
237
|
+
case "rotate":
|
|
238
|
+
if (command2.by === void 0 && command2.to === void 0) p(`give "by" or "to" (degrees).`);
|
|
239
|
+
if (command2.by !== void 0 && command2.to !== void 0) p(`give "by" or "to", not both.`);
|
|
240
|
+
optionalNumber("by");
|
|
241
|
+
optionalNumber("to");
|
|
242
|
+
break;
|
|
243
|
+
case "scale":
|
|
244
|
+
if (!(isNum(command2.to) || isPoint(command2.to))) p(`"to" must be a number or { "x": number, "y": number }.`);
|
|
245
|
+
break;
|
|
246
|
+
case "fade":
|
|
247
|
+
if (!(isNum(command2.to) && command2.to >= 0 && command2.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
|
|
248
|
+
break;
|
|
249
|
+
case "style":
|
|
250
|
+
if (command2.fill === void 0 && command2.stroke === void 0 && command2.strokeWidth === void 0) p(`give at least one of "fill", "stroke" or "strokeWidth".`);
|
|
251
|
+
if (command2.fill !== void 0 && typeof command2.fill !== "string") p(`"fill" must be a CSS colour.`);
|
|
252
|
+
if (command2.stroke !== void 0 && typeof command2.stroke !== "string") p(`"stroke" must be a CSS colour.`);
|
|
253
|
+
if (command2.strokeWidth !== void 0 && !(isNum(command2.strokeWidth) && command2.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
|
|
254
|
+
break;
|
|
255
|
+
case "morph":
|
|
256
|
+
if (typeof command2.into !== "string" || !command2.into) p(`"into" must be the id of a node whose shape to take.`);
|
|
257
|
+
break;
|
|
258
|
+
case "follow":
|
|
259
|
+
if (typeof command2.path !== "string" || !command2.path) p(`"path" must be the id of a path node.`);
|
|
260
|
+
if (command2.rotate !== void 0 && typeof command2.rotate !== "boolean") p(`"rotate" must be true or false.`);
|
|
261
|
+
fraction("from");
|
|
262
|
+
fraction("to");
|
|
263
|
+
break;
|
|
264
|
+
case "pulse":
|
|
265
|
+
case "shake":
|
|
266
|
+
optionalNumber("amount");
|
|
267
|
+
if (command2.count !== void 0 && !(Number.isInteger(command2.count) && command2.count >= 1)) p(`"count" must be a whole number \u2265 1.`);
|
|
268
|
+
break;
|
|
269
|
+
case "effect":
|
|
270
|
+
if (typeof command2.effect !== "string" || !command2.effect) p(`"effect" must be the id of one of the node's effects.`);
|
|
271
|
+
if (typeof command2.param !== "string" || !command2.param) p(`"param" must name a numeric parameter of the effect.`);
|
|
272
|
+
if (!isNum(command2.to)) p(`"to" must be a number.`);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
return problems;
|
|
276
|
+
}
|
|
277
|
+
function shotShapeProblems(shot, where) {
|
|
278
|
+
if (!isObject(shot)) return [`${where} must be an object.`];
|
|
279
|
+
const problems = [];
|
|
280
|
+
unknownKeys(shot, SHOT_KEYS, where, problems);
|
|
281
|
+
if (typeof shot.id !== "string" || !/^[A-Za-z0-9_][\w.-]*$/.test(shot.id)) problems.push(`${where}: "id" must be letters, digits, "_", "-" or "." (e.g. "intro").`);
|
|
282
|
+
const name = typeof shot.id === "string" ? `Shot "${shot.id}"` : where;
|
|
283
|
+
if (shot.label !== void 0 && typeof shot.label !== "string") problems.push(`${name}: "label" must be a string.`);
|
|
284
|
+
if (shot.hold !== void 0 && !(isNum(shot.hold) && shot.hold >= 0)) problems.push(`${name}: "hold" must be a number of milliseconds \u2265 0.`);
|
|
285
|
+
if (shot.camera !== void 0) {
|
|
286
|
+
const c = shot.camera;
|
|
287
|
+
if (!isObject(c)) problems.push(`${name}: "camera" must be an object.`);
|
|
288
|
+
else {
|
|
289
|
+
unknownKeys(c, CAMERA_KEYS, `${name} camera`, problems);
|
|
290
|
+
const isRect = ["x", "y", "width", "height"].every((k) => isNum(c[k]));
|
|
291
|
+
if (typeof c.frame === "string") {
|
|
292
|
+
if (c.padding !== void 0 && !isNum(c.padding)) problems.push(`${name} camera: "padding" must be a number.`);
|
|
293
|
+
} else if (!isRect) problems.push(`${name} camera: give { x, y, width, height } or { frame: "<node id>" }.`);
|
|
294
|
+
if (c.ease !== void 0 && !_chunkXZ4L3R4Kcjs.isAnimationEasing.call(void 0, c.ease)) problems.push(`${name} camera: "ease" is not an easing.`);
|
|
295
|
+
if (c.ms !== void 0 && !(isNum(c.ms) && c.ms >= 0)) problems.push(`${name} camera: "ms" must be a number \u2265 0.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!Array.isArray(shot.run)) problems.push(`${name}: "run" must be a list of commands (it may be empty).`);
|
|
299
|
+
else shot.run.forEach((command2, i) => problems.push(...commandShapeProblems(command2, `${name}, command ${i + 1}`)));
|
|
300
|
+
return problems;
|
|
301
|
+
}
|
|
302
|
+
function motionSceneShapeProblems(input) {
|
|
303
|
+
if (!isObject(input)) return ["The scene must be an object."];
|
|
304
|
+
const problems = [];
|
|
305
|
+
unknownKeys(input, SCENE_KEYS, "Scene", problems);
|
|
306
|
+
if (input.version !== 1) problems.push('Scene: "version" must be 1.');
|
|
307
|
+
if (typeof input.name !== "string" || !input.name) problems.push('Scene: "name" must be a non-empty string.');
|
|
308
|
+
if (input.description !== void 0 && typeof input.description !== "string") problems.push('Scene: "description" must be a string.');
|
|
309
|
+
const art = input.artwork;
|
|
310
|
+
if (!isObject(art) || !isNum(art.width) || !isNum(art.height) || !(art.width > 0) || !(art.height > 0) || !Array.isArray(art.nodes))
|
|
311
|
+
problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
|
|
312
|
+
if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
|
|
313
|
+
if (input.fps !== void 0 && !(isNum(input.fps) && input.fps > 0 && input.fps <= 120)) problems.push('Scene: "fps" must be a number from 1 to 120.');
|
|
314
|
+
if (input.target !== void 0 && !(Array.isArray(input.target) && input.target.length === 2 && input.target.every(isNum) && input.target[0] <= input.target[1]))
|
|
315
|
+
problems.push('Scene: "target" must be [min, max] seconds.');
|
|
316
|
+
if (input.controls !== void 0) {
|
|
317
|
+
if (!Array.isArray(input.controls)) problems.push('Scene: "controls" must be a list.');
|
|
318
|
+
else
|
|
319
|
+
input.controls.forEach((c, i) => {
|
|
320
|
+
if (!isObject(c) || typeof c.name !== "string" || !/^[A-Za-z_]\w*$/.test(c.name) || c.type !== "color" || typeof c.default !== "string" || !/^#[0-9a-fA-F]{6}$/.test(c.default))
|
|
321
|
+
problems.push(`Scene: control ${i + 1} must be { name (an identifier), type: "color", default: "#rrggbb", label? }.`);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
if (input.presets !== void 0 && !(Array.isArray(input.presets) && input.presets.every((p) => isObject(p) && typeof p.name === "string" && isObject(p.values))))
|
|
325
|
+
problems.push('Scene: "presets" must be a list of { name, values }.');
|
|
326
|
+
if (!Array.isArray(input.shots)) problems.push('Scene: "shots" must be a list.');
|
|
327
|
+
else input.shots.forEach((shot, i) => problems.push(...shotShapeProblems(shot, `Shot ${i + 1}`)));
|
|
328
|
+
return problems;
|
|
329
|
+
}
|
|
330
|
+
function parseMotionScene(input) {
|
|
331
|
+
let value = input;
|
|
332
|
+
if (typeof input === "string") {
|
|
333
|
+
try {
|
|
334
|
+
value = JSON.parse(input);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw new MotionSceneFormatError([`The scene is not valid JSON: ${error.message}`]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const problems = motionSceneShapeProblems(value);
|
|
340
|
+
if (problems.length) throw new MotionSceneFormatError(problems);
|
|
341
|
+
return value;
|
|
342
|
+
}
|
|
343
|
+
function motionSceneToJson(scene, space = 2) {
|
|
344
|
+
return JSON.stringify(scene, null, space);
|
|
345
|
+
}
|
|
346
|
+
var isMotionShot = (shot) => shotShapeProblems(shot, "Shot").length === 0;
|
|
347
|
+
var isMotionCommand = (command2) => commandShapeProblems(command2, "Command").length === 0;
|
|
348
|
+
|
|
349
|
+
// src/animation-editor/compile.ts
|
|
350
|
+
var indexCache = /* @__PURE__ */ new WeakMap();
|
|
351
|
+
function indexMotionNodes(nodes) {
|
|
352
|
+
const hit = indexCache.get(nodes);
|
|
353
|
+
if (hit) return hit;
|
|
354
|
+
const out = /* @__PURE__ */ new Map();
|
|
355
|
+
const walk = (list, parentWorld, ancestors) => {
|
|
356
|
+
for (const node of list) {
|
|
357
|
+
const local = _chunkXCXQDPIDcjs.vectorNodeBounds.call(void 0, node);
|
|
358
|
+
const bounds = _chunkXCXQDPIDcjs.vectorNodeBounds.call(void 0, node, parentWorld);
|
|
359
|
+
const center = local ? { x: local.x + local.width / 2, y: local.y + local.height / 2 } : { x: 0, y: 0 };
|
|
360
|
+
if (!out.has(node.id)) out.set(node.id, { node, ancestors, parentWorld, center, bounds });
|
|
361
|
+
if (node.type === "group") walk(node.children, _chunkXCXQDPIDcjs.multiplyMatrices.call(void 0, _chunkXCXQDPIDcjs.nodeMatrix.call(void 0, node), parentWorld), [...ancestors, node.id]);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
walk(nodes, _chunkXCXQDPIDcjs.IDENTITY_MATRIX, []);
|
|
365
|
+
indexCache.set(nodes, out);
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
var solid = (paint) => typeof paint === "string" && paint !== "none" ? paint : void 0;
|
|
369
|
+
function firstPaint(node, key) {
|
|
370
|
+
if (node.type === "path") return solid(node[key]);
|
|
371
|
+
for (const p of _chunkXCXQDPIDcjs.collectPathNodes.call(void 0, node)) {
|
|
372
|
+
const s = solid(p[key]);
|
|
373
|
+
if (s) return s;
|
|
374
|
+
}
|
|
375
|
+
return void 0;
|
|
376
|
+
}
|
|
377
|
+
function motionRestValue(entry, property, artwork) {
|
|
378
|
+
const node = _optionalChain([entry, 'optionalAccess', _4 => _4.node]);
|
|
379
|
+
switch (property) {
|
|
380
|
+
case "opacity":
|
|
381
|
+
return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _5 => _5.opacity]), () => ( 1));
|
|
382
|
+
case "x":
|
|
383
|
+
case "y":
|
|
384
|
+
case "rotation":
|
|
385
|
+
return 0;
|
|
386
|
+
case "scaleX":
|
|
387
|
+
case "scaleY":
|
|
388
|
+
case "fillOpacity":
|
|
389
|
+
case "drawEnd":
|
|
390
|
+
return 1;
|
|
391
|
+
case "drawStart":
|
|
392
|
+
return 0;
|
|
393
|
+
case "follow":
|
|
394
|
+
return -1;
|
|
395
|
+
case "fill":
|
|
396
|
+
case "stroke":
|
|
397
|
+
return _nullishCoalesce((node && firstPaint(node, property)), () => ( "transparent"));
|
|
398
|
+
case "strokeWidth": {
|
|
399
|
+
const path = _optionalChain([node, 'optionalAccess', _6 => _6.type]) === "path" ? node : node ? _chunkXCXQDPIDcjs.collectPathNodes.call(void 0, node)[0] : void 0;
|
|
400
|
+
return _nullishCoalesce(_optionalChain([path, 'optionalAccess', _7 => _7.strokeWidth]), () => ( 1));
|
|
401
|
+
}
|
|
402
|
+
case "shape":
|
|
403
|
+
return _nullishCoalesce(_optionalChain([node, 'optionalAccess', _8 => _8.id]), () => ( ""));
|
|
404
|
+
case "camera":
|
|
405
|
+
return [0, 0, artwork.width, artwork.height];
|
|
406
|
+
}
|
|
407
|
+
const [, effectId, param] = property.split(":");
|
|
408
|
+
const effect = _optionalChain([node, 'optionalAccess', _9 => _9.effects, 'optionalAccess', _10 => _10.find, 'call', _11 => _11((e) => e.id === effectId)]);
|
|
409
|
+
const v = effect ? effect.params[param] : void 0;
|
|
410
|
+
return typeof v === "number" ? v : 0;
|
|
411
|
+
}
|
|
412
|
+
function interpolateMotionValue(from, to, u) {
|
|
413
|
+
if (typeof from === "number" && typeof to === "number") return from + (to - from) * u;
|
|
414
|
+
if (Array.isArray(from) && Array.isArray(to) && from.length === to.length) return from.map((f, i) => f + (to[i] - f) * u);
|
|
415
|
+
if (u <= 0) return from;
|
|
416
|
+
if (u >= 1) return to;
|
|
417
|
+
if (typeof from === "string" && typeof to === "string") {
|
|
418
|
+
const mixed = _chunkMR3Z4OQIcjs.mixColorsOklab.call(void 0, from, to, u);
|
|
419
|
+
if (mixed !== null) return mixed;
|
|
420
|
+
}
|
|
421
|
+
return u < 1 ? from : to;
|
|
422
|
+
}
|
|
423
|
+
function segmentProgress(segment, time) {
|
|
424
|
+
const span = segment.end - segment.start;
|
|
425
|
+
const u = span <= 0 ? 1 : Math.min(1, Math.max(0, (time - segment.start) / span));
|
|
426
|
+
return { u, eased: segment.wave ? u : _chunkXZ4L3R4Kcjs.evaluateEasing.call(void 0, segment.ease, u, span) };
|
|
427
|
+
}
|
|
428
|
+
function segmentValue(segment, time) {
|
|
429
|
+
const { u, eased } = segmentProgress(segment, time);
|
|
430
|
+
const v = interpolateMotionValue(segment.from, segment.to, eased);
|
|
431
|
+
if (!segment.wave || typeof v !== "number") return v;
|
|
432
|
+
const { kind, amount, count } = segment.wave;
|
|
433
|
+
if (kind === "pulse") return v * (1 + amount * Math.sin(Math.PI * u * count) ** 2);
|
|
434
|
+
return v + amount * Math.sin(2 * Math.PI * u * count) * (1 - u);
|
|
435
|
+
}
|
|
436
|
+
function activeSegment(track, time) {
|
|
437
|
+
if (!_optionalChain([track, 'optionalAccess', _12 => _12.length])) return void 0;
|
|
438
|
+
let found;
|
|
439
|
+
for (const s of track) if (s.start <= time && (!found || s.start >= found.start)) found = s;
|
|
440
|
+
return found;
|
|
441
|
+
}
|
|
442
|
+
function trackValue(track, time) {
|
|
443
|
+
if (!_optionalChain([track, 'optionalAccess', _13 => _13.length])) return void 0;
|
|
444
|
+
const s = activeSegment(track, time);
|
|
445
|
+
if (s) return segmentValue(s, time);
|
|
446
|
+
let first = track[0];
|
|
447
|
+
for (const t of track) if (t.start < first.start) first = t;
|
|
448
|
+
return first.from;
|
|
449
|
+
}
|
|
450
|
+
var motionTrackKey = (node, property) => `${node}\0${property}`;
|
|
451
|
+
function fitMotionRect(rect, aspect) {
|
|
452
|
+
const cx = rect.x + rect.width / 2;
|
|
453
|
+
const cy = rect.y + rect.height / 2;
|
|
454
|
+
let { width, height } = rect;
|
|
455
|
+
if (width / height < aspect) width = height * aspect;
|
|
456
|
+
else height = width / aspect;
|
|
457
|
+
return { x: cx - width / 2, y: cy - height / 2, width, height };
|
|
458
|
+
}
|
|
459
|
+
function resolveMotionCamera(camera, scene) {
|
|
460
|
+
if ("frame" in camera && typeof camera.frame === "string") {
|
|
461
|
+
const entry = indexMotionNodes(scene.artwork.nodes).get(camera.frame);
|
|
462
|
+
if (!_optionalChain([entry, 'optionalAccess', _14 => _14.bounds])) return null;
|
|
463
|
+
const pad = _nullishCoalesce(camera.padding, () => ( 24));
|
|
464
|
+
const b = entry.bounds;
|
|
465
|
+
const rect = { x: b.x - pad, y: b.y - pad, width: Math.max(1, b.width + pad * 2), height: Math.max(1, b.height + pad * 2) };
|
|
466
|
+
return fitMotionRect(rect, scene.artwork.width / scene.artwork.height);
|
|
467
|
+
}
|
|
468
|
+
const r = camera;
|
|
469
|
+
return { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
470
|
+
}
|
|
471
|
+
function motionPathLength(node, parentWorld = _chunkXCXQDPIDcjs.IDENTITY_MATRIX) {
|
|
472
|
+
let longest = 0;
|
|
473
|
+
const paths = node.type === "path" ? [node] : _chunkXCXQDPIDcjs.collectPathNodes.call(void 0, node);
|
|
474
|
+
for (const p of paths) {
|
|
475
|
+
try {
|
|
476
|
+
longest = Math.max(longest, _chunkO2UH7LZEcjs.bezierPathLength.call(void 0, _chunkXCXQDPIDcjs.vectorPathWorldBezier.call(void 0, p, parentWorld, { effects: false })));
|
|
477
|
+
} catch (e2) {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return longest;
|
|
481
|
+
}
|
|
482
|
+
function motionCommandDuration(command2, scene, target) {
|
|
483
|
+
if (command2.type === "wait") return command2.duration;
|
|
484
|
+
if (command2.duration !== void 0) return command2.duration;
|
|
485
|
+
if (command2.type === "draw" || command2.type === "enter" && command2.style === "draw" || command2.type === "exit" && command2.style === "undraw") {
|
|
486
|
+
const entry = target ? indexMotionNodes(scene.artwork.nodes).get(target) : void 0;
|
|
487
|
+
const length = entry ? motionPathLength(entry.node) : 0;
|
|
488
|
+
const ms = length * MOTION_DRAW_MS_PER_UNIT;
|
|
489
|
+
return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms)));
|
|
490
|
+
}
|
|
491
|
+
return MOTION_DEFAULT_DURATIONS[command2.type];
|
|
492
|
+
}
|
|
493
|
+
var MOTION_EASE_OUT = { type: "cubic-bezier", x1: 1 / 3, y1: 1, x2: 2 / 3, y2: 1 };
|
|
494
|
+
var MOTION_EASE_IN_OUT = { type: "cubic-bezier", x1: 0.65, y1: 0, x2: 0.35, y2: 1 };
|
|
495
|
+
var DIRECTION = { up: { x: 0, y: -1 }, down: { x: 0, y: 1 }, left: { x: -1, y: 0 }, right: { x: 1, y: 0 } };
|
|
496
|
+
var timelineCache = /* @__PURE__ */ new WeakMap();
|
|
497
|
+
function compileMotionScene(scene) {
|
|
498
|
+
const hit = timelineCache.get(scene);
|
|
499
|
+
if (hit) return hit;
|
|
500
|
+
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
501
|
+
const artwork = scene.artwork;
|
|
502
|
+
const shots = [];
|
|
503
|
+
const commands = [];
|
|
504
|
+
const cameraMoves = [];
|
|
505
|
+
let t = 0;
|
|
506
|
+
scene.shots.forEach((shot, index) => {
|
|
507
|
+
const start = t;
|
|
508
|
+
let cursor = start;
|
|
509
|
+
if (shot.camera) {
|
|
510
|
+
const rect = resolveMotionCamera(shot.camera, scene);
|
|
511
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
512
|
+
const ms = index === 0 ? 0 : _nullishCoalesce(shot.camera.ms, () => ( MOTION_CAMERA_MS));
|
|
513
|
+
cameraMoves.push({ shot: shot.id, start, end: start + ms, to: rect, ease: _nullishCoalesce(shot.camera.ease, () => ( MOTION_EASE_IN_OUT)) });
|
|
514
|
+
cursor = start + ms;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const commandsStart = cursor;
|
|
518
|
+
let runEnd = cursor;
|
|
519
|
+
let prevStart = cursor;
|
|
520
|
+
shot.run.forEach((command2, i) => {
|
|
521
|
+
const base = command2.with === "previous" && i > 0 ? prevStart : runEnd;
|
|
522
|
+
const begin = base + (command2.type === "wait" ? 0 : _nullishCoalesce(command2.delay, () => ( 0)));
|
|
523
|
+
const targets = command2.type === "wait" ? [] : command2.targets;
|
|
524
|
+
const durations = command2.type === "wait" ? [command2.duration] : targets.map((id) => motionCommandDuration(command2, scene, id));
|
|
525
|
+
const stagger = command2.type === "wait" ? 0 : _nullishCoalesce(command2.stagger, () => ( 0));
|
|
526
|
+
const end2 = durations.reduce((m, d, k) => Math.max(m, begin + k * stagger + d), begin);
|
|
527
|
+
commands.push({ shot: shot.id, shotIndex: index, command: i, type: command2.type, start: begin, end: end2, duration: _nullishCoalesce(durations[0], () => ( 0)) });
|
|
528
|
+
prevStart = begin;
|
|
529
|
+
runEnd = Math.max(runEnd, end2);
|
|
530
|
+
});
|
|
531
|
+
const end = runEnd + (_nullishCoalesce(shot.hold, () => ( MOTION_DEFAULT_HOLD)));
|
|
532
|
+
shots.push({ id: shot.id, index, ...shot.label ? { label: shot.label } : {}, start, commandsStart, commandsEnd: runEnd, end });
|
|
533
|
+
t = end;
|
|
534
|
+
});
|
|
535
|
+
const tracks = /* @__PURE__ */ new Map();
|
|
536
|
+
const segments = [];
|
|
537
|
+
const valueAt = (node, property, time) => {
|
|
538
|
+
const v = trackValue(tracks.get(motionTrackKey(node, property)), time);
|
|
539
|
+
return _nullishCoalesce(v, () => ( motionRestValue(nodes.get(node), property, artwork)));
|
|
540
|
+
};
|
|
541
|
+
const push = (segment) => {
|
|
542
|
+
const key = motionTrackKey(segment.node, segment.property);
|
|
543
|
+
let track = tracks.get(key);
|
|
544
|
+
if (!track) tracks.set(key, track = []);
|
|
545
|
+
track.push(segment);
|
|
546
|
+
segments.push(segment);
|
|
547
|
+
};
|
|
548
|
+
const jobs = [];
|
|
549
|
+
let order = 0;
|
|
550
|
+
for (const move of cameraMoves) {
|
|
551
|
+
jobs.push({
|
|
552
|
+
start: move.start,
|
|
553
|
+
order: order++,
|
|
554
|
+
run: () => push({ node: "", property: "camera", start: move.start, end: move.end, from: valueAt("", "camera", move.start), to: [move.to.x, move.to.y, move.to.width, move.to.height], ease: move.ease, shot: move.shot, command: -1 })
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
for (const timing of commands) {
|
|
558
|
+
const shot = scene.shots[timing.shotIndex];
|
|
559
|
+
const command2 = shot.run[timing.command];
|
|
560
|
+
if (command2.type === "wait") continue;
|
|
561
|
+
command2.targets.forEach((target, k) => {
|
|
562
|
+
const entry = nodes.get(target);
|
|
563
|
+
if (!entry) return;
|
|
564
|
+
const start = timing.start + k * (_nullishCoalesce(command2.stagger, () => ( 0)));
|
|
565
|
+
const duration2 = motionCommandDuration(command2, scene, target);
|
|
566
|
+
jobs.push({ start, order: order++, run: () => compileCommand(command2, target, entry, start, duration2, timing) });
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
|
|
570
|
+
function compileCommand(command2, node, entry, start, duration2, timing) {
|
|
571
|
+
const end = start + duration2;
|
|
572
|
+
const where = { shot: timing.shot, command: timing.command };
|
|
573
|
+
const seg = (property, to, ease2, s = start, e = end, extra = {}) => push({ node, property, start: s, end: e, from: _nullishCoalesce(extra.from, () => ( valueAt(node, property, s))), to, ease: ease2, ...where, ...extra });
|
|
574
|
+
const rest = (p) => motionRestValue(entry, p, artwork);
|
|
575
|
+
const num = (p, time = start) => {
|
|
576
|
+
const v = valueAt(node, p, time);
|
|
577
|
+
return typeof v === "number" ? v : 0;
|
|
578
|
+
};
|
|
579
|
+
const ease = command2.ease;
|
|
580
|
+
switch (command2.type) {
|
|
581
|
+
case "enter": {
|
|
582
|
+
const style = _nullishCoalesce(command2.style, () => ( "fade"));
|
|
583
|
+
const opacity = rest("opacity");
|
|
584
|
+
if (style === "draw") {
|
|
585
|
+
seg("drawEnd", 1, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, start + duration2 * 0.75, { from: 0 });
|
|
586
|
+
seg("fillOpacity", 1, MOTION_EASE_OUT, start + duration2 * 0.6, end, { from: 0 });
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
if (style === "pop") {
|
|
590
|
+
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.4, { from: 0 });
|
|
591
|
+
seg("scaleX", 1, _nullishCoalesce(ease, () => ( "ease-out-back")), start, end, { from: 0.5 });
|
|
592
|
+
seg("scaleY", 1, _nullishCoalesce(ease, () => ( "ease-out-back")), start, end, { from: 0.5 });
|
|
593
|
+
} else if (style === "grow") {
|
|
594
|
+
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.3, { from: 0 });
|
|
595
|
+
seg("scaleX", 1, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: 0 });
|
|
596
|
+
seg("scaleY", 1, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: 0 });
|
|
597
|
+
} else if (style === "slide") {
|
|
598
|
+
const dir = DIRECTION[_nullishCoalesce(command2.direction, () => ( "up"))];
|
|
599
|
+
const distance = _nullishCoalesce(command2.distance, () => ( 40));
|
|
600
|
+
const x = num("x");
|
|
601
|
+
const y = num("y");
|
|
602
|
+
seg("opacity", opacity, MOTION_EASE_OUT, start, end, { from: 0 });
|
|
603
|
+
seg("x", x, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: x - dir.x * distance });
|
|
604
|
+
seg("y", y, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: y - dir.y * distance });
|
|
605
|
+
} else seg("opacity", opacity, _nullishCoalesce(ease, () => ( MOTION_EASE_OUT)), start, end, { from: 0 });
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case "exit": {
|
|
609
|
+
const style = _nullishCoalesce(command2.style, () => ( "fade"));
|
|
610
|
+
if (style === "undraw") {
|
|
611
|
+
seg("fillOpacity", 0, "ease-in", start, start + duration2 * 0.4);
|
|
612
|
+
seg("drawEnd", 0, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
613
|
+
} else if (style === "shrink") {
|
|
614
|
+
seg("scaleX", 0, _nullishCoalesce(ease, () => ( "ease-in")));
|
|
615
|
+
seg("scaleY", 0, _nullishCoalesce(ease, () => ( "ease-in")));
|
|
616
|
+
seg("opacity", 0, "ease-in", start + duration2 * 0.6, end);
|
|
617
|
+
} else if (style === "slide") {
|
|
618
|
+
const dir = DIRECTION[_nullishCoalesce(command2.direction, () => ( "down"))];
|
|
619
|
+
const distance = _nullishCoalesce(command2.distance, () => ( 40));
|
|
620
|
+
seg("x", num("x") + dir.x * distance, _nullishCoalesce(ease, () => ( "ease-in")));
|
|
621
|
+
seg("y", num("y") + dir.y * distance, _nullishCoalesce(ease, () => ( "ease-in")));
|
|
622
|
+
seg("opacity", 0, _nullishCoalesce(ease, () => ( "ease-in")));
|
|
623
|
+
} else seg("opacity", 0, _nullishCoalesce(ease, () => ( "ease-in")));
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "draw":
|
|
627
|
+
{
|
|
628
|
+
const drawn = tracks.has(motionTrackKey(node, "drawEnd"));
|
|
629
|
+
seg("drawEnd", _nullishCoalesce(command2.to, () => ( 1)), _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, command2.from !== void 0 ? { from: command2.from } : drawn ? {} : { from: 0 });
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
case "move": {
|
|
634
|
+
const e = _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT));
|
|
635
|
+
let dx = _nullishCoalesce(_optionalChain([command2, 'access', _15 => _15.by, 'optionalAccess', _16 => _16.x]), () => ( 0));
|
|
636
|
+
let dy = _nullishCoalesce(_optionalChain([command2, 'access', _17 => _17.by, 'optionalAccess', _18 => _18.y]), () => ( 0));
|
|
637
|
+
if (command2.to) {
|
|
638
|
+
const inv = _chunkXCXQDPIDcjs.invertMatrix.call(void 0, entry.parentWorld);
|
|
639
|
+
const target = _chunkXCXQDPIDcjs.applyMatrix.call(void 0, inv, command2.to);
|
|
640
|
+
dx = target.x - (entry.center.x + num("x"));
|
|
641
|
+
dy = target.y - (entry.center.y + num("y"));
|
|
642
|
+
}
|
|
643
|
+
seg("x", num("x") + dx, e);
|
|
644
|
+
seg("y", num("y") + dy, e);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
case "rotate":
|
|
648
|
+
seg("rotation", _nullishCoalesce(command2.to, () => ( num("rotation") + (_nullishCoalesce(command2.by, () => ( 0))))), _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
649
|
+
break;
|
|
650
|
+
case "scale": {
|
|
651
|
+
const to = typeof command2.to === "number" ? { x: command2.to, y: command2.to } : command2.to;
|
|
652
|
+
seg("scaleX", to.x, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
653
|
+
seg("scaleY", to.y, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case "fade":
|
|
657
|
+
seg("opacity", command2.to, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
658
|
+
break;
|
|
659
|
+
case "style":
|
|
660
|
+
if (command2.fill !== void 0) seg("fill", command2.fill, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
661
|
+
if (command2.stroke !== void 0) seg("stroke", command2.stroke, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
662
|
+
if (command2.strokeWidth !== void 0) seg("strokeWidth", command2.strokeWidth, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
663
|
+
break;
|
|
664
|
+
case "morph":
|
|
665
|
+
seg("shape", command2.into, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
666
|
+
break;
|
|
667
|
+
case "follow":
|
|
668
|
+
seg("follow", _nullishCoalesce(command2.to, () => ( 1)), _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)), start, end, { from: _nullishCoalesce(command2.from, () => ( 0)), path: command2.path, rotate: _nullishCoalesce(command2.rotate, () => ( false)) });
|
|
669
|
+
break;
|
|
670
|
+
case "pulse": {
|
|
671
|
+
const wave = { kind: "pulse", amount: _nullishCoalesce(command2.amount, () => ( 0.15)), count: _nullishCoalesce(command2.count, () => ( 1)) };
|
|
672
|
+
seg("scaleX", num("scaleX"), "linear", start, end, { wave });
|
|
673
|
+
seg("scaleY", num("scaleY"), "linear", start, end, { wave });
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
case "shake":
|
|
677
|
+
seg("x", num("x"), "linear", start, end, { wave: { kind: "shake", amount: _nullishCoalesce(command2.amount, () => ( 8)), count: _nullishCoalesce(command2.count, () => ( 3)) } });
|
|
678
|
+
break;
|
|
679
|
+
case "effect":
|
|
680
|
+
seg(`effect:${command2.effect}:${command2.param}`, command2.to, _nullishCoalesce(ease, () => ( MOTION_EASE_IN_OUT)));
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const track of tracks.values()) track.sort((a, b) => a.start - b.start);
|
|
685
|
+
segments.sort((a, b) => a.start - b.start);
|
|
686
|
+
const duration = shots.length ? shots[shots.length - 1].end : 0;
|
|
687
|
+
const timeline = { duration, shots, commands, segments, tracks };
|
|
688
|
+
timelineCache.set(scene, timeline);
|
|
689
|
+
return timeline;
|
|
690
|
+
}
|
|
691
|
+
function motionShotAt(timeline, time) {
|
|
692
|
+
return _nullishCoalesce(timeline.shots.find((s) => time < s.end), () => ( timeline.shots[timeline.shots.length - 1]));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/animation-editor/evaluate.ts
|
|
696
|
+
var MOTION_DRAW_EFFECT_ID = "~draw";
|
|
697
|
+
var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
|
|
698
|
+
var tidy = (v) => Math.round(v * 1e6) / 1e6 || 0;
|
|
699
|
+
function applyMotionControlValues(scene, values) {
|
|
700
|
+
if (!values || !_optionalChain([scene, 'access', _19 => _19.controls, 'optionalAccess', _20 => _20.length])) return scene;
|
|
701
|
+
const map = /* @__PURE__ */ new Map();
|
|
702
|
+
for (const control of scene.controls) {
|
|
703
|
+
const value = values[control.name];
|
|
704
|
+
if (typeof value === "string" && _chunkA6PHCAGXcjs.normalizeTokenColor.call(void 0, value) !== null) map.set(control.default.toLowerCase(), value);
|
|
705
|
+
}
|
|
706
|
+
if (!map.size) return scene;
|
|
707
|
+
const walk = (v) => {
|
|
708
|
+
if (typeof v === "string") return _nullishCoalesce(map.get(v.toLowerCase()), () => ( v));
|
|
709
|
+
if (Array.isArray(v)) return v.map(walk);
|
|
710
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk(x)]));
|
|
711
|
+
return v;
|
|
712
|
+
};
|
|
713
|
+
return { ...scene, artwork: walk(scene.artwork), shots: walk(scene.shots) };
|
|
714
|
+
}
|
|
715
|
+
function fadeColor(color, alpha) {
|
|
716
|
+
const hex = _chunkA6PHCAGXcjs.normalizeTokenColor.call(void 0, color);
|
|
717
|
+
if (hex === null) return color;
|
|
718
|
+
const c = _chunkA6PHCAGXcjs.hexToRgba.call(void 0, hex);
|
|
719
|
+
return _chunkA6PHCAGXcjs.rgbaToHex.call(void 0, c.r, c.g, c.b, c.a * alpha);
|
|
720
|
+
}
|
|
721
|
+
function fadePaint(paint, alpha) {
|
|
722
|
+
if (alpha >= 1 || paint == null || paint === "none") return paint;
|
|
723
|
+
if (typeof paint === "string") return fadeColor(paint, alpha);
|
|
724
|
+
if ("stops" in paint && Array.isArray(paint.stops)) return { ...paint, stops: paint.stops.map((s) => ({ ...s, opacity: (_nullishCoalesce(s.opacity, () => ( 1))) * alpha })) };
|
|
725
|
+
return paint;
|
|
726
|
+
}
|
|
727
|
+
function mapPaths(node, fn) {
|
|
728
|
+
if (node.type === "path") return fn(node);
|
|
729
|
+
return { ...node, children: node.children.map((c) => mapPaths(c, fn)) };
|
|
730
|
+
}
|
|
731
|
+
var morphCache = /* @__PURE__ */ new WeakMap();
|
|
732
|
+
function morphBetween(a, b) {
|
|
733
|
+
let inner = morphCache.get(a);
|
|
734
|
+
if (!inner) morphCache.set(a, inner = /* @__PURE__ */ new WeakMap());
|
|
735
|
+
let morph = inner.get(b);
|
|
736
|
+
if (!morph) inner.set(b, morph = _chunkXCXQDPIDcjs.createVectorMorph.call(void 0, a, b, { id: a.id }));
|
|
737
|
+
return morph;
|
|
738
|
+
}
|
|
739
|
+
function shapeIn(entry, shape) {
|
|
740
|
+
if (shape === entry) return entry.node;
|
|
741
|
+
const m = _chunkXCXQDPIDcjs.multiplyMatrices.call(void 0, _chunkXCXQDPIDcjs.multiplyMatrices.call(void 0, _chunkXCXQDPIDcjs.nodeMatrix.call(void 0, shape.node), shape.parentWorld), _chunkXCXQDPIDcjs.invertMatrix.call(void 0, entry.parentWorld));
|
|
742
|
+
return { ...shape.node, ..._chunkXCXQDPIDcjs.isIdentityMatrix.call(void 0, m) ? { transform: void 0 } : { transform: m } };
|
|
743
|
+
}
|
|
744
|
+
function shapeAt(entry, timeline, nodes, time) {
|
|
745
|
+
const track = timeline.tracks.get(motionTrackKey(entry.node.id, "shape"));
|
|
746
|
+
if (!_optionalChain([track, 'optionalAccess', _21 => _21.length])) return null;
|
|
747
|
+
const s = activeSegment(track, time);
|
|
748
|
+
const fromId = String(s ? s.from : track[0].from);
|
|
749
|
+
if (!s) return fromId === entry.node.id ? null : nodes.get(fromId) ? shapeIn(entry, nodes.get(fromId)) : null;
|
|
750
|
+
const a = nodes.get(fromId);
|
|
751
|
+
const b = nodes.get(String(s.to));
|
|
752
|
+
if (!a || !b) return null;
|
|
753
|
+
const { eased } = segmentProgress(s, time);
|
|
754
|
+
const u = Math.min(1, Math.max(0, eased));
|
|
755
|
+
if (u <= 0) return a === entry ? null : shapeIn(entry, a);
|
|
756
|
+
if (u >= 1) return b === entry ? null : shapeIn(entry, b);
|
|
757
|
+
return morphBetween(shapeIn(entry, a), shapeIn(entry, b)).at(u);
|
|
758
|
+
}
|
|
759
|
+
function followAt(entry, timeline, nodes, time) {
|
|
760
|
+
const track = timeline.tracks.get(motionTrackKey(entry.node.id, "follow"));
|
|
761
|
+
const s = _nullishCoalesce(activeSegment(track, time), () => ( _optionalChain([track, 'optionalAccess', _22 => _22[0]])));
|
|
762
|
+
if (!_optionalChain([s, 'optionalAccess', _23 => _23.path])) return null;
|
|
763
|
+
const path = nodes.get(s.path);
|
|
764
|
+
if (!path || path.node.type !== "path") return null;
|
|
765
|
+
const progress = Number(trackValue(track, time));
|
|
766
|
+
if (!(progress >= 0)) return null;
|
|
767
|
+
try {
|
|
768
|
+
const bezier = _chunkXCXQDPIDcjs.vectorPathWorldBezier.call(void 0, path.node, path.parentWorld, { effects: false });
|
|
769
|
+
const at = _chunkO2UH7LZEcjs.bezierPathPointAtLength.call(void 0, bezier, Math.min(1, Math.max(0, progress)) * _chunkO2UH7LZEcjs.bezierPathLength.call(void 0, bezier));
|
|
770
|
+
if (!at) return null;
|
|
771
|
+
const p = _chunkXCXQDPIDcjs.applyMatrix.call(void 0, _chunkXCXQDPIDcjs.invertMatrix.call(void 0, entry.parentWorld), { x: at.point[0], y: at.point[1] });
|
|
772
|
+
return { x: p.x, y: p.y, angle: s.rotate ? at.angle * 180 / Math.PI : 0 };
|
|
773
|
+
} catch (e3) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function motionNodeOffset(entry, timeline, time, nodes = indexMotionNodes([entry.node])) {
|
|
778
|
+
const read = (p, rest) => {
|
|
779
|
+
const v = trackValue(timeline.tracks.get(motionTrackKey(entry.node.id, p)), time);
|
|
780
|
+
return typeof v === "number" ? v : rest;
|
|
781
|
+
};
|
|
782
|
+
if (!TRANSFORM_PROPS.some((p) => timeline.tracks.has(motionTrackKey(entry.node.id, p)))) return null;
|
|
783
|
+
let dx = read("x", 0);
|
|
784
|
+
let dy = read("y", 0);
|
|
785
|
+
let rotation = read("rotation", 0);
|
|
786
|
+
const scaleX = read("scaleX", 1);
|
|
787
|
+
const scaleY = read("scaleY", 1);
|
|
788
|
+
const follow = followAt(entry, timeline, nodes, time);
|
|
789
|
+
if (follow) {
|
|
790
|
+
dx += follow.x - entry.center.x;
|
|
791
|
+
dy += follow.y - entry.center.y;
|
|
792
|
+
rotation += follow.angle;
|
|
793
|
+
}
|
|
794
|
+
const c = entry.center;
|
|
795
|
+
const matrix = _chunkXCXQDPIDcjs.multiplyMatrices.call(void 0, _chunkXCXQDPIDcjs.multiplyMatrices.call(void 0, _chunkXCXQDPIDcjs.scaleMatrix.call(void 0, scaleX, scaleY, c), _chunkXCXQDPIDcjs.rotateMatrix.call(void 0, rotation, c)), _chunkXCXQDPIDcjs.translateMatrix.call(void 0, dx, dy));
|
|
796
|
+
return { matrix, dx, dy, rotation, scaleX, scaleY };
|
|
797
|
+
}
|
|
798
|
+
function motionFrameAt(scene, time, options = {}) {
|
|
799
|
+
const s = applyMotionControlValues(scene, options.values);
|
|
800
|
+
const timeline = compileMotionScene(s);
|
|
801
|
+
const nodes = indexMotionNodes(s.artwork.nodes);
|
|
802
|
+
const drawOn = options.draw !== false;
|
|
803
|
+
const value = (id, p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
804
|
+
const animated = new Set(timeline.segments.map((seg) => seg.node));
|
|
805
|
+
const visit = (node) => {
|
|
806
|
+
let out = node.type === "group" ? { ...node, children: node.children.map(visit) } : node;
|
|
807
|
+
if (!animated.has(node.id)) return out;
|
|
808
|
+
const entry = nodes.get(node.id);
|
|
809
|
+
if (!entry) return out;
|
|
810
|
+
const id = node.id;
|
|
811
|
+
const shape = shapeAt(entry, timeline, nodes, time);
|
|
812
|
+
if (shape) {
|
|
813
|
+
const { name, visible, opacity: opacity2, locked } = node;
|
|
814
|
+
out = { ...shape, id, ...name ? { name } : {}, ...locked ? { locked } : {}, visible, ...opacity2 !== void 0 ? { opacity: opacity2 } : {} };
|
|
815
|
+
if (out.visible === void 0) delete out.visible;
|
|
816
|
+
}
|
|
817
|
+
const effectTracks = [...timeline.tracks.keys()].filter((k) => k.startsWith(`${id}\0effect:`));
|
|
818
|
+
if (effectTracks.length && _optionalChain([out, 'access', _24 => _24.effects, 'optionalAccess', _25 => _25.length])) {
|
|
819
|
+
out = {
|
|
820
|
+
...out,
|
|
821
|
+
effects: out.effects.map((effect) => {
|
|
822
|
+
let params = effect.params;
|
|
823
|
+
for (const key of effectTracks) {
|
|
824
|
+
const [, effectId, param] = key.split("\0")[1].split(":");
|
|
825
|
+
if (effectId !== effect.id) continue;
|
|
826
|
+
const v = trackValue(timeline.tracks.get(key), time);
|
|
827
|
+
if (typeof v === "number") params = { ...params, [param]: v };
|
|
828
|
+
}
|
|
829
|
+
return params === effect.params ? effect : { ...effect, params };
|
|
830
|
+
})
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const fill = value(id, "fill");
|
|
834
|
+
const stroke = value(id, "stroke");
|
|
835
|
+
const strokeWidth = value(id, "strokeWidth");
|
|
836
|
+
const fillOpacity = value(id, "fillOpacity");
|
|
837
|
+
const drawStart = value(id, "drawStart");
|
|
838
|
+
const drawEnd = value(id, "drawEnd");
|
|
839
|
+
const draw = drawOn && (drawStart !== void 0 || drawEnd !== void 0);
|
|
840
|
+
if (fill !== void 0 || stroke !== void 0 || strokeWidth !== void 0 || fillOpacity !== void 0 || draw) {
|
|
841
|
+
out = mapPaths(out, (path) => {
|
|
842
|
+
let p = path;
|
|
843
|
+
if (typeof fill === "string") p = { ...p, fill };
|
|
844
|
+
if (typeof stroke === "string") p = { ...p, stroke };
|
|
845
|
+
if (typeof strokeWidth === "number") p = { ...p, strokeWidth: Math.max(0, tidy(strokeWidth)) };
|
|
846
|
+
if (typeof fillOpacity === "number" && fillOpacity < 1) p = { ...p, fill: fadePaint(p.fill, Math.max(0, fillOpacity)) };
|
|
847
|
+
if (draw) {
|
|
848
|
+
const start = Math.min(1, Math.max(0, typeof drawStart === "number" ? drawStart : 0));
|
|
849
|
+
const end = Math.min(1, Math.max(0, typeof drawEnd === "number" ? drawEnd : 1));
|
|
850
|
+
if (start > 0 || end < 1) {
|
|
851
|
+
const trim = _chunkXCXQDPIDcjs.createVectorEffect.call(void 0, "trim", MOTION_DRAW_EFFECT_ID, { start: tidy(start * 100), end: tidy(end * 100) });
|
|
852
|
+
p = { ...p, effects: [trim, ...(_nullishCoalesce(p.effects, () => ( []))).filter((e) => e.id !== MOTION_DRAW_EFFECT_ID)] };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return p;
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
const opacity = value(id, "opacity");
|
|
859
|
+
if (typeof opacity === "number") {
|
|
860
|
+
const o = Math.min(1, Math.max(0, tidy(opacity)));
|
|
861
|
+
out = { ...out, opacity: o };
|
|
862
|
+
if (o >= 1) delete out.opacity;
|
|
863
|
+
}
|
|
864
|
+
const offset = options.offsets === false ? null : motionNodeOffset(entry, timeline, time, nodes);
|
|
865
|
+
if (offset && !_chunkXCXQDPIDcjs.isIdentityMatrix.call(void 0, offset.matrix)) {
|
|
866
|
+
const m = _chunkXCXQDPIDcjs.multiplyMatrices.call(void 0, _chunkXCXQDPIDcjs.nodeMatrix.call(void 0, out), offset.matrix).map(tidy);
|
|
867
|
+
out = { ...out, transform: m };
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
};
|
|
871
|
+
const document = { ...s.artwork, nodes: s.artwork.nodes.map(visit) };
|
|
872
|
+
const cam = trackValue(timeline.tracks.get(motionTrackKey("", "camera")), time);
|
|
873
|
+
const box = Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
|
|
874
|
+
return { time, document, camera: { x: tidy(box[0]), y: tidy(box[1]), width: tidy(box[2]), height: tidy(box[3]) } };
|
|
875
|
+
}
|
|
876
|
+
function motionFrameToSvg(scene, time, options = {}) {
|
|
877
|
+
const frame = motionFrameAt(scene, time, options);
|
|
878
|
+
return withViewBox(_chunkXCXQDPIDcjs.vectorDocumentToSvg.call(void 0, frame.document, options), frame.camera);
|
|
879
|
+
}
|
|
880
|
+
function withViewBox(svg, box) {
|
|
881
|
+
const f = (n) => String(Math.round(n * 1e3) / 1e3);
|
|
882
|
+
return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(box.x)} ${f(box.y)} ${f(box.width)} ${f(box.height)}"`);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/animation-editor/check.ts
|
|
886
|
+
var MOTION_MIN_CHANGE_MS = 120;
|
|
887
|
+
var NUMERIC_PARAMS = (node, effectId) => {
|
|
888
|
+
const effect = _optionalChain([node, 'access', _26 => _26.effects, 'optionalAccess', _27 => _27.find, 'call', _28 => _28((e) => e.id === effectId)]);
|
|
889
|
+
if (!effect) return null;
|
|
890
|
+
return Object.entries(effect.params).filter(([, v]) => typeof v === "number").map(([k]) => k);
|
|
891
|
+
};
|
|
892
|
+
function usesColor(scene, color) {
|
|
893
|
+
const target = color.toLowerCase();
|
|
894
|
+
let found = false;
|
|
895
|
+
const walk = (v) => {
|
|
896
|
+
if (found) return;
|
|
897
|
+
if (typeof v === "string") found = v.toLowerCase() === target;
|
|
898
|
+
else if (Array.isArray(v)) v.forEach(walk);
|
|
899
|
+
else if (v && typeof v === "object") Object.values(v).forEach(walk);
|
|
900
|
+
};
|
|
901
|
+
walk(scene.artwork);
|
|
902
|
+
walk(scene.shots);
|
|
903
|
+
return found;
|
|
904
|
+
}
|
|
905
|
+
function visibleAt(timeline, id, time, node) {
|
|
906
|
+
const num = (p, rest) => {
|
|
907
|
+
const v = trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
908
|
+
return typeof v === "number" ? v : rest;
|
|
909
|
+
};
|
|
910
|
+
if (node.visible === false) return false;
|
|
911
|
+
if (num("opacity", _nullishCoalesce(node.opacity, () => ( 1))) <= 0.01) return false;
|
|
912
|
+
if (Math.abs(num("scaleX", 1) * num("scaleY", 1)) <= 1e-4) return false;
|
|
913
|
+
const hasFill = node.type === "path" ? !!node.fill && node.fill !== "none" : _chunkXCXQDPIDcjs.collectPathNodes.call(void 0, node).some((p) => p.fill && p.fill !== "none");
|
|
914
|
+
return num("drawEnd", 1) > 0.01 || hasFill && num("fillOpacity", 1) > 0.01;
|
|
915
|
+
}
|
|
916
|
+
function checkMotionScene(scene) {
|
|
917
|
+
const issues = [];
|
|
918
|
+
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
919
|
+
const ids = [...nodes.keys()];
|
|
920
|
+
const timeline = compileMotionScene(scene);
|
|
921
|
+
const artAspect = scene.artwork.width / scene.artwork.height;
|
|
922
|
+
if (!scene.shots.length) issues.push(motionIssue("no-shots", "The scene has no shots yet; add one with a list of commands."));
|
|
923
|
+
const seen = /* @__PURE__ */ new Set();
|
|
924
|
+
for (const shot of scene.shots) {
|
|
925
|
+
if (seen.has(shot.id)) issues.push(motionIssue("duplicate-shot-id", `Shot id "${shot.id}" is used twice; give each shot its own id.`, { shot: shot.id }));
|
|
926
|
+
seen.add(shot.id);
|
|
927
|
+
}
|
|
928
|
+
const known = (id, code, what, where) => {
|
|
929
|
+
if (nodes.has(id)) return true;
|
|
930
|
+
issues.push(motionIssue(code, `There is no ${what} ${namedWithSuggestion(id, ids)} in the artwork${ids.length ? "" : " (it has no nodes yet)"}.`, { ...where, ids: [id] }));
|
|
931
|
+
return false;
|
|
932
|
+
};
|
|
933
|
+
scene.shots.forEach((shot, shotIndex2) => {
|
|
934
|
+
if (shot.camera && "frame" in shot.camera && typeof shot.camera.frame === "string") known(shot.camera.frame, "unknown-frame", "node to frame", { shot: shot.id });
|
|
935
|
+
else if (shot.camera) {
|
|
936
|
+
const c = shot.camera;
|
|
937
|
+
if (!(c.width > 0 && c.height > 0)) issues.push(motionIssue("invalid-camera", `The camera needs a positive width and height (got ${c.width} \xD7 ${c.height}).`, { shot: shot.id }));
|
|
938
|
+
else if (Math.abs(c.width / c.height / artAspect - 1) > 0.02)
|
|
939
|
+
issues.push(motionIssue("camera-aspect", `The camera is ${c.width} \xD7 ${c.height}, a different shape from the ${scene.artwork.width} \xD7 ${scene.artwork.height} artwork, so it shows bands at the sides; frame a node instead to match.`, { shot: shot.id }));
|
|
940
|
+
}
|
|
941
|
+
if (!shot.run.length && !shot.camera && shotIndex2 > 0) issues.push(motionIssue("empty-shot", "This shot only holds; add commands or a camera move, or remove it.", { shot: shot.id }));
|
|
942
|
+
shot.run.forEach((command2, i) => {
|
|
943
|
+
const where = { shot: shot.id, command: i };
|
|
944
|
+
if (command2.type === "wait") return;
|
|
945
|
+
const targets = command2.targets.filter((id) => known(id, "unknown-target", "node", where));
|
|
946
|
+
const timing = timeline.commands.find((c) => c.shot === shot.id && c.command === i);
|
|
947
|
+
if (timing && timing.duration > 0 && timing.duration < MOTION_MIN_CHANGE_MS)
|
|
948
|
+
issues.push(motionIssue("too-fast", `This ${command2.type} lasts ${timing.duration} ms, too quick to follow; make it at least ${MOTION_MIN_CHANGE_MS} ms.`, { ...where, measured: timing.duration, needed: MOTION_MIN_CHANGE_MS, unit: "ms", ids: targets }));
|
|
949
|
+
if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
|
|
950
|
+
issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
|
|
951
|
+
if (command2.type === "morph") known(command2.into, "unknown-shape", "shape", where);
|
|
952
|
+
if (command2.type === "style") {
|
|
953
|
+
for (const key of ["fill", "stroke"]) {
|
|
954
|
+
const color = command2[key];
|
|
955
|
+
if (color === void 0) continue;
|
|
956
|
+
if (_chunkA6PHCAGXcjs.normalizeTokenColor.call(void 0, color) === null) {
|
|
957
|
+
issues.push(motionIssue("invalid-color", `${key} "${color}" is not a colour this editor reads; use hex ("#0ea5e9"), rgb() or rgba().`, where));
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
for (const id of targets) {
|
|
961
|
+
const node = nodes.get(id).node;
|
|
962
|
+
const paths = node.type === "path" ? [node] : _chunkXCXQDPIDcjs.collectPathNodes.call(void 0, node);
|
|
963
|
+
if (paths.some((p) => p[key] !== void 0 && p[key] !== null && typeof p[key] !== "string"))
|
|
964
|
+
issues.push(motionIssue("paint-not-color", `"${id}" has a gradient ${key}; it switches to ${color} at the end instead of blending.`, { ...where, ids: [id] }));
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (command2.type === "effect") {
|
|
969
|
+
for (const id of targets) {
|
|
970
|
+
const node = nodes.get(id).node;
|
|
971
|
+
const params = NUMERIC_PARAMS(node, command2.effect);
|
|
972
|
+
const effects = (_nullishCoalesce(node.effects, () => ( []))).map((e) => e.id);
|
|
973
|
+
if (!params)
|
|
974
|
+
issues.push(motionIssue("unknown-effect", `"${id}" has no effect ${namedWithSuggestion(command2.effect, effects)}${effects.length ? `; its effects are ${effects.map((e) => `"${e}"`).join(", ")}` : "; it has no live effects"}.`, { ...where, ids: [id] }));
|
|
975
|
+
else if (!params.includes(command2.param))
|
|
976
|
+
issues.push(motionIssue("unknown-effect-param", `Effect "${command2.effect}" has no numeric parameter ${namedWithSuggestion(command2.param, params)}; use one of ${params.join(", ")}.`, { ...where, ids: [id] }));
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (timing) {
|
|
980
|
+
for (const id of targets) {
|
|
981
|
+
const node = nodes.get(id).node;
|
|
982
|
+
const before = Math.max(0, timing.start - 0.5);
|
|
983
|
+
const visible = timing.start > 0 ? visibleAt(timeline, id, before, node) : node.visible !== false;
|
|
984
|
+
if (command2.type === "enter" && visible && timing.start > 0)
|
|
985
|
+
issues.push(motionIssue("enter-visible", `"${id}" is already showing when it enters, so it blinks out first; remove the enter or exit it earlier.`, { ...where, ids: [id] }));
|
|
986
|
+
const probe = command2.type === "exit" ? timing.start + 0.5 : (timing.start + timing.end) / 2;
|
|
987
|
+
if (command2.type !== "enter" && command2.type !== "draw" && !visibleAt(timeline, id, probe, node))
|
|
988
|
+
issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${command2.type} starts, so it is not seen; enter it first.`, { ...where, ids: [id] }));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
for (const [key, track] of timeline.tracks) {
|
|
994
|
+
for (let i = 1; i < track.length; i++) {
|
|
995
|
+
const a = track[i - 1];
|
|
996
|
+
const b = track[i];
|
|
997
|
+
if (b.start < a.end - 1 && (a.shot !== b.shot || a.command !== b.command)) {
|
|
998
|
+
const property = key.split("\0")[1];
|
|
999
|
+
if (!a.node) continue;
|
|
1000
|
+
issues.push(
|
|
1001
|
+
motionIssue("overlap", `"${a.node}" ${property} is changed by two commands at once (shot "${a.shot}" command ${a.command + 1} and shot "${b.shot}" command ${b.command + 1}); the later one takes over. Start it after the first ends, or use with: "after".`, {
|
|
1002
|
+
shot: b.shot,
|
|
1003
|
+
command: b.command,
|
|
1004
|
+
ids: [a.node]
|
|
1005
|
+
})
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const timing of timeline.commands) {
|
|
1012
|
+
const command2 = scene.shots[timing.shotIndex].run[timing.command];
|
|
1013
|
+
if (command2.type === "wait" || command2.type === "exit") continue;
|
|
1014
|
+
const frame = motionFrameAt(scene, timing.end);
|
|
1015
|
+
const cam = frame.camera;
|
|
1016
|
+
const docIndex = indexMotionNodes(frame.document.nodes);
|
|
1017
|
+
for (const id of command2.targets) {
|
|
1018
|
+
const b = _optionalChain([docIndex, 'access', _29 => _29.get, 'call', _30 => _30(id), 'optionalAccess', _31 => _31.bounds]);
|
|
1019
|
+
if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
|
|
1020
|
+
const outside = b.x + b.width < cam.x || b.y + b.height < cam.y || b.x > cam.x + cam.width || b.y > cam.y + cam.height;
|
|
1021
|
+
if (outside) {
|
|
1022
|
+
reported.add(id);
|
|
1023
|
+
issues.push(motionIssue("out-of-frame", `"${id}" is outside the camera when this ${command2.type} ends, so it is not seen; move it into frame or move the camera.`, { shot: timing.shot, command: timing.command, ids: [id] }));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (scene.controls) {
|
|
1028
|
+
for (const control of scene.controls) {
|
|
1029
|
+
if (!usesColor(scene, control.default))
|
|
1030
|
+
issues.push(motionIssue("unknown-control-color", `Control "${control.name}" stands for ${control.default}, which the scene never uses; set its default to a colour in the artwork.`));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (scene.target && scene.shots.length) {
|
|
1034
|
+
const seconds = Math.round(timeline.duration / 100) / 10;
|
|
1035
|
+
const [min, max] = scene.target;
|
|
1036
|
+
if (seconds > max) issues.push(motionIssue("scene-too-long", `The scene runs ${seconds} s, past its ${max} s target; shorten holds or durations.`, { measured: seconds, needed: max, unit: "s" }));
|
|
1037
|
+
if (seconds < min) issues.push(motionIssue("scene-too-short", `The scene runs ${seconds} s, under its ${min} s target; hold longer or add shots.`, { measured: seconds, needed: min, unit: "s" }));
|
|
1038
|
+
}
|
|
1039
|
+
return issues;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/animation-editor/edit.ts
|
|
1043
|
+
var MOTION_EDIT_OPS = [
|
|
1044
|
+
"set_settings",
|
|
1045
|
+
"insert_shots",
|
|
1046
|
+
"update_shot",
|
|
1047
|
+
"delete_shots",
|
|
1048
|
+
"move_shot",
|
|
1049
|
+
"add_commands",
|
|
1050
|
+
"update_command",
|
|
1051
|
+
"delete_commands",
|
|
1052
|
+
"set_artwork",
|
|
1053
|
+
"set_controls",
|
|
1054
|
+
"replace_scene"
|
|
1055
|
+
];
|
|
1056
|
+
var EditError = class extends Error {
|
|
1057
|
+
};
|
|
1058
|
+
var fail = (message) => {
|
|
1059
|
+
throw new EditError(message);
|
|
1060
|
+
};
|
|
1061
|
+
function shotIndex(scene, id) {
|
|
1062
|
+
const i = scene.shots.findIndex((s) => s.id === id);
|
|
1063
|
+
if (i < 0) fail(`There is no shot ${namedWithSuggestion(id, scene.shots.map((s) => s.id))}; the shots are ${scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
|
|
1064
|
+
return i;
|
|
1065
|
+
}
|
|
1066
|
+
function position(at, length, what) {
|
|
1067
|
+
if (at === void 0) return length;
|
|
1068
|
+
if (!Number.isInteger(at) || at < 0 || at > length) fail(`${what} must be a whole number from 0 to ${length} (got ${at}).`);
|
|
1069
|
+
return at;
|
|
1070
|
+
}
|
|
1071
|
+
var replaceShot = (scene, index, shot) => ({ ...scene, shots: scene.shots.map((s, i) => i === index ? shot : s) });
|
|
1072
|
+
function freshShotId(scene, base = "shot") {
|
|
1073
|
+
const used = new Set(scene.shots.map((s) => s.id));
|
|
1074
|
+
if (!used.has(base)) return base;
|
|
1075
|
+
let n = 2;
|
|
1076
|
+
while (used.has(`${base}-${n}`)) n++;
|
|
1077
|
+
return `${base}-${n}`;
|
|
1078
|
+
}
|
|
1079
|
+
function applyOne(scene, edit) {
|
|
1080
|
+
switch (edit.op) {
|
|
1081
|
+
case "set_settings": {
|
|
1082
|
+
const next = { ...scene };
|
|
1083
|
+
if (edit.name !== void 0) next.name = edit.name;
|
|
1084
|
+
if (edit.description === null) delete next.description;
|
|
1085
|
+
else if (edit.description !== void 0) next.description = edit.description;
|
|
1086
|
+
if (edit.loop !== void 0) next.loop = edit.loop;
|
|
1087
|
+
if (edit.fps !== void 0) next.fps = edit.fps;
|
|
1088
|
+
if (edit.target === null) delete next.target;
|
|
1089
|
+
else if (edit.target !== void 0) next.target = edit.target;
|
|
1090
|
+
return next;
|
|
1091
|
+
}
|
|
1092
|
+
case "insert_shots": {
|
|
1093
|
+
if (!edit.shots.length) fail("Give at least one shot.");
|
|
1094
|
+
edit.shots.forEach((shot, i) => {
|
|
1095
|
+
const problems = shotShapeProblems(shot, `Shot ${i + 1}`);
|
|
1096
|
+
if (problems.length) fail(problems.join(" "));
|
|
1097
|
+
});
|
|
1098
|
+
const ids = new Set(scene.shots.map((s) => s.id));
|
|
1099
|
+
for (const shot of edit.shots) {
|
|
1100
|
+
if (ids.has(shot.id)) fail(`Shot id "${shot.id}" is already used; choose another (e.g. "${freshShotId(scene, shot.id)}").`);
|
|
1101
|
+
ids.add(shot.id);
|
|
1102
|
+
}
|
|
1103
|
+
const at = position(edit.at, scene.shots.length, '"at"');
|
|
1104
|
+
return { ...scene, shots: [...scene.shots.slice(0, at), ...edit.shots, ...scene.shots.slice(at)] };
|
|
1105
|
+
}
|
|
1106
|
+
case "update_shot": {
|
|
1107
|
+
const i = shotIndex(scene, edit.shot);
|
|
1108
|
+
const shot = { ...scene.shots[i], ..._nullishCoalesce(edit.set, () => ( {})) };
|
|
1109
|
+
for (const key of _nullishCoalesce(edit.remove, () => ( []))) delete shot[key];
|
|
1110
|
+
if (shot.id !== edit.shot && scene.shots.some((s) => s.id === shot.id)) fail(`Shot id "${shot.id}" is already used; choose another.`);
|
|
1111
|
+
const problems = shotShapeProblems(shot, `Shot "${edit.shot}"`);
|
|
1112
|
+
if (problems.length) fail(problems.join(" "));
|
|
1113
|
+
return replaceShot(scene, i, shot);
|
|
1114
|
+
}
|
|
1115
|
+
case "delete_shots": {
|
|
1116
|
+
if (!edit.shots.length) fail("Give at least one shot id.");
|
|
1117
|
+
edit.shots.forEach((id) => shotIndex(scene, id));
|
|
1118
|
+
const drop = new Set(edit.shots);
|
|
1119
|
+
return { ...scene, shots: scene.shots.filter((s) => !drop.has(s.id)) };
|
|
1120
|
+
}
|
|
1121
|
+
case "move_shot": {
|
|
1122
|
+
const i = shotIndex(scene, edit.shot);
|
|
1123
|
+
const to = position(edit.to, scene.shots.length - 1, '"to"');
|
|
1124
|
+
const shots = [...scene.shots];
|
|
1125
|
+
const [shot] = shots.splice(i, 1);
|
|
1126
|
+
shots.splice(to, 0, shot);
|
|
1127
|
+
return { ...scene, shots };
|
|
1128
|
+
}
|
|
1129
|
+
case "add_commands": {
|
|
1130
|
+
const i = shotIndex(scene, edit.shot);
|
|
1131
|
+
if (!edit.commands.length) fail("Give at least one command.");
|
|
1132
|
+
edit.commands.forEach((command2, k) => {
|
|
1133
|
+
const problems = commandShapeProblems(command2, `Command ${k + 1}`);
|
|
1134
|
+
if (problems.length) fail(problems.join(" "));
|
|
1135
|
+
});
|
|
1136
|
+
const run = scene.shots[i].run;
|
|
1137
|
+
const at = position(edit.at, run.length, '"at"');
|
|
1138
|
+
return replaceShot(scene, i, { ...scene.shots[i], run: [...run.slice(0, at), ...edit.commands, ...run.slice(at)] });
|
|
1139
|
+
}
|
|
1140
|
+
case "update_command": {
|
|
1141
|
+
const i = shotIndex(scene, edit.shot);
|
|
1142
|
+
const run = scene.shots[i].run;
|
|
1143
|
+
if (!Number.isInteger(edit.command) || edit.command < 0 || edit.command >= run.length)
|
|
1144
|
+
fail(`Shot "${edit.shot}" has ${run.length} command${run.length === 1 ? "" : "s"}; "command" must be an index from 0 to ${run.length - 1} (got ${edit.command}).`);
|
|
1145
|
+
const command2 = { ...run[edit.command], ..._nullishCoalesce(edit.set, () => ( {})) };
|
|
1146
|
+
for (const key of _nullishCoalesce(edit.remove, () => ( []))) {
|
|
1147
|
+
if (key === "type") fail('"type" cannot be removed; replace the command instead.');
|
|
1148
|
+
delete command2[key];
|
|
1149
|
+
}
|
|
1150
|
+
const problems = commandShapeProblems(command2, `Command ${edit.command}`);
|
|
1151
|
+
if (problems.length) fail(problems.join(" "));
|
|
1152
|
+
return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ? command2 : c) });
|
|
1153
|
+
}
|
|
1154
|
+
case "delete_commands": {
|
|
1155
|
+
const i = shotIndex(scene, edit.shot);
|
|
1156
|
+
const run = scene.shots[i].run;
|
|
1157
|
+
if (!edit.commands.length) fail("Give at least one command index.");
|
|
1158
|
+
for (const k of edit.commands)
|
|
1159
|
+
if (!Number.isInteger(k) || k < 0 || k >= run.length) fail(`Shot "${edit.shot}" has no command ${k}; indexes run from 0 to ${run.length - 1}.`);
|
|
1160
|
+
const drop = new Set(edit.commands);
|
|
1161
|
+
return replaceShot(scene, i, { ...scene.shots[i], run: run.filter((_, k) => !drop.has(k)) });
|
|
1162
|
+
}
|
|
1163
|
+
case "set_artwork":
|
|
1164
|
+
return { ...scene, artwork: edit.artwork };
|
|
1165
|
+
case "set_controls": {
|
|
1166
|
+
const next = { ...scene, controls: edit.controls };
|
|
1167
|
+
if (!edit.controls.length) delete next.controls;
|
|
1168
|
+
if (edit.presets !== void 0) {
|
|
1169
|
+
if (edit.presets.length) next.presets = edit.presets;
|
|
1170
|
+
else delete next.presets;
|
|
1171
|
+
}
|
|
1172
|
+
const names = new Set(edit.controls.map((c) => c.name));
|
|
1173
|
+
for (const preset of _nullishCoalesce(next.presets, () => ( [])))
|
|
1174
|
+
for (const key of Object.keys(preset.values))
|
|
1175
|
+
if (!names.has(key)) fail(`Preset "${preset.name}" sets ${namedWithSuggestion(key, names)}, which is not a control.`);
|
|
1176
|
+
return next;
|
|
1177
|
+
}
|
|
1178
|
+
case "replace_scene":
|
|
1179
|
+
return edit.scene;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
function errorKeys(issues) {
|
|
1183
|
+
return new Set(issues.filter((i) => i.severity === "error").map(motionIssueKey));
|
|
1184
|
+
}
|
|
1185
|
+
function editHints(before, after) {
|
|
1186
|
+
const old = new Set(before.map(motionIssueKey));
|
|
1187
|
+
return after.filter((i) => i.code === "overlap" && !old.has(motionIssueKey(i))).map(motionIssueLine);
|
|
1188
|
+
}
|
|
1189
|
+
function applyMotionEdits(scene, edits) {
|
|
1190
|
+
const list = Array.isArray(edits) ? edits : [edits];
|
|
1191
|
+
const before = checkMotionScene(scene);
|
|
1192
|
+
let next = scene;
|
|
1193
|
+
for (let i = 0; i < list.length; i++) {
|
|
1194
|
+
const edit = list[i];
|
|
1195
|
+
const prefix = list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _32 => _32.op])}): ` : "";
|
|
1196
|
+
if (!edit || !MOTION_EDIT_OPS.includes(edit.op))
|
|
1197
|
+
return { ok: false, error: `${prefix}Unknown op ${namedWithSuggestion(String(_optionalChain([edit, 'optionalAccess', _33 => _33.op])), MOTION_EDIT_OPS)}. Nothing was changed.` };
|
|
1198
|
+
try {
|
|
1199
|
+
next = applyOne(next, edit);
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
if (error instanceof EditError) return { ok: false, error: `${prefix}${error.message} Nothing was changed.` };
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
const problems = motionSceneShapeProblems(next);
|
|
1205
|
+
if (problems.length) return { ok: false, error: `${prefix}Nothing was changed: ${problems.join(" ")}` };
|
|
1206
|
+
}
|
|
1207
|
+
const issues = checkMotionScene(next);
|
|
1208
|
+
const had = errorKeys(before);
|
|
1209
|
+
const added = issues.filter((i) => i.severity === "error" && !had.has(motionIssueKey(i)));
|
|
1210
|
+
if (added.length) return { ok: false, error: `Nothing was changed: ${added.map(motionIssueLine).join(" ")}` };
|
|
1211
|
+
return { ok: true, scene: next, issues, hints: editHints(before, issues) };
|
|
1212
|
+
}
|
|
1213
|
+
function createMotionScene(artwork, init = {}) {
|
|
1214
|
+
return { version: 1, name: _nullishCoalesce(init.name, () => ( "animation")), ...init, artwork, shots: _nullishCoalesce(init.shots, () => ( [])) };
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/animation-editor/flair.ts
|
|
1218
|
+
var FLAIR_EASES = {
|
|
1219
|
+
linear: (t) => t,
|
|
1220
|
+
"in-out": (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
|
|
1221
|
+
out: (t) => 1 - (1 - t) ** 3
|
|
1222
|
+
};
|
|
1223
|
+
function flairEaseFor(ease) {
|
|
1224
|
+
for (const name of ["linear", "out", "in-out"]) {
|
|
1225
|
+
let worst = 0;
|
|
1226
|
+
for (let i = 1; i < 40; i++) {
|
|
1227
|
+
const t = i / 40;
|
|
1228
|
+
worst = Math.max(worst, Math.abs(_chunkXZ4L3R4Kcjs.evaluateEasing.call(void 0, ease, t, 1e3) - FLAIR_EASES[name](t)));
|
|
1229
|
+
}
|
|
1230
|
+
if (worst < 0.01) return name;
|
|
1231
|
+
}
|
|
1232
|
+
return void 0;
|
|
1233
|
+
}
|
|
1234
|
+
var TRANSFORM_PROPS2 = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
|
|
1235
|
+
var LINEAR_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "rotation", "scaleX", "scaleY"]);
|
|
1236
|
+
function hex6(color) {
|
|
1237
|
+
const hex = _chunkA6PHCAGXcjs.normalizeTokenColor.call(void 0, color);
|
|
1238
|
+
return hex ? hex.slice(0, 7) : "#000000";
|
|
1239
|
+
}
|
|
1240
|
+
function toFlairSvgMotion(scene, options = {}) {
|
|
1241
|
+
const fps = _nullishCoalesce(_nullishCoalesce(options.fps, () => ( scene.fps)), () => ( 30));
|
|
1242
|
+
const precision = _nullishCoalesce(options.precision, () => ( 3));
|
|
1243
|
+
const round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
1244
|
+
const f = (n) => String(round2(n));
|
|
1245
|
+
const timeline = compileMotionScene(scene);
|
|
1246
|
+
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
1247
|
+
const notes = [];
|
|
1248
|
+
const channels = [];
|
|
1249
|
+
const track = (id, p) => _nullishCoalesce(timeline.tracks.get(motionTrackKey(id, p)), () => ( []));
|
|
1250
|
+
const owner = (path, p) => {
|
|
1251
|
+
const entry = nodes.get(path.id);
|
|
1252
|
+
if (!entry) return void 0;
|
|
1253
|
+
return [...entry.ancestors, path.id].find((id) => timeline.tracks.has(motionTrackKey(id, p)));
|
|
1254
|
+
};
|
|
1255
|
+
const animatedNodes = new Set(timeline.segments.map((s) => s.node).filter(Boolean));
|
|
1256
|
+
const pathsSeen = /* @__PURE__ */ new Set();
|
|
1257
|
+
for (const id of animatedNodes) {
|
|
1258
|
+
const entry = nodes.get(id);
|
|
1259
|
+
const node = entry.node;
|
|
1260
|
+
if (timeline.tracks.has(motionTrackKey(id, "opacity")))
|
|
1261
|
+
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity")], exact: (s) => !s.wave, value: (t) => round2(Math.min(1, Math.max(0, Number(trackValue(track(id, "opacity"), t))))) });
|
|
1262
|
+
const transformSegments = TRANSFORM_PROPS2.flatMap((p) => track(id, p));
|
|
1263
|
+
if (transformSegments.length) {
|
|
1264
|
+
const base = node.transform ? ` matrix(${node.transform.map(f).join(" ")})` : "";
|
|
1265
|
+
const c = entry.center;
|
|
1266
|
+
channels.push({
|
|
1267
|
+
id,
|
|
1268
|
+
kind: "attr",
|
|
1269
|
+
attr: "transform",
|
|
1270
|
+
segments: transformSegments,
|
|
1271
|
+
exact: (s) => LINEAR_TRANSFORM.has(s.property) && !s.wave,
|
|
1272
|
+
value: (t) => {
|
|
1273
|
+
const o = motionNodeOffset(entry, timeline, t, nodes);
|
|
1274
|
+
return `translate(${f(o.dx)} ${f(o.dy)}) rotate(${f(o.rotation)} ${f(c.x)} ${f(c.y)}) translate(${f(c.x)} ${f(c.y)}) scale(${f(o.scaleX)} ${f(o.scaleY)}) translate(${f(-c.x)} ${f(-c.y)})${base}`;
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
const paths = node.type === "path" ? [node] : _chunkXCXQDPIDcjs.collectPathNodes.call(void 0, node);
|
|
1279
|
+
for (const path of paths) {
|
|
1280
|
+
if (pathsSeen.has(path.id)) continue;
|
|
1281
|
+
pathsSeen.add(path.id);
|
|
1282
|
+
const paint = (p, attr) => {
|
|
1283
|
+
const from = owner(path, p);
|
|
1284
|
+
if (from) channels.push({ id: path.id, kind: "attr", attr, segments: [...track(from, p)], exact: () => true, value: (t) => hex6(trackValue(track(from, p), t)) });
|
|
1285
|
+
};
|
|
1286
|
+
paint("fill", "fill");
|
|
1287
|
+
paint("stroke", "stroke");
|
|
1288
|
+
const width = owner(path, "strokeWidth");
|
|
1289
|
+
if (width) channels.push({ id: path.id, kind: "attr", attr: "stroke-width", segments: [...track(width, "strokeWidth")], exact: () => true, value: (t) => round2(Number(trackValue(track(width, "strokeWidth"), t))) });
|
|
1290
|
+
const fade = owner(path, "fillOpacity");
|
|
1291
|
+
if (fade) channels.push({ id: path.id, kind: "attr", attr: "fill-opacity", segments: [...track(fade, "fillOpacity")], exact: () => true, value: (t) => round2(Number(trackValue(track(fade, "fillOpacity"), t))) });
|
|
1292
|
+
const draw = owner(path, "drawEnd");
|
|
1293
|
+
if (draw) channels.push({ id: path.id, kind: "draw", segments: [...track(draw, "drawEnd")], exact: () => true, value: (t) => round2(Math.min(1, Math.max(0, Number(trackValue(track(draw, "drawEnd"), t))))) });
|
|
1294
|
+
if (owner(path, "drawStart")) notes.push(`"${path.id}": drawing from a start other than 0 is not exported; Flair draws from the start of the path.`);
|
|
1295
|
+
}
|
|
1296
|
+
const shapeSegments = [...timeline.tracks.entries()].filter(([k]) => k.startsWith(`${id}\0`) && (k.endsWith("\0shape") || k.includes("\0effect:"))).flatMap(([, v]) => v);
|
|
1297
|
+
if (shapeSegments.length) {
|
|
1298
|
+
if (node.type !== "path") notes.push(`"${id}" is a group: its morph or effect animation is not exported (Flair tweens one path's "d").`);
|
|
1299
|
+
else {
|
|
1300
|
+
const inverse = _chunkXCXQDPIDcjs.invertMatrix.call(void 0, _chunkXCXQDPIDcjs.nodeMatrix.call(void 0, node));
|
|
1301
|
+
channels.push({
|
|
1302
|
+
id,
|
|
1303
|
+
kind: "attr",
|
|
1304
|
+
attr: "d",
|
|
1305
|
+
segments: shapeSegments,
|
|
1306
|
+
exact: () => false,
|
|
1307
|
+
value: (t) => {
|
|
1308
|
+
const frame = motionFrameAt(scene, t, { draw: false, offsets: false });
|
|
1309
|
+
const now = _chunkXCXQDPIDcjs.expandVectorAppearance.call(void 0, _optionalChain([indexMotionNodes, 'call', _34 => _34(frame.document.nodes), 'access', _35 => _35.get, 'call', _36 => _36(id), 'optionalAccess', _37 => _37.node]));
|
|
1310
|
+
if (!now || now.type !== "path") return "";
|
|
1311
|
+
const bezier = _chunkO2UH7LZEcjs.transformBezierPath.call(void 0, _chunkXCXQDPIDcjs.vectorPathWorldBezier.call(void 0, now, _chunkXCXQDPIDcjs.IDENTITY_MATRIX, { effects: false }), inverse);
|
|
1312
|
+
return _chunkO2UH7LZEcjs.bezierPathToSvg.call(void 0, bezier, { precision: Math.min(precision, 2) });
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const camera = track("", "camera");
|
|
1319
|
+
if (camera.length) channels.push({ id: "", kind: "view", segments: [...camera], exact: () => true, value: (t) => trackValue(camera, t).map(round2) });
|
|
1320
|
+
const animate = [];
|
|
1321
|
+
let sampled = 0;
|
|
1322
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1323
|
+
const emit = (channel, delay, ms, from, to, ease) => {
|
|
1324
|
+
const base = { ...channel.id ? { id: channel.id } : {}, ...delay > 0 ? { delay: round2(delay) } : {}, ms: round2(ms), ease };
|
|
1325
|
+
if (channel.kind === "view") animate.push({ ...base, view: to });
|
|
1326
|
+
else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
|
|
1327
|
+
else animate.push({ ...base, attrs: { [channel.attr]: [from, to] } });
|
|
1328
|
+
};
|
|
1329
|
+
for (const channel of channels) {
|
|
1330
|
+
const cuts = channel.segments.filter((s) => s.end <= s.start);
|
|
1331
|
+
for (const cut of cuts) emit(channel, cut.start, 0, channel.value(Math.max(0, cut.start - 1e-3)), channel.value(cut.start), "linear");
|
|
1332
|
+
const times = [...new Set(channel.segments.flatMap((s) => [s.start, s.end]))].sort((a, b) => a - b);
|
|
1333
|
+
for (let i = 0; i + 1 < times.length; i++) {
|
|
1334
|
+
const a = times[i];
|
|
1335
|
+
const b = times[i + 1];
|
|
1336
|
+
const active = channel.segments.filter((s) => s.start < b && s.end > a);
|
|
1337
|
+
if (!active.length) continue;
|
|
1338
|
+
const first = active[0];
|
|
1339
|
+
const together = active.every((x) => x.start === a && x.end === b && channel.exact(x) && JSON.stringify(x.ease) === JSON.stringify(first.ease));
|
|
1340
|
+
const ease = together ? flairEaseFor(first.ease) : void 0;
|
|
1341
|
+
if (ease) {
|
|
1342
|
+
const from = channel.value(a);
|
|
1343
|
+
const to = channel.value(b);
|
|
1344
|
+
if (!same(from, to) || channel.kind === "view") emit(channel, a, b - a, from, to, ease);
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
const n = Math.max(1, Math.ceil((b - a) * fps / 1e3));
|
|
1348
|
+
let prev = channel.value(a);
|
|
1349
|
+
for (let k = 1; k <= n; k++) {
|
|
1350
|
+
const t0 = a + (b - a) * (k - 1) / n;
|
|
1351
|
+
const t1 = a + (b - a) * k / n;
|
|
1352
|
+
const next = channel.value(t1);
|
|
1353
|
+
if (!same(prev, next)) {
|
|
1354
|
+
emit(channel, t0, t1 - t0, prev, next, "linear");
|
|
1355
|
+
sampled++;
|
|
1356
|
+
}
|
|
1357
|
+
prev = next;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
animate.sort((x, y) => (_nullishCoalesce(x.delay, () => ( 0))) - (_nullishCoalesce(y.delay, () => ( 0))));
|
|
1362
|
+
let svg = _chunkXCXQDPIDcjs.vectorDocumentToSvg.call(void 0, { ...scene.artwork, nodes: _chunkXCXQDPIDcjs.expandVectorAppearances.call(void 0, scene.artwork.nodes) }, { pretty: false, precision });
|
|
1363
|
+
const lastChange = animate.reduce((m, a) => Math.max(m, (_nullishCoalesce(a.delay, () => ( 0))) + (_nullishCoalesce(a.ms, () => ( 0)))), 0);
|
|
1364
|
+
let motion = { svg, animate };
|
|
1365
|
+
if (_optionalChain([scene, 'access', _38 => _38.controls, 'optionalAccess', _39 => _39.length])) {
|
|
1366
|
+
const declarations = scene.controls.map((c) => `{{${c.name}: color default=${c.default.toUpperCase()}${c.label ? ` label='${c.label.replace(/'/g, "\u2019")}'` : ""}}}`).join(" ");
|
|
1367
|
+
const swap = (text) => scene.controls.reduce((s, c) => s.replace(new RegExp(`${c.default}(?![0-9a-fA-F])`, "gi"), `{{${c.name}}}`), text);
|
|
1368
|
+
svg = svg.replace(/^(<svg[^>]*>)/, `$1<!-- ${declarations} -->`);
|
|
1369
|
+
const header = svg.indexOf("-->") + 3;
|
|
1370
|
+
svg = svg.slice(0, header) + swap(svg.slice(header));
|
|
1371
|
+
motion = { svg, animate: animate.map((a) => a.attrs ? { ...a, attrs: Object.fromEntries(Object.entries(a.attrs).map(([k, [x, y]]) => [k, [typeof x === "string" ? swap(x) : x, typeof y === "string" ? swap(y) : y]])) } : a) };
|
|
1372
|
+
}
|
|
1373
|
+
return { motion, duration: timeline.duration, hold: Math.max(0, round2(timeline.duration - lastChange)), sampled, notes: [...new Set(notes)] };
|
|
1374
|
+
}
|
|
1375
|
+
var kebab = (text) => text.trim().replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "animation";
|
|
1376
|
+
function toFlairVectorEffect(scene, options = {}) {
|
|
1377
|
+
const out = toFlairSvgMotion(scene, options);
|
|
1378
|
+
return {
|
|
1379
|
+
kind: "vector",
|
|
1380
|
+
id: _nullishCoalesce(options.id, () => ( kebab(scene.name))),
|
|
1381
|
+
name: _nullishCoalesce(options.name, () => ( scene.name)),
|
|
1382
|
+
description: _nullishCoalesce(_nullishCoalesce(options.description, () => ( scene.description)), () => ( "")),
|
|
1383
|
+
category: _nullishCoalesce(options.category, () => ( "icon")),
|
|
1384
|
+
tags: _nullishCoalesce(options.tags, () => ( [])),
|
|
1385
|
+
motion: out.motion,
|
|
1386
|
+
hold: out.hold,
|
|
1387
|
+
..._optionalChain([scene, 'access', _40 => _40.presets, 'optionalAccess', _41 => _41.length]) ? { presets: scene.presets } : {},
|
|
1388
|
+
...options.trigger ? { trigger: options.trigger } : {}
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
function toFlairEffectDocument(scene, options = {}) {
|
|
1392
|
+
const out = toFlairSvgMotion(scene, options);
|
|
1393
|
+
return {
|
|
1394
|
+
version: 1,
|
|
1395
|
+
name: scene.name,
|
|
1396
|
+
effect: { kind: "vector", motion: out.motion, hold: out.hold },
|
|
1397
|
+
values: Object.fromEntries((_nullishCoalesce(scene.controls, () => ( []))).map((c) => [c.name, c.default])),
|
|
1398
|
+
tracks: {},
|
|
1399
|
+
duration: out.duration,
|
|
1400
|
+
loop: (_nullishCoalesce(scene.loop, () => ( "none"))) !== "none",
|
|
1401
|
+
presets: _nullishCoalesce(scene.presets, () => ( []))
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/animation-editor/controller.ts
|
|
1406
|
+
var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
|
|
1407
|
+
var AnimationEditorController = (_class = class {
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
__init() {this.selection = null}
|
|
1413
|
+
|
|
1414
|
+
__init2() {this.error = null}
|
|
1415
|
+
__init3() {this.revision = 0}
|
|
1416
|
+
__init4() {this.applying = false}
|
|
1417
|
+
|
|
1418
|
+
__init5() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1419
|
+
__init6() {this.unsubscribers = []}
|
|
1420
|
+
|
|
1421
|
+
constructor(options = {}) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);_class.prototype.__init5.call(this);_class.prototype.__init6.call(this);_class.prototype.__init7.call(this);_class.prototype.__init8.call(this);
|
|
1422
|
+
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
1423
|
+
this.scene = options.scene ? parseMotionScene(options.scene) : createMotionScene(BLANK);
|
|
1424
|
+
this.issues = checkMotionScene(this.scene);
|
|
1425
|
+
this.readOnly = _nullishCoalesce(options.readOnly, () => ( false));
|
|
1426
|
+
this.onChange = options.onChange;
|
|
1427
|
+
this.player = new (0, _chunkO4ONBPZWcjs.AnimationPlayer)({ clock: options.clock, duration: () => compileMotionScene(this.scene).duration, loop: _nullishCoalesce(this.scene.loop, () => ( "none")) });
|
|
1428
|
+
this.state = this.computeState();
|
|
1429
|
+
this.connect();
|
|
1430
|
+
}
|
|
1431
|
+
connect() {
|
|
1432
|
+
if (this.unsubscribers.length) return;
|
|
1433
|
+
this.unsubscribers.push(
|
|
1434
|
+
this.player.subscribe(() => this.notify()),
|
|
1435
|
+
this.history.subscribe(() => {
|
|
1436
|
+
if (!this.applying) this.notify();
|
|
1437
|
+
})
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Pauses and stops listening to the player and history. Subscribing again reconnects, so a
|
|
1442
|
+
* controller survives React StrictMode's unmount / remount.
|
|
1443
|
+
*/
|
|
1444
|
+
destroy() {
|
|
1445
|
+
this.player.pause();
|
|
1446
|
+
this.unsubscribers.splice(0).forEach((u) => u());
|
|
1447
|
+
}
|
|
1448
|
+
__init7() {this.getState = () => this.state}
|
|
1449
|
+
__init8() {this.subscribe = (listener) => {
|
|
1450
|
+
this.connect();
|
|
1451
|
+
this.listeners.add(listener);
|
|
1452
|
+
return () => {
|
|
1453
|
+
this.listeners.delete(listener);
|
|
1454
|
+
};
|
|
1455
|
+
}}
|
|
1456
|
+
/** The current scene (immutable; edit through the controller). */
|
|
1457
|
+
getScene() {
|
|
1458
|
+
return this.scene;
|
|
1459
|
+
}
|
|
1460
|
+
computeState() {
|
|
1461
|
+
const timeline = compileMotionScene(this.scene);
|
|
1462
|
+
const p = this.player.getState();
|
|
1463
|
+
const h = this.history.getState();
|
|
1464
|
+
return {
|
|
1465
|
+
scene: this.scene,
|
|
1466
|
+
duration: timeline.duration,
|
|
1467
|
+
shots: timeline.shots,
|
|
1468
|
+
issues: this.issues,
|
|
1469
|
+
time: p.time,
|
|
1470
|
+
playing: p.playing,
|
|
1471
|
+
loop: p.loop,
|
|
1472
|
+
currentShot: _nullishCoalesce(_optionalChain([motionShotAt, 'call', _42 => _42(timeline, p.time), 'optionalAccess', _43 => _43.id]), () => ( null)),
|
|
1473
|
+
selection: this.selection,
|
|
1474
|
+
readOnly: this.readOnly,
|
|
1475
|
+
error: this.error,
|
|
1476
|
+
canUndo: h.canUndo,
|
|
1477
|
+
canRedo: h.canRedo,
|
|
1478
|
+
undoLabel: h.undoName,
|
|
1479
|
+
redoLabel: h.redoName,
|
|
1480
|
+
revision: this.revision
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
notify() {
|
|
1484
|
+
this.revision++;
|
|
1485
|
+
this.state = this.computeState();
|
|
1486
|
+
const state = this.state;
|
|
1487
|
+
this.listeners.forEach((listener) => {
|
|
1488
|
+
try {
|
|
1489
|
+
listener(state);
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
console.error("[AnimationEditorController] Error in state listener:", error);
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
setScene(scene) {
|
|
1496
|
+
this.applying = true;
|
|
1497
|
+
try {
|
|
1498
|
+
this.scene = scene;
|
|
1499
|
+
this.issues = checkMotionScene(scene);
|
|
1500
|
+
if (this.selection) {
|
|
1501
|
+
const shot = scene.shots.find((s) => s.id === this.selection.shot);
|
|
1502
|
+
if (!shot) this.selection = null;
|
|
1503
|
+
else if (this.selection.command !== void 0 && this.selection.command >= shot.run.length) this.selection = { shot: shot.id };
|
|
1504
|
+
}
|
|
1505
|
+
this.player.setLoop(_nullishCoalesce(scene.loop, () => ( "none")));
|
|
1506
|
+
this.player.setDuration(() => compileMotionScene(this.scene).duration);
|
|
1507
|
+
} finally {
|
|
1508
|
+
this.applying = false;
|
|
1509
|
+
}
|
|
1510
|
+
this.notify();
|
|
1511
|
+
_optionalChain([this, 'access', _44 => _44.onChange, 'optionalCall', _45 => _45(scene)]);
|
|
1512
|
+
}
|
|
1513
|
+
commit(label, next) {
|
|
1514
|
+
const before = this.scene;
|
|
1515
|
+
this.error = null;
|
|
1516
|
+
if (next === before) {
|
|
1517
|
+
this.notify();
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const command2 = {
|
|
1521
|
+
name: label,
|
|
1522
|
+
tag: "animation-editor",
|
|
1523
|
+
execute: () => this.setScene(next),
|
|
1524
|
+
undo: () => this.setScene(before),
|
|
1525
|
+
redo: () => this.setScene(next)
|
|
1526
|
+
};
|
|
1527
|
+
this.history.execute(command2);
|
|
1528
|
+
}
|
|
1529
|
+
refuse(message) {
|
|
1530
|
+
this.error = message;
|
|
1531
|
+
this.notify();
|
|
1532
|
+
return { ok: false, error: message };
|
|
1533
|
+
}
|
|
1534
|
+
/** Why edits are refused now, or `null` when they are allowed. */
|
|
1535
|
+
readOnlyReason() {
|
|
1536
|
+
return this.readOnly ? "The animation is read-only." : null;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Applies edits as one undo step, all or nothing (see `applyMotionEdits`). With
|
|
1540
|
+
* `dryRun` it reports what would happen and changes nothing.
|
|
1541
|
+
*/
|
|
1542
|
+
apply(edits, label = "Edit animation", options = {}) {
|
|
1543
|
+
const reason = this.readOnlyReason();
|
|
1544
|
+
if (reason) return this.refuse(reason);
|
|
1545
|
+
const result = applyMotionEdits(this.scene, edits);
|
|
1546
|
+
if (!result.ok) return options.dryRun ? result : this.refuse(result.error);
|
|
1547
|
+
if (!options.dryRun) this.commit(label, result.scene);
|
|
1548
|
+
return result;
|
|
1549
|
+
}
|
|
1550
|
+
/** Replaces the artwork (e.g. from a vector editor's `onChange`), as one undo step. */
|
|
1551
|
+
setArtwork(artwork, label = "Change artwork") {
|
|
1552
|
+
return this.apply({ op: "set_artwork", artwork }, label);
|
|
1553
|
+
}
|
|
1554
|
+
/** Opens a scene (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
|
|
1555
|
+
load(scene, options = {}) {
|
|
1556
|
+
let parsed;
|
|
1557
|
+
try {
|
|
1558
|
+
parsed = parseMotionScene(scene);
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
return this.refuse(error.message);
|
|
1561
|
+
}
|
|
1562
|
+
if (options.clearHistory) {
|
|
1563
|
+
this.history.clear();
|
|
1564
|
+
this.error = null;
|
|
1565
|
+
this.selection = null;
|
|
1566
|
+
this.setScene(parsed);
|
|
1567
|
+
this.player.seek(0);
|
|
1568
|
+
return { ok: true, scene: parsed, issues: this.issues, hints: [] };
|
|
1569
|
+
}
|
|
1570
|
+
return this.apply({ op: "replace_scene", scene: parsed }, "Replace animation");
|
|
1571
|
+
}
|
|
1572
|
+
setReadOnly(readOnly) {
|
|
1573
|
+
if (this.readOnly === readOnly) return;
|
|
1574
|
+
this.readOnly = readOnly;
|
|
1575
|
+
this.notify();
|
|
1576
|
+
}
|
|
1577
|
+
/** Selects a shot (and optionally a command in it); `null` clears. Unknown shots clear. */
|
|
1578
|
+
select(selection) {
|
|
1579
|
+
const shot = selection && this.scene.shots.find((s) => s.id === selection.shot);
|
|
1580
|
+
const next = shot ? { shot: shot.id, ...selection.command !== void 0 && selection.command < shot.run.length ? { command: selection.command } : {} } : null;
|
|
1581
|
+
if (JSON.stringify(next) === JSON.stringify(this.selection)) return;
|
|
1582
|
+
this.selection = next;
|
|
1583
|
+
this.notify();
|
|
1584
|
+
}
|
|
1585
|
+
play() {
|
|
1586
|
+
this.player.play();
|
|
1587
|
+
}
|
|
1588
|
+
pause() {
|
|
1589
|
+
this.player.pause();
|
|
1590
|
+
}
|
|
1591
|
+
toggle() {
|
|
1592
|
+
this.player.toggle();
|
|
1593
|
+
}
|
|
1594
|
+
/** Moves the playhead (ms). */
|
|
1595
|
+
seek(time) {
|
|
1596
|
+
this.player.seek(time);
|
|
1597
|
+
}
|
|
1598
|
+
/** Moves the playhead to the start or end of a shot. Returns false for an unknown shot. */
|
|
1599
|
+
seekShot(id, at = "start") {
|
|
1600
|
+
const shot = compileMotionScene(this.scene).shots.find((s) => s.id === id);
|
|
1601
|
+
if (!shot) return false;
|
|
1602
|
+
this.player.seek(at === "start" ? shot.start : shot.end);
|
|
1603
|
+
return true;
|
|
1604
|
+
}
|
|
1605
|
+
undo() {
|
|
1606
|
+
return this.history.undo();
|
|
1607
|
+
}
|
|
1608
|
+
redo() {
|
|
1609
|
+
return this.history.redo();
|
|
1610
|
+
}
|
|
1611
|
+
/** The scene at `time` (default: the playhead). */
|
|
1612
|
+
frame(time = this.player.currentTime, options = {}) {
|
|
1613
|
+
return motionFrameAt(this.scene, time, options);
|
|
1614
|
+
}
|
|
1615
|
+
/** The scene at `time` (default: the playhead) as SVG. */
|
|
1616
|
+
toSvg(time = this.player.currentTime, options = {}) {
|
|
1617
|
+
return motionFrameToSvg(this.scene, time, options);
|
|
1618
|
+
}
|
|
1619
|
+
/** The scene as a Flair `SvgMotion`. */
|
|
1620
|
+
toFlairMotion(options = {}) {
|
|
1621
|
+
return toFlairSvgMotion(this.scene, options);
|
|
1622
|
+
}
|
|
1623
|
+
/** The scene as a Flair catalog vector effect. */
|
|
1624
|
+
toFlairEffect(options = {}) {
|
|
1625
|
+
return toFlairVectorEffect(this.scene, options);
|
|
1626
|
+
}
|
|
1627
|
+
/** The scene as a Flair `EffectDocument`. */
|
|
1628
|
+
toFlairDocument(options = {}) {
|
|
1629
|
+
return toFlairEffectDocument(this.scene, options);
|
|
1630
|
+
}
|
|
1631
|
+
}, _class);
|
|
1632
|
+
|
|
1633
|
+
// src/animation-editor/tools.ts
|
|
1634
|
+
var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
1635
|
+
var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
1636
|
+
var WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
1637
|
+
var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
1638
|
+
var object = (properties, required = [], description) => ({
|
|
1639
|
+
type: "object",
|
|
1640
|
+
...description ? { description } : {},
|
|
1641
|
+
properties,
|
|
1642
|
+
...required.length ? { required } : {},
|
|
1643
|
+
additionalProperties: false
|
|
1644
|
+
});
|
|
1645
|
+
var SHOT_ID = { type: "string", minLength: 1, description: 'A shot id from get_animation (e.g. "intro").' };
|
|
1646
|
+
var NODE_IDS = { type: "array", items: { type: "string", minLength: 1 }, minItems: 1, description: `Node ids from get_animation's "elements" (a group animates as one).` };
|
|
1647
|
+
var MS = (what) => ({ type: "number", minimum: 0, description: `${what} in milliseconds.` });
|
|
1648
|
+
var FRACTION = (what) => ({ type: "number", minimum: 0, maximum: 1, description: what });
|
|
1649
|
+
var POINT = (what) => object({ x: { type: "number", description: "Document units." }, y: { type: "number", description: "Document units." } }, ["x", "y"], what);
|
|
1650
|
+
var EASE = {
|
|
1651
|
+
description: `Easing: a preset (${_chunkXZ4L3R4Kcjs.ANIMATION_EASING_PRESETS.join(", ")}) or { "type": "cubic-bezier", "x1", "y1", "x2", "y2" }. Default: a cubic ease-out for entrances, cubic ease-in-out for moves.`,
|
|
1652
|
+
anyOf: [
|
|
1653
|
+
{ type: "string", enum: [..._chunkXZ4L3R4Kcjs.ANIMATION_EASING_PRESETS] },
|
|
1654
|
+
object({ type: { const: "cubic-bezier", description: "Curve kind." }, x1: { type: "number", description: "Out handle x (0\u20131)." }, y1: { type: "number", description: "Out handle y." }, x2: { type: "number", description: "In handle x (0\u20131)." }, y2: { type: "number", description: "In handle y." } }, ["type", "x1", "y1", "x2", "y2"])
|
|
1655
|
+
]
|
|
1656
|
+
};
|
|
1657
|
+
var BASE = {
|
|
1658
|
+
targets: NODE_IDS,
|
|
1659
|
+
duration: MS("Length for each target (each type has a default; draw follows the path length)"),
|
|
1660
|
+
delay: MS("Wait before starting (default 0)"),
|
|
1661
|
+
stagger: MS("Start each further target this much later (default 0)"),
|
|
1662
|
+
with: { type: "string", enum: ["after", "previous"], description: '"after" (default) starts when the commands before it finish; "previous" starts together with the command before.' },
|
|
1663
|
+
ease: EASE,
|
|
1664
|
+
note: { type: "string", description: "Why this command is here, in a few words." }
|
|
1665
|
+
};
|
|
1666
|
+
var command = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
|
|
1667
|
+
var MOTION_COMMAND_SCHEMA = {
|
|
1668
|
+
description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
|
|
1669
|
+
oneOf: [
|
|
1670
|
+
command("enter", "Brings hidden targets in; they are hidden before this shot.", {
|
|
1671
|
+
style: { type: "string", enum: [...MOTION_ENTER_STYLES], description: "fade (default), pop, grow, slide, or draw (outline drawn on, then the fill fades in)." },
|
|
1672
|
+
direction: { type: "string", enum: [...MOTION_DIRECTIONS], description: "For slide: direction of travel (default up)." },
|
|
1673
|
+
distance: { type: "number", description: "For slide: document units (default 40)." }
|
|
1674
|
+
}),
|
|
1675
|
+
command("exit", "Takes targets out.", {
|
|
1676
|
+
style: { type: "string", enum: [...MOTION_EXIT_STYLES], description: "fade (default), shrink, slide or undraw." },
|
|
1677
|
+
direction: { type: "string", enum: [...MOTION_DIRECTIONS], description: "For slide: direction of travel (default down)." },
|
|
1678
|
+
distance: { type: "number", description: "For slide: document units (default 40)." }
|
|
1679
|
+
}),
|
|
1680
|
+
command("draw", "Draws strokes on (or off) to a fraction of their length.", { to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
|
|
1681
|
+
command("move", "Moves targets by an offset or so their centre lands on a point.", { by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
|
|
1682
|
+
command("rotate", "Turns targets about their centre (degrees, clockwise).", { by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
|
|
1683
|
+
command("scale", "Scales targets about their centre (1 = rest size).", { to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] } }, ["to"]),
|
|
1684
|
+
command("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
|
|
1685
|
+
command("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
|
|
1686
|
+
fill: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
|
|
1687
|
+
stroke: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
|
|
1688
|
+
strokeWidth: { type: "number", minimum: 0, description: "Document units." }
|
|
1689
|
+
}),
|
|
1690
|
+
command("morph", "Morphs each target into the shape (and paint) of another node, which usually stays hidden.", { into: { type: "string", minLength: 1, description: "Id of the node whose shape to take." } }, ["into"]),
|
|
1691
|
+
command("follow", "Moves targets along a path node, centre on the path.", {
|
|
1692
|
+
path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
|
|
1693
|
+
rotate: { type: "boolean", description: "Turn with the path (default false)." },
|
|
1694
|
+
from: FRACTION("Where to start along the path (default 0)."),
|
|
1695
|
+
to: FRACTION("Where to end along the path (default 1).")
|
|
1696
|
+
}, ["path"]),
|
|
1697
|
+
command("pulse", "Scales up and back to draw the eye.", { amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
|
|
1698
|
+
command("shake", "Shakes side to side, settling.", { amount: { type: "number", description: "Document units (default 8)." }, count: { type: "integer", minimum: 1, description: "Shakes (default 3)." } }),
|
|
1699
|
+
command("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
|
|
1700
|
+
effect: { type: "string", minLength: 1, description: `Effect id from get_animation's element "effects".` },
|
|
1701
|
+
param: { type: "string", minLength: 1, description: "A numeric parameter of that effect (listed with it)." },
|
|
1702
|
+
to: { type: "number", description: "Value at the end." }
|
|
1703
|
+
}, ["effect", "param", "to"]),
|
|
1704
|
+
object(
|
|
1705
|
+
{ type: { const: "wait", description: "Waits." }, duration: MS("How long"), with: BASE.with, note: BASE.note },
|
|
1706
|
+
["type", "duration"],
|
|
1707
|
+
"Waits before the next command."
|
|
1708
|
+
)
|
|
1709
|
+
]
|
|
1710
|
+
};
|
|
1711
|
+
var CAMERA = {
|
|
1712
|
+
description: `Where the camera looks: a rectangle { x, y, width, height } in document units, or { frame: "<node id>", padding } to frame a node (fitted to the artwork's shape). It eases there over "ms" (default 1000); the first shot cuts.`,
|
|
1713
|
+
anyOf: [
|
|
1714
|
+
object({ x: { type: "number", description: "Left." }, y: { type: "number", description: "Top." }, width: { type: "number", exclusiveMinimum: 0, description: "Width." }, height: { type: "number", exclusiveMinimum: 0, description: "Height." }, ease: EASE, ms: MS("Camera move length") }, ["x", "y", "width", "height"]),
|
|
1715
|
+
object({ frame: { type: "string", minLength: 1, description: "Node id to frame." }, padding: { type: "number", description: "Margin around it (default 24)." }, ease: EASE, ms: MS("Camera move length") }, ["frame"])
|
|
1716
|
+
]
|
|
1717
|
+
};
|
|
1718
|
+
var SHOT_FIELDS = {
|
|
1719
|
+
label: { type: "string", description: "What happens, in a few words." },
|
|
1720
|
+
camera: CAMERA,
|
|
1721
|
+
run: { type: "array", items: MOTION_COMMAND_SCHEMA, description: "Commands, in order." },
|
|
1722
|
+
hold: MS("Hold after the commands (default 600)")
|
|
1723
|
+
};
|
|
1724
|
+
var SHOT = object({ id: { type: "string", pattern: "^[A-Za-z0-9_][\\w.-]*$", description: 'Unique shot id, e.g. "intro".' }, ...SHOT_FIELDS }, ["id", "run"], "A shot.");
|
|
1725
|
+
var CONTROL = object(
|
|
1726
|
+
{
|
|
1727
|
+
name: { type: "string", pattern: "^[A-Za-z_]\\w*$", description: 'Identifier, e.g. "accent".' },
|
|
1728
|
+
type: { const: "color", description: "Controls are colours." },
|
|
1729
|
+
default: { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: "A #rrggbb colour the scene uses; the control stands for every use of it." },
|
|
1730
|
+
label: { type: "string", description: "Label in a controls panel." }
|
|
1731
|
+
},
|
|
1732
|
+
["name", "type", "default"],
|
|
1733
|
+
"A colour parameter of the Flair effect made from the scene."
|
|
1734
|
+
);
|
|
1735
|
+
var PRESET = object({ name: { type: "string", minLength: 1, description: "Preset name." }, values: { type: "object", additionalProperties: { type: "string" }, description: "Control name \u2192 colour." } }, ["name", "values"], "Named control values.");
|
|
1736
|
+
var SETTINGS = {
|
|
1737
|
+
name: { type: "string", minLength: 1, description: 'Short name, e.g. "logo-reveal".' },
|
|
1738
|
+
description: { type: ["string", "null"], description: "What it shows; null clears." },
|
|
1739
|
+
loop: { type: "string", enum: ["none", "loop", "ping-pong"], description: "Playback past the end (default none)." },
|
|
1740
|
+
fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for exports (default 30)." },
|
|
1741
|
+
target: { anyOf: [{ type: "array", items: { type: "number", minimum: 0 }, minItems: 2, maxItems: 2 }, { type: "null" }], description: "Wanted length [min, max] in seconds; null clears." }
|
|
1742
|
+
};
|
|
1743
|
+
var EDIT_OP = {
|
|
1744
|
+
description: `One edit (${MOTION_EDIT_OPS.filter((o) => o !== "replace_scene" && o !== "set_artwork").join(", ")}), shaped like the tool of the same name with an "op" field.`,
|
|
1745
|
+
oneOf: [
|
|
1746
|
+
object({ op: { const: "set_settings", description: "Like set_animation_settings." }, ...SETTINGS }, ["op"]),
|
|
1747
|
+
object({ op: { const: "insert_shots", description: "Like insert_shots." }, shots: { type: "array", items: SHOT, minItems: 1, description: "Shots to add." }, at: { type: "integer", minimum: 0, description: "Position (default: the end)." } }, ["op", "shots"]),
|
|
1748
|
+
object({ op: { const: "update_shot", description: "Like update_shot." }, shot: SHOT_ID, set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold"] }, description: "Fields to clear." } }, ["op", "shot"]),
|
|
1749
|
+
object({ op: { const: "delete_shots", description: "Like delete_shots." }, shots: { type: "array", items: SHOT_ID, minItems: 1, description: "Shot ids." } }, ["op", "shots"]),
|
|
1750
|
+
object({ op: { const: "move_shot", description: "Like move_shot." }, shot: SHOT_ID, to: { type: "integer", minimum: 0, description: "New position (0 = first)." } }, ["op", "shot", "to"]),
|
|
1751
|
+
object({ op: { const: "add_commands", description: "Like add_commands." }, shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the shot's run (default: the end)." } }, ["op", "shot", "commands"]),
|
|
1752
|
+
object({ op: { const: "update_command", description: "Like update_command." }, shot: SHOT_ID, command: { type: "integer", minimum: 0, description: "Index in the shot's run." }, set: { type: "object", description: "Command fields to replace." }, remove: { type: "array", items: { type: "string" }, description: "Command fields to clear." } }, ["op", "shot", "command"]),
|
|
1753
|
+
object({ op: { const: "delete_commands", description: "Like delete_commands." }, shot: SHOT_ID, commands: { type: "array", items: { type: "integer", minimum: 0 }, minItems: 1, description: "Indexes in the shot's run." } }, ["op", "shot", "commands"]),
|
|
1754
|
+
object({ op: { const: "set_controls", description: "Like set_controls." }, controls: { type: "array", items: CONTROL, description: "Controls (empty clears)." }, presets: { type: "array", items: PRESET, description: "Presets (empty clears)." } }, ["op", "controls"])
|
|
1755
|
+
]
|
|
1756
|
+
};
|
|
1757
|
+
function describeElement(node, ancestors) {
|
|
1758
|
+
const effects = (_nullishCoalesce(node.effects, () => ( []))).map((e) => ({
|
|
1759
|
+
id: e.id,
|
|
1760
|
+
type: e.type,
|
|
1761
|
+
numericParams: Object.fromEntries(Object.entries(e.params).filter(([, v]) => typeof v === "number"))
|
|
1762
|
+
}));
|
|
1763
|
+
return {
|
|
1764
|
+
id: node.id,
|
|
1765
|
+
type: node.type,
|
|
1766
|
+
...node.name ? { name: node.name } : {},
|
|
1767
|
+
...ancestors.length ? { parent: ancestors[ancestors.length - 1] } : {},
|
|
1768
|
+
...node.visible === false ? { hidden: true } : {},
|
|
1769
|
+
...node.type === "path" && typeof node.fill === "string" ? { fill: node.fill } : {},
|
|
1770
|
+
...node.type === "path" && typeof node.stroke === "string" ? { stroke: node.stroke } : {},
|
|
1771
|
+
...node.type === "group" ? { children: node.children.map((c) => c.id) } : {},
|
|
1772
|
+
...effects.length ? { effects } : {}
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
var round = (n) => Math.round(n * 10) / 10;
|
|
1776
|
+
function describeMotionElements(scene) {
|
|
1777
|
+
return [...indexMotionNodes(scene.artwork.nodes).values()].map((e) => ({
|
|
1778
|
+
...describeElement(e.node, e.ancestors),
|
|
1779
|
+
...e.bounds ? { bounds: { x: round(e.bounds.x), y: round(e.bounds.y), width: round(e.bounds.width), height: round(e.bounds.height) } } : {}
|
|
1780
|
+
}));
|
|
1781
|
+
}
|
|
1782
|
+
function describeMotionShots(scene) {
|
|
1783
|
+
const timeline = compileMotionScene(scene);
|
|
1784
|
+
return scene.shots.map((shot, i) => {
|
|
1785
|
+
const t = timeline.shots[i];
|
|
1786
|
+
return {
|
|
1787
|
+
id: shot.id,
|
|
1788
|
+
...shot.label ? { label: shot.label } : {},
|
|
1789
|
+
start: t.start,
|
|
1790
|
+
end: t.end,
|
|
1791
|
+
...shot.camera ? { camera: shot.camera } : {},
|
|
1792
|
+
...shot.hold !== void 0 ? { hold: shot.hold } : {},
|
|
1793
|
+
commands: shot.run.map((c, index) => {
|
|
1794
|
+
const ct = timeline.commands.find((x) => x.shotIndex === i && x.command === index);
|
|
1795
|
+
return { index, ...c, start: ct.start, end: ct.end };
|
|
1796
|
+
})
|
|
1797
|
+
};
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
var issueSummary = (issues) => ({
|
|
1801
|
+
errors: issues.filter((i) => i.severity === "error").length,
|
|
1802
|
+
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
1803
|
+
notes: issues.filter((i) => i.severity === "note").length,
|
|
1804
|
+
lines: issues.map((i) => `${i.severity}: ${motionIssueLine(i)}`)
|
|
1805
|
+
});
|
|
1806
|
+
function createAnimationEditorToolProvider(controller, options = {}) {
|
|
1807
|
+
const readOnlyReason = () => {
|
|
1808
|
+
const r = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
|
|
1809
|
+
if (r) return typeof r === "string" ? r : "This animation is read-only.";
|
|
1810
|
+
return controller.readOnlyReason();
|
|
1811
|
+
};
|
|
1812
|
+
const scene = () => controller.getScene();
|
|
1813
|
+
const outcome = (result, extra = {}) => {
|
|
1814
|
+
if (!result.ok) throw new Error(result.error);
|
|
1815
|
+
const timeline = compileMotionScene(result.scene);
|
|
1816
|
+
return {
|
|
1817
|
+
...extra,
|
|
1818
|
+
duration: timeline.duration,
|
|
1819
|
+
shots: timeline.shots.map((s) => ({ id: s.id, start: s.start, end: s.end })),
|
|
1820
|
+
issues: issueSummary(result.issues),
|
|
1821
|
+
...result.hints.length ? { hints: result.hints } : {}
|
|
1822
|
+
};
|
|
1823
|
+
};
|
|
1824
|
+
const apply = (edits, label, extra) => {
|
|
1825
|
+
const result = controller.apply(edits, label);
|
|
1826
|
+
return outcome(result, result.ok && extra ? extra(result.scene) : {});
|
|
1827
|
+
};
|
|
1828
|
+
const getAnimation = {
|
|
1829
|
+
name: "get_animation",
|
|
1830
|
+
title: "Read the animation",
|
|
1831
|
+
description: "Returns the vector animation: its name, artwork size, length (ms), loop mode, the artwork's elements (id, type, name, parent, bounds, solid paints, live effects with their numeric parameters), every shot with its commands and their start/end times, Flair controls, and a summary of issues. Use the ids it returns with the write tools.",
|
|
1832
|
+
inputSchema: object({ includeElements: { type: "boolean", description: "Include the artwork's elements (default true)." } }),
|
|
1833
|
+
annotations: { title: "Read the animation", ...READ },
|
|
1834
|
+
handler: ({ includeElements = true }) => {
|
|
1835
|
+
const s = scene();
|
|
1836
|
+
const reason = readOnlyReason();
|
|
1837
|
+
return {
|
|
1838
|
+
name: s.name,
|
|
1839
|
+
...s.description ? { description: s.description } : {},
|
|
1840
|
+
artwork: { width: s.artwork.width, height: s.artwork.height },
|
|
1841
|
+
duration: compileMotionScene(s).duration,
|
|
1842
|
+
loop: _nullishCoalesce(s.loop, () => ( "none")),
|
|
1843
|
+
...s.target ? { target: s.target } : {},
|
|
1844
|
+
...includeElements ? { elements: describeMotionElements(s) } : {},
|
|
1845
|
+
shots: describeMotionShots(s),
|
|
1846
|
+
...s.controls ? { controls: s.controls } : {},
|
|
1847
|
+
...s.presets ? { presets: s.presets } : {},
|
|
1848
|
+
issues: issueSummary(controller.getState().issues),
|
|
1849
|
+
...reason ? { readOnly: reason } : {}
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const getShot = {
|
|
1854
|
+
name: "get_shot",
|
|
1855
|
+
title: "Read one shot",
|
|
1856
|
+
description: "Returns one shot as stored (id, label, camera, run, hold) with its start/end and each command's start/end in ms.",
|
|
1857
|
+
inputSchema: object({ shot: SHOT_ID }, ["shot"]),
|
|
1858
|
+
annotations: { title: "Read one shot", ...READ },
|
|
1859
|
+
handler: ({ shot }) => {
|
|
1860
|
+
const found = describeMotionShots(scene()).find((s) => s.id === shot);
|
|
1861
|
+
if (!found) throw new Error(`There is no shot "${shot}"; the shots are ${scene().shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
|
|
1862
|
+
return found;
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1865
|
+
const findElements = {
|
|
1866
|
+
name: "find_animation_elements",
|
|
1867
|
+
title: "Find artwork elements",
|
|
1868
|
+
description: "Finds artwork elements whose id or name contains the text (case-insensitive). Returns their id, type, name, parent, bounds, paints and live effects.",
|
|
1869
|
+
inputSchema: object({ query: { type: "string", minLength: 1, description: "Text to look for in ids and names." }, limit: { type: "integer", minimum: 1, maximum: 200, description: "Most results (default 20)." } }, ["query"]),
|
|
1870
|
+
annotations: { title: "Find artwork elements", ...READ },
|
|
1871
|
+
handler: ({ query, limit = 20 }) => {
|
|
1872
|
+
const q = String(query).toLowerCase();
|
|
1873
|
+
const all = describeMotionElements(scene());
|
|
1874
|
+
const elements = all.filter((e) => e.id.toLowerCase().includes(q) || "name" in e && String(e.name).toLowerCase().includes(q));
|
|
1875
|
+
return { elements: elements.slice(0, limit), total: elements.length };
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
const checkAnimation = {
|
|
1879
|
+
name: "check_animation",
|
|
1880
|
+
title: "Check the animation",
|
|
1881
|
+
description: "Lists the animation's problems: each with a stable code (unknown-target, overlap, out-of-frame, too-fast, enter-visible\u2026), severity (error / warning / note), the shot and command index it is in, the node ids it is about, measured vs needed values, and a message saying how to fix it.",
|
|
1882
|
+
inputSchema: object({}),
|
|
1883
|
+
annotations: { title: "Check the animation", ...READ },
|
|
1884
|
+
handler: () => {
|
|
1885
|
+
const issues = controller.getState().issues;
|
|
1886
|
+
return { ...issueSummary(issues), issues };
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
const renderFrame = {
|
|
1890
|
+
name: "render_animation_frame",
|
|
1891
|
+
title: "Render a frame",
|
|
1892
|
+
description: 'Draws the animation at one moment without changing it, to check it: standalone SVG by default, or a PNG image (format "png") when the host can rasterize. Give a time in ms, or a shot and where in it.',
|
|
1893
|
+
inputSchema: object({
|
|
1894
|
+
time: MS("Time from the start"),
|
|
1895
|
+
shot: { ...SHOT_ID, description: 'Render a moment of this shot instead of "time".' },
|
|
1896
|
+
at: { type: "string", enum: ["start", "middle", "end"], description: "With shot: which moment (default end: everything drawn, before the next shot)." },
|
|
1897
|
+
format: { type: "string", enum: ["svg", "png"], description: "svg (default) or png (only when the host supports it)." }
|
|
1898
|
+
}),
|
|
1899
|
+
annotations: { title: "Render a frame", ...READ },
|
|
1900
|
+
handler: async ({ time, shot, at = "end", format = "svg" }) => {
|
|
1901
|
+
const s = scene();
|
|
1902
|
+
let t = _nullishCoalesce(time, () => ( controller.getState().time));
|
|
1903
|
+
if (shot !== void 0) {
|
|
1904
|
+
const timing = compileMotionScene(s).shots.find((x) => x.id === shot);
|
|
1905
|
+
if (!timing) throw new Error(`There is no shot "${shot}"; the shots are ${s.shots.map((x) => `"${x.id}"`).join(", ") || "none yet"}.`);
|
|
1906
|
+
t = at === "start" ? timing.start : at === "middle" ? (timing.commandsStart + timing.commandsEnd) / 2 : Math.max(timing.start, timing.end - 1);
|
|
1907
|
+
}
|
|
1908
|
+
const svg = controller.toSvg(t, { pretty: false });
|
|
1909
|
+
if (format === "png") {
|
|
1910
|
+
if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
|
|
1911
|
+
const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
|
|
1912
|
+
return { content: [{ type: "image", data: image.data, mimeType: image.mimeType }] };
|
|
1913
|
+
}
|
|
1914
|
+
return { time: t, svg };
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
const exportFlairMotion = {
|
|
1918
|
+
name: "export_flair_motion",
|
|
1919
|
+
title: "Export as Flair vector motion",
|
|
1920
|
+
description: "Returns the animation as a Flair SvgMotion ({ svg, animate }) that Flair's SvgPlayer plays, plus its hold, how many changes were sampled into linear steps, and notes on anything not carried over.",
|
|
1921
|
+
inputSchema: object({ fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for changes Flair cannot express exactly (default: the scene's fps, else 30)." } }),
|
|
1922
|
+
annotations: { title: "Export as Flair vector motion", ...READ },
|
|
1923
|
+
handler: ({ fps }) => controller.toFlairMotion(fps ? { fps } : {})
|
|
1924
|
+
};
|
|
1925
|
+
const exportFlairEffect = {
|
|
1926
|
+
name: "export_flair_effect",
|
|
1927
|
+
title: "Export as a Flair effect",
|
|
1928
|
+
description: `Returns the animation as a Flair catalog vector effect: { kind: "vector", id, name, description, category, tags, motion, hold, presets }, with each colour control declared as a {{name: color}} placeholder. Add it to a Flair catalog or load it in Flair's effect editor.`,
|
|
1929
|
+
inputSchema: object({
|
|
1930
|
+
id: { type: "string", pattern: "^[a-z0-9]+(-[a-z0-9]+)*$", description: "Kebab-case id (default: from the scene's name)." },
|
|
1931
|
+
name: { type: "string", minLength: 1, description: "Display name (default: the scene's name)." },
|
|
1932
|
+
description: { type: "string", description: "One sentence (default: the scene's description)." },
|
|
1933
|
+
category: { type: "string", minLength: 1, description: "Flair catalog category, e.g. icon, loader, ui, data, text, morph (default icon)." },
|
|
1934
|
+
tags: { type: "array", items: { type: "string" }, description: "Search tags (default none)." }
|
|
1935
|
+
}),
|
|
1936
|
+
annotations: { title: "Export as a Flair effect", ...READ },
|
|
1937
|
+
handler: (args) => controller.toFlairEffect(args)
|
|
1938
|
+
};
|
|
1939
|
+
const setArtworkSvg = {
|
|
1940
|
+
name: "set_animation_artwork_svg",
|
|
1941
|
+
title: "Replace the artwork with SVG",
|
|
1942
|
+
description: "Replaces the artwork with an SVG (paths, shapes, groups, gradients; one undo step). Give every element you will animate an id: commands target ids. Shots that name ids the new artwork lacks are refused. Returns the elements.",
|
|
1943
|
+
inputSchema: object({ svg: { type: "string", minLength: 1, description: "A standalone <svg> with a viewBox." } }, ["svg"]),
|
|
1944
|
+
annotations: { title: "Replace the artwork with SVG", ...DESTRUCTIVE },
|
|
1945
|
+
handler: ({ svg }) => {
|
|
1946
|
+
let artwork;
|
|
1947
|
+
try {
|
|
1948
|
+
artwork = _chunkXCXQDPIDcjs.parseVectorSvg.call(void 0, svg);
|
|
1949
|
+
} catch (error) {
|
|
1950
|
+
throw new Error(`The SVG could not be read: ${error.message}. Nothing was changed.`);
|
|
1951
|
+
}
|
|
1952
|
+
return apply({ op: "set_artwork", artwork }, "Replace artwork", (s) => ({ elements: describeMotionElements(s) }));
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
const insertShots = {
|
|
1956
|
+
name: "insert_shots",
|
|
1957
|
+
title: "Add shots",
|
|
1958
|
+
description: "Adds shots (one undo step) at a position (default: the end). Returns every shot's new timing and any issues.",
|
|
1959
|
+
inputSchema: object({ shots: { type: "array", items: SHOT, minItems: 1, description: "Shots to add." }, at: { type: "integer", minimum: 0, description: "Position, 0 = first (default: the end)." } }, ["shots"]),
|
|
1960
|
+
annotations: { title: "Add shots", ...WRITE },
|
|
1961
|
+
handler: ({ shots, at }) => apply({ op: "insert_shots", shots, at }, "Add shots", () => ({ added: shots.map((s) => s.id) }))
|
|
1962
|
+
};
|
|
1963
|
+
const updateShot = {
|
|
1964
|
+
name: "update_shot",
|
|
1965
|
+
title: "Change a shot",
|
|
1966
|
+
description: 'Replaces the fields of one shot given in "set" (label, camera, run, hold, id) and clears those in "remove". Returns the new timing and issues.',
|
|
1967
|
+
inputSchema: object(
|
|
1968
|
+
{
|
|
1969
|
+
shot: SHOT_ID,
|
|
1970
|
+
set: object({ id: SHOT.properties.id, ...SHOT_FIELDS }, [], "Fields to replace."),
|
|
1971
|
+
remove: { type: "array", items: { type: "string", enum: ["label", "camera", "hold"] }, description: "Fields to clear." }
|
|
1972
|
+
},
|
|
1973
|
+
["shot"]
|
|
1974
|
+
),
|
|
1975
|
+
annotations: { title: "Change a shot", ...WRITE_IDEMPOTENT },
|
|
1976
|
+
handler: ({ shot, set, remove }) => apply({ op: "update_shot", shot, set, remove }, "Change shot")
|
|
1977
|
+
};
|
|
1978
|
+
const deleteShots = {
|
|
1979
|
+
name: "delete_shots",
|
|
1980
|
+
title: "Delete shots",
|
|
1981
|
+
description: "Removes shots by id (one undo step). Returns the remaining timing.",
|
|
1982
|
+
inputSchema: object({ shots: { type: "array", items: SHOT_ID, minItems: 1, description: "Shot ids." } }, ["shots"]),
|
|
1983
|
+
annotations: { title: "Delete shots", ...DESTRUCTIVE },
|
|
1984
|
+
handler: ({ shots }) => apply({ op: "delete_shots", shots }, "Delete shots")
|
|
1985
|
+
};
|
|
1986
|
+
const moveShot = {
|
|
1987
|
+
name: "move_shot",
|
|
1988
|
+
title: "Reorder a shot",
|
|
1989
|
+
description: "Moves a shot to a new position (0 = first). Returns the new timing.",
|
|
1990
|
+
inputSchema: object({ shot: SHOT_ID, to: { type: "integer", minimum: 0, description: "New position, 0 = first." } }, ["shot", "to"]),
|
|
1991
|
+
annotations: { title: "Reorder a shot", ...WRITE_IDEMPOTENT },
|
|
1992
|
+
handler: ({ shot, to }) => apply({ op: "move_shot", shot, to }, "Move shot")
|
|
1993
|
+
};
|
|
1994
|
+
const addCommands = {
|
|
1995
|
+
name: "add_commands",
|
|
1996
|
+
title: "Add commands to a shot",
|
|
1997
|
+
description: "Adds commands to a shot's run (one undo step), at an index (default: the end). Returns the indexes they got, their timing and any issues.",
|
|
1998
|
+
inputSchema: object({ shot: SHOT_ID, commands: { type: "array", items: MOTION_COMMAND_SCHEMA, minItems: 1, description: "Commands to add." }, at: { type: "integer", minimum: 0, description: "Index in the run (default: the end)." } }, ["shot", "commands"]),
|
|
1999
|
+
annotations: { title: "Add commands to a shot", ...WRITE },
|
|
2000
|
+
handler: ({ shot, commands, at }) => {
|
|
2001
|
+
const before = _nullishCoalesce(_optionalChain([scene, 'call', _46 => _46(), 'access', _47 => _47.shots, 'access', _48 => _48.find, 'call', _49 => _49((s) => s.id === shot), 'optionalAccess', _50 => _50.run, 'access', _51 => _51.length]), () => ( 0));
|
|
2002
|
+
const first = _nullishCoalesce(at, () => ( before));
|
|
2003
|
+
return apply({ op: "add_commands", shot, commands, at }, "Add commands", (s) => {
|
|
2004
|
+
const shots = describeMotionShots(s);
|
|
2005
|
+
const added = shots.find((x) => x.id === shot).commands.slice(first, first + commands.length);
|
|
2006
|
+
return { added: added.map((c) => ({ index: c.index, type: c.type, start: c.start, end: c.end })) };
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
const updateCommand = {
|
|
2011
|
+
name: "update_command",
|
|
2012
|
+
title: "Change a command",
|
|
2013
|
+
description: 'Replaces fields of one command (by shot and index) given in "set" and clears those in "remove". Returns the command as it now stands with its timing.',
|
|
2014
|
+
inputSchema: object(
|
|
2015
|
+
{
|
|
2016
|
+
shot: SHOT_ID,
|
|
2017
|
+
command: { type: "integer", minimum: 0, description: "Index in the shot's run (from get_animation)." },
|
|
2018
|
+
set: { type: "object", description: "Command fields to replace (see add_commands for each type's fields)." },
|
|
2019
|
+
remove: { type: "array", items: { type: "string" }, description: 'Optional fields to clear (e.g. "delay").' }
|
|
2020
|
+
},
|
|
2021
|
+
["shot", "command"]
|
|
2022
|
+
),
|
|
2023
|
+
annotations: { title: "Change a command", ...WRITE_IDEMPOTENT },
|
|
2024
|
+
handler: ({ shot, command: command2, set, remove }) => apply({ op: "update_command", shot, command: command2, set, remove }, "Change command", (s) => ({ command: describeMotionShots(s).find((x) => x.id === shot).commands[command2] }))
|
|
2025
|
+
};
|
|
2026
|
+
const deleteCommands = {
|
|
2027
|
+
name: "delete_commands",
|
|
2028
|
+
title: "Delete commands",
|
|
2029
|
+
description: "Removes commands from a shot by index (one undo step). Later commands move up. Returns the new timing.",
|
|
2030
|
+
inputSchema: object({ shot: SHOT_ID, commands: { type: "array", items: { type: "integer", minimum: 0 }, minItems: 1, description: "Indexes in the shot's run." } }, ["shot", "commands"]),
|
|
2031
|
+
annotations: { title: "Delete commands", ...DESTRUCTIVE },
|
|
2032
|
+
handler: ({ shot, commands }) => apply({ op: "delete_commands", shot, commands }, "Delete commands")
|
|
2033
|
+
};
|
|
2034
|
+
const setSettings = {
|
|
2035
|
+
name: "set_animation_settings",
|
|
2036
|
+
title: "Change animation settings",
|
|
2037
|
+
description: "Sets the name, description, loop mode, export frame rate or target length. Returns the timing.",
|
|
2038
|
+
inputSchema: object(SETTINGS),
|
|
2039
|
+
annotations: { title: "Change animation settings", ...WRITE_IDEMPOTENT },
|
|
2040
|
+
handler: (args) => apply({ op: "set_settings", ...args }, "Change settings")
|
|
2041
|
+
};
|
|
2042
|
+
const setControls = {
|
|
2043
|
+
name: "set_animation_controls",
|
|
2044
|
+
title: "Set Flair controls",
|
|
2045
|
+
description: "Sets the colour controls (and presets) a Flair effect made from this animation exposes: each control stands for every use of its default colour. Replaces the existing controls; an empty list clears them. Returns the issues.",
|
|
2046
|
+
inputSchema: object({ controls: { type: "array", items: CONTROL, description: "Controls (empty clears)." }, presets: { type: "array", items: PRESET, description: "Presets (empty clears; omit to keep)." } }, ["controls"]),
|
|
2047
|
+
annotations: { title: "Set Flair controls", ...WRITE_IDEMPOTENT },
|
|
2048
|
+
handler: ({ controls, presets }) => apply({ op: "set_controls", controls, presets }, "Set controls")
|
|
2049
|
+
};
|
|
2050
|
+
const editAnimation = {
|
|
2051
|
+
name: "edit_animation",
|
|
2052
|
+
title: "Make several edits at once",
|
|
2053
|
+
description: 'Applies several edits in order as one undo step, all or nothing: if one fails, nothing changes and the error names the op ("op 2 (add_commands): \u2026"). With dry_run it only reports the timing and issues the edits would give.',
|
|
2054
|
+
inputSchema: object({ ops: { type: "array", items: EDIT_OP, minItems: 1, description: "Edits, applied in order." }, dry_run: { type: "boolean", description: "Check without applying (default false)." } }, ["ops"]),
|
|
2055
|
+
annotations: { title: "Make several edits at once", ...WRITE },
|
|
2056
|
+
handler: ({ ops, dry_run = false }) => {
|
|
2057
|
+
const result = controller.apply(ops, "Edit animation", { dryRun: dry_run });
|
|
2058
|
+
return outcome(result, dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {});
|
|
2059
|
+
}
|
|
2060
|
+
};
|
|
2061
|
+
const writeAnimation = {
|
|
2062
|
+
name: "write_animation",
|
|
2063
|
+
title: "Replace the animation's shots",
|
|
2064
|
+
description: "Replaces every shot (and optionally the settings and controls), keeping the artwork, as one undo step. For small changes prefer the per-operation tools. Returns the timing and issues.",
|
|
2065
|
+
inputSchema: object({ shots: { type: "array", items: SHOT, description: "All shots, in order." }, ...SETTINGS, controls: { type: "array", items: CONTROL, description: "Colour controls." }, presets: { type: "array", items: PRESET, description: "Presets." } }, ["shots"]),
|
|
2066
|
+
annotations: { title: "Replace the animation's shots", ...DESTRUCTIVE },
|
|
2067
|
+
handler: ({ shots, controls, presets, ...settings }) => {
|
|
2068
|
+
const current = scene();
|
|
2069
|
+
const next = { ...current, shots };
|
|
2070
|
+
const edits = [{ op: "replace_scene", scene: next }];
|
|
2071
|
+
if (Object.keys(settings).length) edits.push({ op: "set_settings", ...settings });
|
|
2072
|
+
if (controls) edits.push({ op: "set_controls", controls, presets });
|
|
2073
|
+
return apply(edits, "Replace animation");
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
const seekAnimation = {
|
|
2077
|
+
name: "seek_animation",
|
|
2078
|
+
title: "Show a moment",
|
|
2079
|
+
description: "Moves the playhead the person sees to a time or the start of a shot, and plays or pauses. Does not change the animation. Returns the playhead time and shot.",
|
|
2080
|
+
inputSchema: object({ time: MS("Time from the start"), shot: { ...SHOT_ID, description: 'Go to the start of this shot instead of "time".' }, play: { type: "boolean", description: "true plays from there, false pauses (default: leave as is)." } }),
|
|
2081
|
+
annotations: { title: "Show a moment", ...WRITE_IDEMPOTENT },
|
|
2082
|
+
handler: ({ time, shot, play }) => {
|
|
2083
|
+
if (shot !== void 0 && !controller.seekShot(shot)) throw new Error(`There is no shot "${shot}"; the shots are ${scene().shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`);
|
|
2084
|
+
if (shot === void 0 && time !== void 0) controller.seek(time);
|
|
2085
|
+
if (play === true) controller.play();
|
|
2086
|
+
if (play === false) controller.pause();
|
|
2087
|
+
const state = controller.getState();
|
|
2088
|
+
return { time: state.time, shot: state.currentShot, playing: state.playing };
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
const undo = {
|
|
2092
|
+
name: "undo_animation",
|
|
2093
|
+
title: "Undo an animation edit",
|
|
2094
|
+
description: "Undoes the last edit. Returns whether something was undone and its label.",
|
|
2095
|
+
inputSchema: object({}),
|
|
2096
|
+
annotations: { title: "Undo an animation edit", ...WRITE },
|
|
2097
|
+
handler: () => {
|
|
2098
|
+
const label = controller.getState().undoLabel;
|
|
2099
|
+
const undone = controller.undo();
|
|
2100
|
+
return { undone, ...undone ? { label } : {} };
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
const redo = {
|
|
2104
|
+
name: "redo_animation",
|
|
2105
|
+
title: "Redo an animation edit",
|
|
2106
|
+
description: "Redoes the last undone edit. Returns whether something was redone and its label.",
|
|
2107
|
+
inputSchema: object({}),
|
|
2108
|
+
annotations: { title: "Redo an animation edit", ...WRITE },
|
|
2109
|
+
handler: () => {
|
|
2110
|
+
const label = controller.getState().redoLabel;
|
|
2111
|
+
const redone = controller.redo();
|
|
2112
|
+
return { redone, ...redone ? { label } : {} };
|
|
2113
|
+
}
|
|
2114
|
+
};
|
|
2115
|
+
const reads = [getAnimation, getShot, findElements, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
|
|
2116
|
+
const writes = [setArtworkSvg, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
2117
|
+
return {
|
|
2118
|
+
id: _nullishCoalesce(options.id, () => ( "animation")),
|
|
2119
|
+
namespace: options.namespace,
|
|
2120
|
+
tools: () => readOnlyReason() ? reads : [...reads, ...writes],
|
|
2121
|
+
subscribe: (notify) => {
|
|
2122
|
+
let last = readOnlyReason();
|
|
2123
|
+
return controller.subscribe(() => {
|
|
2124
|
+
const now = readOnlyReason();
|
|
2125
|
+
if (now !== last) {
|
|
2126
|
+
last = now;
|
|
2127
|
+
notify();
|
|
2128
|
+
}
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/animation-editor/instructions.ts
|
|
2135
|
+
var ANIMATION_EDITOR_AGENT_INSTRUCTIONS = `You animate vector artwork. A scene is artwork (vector paths and groups, each with an id) plus shots.
|
|
2136
|
+
|
|
2137
|
+
How it plays
|
|
2138
|
+
- Shots play in order. A shot optionally moves the camera, plays its commands, then holds (default 600 ms).
|
|
2139
|
+
- Commands in a shot play one after another; give "with": "previous" to start one together with the command before, "delay" to wait, "stagger" to offset several targets.
|
|
2140
|
+
- Timing comes from the content: each command has a default length (enter 600 ms, move 700 ms, draw follows the path length). Only set "duration" to change the pace.
|
|
2141
|
+
- Anything that enters later is hidden until it enters. Things that never enter are visible from the start.
|
|
2142
|
+
- Every frame is a pure function of time: the same scene always plays the same frames.
|
|
2143
|
+
|
|
2144
|
+
Commands
|
|
2145
|
+
- enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
|
|
2146
|
+
- draw draws strokes on to a fraction; move, rotate, scale and fade change the pose (about the centre); style blends solid colours and stroke width.
|
|
2147
|
+
- morph turns a shape into another node's shape (keep the target hidden); follow moves along a path node; pulse and shake draw the eye.
|
|
2148
|
+
- effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
|
|
2149
|
+
|
|
2150
|
+
Working method
|
|
2151
|
+
1. get_animation to learn the element ids (set_animation_artwork_svg first if there is no artwork: give every element you will animate an id).
|
|
2152
|
+
2. Plan a few shots, each one idea. Add them with insert_shots, or edit_animation to make several edits as one step (dry_run first when unsure).
|
|
2153
|
+
3. check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result.
|
|
2154
|
+
4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
|
|
2155
|
+
|
|
2156
|
+
Keep motion purposeful: one thing moving at a time reads best, ease-out for arrivals, ease-in for exits, and hold long enough to see each result.`;
|
|
2157
|
+
|
|
2158
|
+
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
|
|
2164
|
+
|
|
2165
|
+
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
|
|
2172
|
+
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
|
|
2182
|
+
|
|
2183
|
+
|
|
2184
|
+
|
|
2185
|
+
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
|
|
2197
|
+
|
|
2198
|
+
|
|
2199
|
+
|
|
2200
|
+
|
|
2201
|
+
|
|
2202
|
+
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
|
|
2207
|
+
|
|
2208
|
+
|
|
2209
|
+
|
|
2210
|
+
|
|
2211
|
+
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
exports.ANIMATION_EDITOR_AGENT_INSTRUCTIONS = ANIMATION_EDITOR_AGENT_INSTRUCTIONS; exports.AnimationEditorController = AnimationEditorController; exports.FLAIR_EASES = FLAIR_EASES; exports.MOTION_CAMERA_MS = MOTION_CAMERA_MS; exports.MOTION_COMMAND_SCHEMA = MOTION_COMMAND_SCHEMA; exports.MOTION_COMMAND_TYPES = MOTION_COMMAND_TYPES; exports.MOTION_DEFAULT_DURATIONS = MOTION_DEFAULT_DURATIONS; exports.MOTION_DEFAULT_HOLD = MOTION_DEFAULT_HOLD; exports.MOTION_DIRECTIONS = MOTION_DIRECTIONS; exports.MOTION_DRAW_EFFECT_ID = MOTION_DRAW_EFFECT_ID; exports.MOTION_DRAW_MS_PER_UNIT = MOTION_DRAW_MS_PER_UNIT; exports.MOTION_DRAW_RANGE = MOTION_DRAW_RANGE; exports.MOTION_EASE_IN_OUT = MOTION_EASE_IN_OUT; exports.MOTION_EASE_OUT = MOTION_EASE_OUT; exports.MOTION_EDIT_OPS = MOTION_EDIT_OPS; exports.MOTION_ENTER_STYLES = MOTION_ENTER_STYLES; exports.MOTION_EXIT_STYLES = MOTION_EXIT_STYLES; exports.MOTION_ISSUE_CODES = MOTION_ISSUE_CODES; exports.MOTION_MIN_CHANGE_MS = MOTION_MIN_CHANGE_MS; exports.MotionSceneFormatError = MotionSceneFormatError; exports.activeSegment = activeSegment; exports.applyMotionControlValues = applyMotionControlValues; exports.applyMotionEdits = applyMotionEdits; exports.checkMotionScene = checkMotionScene; exports.commandShapeProblems = commandShapeProblems; exports.compileMotionScene = compileMotionScene; exports.createAnimationEditorToolProvider = createAnimationEditorToolProvider; exports.createMotionScene = createMotionScene; exports.describeMotionElements = describeMotionElements; exports.describeMotionShots = describeMotionShots; exports.fitMotionRect = fitMotionRect; exports.flairEaseFor = flairEaseFor; exports.freshShotId = freshShotId; exports.indexMotionNodes = indexMotionNodes; exports.interpolateMotionValue = interpolateMotionValue; exports.isMotionCommand = isMotionCommand; exports.isMotionShot = isMotionShot; exports.motionCommandDuration = motionCommandDuration; exports.motionFrameAt = motionFrameAt; exports.motionFrameToSvg = motionFrameToSvg; exports.motionIssue = motionIssue; exports.motionIssueKey = motionIssueKey; exports.motionIssueLine = motionIssueLine; exports.motionNodeOffset = motionNodeOffset; exports.motionPathLength = motionPathLength; exports.motionRestValue = motionRestValue; exports.motionSceneShapeProblems = motionSceneShapeProblems; exports.motionSceneToJson = motionSceneToJson; exports.motionShotAt = motionShotAt; exports.motionTrackKey = motionTrackKey; exports.namedWithSuggestion = namedWithSuggestion; exports.parseMotionScene = parseMotionScene; exports.resolveMotionCamera = resolveMotionCamera; exports.segmentProgress = segmentProgress; exports.segmentValue = segmentValue; exports.shotShapeProblems = shotShapeProblems; exports.suggestName = suggestName; exports.toFlairEffectDocument = toFlairEffectDocument; exports.toFlairSvgMotion = toFlairSvgMotion; exports.toFlairVectorEffect = toFlairVectorEffect; exports.trackValue = trackValue; exports.withViewBox = withViewBox;
|
|
2221
|
+
//# sourceMappingURL=index.cjs.map
|