@pixerate/editor 0.73.0 → 0.75.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/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/animation-editor/index.cjs +928 -399
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +130 -3
- package/dist/animation-editor/index.d.ts +130 -3
- package/dist/animation-editor/index.js +819 -290
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/canvas/index.cjs +26 -26
- package/dist/canvas/index.js +2 -2
- package/dist/chart/index.cjs +11 -9
- package/dist/chart/index.cjs.map +1 -1
- package/dist/chart/index.js +5 -3
- package/dist/chart/index.js.map +1 -1
- package/dist/{chunk-3S72F46R.js → chunk-2GFUA4X5.js} +2 -322
- package/dist/chunk-2GFUA4X5.js.map +1 -0
- package/dist/chunk-33AHNSEB.js +323 -0
- package/dist/chunk-33AHNSEB.js.map +1 -0
- package/dist/{chunk-3TLL5OID.js → chunk-4JLB2USO.js} +2 -2
- package/dist/{chunk-NN3JGUSX.cjs → chunk-5PTR7HTL.cjs} +3 -323
- package/dist/chunk-5PTR7HTL.cjs.map +1 -0
- package/dist/{chunk-YJLCBA7V.cjs → chunk-7MD7C6XQ.cjs} +87 -87
- package/dist/{chunk-YJLCBA7V.cjs.map → chunk-7MD7C6XQ.cjs.map} +1 -1
- package/dist/{chunk-4YFMSPBU.js → chunk-EIXXBKUB.js} +6 -4
- package/dist/{chunk-4YFMSPBU.js.map → chunk-EIXXBKUB.js.map} +1 -1
- package/dist/chunk-EMQWJA3G.cjs +323 -0
- package/dist/chunk-EMQWJA3G.cjs.map +1 -0
- package/dist/{chunk-BQQZ6MOI.js → chunk-FGW5OWJ4.js} +6 -4
- package/dist/{chunk-BQQZ6MOI.js.map → chunk-FGW5OWJ4.js.map} +1 -1
- package/dist/{chunk-YZZMX6V7.cjs → chunk-FV5AXTSF.cjs} +11 -9
- package/dist/chunk-FV5AXTSF.cjs.map +1 -0
- package/dist/{chunk-MLREO6J3.cjs → chunk-JEYDXRWQ.cjs} +46 -46
- package/dist/{chunk-MLREO6J3.cjs.map → chunk-JEYDXRWQ.cjs.map} +1 -1
- package/dist/{chunk-7ZSU4MQZ.cjs → chunk-PL3MPFPA.cjs} +10 -8
- package/dist/chunk-PL3MPFPA.cjs.map +1 -0
- package/dist/{chunk-O2UH7LZE.cjs → chunk-XGD3FSNC.cjs} +239 -239
- package/dist/chunk-XGD3FSNC.cjs.map +1 -0
- package/dist/{chunk-VWZB6BCY.js → chunk-Y3NW4VJE.js} +2 -2
- package/dist/{chunk-GTMJ4Y3S.js → chunk-YCSFS3GK.js} +244 -244
- package/dist/{chunk-GTMJ4Y3S.js.map → chunk-YCSFS3GK.js.map} +1 -1
- package/dist/export/index.cjs +4 -3
- package/dist/export/index.cjs.map +1 -1
- package/dist/export/index.js +3 -2
- package/dist/extensions/index.cjs +4 -3
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.js +3 -2
- package/dist/index.cjs +19 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +12 -11
- package/dist/index.js.map +1 -1
- package/dist/vector/index.cjs +532 -532
- package/dist/vector/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-3S72F46R.js.map +0 -1
- package/dist/chunk-7ZSU4MQZ.cjs.map +0 -1
- package/dist/chunk-NN3JGUSX.cjs.map +0 -1
- package/dist/chunk-O2UH7LZE.cjs.map +0 -1
- package/dist/chunk-YZZMX6V7.cjs.map +0 -1
- /package/dist/{chunk-3TLL5OID.js.map → chunk-4JLB2USO.js.map} +0 -0
- /package/dist/{chunk-VWZB6BCY.js.map → chunk-Y3NW4VJE.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
collectPathNodes,
|
|
5
5
|
createVectorEffect,
|
|
6
6
|
createVectorMorph,
|
|
7
|
+
ellipseSubpath,
|
|
7
8
|
expandVectorAppearance,
|
|
8
9
|
expandVectorAppearances,
|
|
9
10
|
invertMatrix,
|
|
@@ -12,20 +13,24 @@ import {
|
|
|
12
13
|
nodeMatrix,
|
|
13
14
|
parseSvgTransform,
|
|
14
15
|
parseVectorSvg,
|
|
16
|
+
rectSubpath,
|
|
15
17
|
rotateMatrix,
|
|
16
18
|
scaleMatrix,
|
|
17
19
|
translateMatrix,
|
|
18
20
|
vectorDocumentToSvg,
|
|
19
21
|
vectorNodeBounds,
|
|
20
22
|
vectorPathWorldBezier
|
|
21
|
-
} from "../chunk-
|
|
23
|
+
} from "../chunk-4JLB2USO.js";
|
|
22
24
|
import {
|
|
23
25
|
bezierPathLength,
|
|
24
26
|
bezierPathPointAtLength,
|
|
25
27
|
bezierPathToSvg,
|
|
26
28
|
toBezierPath,
|
|
27
29
|
transformBezierPath
|
|
28
|
-
} from "../chunk-
|
|
30
|
+
} from "../chunk-YCSFS3GK.js";
|
|
31
|
+
import {
|
|
32
|
+
readZip
|
|
33
|
+
} from "../chunk-33AHNSEB.js";
|
|
29
34
|
import {
|
|
30
35
|
HistoryManager
|
|
31
36
|
} from "../chunk-QVZZGT4X.js";
|
|
@@ -56,6 +61,8 @@ import {
|
|
|
56
61
|
|
|
57
62
|
// src/animation-editor/types.ts
|
|
58
63
|
var MOTION_TRIGGERS = ["click", "hover", "press", "scroll"];
|
|
64
|
+
var MOTION_TEXT_UNITS = ["letter", "word", "line"];
|
|
65
|
+
var MOTION_CASCADES = ["forward", "backward", "center", "edges", "random"];
|
|
59
66
|
var MOTION_DRIVEN_PROPERTIES = ["x", "y", "rotation", "scaleX", "scaleY", "opacity"];
|
|
60
67
|
var MOTION_CLIP_TARGET = "$";
|
|
61
68
|
var MOTION_COMMAND_TYPES = [
|
|
@@ -112,21 +119,21 @@ var MOTION_DEFAULT_HOLD = 600;
|
|
|
112
119
|
var MOTION_CAMERA_MS = 1e3;
|
|
113
120
|
var MOTION_DRAW_MS_PER_UNIT = 1.5;
|
|
114
121
|
var MOTION_DRAW_RANGE = [500, 2500];
|
|
115
|
-
function motionFlairPrimitive(
|
|
116
|
-
switch (
|
|
122
|
+
function motionFlairPrimitive(command3) {
|
|
123
|
+
switch (command3.type) {
|
|
117
124
|
case "enter":
|
|
118
|
-
return `entrance.${
|
|
125
|
+
return `entrance.${command3.style ?? "fade"}`;
|
|
119
126
|
case "exit":
|
|
120
|
-
return `exit.${
|
|
127
|
+
return `exit.${command3.style ?? "fade"}`;
|
|
121
128
|
case "pulse":
|
|
122
129
|
case "shake":
|
|
123
130
|
case "teeter":
|
|
124
131
|
case "wave":
|
|
125
132
|
case "wiggle":
|
|
126
133
|
case "color-pulse":
|
|
127
|
-
return `emphasis.${
|
|
134
|
+
return `emphasis.${command3.type}`;
|
|
128
135
|
case "rotate":
|
|
129
|
-
return
|
|
136
|
+
return command3.by !== void 0 ? "emphasis.spin" : void 0;
|
|
130
137
|
case "follow":
|
|
131
138
|
return "path.follow";
|
|
132
139
|
default:
|
|
@@ -143,6 +150,7 @@ var MOTION_ISSUE_CODES = {
|
|
|
143
150
|
"not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
|
|
144
151
|
"reveal-no-box": { severity: "warning", wording: "A wipe, iris or split cannot clip a node with no width or height (a lone straight line), so it fades instead." },
|
|
145
152
|
"driver-cycle": { severity: "error", wording: "Drivers follow each other in a loop, so none of them has a value." },
|
|
153
|
+
"split-not-text": { severity: "warning", wording: "A command splits a target into letters, but it is not a group of letter paths, so it animates as a whole." },
|
|
146
154
|
"unknown-clip": { severity: "error", wording: "A play command names a clip the scene does not have." },
|
|
147
155
|
"unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
|
|
148
156
|
"match-self": { severity: "error", wording: "A match turns a group into itself or into a group it holds (or that holds it)." },
|
|
@@ -224,7 +232,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
224
232
|
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
225
233
|
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
226
234
|
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
227
|
-
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
|
|
235
|
+
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade"];
|
|
228
236
|
var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
|
|
229
237
|
var COMMAND_KEYS = {
|
|
230
238
|
enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
|
|
@@ -254,108 +262,111 @@ var isPoint = (v) => isObject(v) && isNum(v.x) && isNum(v.y);
|
|
|
254
262
|
function unknownKeys(value, known, where, problems) {
|
|
255
263
|
for (const key of Object.keys(value)) if (!known.includes(key)) problems.push(`${where}: unknown field ${namedWithSuggestion(key, known)}.`);
|
|
256
264
|
}
|
|
257
|
-
function commandShapeProblems(
|
|
265
|
+
function commandShapeProblems(command3, where) {
|
|
258
266
|
const problems = [];
|
|
259
|
-
if (!isObject(
|
|
260
|
-
const type =
|
|
267
|
+
if (!isObject(command3)) return [`${where} must be an object.`];
|
|
268
|
+
const type = command3.type;
|
|
261
269
|
if (typeof type !== "string" || !MOTION_COMMAND_TYPES.includes(type)) {
|
|
262
270
|
return [`${where}: unknown type ${typeof type === "string" ? namedWithSuggestion(type, MOTION_COMMAND_TYPES) : String(type)}; use one of ${MOTION_COMMAND_TYPES.join(", ")}.`];
|
|
263
271
|
}
|
|
264
|
-
unknownKeys(
|
|
272
|
+
unknownKeys(command3, COMMAND_KEYS[type], `${where} (${type})`, problems);
|
|
265
273
|
const p = (msg) => problems.push(`${where} (${type}): ${msg}`);
|
|
266
274
|
const nonNegative = (key) => {
|
|
267
|
-
if (
|
|
275
|
+
if (command3[key] !== void 0 && !(isNum(command3[key]) && command3[key] >= 0)) p(`"${key}" must be a number of milliseconds \u2265 0.`);
|
|
268
276
|
};
|
|
269
277
|
const fraction = (key) => {
|
|
270
|
-
if (
|
|
278
|
+
if (command3[key] !== void 0 && !(isNum(command3[key]) && command3[key] >= 0 && command3[key] <= 1)) p(`"${key}" must be a fraction from 0 to 1.`);
|
|
271
279
|
};
|
|
272
280
|
const optionalNumber = (key) => {
|
|
273
|
-
if (
|
|
281
|
+
if (command3[key] !== void 0 && !isNum(command3[key])) p(`"${key}" must be a number.`);
|
|
274
282
|
};
|
|
275
|
-
if (
|
|
276
|
-
if (DRAW_ORDER_KEYS.some((k) =>
|
|
277
|
-
if (type === "enter" &&
|
|
278
|
-
if (
|
|
279
|
-
if (
|
|
280
|
-
if (
|
|
281
|
-
if (
|
|
282
|
-
if (
|
|
283
|
+
if (command3.with !== void 0 && command3.with !== "after" && command3.with !== "previous") p(`"with" must be "after" or "previous".`);
|
|
284
|
+
if (DRAW_ORDER_KEYS.some((k) => command3[k] !== void 0)) {
|
|
285
|
+
if (type === "enter" && command3.style !== "draw" || type === "exit" && command3.style !== "undraw") p(`"order", "pace", "align", "sortBy" and "origin" go with drawing: style "${type === "enter" ? "draw" : "undraw"}".`);
|
|
286
|
+
if (command3.order !== void 0 && command3.order !== "together" && command3.order !== "sequence") p(`"order" must be "together" or "sequence" (one stroke after another).`);
|
|
287
|
+
if (command3.pace !== void 0 && command3.pace !== "time" && command3.pace !== "speed") p(`"pace" must be "time" (each stroke takes the whole time) or "speed" (all at one speed).`);
|
|
288
|
+
if (command3.align !== void 0 && command3.align !== "start" && command3.align !== "end") p(`"align" must be "start" or "end".`);
|
|
289
|
+
if (command3.sortBy !== void 0 && command3.sortBy !== "document" && command3.sortBy !== "distance") p(`"sortBy" must be "document" or "distance".`);
|
|
290
|
+
if (command3.origin !== void 0 && !isPoint(command3.origin)) p(`"origin" must be { "x": number, "y": number } (document units).`);
|
|
283
291
|
}
|
|
284
|
-
if (
|
|
292
|
+
if (command3.note !== void 0 && typeof command3.note !== "string") p(`"note" must be a string.`);
|
|
285
293
|
if (type === "wait") {
|
|
286
|
-
if (!isNum(
|
|
294
|
+
if (!isNum(command3.duration) || command3.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
|
|
287
295
|
return problems;
|
|
288
296
|
}
|
|
289
|
-
if (!Array.isArray(
|
|
297
|
+
if (!Array.isArray(command3.targets) || command3.targets.length === 0 || !command3.targets.every((t) => typeof t === "string" && t.length > 0)) p(`"targets" must be a non-empty list of node ids.`);
|
|
290
298
|
nonNegative("duration");
|
|
291
299
|
nonNegative("delay");
|
|
292
300
|
nonNegative("stagger");
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
301
|
+
if (command3.ease !== void 0 && !isAnimationEasing(command3.ease)) p(`"ease" is not an easing; use a preset such as "ease-in-out" or "spring-bouncy", or { "type": "cubic-bezier", \u2026 } or { "type": "spring", \u2026 }.`);
|
|
302
|
+
if (command3.repeat !== void 0) {
|
|
295
303
|
if (type === "enter" || type === "exit") p(`"repeat" is not for ${type} (it brings things in or out once); repeat an emphasis or a move instead.`);
|
|
296
|
-
else if (!(Number.isInteger(
|
|
304
|
+
else if (!(Number.isInteger(command3.repeat) && command3.repeat >= 1 && command3.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
|
|
297
305
|
}
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
300
|
-
if (
|
|
306
|
+
if (command3.blend !== void 0 && !MOTION_BLENDS.includes(command3.blend)) p(`"blend" must be "replace" (the change takes over) or "add" (a layer on top).`);
|
|
307
|
+
if (command3.split !== void 0 && !MOTION_TEXT_UNITS.includes(command3.split)) p(`"split" must be letter, word or line (for outlined text: a group of letter paths).`);
|
|
308
|
+
if (command3.cascade !== void 0 && !MOTION_CASCADES.includes(command3.cascade)) p(`"cascade" must be one of ${MOTION_CASCADES.join(", ")}.`);
|
|
309
|
+
if (command3.cascade !== void 0 && command3.split === void 0) p(`"cascade" goes with "split".`);
|
|
310
|
+
if (command3.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command3.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
|
|
311
|
+
if (command3.steps !== void 0 && !(isNum(command3.steps) && command3.steps > 0 && command3.steps <= 120)) p(`"steps" must be frames per second from above 0 to 120 (12 holds each pose for two frames at 24 fps).`);
|
|
301
312
|
switch (type) {
|
|
302
313
|
case "enter":
|
|
303
314
|
case "exit": {
|
|
304
315
|
const styles = type === "enter" ? MOTION_ENTER_STYLES : MOTION_EXIT_STYLES;
|
|
305
|
-
if (
|
|
306
|
-
p(`"style" must be one of ${styles.join(", ")}${typeof
|
|
307
|
-
if (
|
|
316
|
+
if (command3.style !== void 0 && !(typeof command3.style === "string" && styles.includes(command3.style)))
|
|
317
|
+
p(`"style" must be one of ${styles.join(", ")}${typeof command3.style === "string" ? ` (got ${namedWithSuggestion(command3.style, styles)})` : ""}.`);
|
|
318
|
+
if (command3.direction !== void 0 && !MOTION_DIRECTIONS.includes(command3.direction)) p(`"direction" must be one of ${MOTION_DIRECTIONS.join(", ")}.`);
|
|
308
319
|
optionalNumber("distance");
|
|
309
|
-
if (
|
|
310
|
-
if (
|
|
311
|
-
if (
|
|
320
|
+
if (command3.scale !== void 0 && !(isNum(command3.scale) && command3.scale >= 0)) p(`"scale" must be a number \u2265 0 (the starting scale for pop and zoom).`);
|
|
321
|
+
if (command3.shape !== void 0 && !MOTION_IRIS_SHAPES.includes(command3.shape)) p(`"shape" must be one of ${MOTION_IRIS_SHAPES.join(", ")} (the opening of an iris).`);
|
|
322
|
+
if (command3.orientation !== void 0 && !MOTION_SPLIT_ORIENTATIONS.includes(command3.orientation)) p(`"orientation" must be one of ${MOTION_SPLIT_ORIENTATIONS.join(", ")} (the line a split opens from).`);
|
|
312
323
|
break;
|
|
313
324
|
}
|
|
314
325
|
case "draw":
|
|
315
326
|
fraction("from");
|
|
316
327
|
fraction("to");
|
|
317
328
|
fraction("comet");
|
|
318
|
-
if (
|
|
329
|
+
if (command3.comet !== void 0 && (command3.from !== void 0 || command3.to !== void 0)) p(`"comet" runs the whole path; leave out "from" and "to".`);
|
|
319
330
|
break;
|
|
320
331
|
case "move":
|
|
321
|
-
if (
|
|
322
|
-
if (
|
|
323
|
-
if (
|
|
324
|
-
if (
|
|
325
|
-
if (
|
|
326
|
-
if (
|
|
327
|
-
if (
|
|
332
|
+
if (command3.by === void 0 && command3.to === void 0) p(`give "by" (an offset) or "to" (a point).`);
|
|
333
|
+
if (command3.by !== void 0 && command3.to !== void 0) p(`give "by" or "to", not both.`);
|
|
334
|
+
if (command3.by !== void 0 && !isPoint(command3.by)) p(`"by" must be { "x": number, "y": number }.`);
|
|
335
|
+
if (command3.to !== void 0 && !isPoint(command3.to)) p(`"to" must be { "x": number, "y": number }.`);
|
|
336
|
+
if (command3.via !== void 0 && !(Array.isArray(command3.via) && command3.via.length > 0 && command3.via.length <= 64 && command3.via.every(isPoint))) p(`"via" must be a list of 1 to 64 points { "x": number, "y": number } (document units).`);
|
|
337
|
+
if (command3.pacing !== void 0 && command3.pacing !== "even" && command3.pacing !== "equal") p(`"pacing" must be "even" (steady speed) or "equal" (each leg the same time).`);
|
|
338
|
+
if (command3.pacing !== void 0 && command3.via === void 0) p(`"pacing" goes with "via".`);
|
|
328
339
|
break;
|
|
329
340
|
case "rotate":
|
|
330
|
-
if (
|
|
331
|
-
if (
|
|
341
|
+
if (command3.by === void 0 && command3.to === void 0) p(`give "by" or "to" (degrees).`);
|
|
342
|
+
if (command3.by !== void 0 && command3.to !== void 0) p(`give "by" or "to", not both.`);
|
|
332
343
|
optionalNumber("by");
|
|
333
344
|
optionalNumber("to");
|
|
334
345
|
break;
|
|
335
346
|
case "scale":
|
|
336
|
-
if (!(isNum(
|
|
337
|
-
if (
|
|
347
|
+
if (!(isNum(command3.to) || isPoint(command3.to))) p(`"to" must be a number or { "x": number, "y": number }.`);
|
|
348
|
+
if (command3.geometric !== void 0 && typeof command3.geometric !== "boolean") p(`"geometric" must be true or false.`);
|
|
338
349
|
break;
|
|
339
350
|
case "fade":
|
|
340
|
-
if (!(isNum(
|
|
351
|
+
if (!(isNum(command3.to) && command3.to >= 0 && command3.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
|
|
341
352
|
break;
|
|
342
353
|
case "style":
|
|
343
|
-
if (
|
|
344
|
-
if (
|
|
345
|
-
if (
|
|
346
|
-
if (
|
|
354
|
+
if (command3.fill === void 0 && command3.stroke === void 0 && command3.strokeWidth === void 0) p(`give at least one of "fill", "stroke" or "strokeWidth".`);
|
|
355
|
+
if (command3.fill !== void 0 && typeof command3.fill !== "string") p(`"fill" must be a CSS colour.`);
|
|
356
|
+
if (command3.stroke !== void 0 && typeof command3.stroke !== "string") p(`"stroke" must be a CSS colour.`);
|
|
357
|
+
if (command3.strokeWidth !== void 0 && !(isNum(command3.strokeWidth) && command3.strokeWidth >= 0)) p(`"strokeWidth" must be a number \u2265 0.`);
|
|
347
358
|
break;
|
|
348
359
|
case "play":
|
|
349
|
-
if (typeof
|
|
350
|
-
if (
|
|
360
|
+
if (typeof command3.clip !== "string" || !command3.clip) p(`"clip" must be the name of one of the scene's clips.`);
|
|
361
|
+
if (command3.speed !== void 0 && !(isNum(command3.speed) && command3.speed >= 0.05 && command3.speed <= 20)) p(`"speed" must be a number from 0.05 to 20 (2 plays the clip twice as fast).`);
|
|
351
362
|
break;
|
|
352
363
|
case "morph":
|
|
353
364
|
case "match":
|
|
354
|
-
if (typeof
|
|
365
|
+
if (typeof command3.into !== "string" || !command3.into) p(`"into" must be the id of a ${type === "match" ? "group to become" : "node whose shape to take"}.`);
|
|
355
366
|
break;
|
|
356
367
|
case "follow":
|
|
357
|
-
if (typeof
|
|
358
|
-
if (
|
|
368
|
+
if (typeof command3.path !== "string" || !command3.path) p(`"path" must be the id of a path node.`);
|
|
369
|
+
if (command3.rotate !== void 0 && typeof command3.rotate !== "boolean") p(`"rotate" must be true or false.`);
|
|
359
370
|
fraction("from");
|
|
360
371
|
fraction("to");
|
|
361
372
|
break;
|
|
@@ -366,22 +377,22 @@ function commandShapeProblems(command2, where) {
|
|
|
366
377
|
case "color-pulse":
|
|
367
378
|
optionalNumber("amount");
|
|
368
379
|
optionalNumber("angle");
|
|
369
|
-
if (
|
|
370
|
-
if (
|
|
371
|
-
if (
|
|
372
|
-
if (
|
|
380
|
+
if (command3.count !== void 0 && !(Number.isInteger(command3.count) && command3.count >= 1)) p(`"count" must be a whole number \u2265 1.`);
|
|
381
|
+
if (command3.axis !== void 0 && command3.axis !== "horizontal" && command3.axis !== "vertical") p(`"axis" must be "horizontal" or "vertical".`);
|
|
382
|
+
if (command3.color !== void 0 && typeof command3.color !== "string") p(`"color" must be a CSS colour.`);
|
|
383
|
+
if (command3.paint !== void 0 && command3.paint !== "fill" && command3.paint !== "stroke") p(`"paint" must be "fill" or "stroke".`);
|
|
373
384
|
break;
|
|
374
385
|
case "wiggle":
|
|
375
386
|
optionalNumber("amount");
|
|
376
387
|
optionalNumber("rotate");
|
|
377
|
-
if (
|
|
378
|
-
if (
|
|
379
|
-
if (
|
|
388
|
+
if (command3.frequency !== void 0 && !(isNum(command3.frequency) && command3.frequency > 0)) p(`"frequency" must be a number of Hz > 0.`);
|
|
389
|
+
if (command3.octaves !== void 0 && !(Number.isInteger(command3.octaves) && command3.octaves >= 1 && command3.octaves <= 8)) p(`"octaves" must be a whole number from 1 to 8.`);
|
|
390
|
+
if (command3.seed !== void 0 && !(Number.isInteger(command3.seed) && command3.seed >= 0)) p(`"seed" must be a whole number \u2265 0.`);
|
|
380
391
|
break;
|
|
381
392
|
case "effect":
|
|
382
|
-
if (typeof
|
|
383
|
-
if (typeof
|
|
384
|
-
if (!isNum(
|
|
393
|
+
if (typeof command3.effect !== "string" || !command3.effect) p(`"effect" must be the id of one of the node's effects.`);
|
|
394
|
+
if (typeof command3.param !== "string" || !command3.param) p(`"param" must name a numeric parameter of the effect.`);
|
|
395
|
+
if (!isNum(command3.to)) p(`"to" must be a number.`);
|
|
385
396
|
break;
|
|
386
397
|
}
|
|
387
398
|
return problems;
|
|
@@ -398,9 +409,9 @@ function clipsShapeProblems(clips) {
|
|
|
398
409
|
}
|
|
399
410
|
unknownKeys(clip, ["description", "run"], where, problems);
|
|
400
411
|
if (clip.description !== void 0 && typeof clip.description !== "string") problems.push(`${where}: "description" must be a string.`);
|
|
401
|
-
clip.run.forEach((
|
|
402
|
-
if (isObject(
|
|
403
|
-
else problems.push(...commandShapeProblems(
|
|
412
|
+
clip.run.forEach((command3, i) => {
|
|
413
|
+
if (isObject(command3) && command3.type === "play") problems.push(`${where}, command ${i + 1}: a clip cannot play another clip; copy its commands in instead.`);
|
|
414
|
+
else problems.push(...commandShapeProblems(command3, `${where}, command ${i + 1}`));
|
|
404
415
|
});
|
|
405
416
|
}
|
|
406
417
|
return problems;
|
|
@@ -456,7 +467,7 @@ function shotShapeProblems(shot, where) {
|
|
|
456
467
|
}
|
|
457
468
|
}
|
|
458
469
|
if (!Array.isArray(shot.run)) problems.push(`${name}: "run" must be a list of commands (it may be empty).`);
|
|
459
|
-
else shot.run.forEach((
|
|
470
|
+
else shot.run.forEach((command3, i) => problems.push(...commandShapeProblems(command3, `${name}, command ${i + 1}`)));
|
|
460
471
|
return problems;
|
|
461
472
|
}
|
|
462
473
|
function motionSceneShapeProblems(input) {
|
|
@@ -523,7 +534,7 @@ function motionSceneToJson(scene, space = 2) {
|
|
|
523
534
|
return JSON.stringify(scene, null, space);
|
|
524
535
|
}
|
|
525
536
|
var isMotionShot = (shot) => shotShapeProblems(shot, "Shot").length === 0;
|
|
526
|
-
var isMotionCommand = (
|
|
537
|
+
var isMotionCommand = (command3) => commandShapeProblems(command3, "Command").length === 0;
|
|
527
538
|
|
|
528
539
|
// src/animation-editor/match.ts
|
|
529
540
|
var SEPARATOR = /[-_/.:]/;
|
|
@@ -611,6 +622,87 @@ function smoothMotionTimes(points, start, duration, pacing = "even") {
|
|
|
611
622
|
return times;
|
|
612
623
|
}
|
|
613
624
|
|
|
625
|
+
// src/animation-editor/text.ts
|
|
626
|
+
function motionTextLayout(nodes, id) {
|
|
627
|
+
const group = nodes.get(id)?.node;
|
|
628
|
+
if (!group || group.type !== "group") return null;
|
|
629
|
+
const letters = collectPathNodes(group).map((p) => ({ id: p.id, b: nodes.get(p.id)?.bounds })).filter((l) => !!l.b && l.b.width + l.b.height > 0);
|
|
630
|
+
if (!letters.length) return null;
|
|
631
|
+
const lines = [];
|
|
632
|
+
for (const l of [...letters].sort((a, c) => a.b.y + a.b.height / 2 - (c.b.y + c.b.height / 2))) {
|
|
633
|
+
const cy = l.b.y + l.b.height / 2;
|
|
634
|
+
const line = lines.find((x) => cy >= x.top && cy <= x.bottom);
|
|
635
|
+
if (line) {
|
|
636
|
+
line.letters.push(l);
|
|
637
|
+
line.top = Math.min(line.top, l.b.y);
|
|
638
|
+
line.bottom = Math.max(line.bottom, l.b.y + l.b.height);
|
|
639
|
+
} else lines.push({ top: l.b.y, bottom: l.b.y + l.b.height, letters: [l] });
|
|
640
|
+
}
|
|
641
|
+
lines.sort((a, c) => a.top - c.top);
|
|
642
|
+
return {
|
|
643
|
+
lines: lines.map((line) => {
|
|
644
|
+
const row = [...line.letters].sort((a, c) => a.b.x - c.b.x);
|
|
645
|
+
const heights = row.map((l) => l.b.height).sort((a, c) => a - c);
|
|
646
|
+
const gap = heights[Math.floor(heights.length / 2)] * 0.35;
|
|
647
|
+
const words = [];
|
|
648
|
+
row.forEach((l, i) => {
|
|
649
|
+
const prev = row[i - 1];
|
|
650
|
+
if (!prev || l.b.x - (prev.b.x + prev.b.width) > gap) words.push({ letters: [] });
|
|
651
|
+
words[words.length - 1].letters.push(l.id);
|
|
652
|
+
});
|
|
653
|
+
return { words };
|
|
654
|
+
})
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function motionTextUnits(layout, by) {
|
|
658
|
+
if (by === "line") return layout.lines.map((l) => l.words.flatMap((w) => w.letters));
|
|
659
|
+
if (by === "word") return layout.lines.flatMap((l) => l.words.map((w) => w.letters));
|
|
660
|
+
return layout.lines.flatMap((l) => l.words.flatMap((w) => w.letters.map((id) => [id])));
|
|
661
|
+
}
|
|
662
|
+
function motionCascade(units, cascade = "forward", seed = 1) {
|
|
663
|
+
const n = units.length;
|
|
664
|
+
const mid = (n - 1) / 2;
|
|
665
|
+
const indexed = units.map((u, i) => ({ u, i }));
|
|
666
|
+
if (cascade === "backward") return [...units].reverse();
|
|
667
|
+
if (cascade === "center") return indexed.sort((a, b) => Math.abs(a.i - mid) - Math.abs(b.i - mid) || a.i - b.i).map((x) => x.u);
|
|
668
|
+
if (cascade === "edges") return indexed.sort((a, b) => Math.abs(b.i - mid) - Math.abs(a.i - mid) || a.i - b.i).map((x) => x.u);
|
|
669
|
+
if (cascade === "random") {
|
|
670
|
+
let s = seed >>> 0 || 1;
|
|
671
|
+
const rand = () => {
|
|
672
|
+
s = s + 1831565813 >>> 0;
|
|
673
|
+
let t = s;
|
|
674
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
675
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
676
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
677
|
+
};
|
|
678
|
+
const out = [...units];
|
|
679
|
+
for (let i = n - 1; i > 0; i--) {
|
|
680
|
+
const j = Math.floor(rand() * (i + 1));
|
|
681
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
return [...units];
|
|
686
|
+
}
|
|
687
|
+
var MOTION_TEXT_STAGGER = { letter: 40, word: 120, line: 300 };
|
|
688
|
+
function motionCommandSlots(nodes, command3) {
|
|
689
|
+
if (!command3.split) return { slots: command3.targets.map((id, k) => ({ id, slot: k })), stagger: command3.stagger ?? 0 };
|
|
690
|
+
const slots = [];
|
|
691
|
+
let slot = 0;
|
|
692
|
+
for (const target of command3.targets) {
|
|
693
|
+
const layout = motionTextLayout(nodes, target);
|
|
694
|
+
if (!layout) {
|
|
695
|
+
slots.push({ id: target, slot: slot++ });
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
for (const unit of motionCascade(motionTextUnits(layout, command3.split), command3.cascade, motionSeedFromId(target))) {
|
|
699
|
+
for (const id of unit) slots.push({ id, slot });
|
|
700
|
+
slot++;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return { slots, stagger: command3.stagger ?? MOTION_TEXT_STAGGER[command3.split] };
|
|
704
|
+
}
|
|
705
|
+
|
|
614
706
|
// src/animation-editor/compile.ts
|
|
615
707
|
var indexCache = /* @__PURE__ */ new WeakMap();
|
|
616
708
|
function indexMotionNodes(nodes) {
|
|
@@ -886,13 +978,13 @@ function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */
|
|
|
886
978
|
}
|
|
887
979
|
return out;
|
|
888
980
|
}
|
|
889
|
-
function motionCommandRepeat(
|
|
890
|
-
if (
|
|
891
|
-
const n =
|
|
981
|
+
function motionCommandRepeat(command3) {
|
|
982
|
+
if (command3.type === "wait" || command3.type === "enter" || command3.type === "exit") return 1;
|
|
983
|
+
const n = command3.repeat ?? 1;
|
|
892
984
|
return Number.isInteger(n) && n >= 1 ? n : 1;
|
|
893
985
|
}
|
|
894
|
-
function motionCommandDuration(
|
|
895
|
-
return motionCycleDuration(
|
|
986
|
+
function motionCommandDuration(command3, scene, target) {
|
|
987
|
+
return motionCycleDuration(command3, scene, target) * motionCommandRepeat(command3);
|
|
896
988
|
}
|
|
897
989
|
function motionClipLayout(scene, clip, target, speed = 1) {
|
|
898
990
|
const run = scene.clips?.[clip]?.run;
|
|
@@ -909,36 +1001,36 @@ function motionClipLayout(scene, clip, target, speed = 1) {
|
|
|
909
1001
|
return;
|
|
910
1002
|
}
|
|
911
1003
|
if (raw.type === "play") return;
|
|
912
|
-
const
|
|
913
|
-
const begin = base + (
|
|
1004
|
+
const command3 = { ...raw, targets: raw.targets.map((t) => t === MOTION_CLIP_TARGET ? target : t) };
|
|
1005
|
+
const begin = base + (command3.delay ?? 0) * f;
|
|
914
1006
|
let end = begin;
|
|
915
|
-
|
|
916
|
-
const cycle = motionCycleDuration(
|
|
917
|
-
const s = begin + k * (
|
|
918
|
-
items.push({ command:
|
|
919
|
-
end = Math.max(end, s + cycle * motionCommandRepeat(
|
|
1007
|
+
command3.targets.forEach((t, k) => {
|
|
1008
|
+
const cycle = motionCycleDuration(command3, scene, t) * f;
|
|
1009
|
+
const s = begin + k * (command3.stagger ?? 0) * f;
|
|
1010
|
+
items.push({ command: command3, target: t, start: s, cycle });
|
|
1011
|
+
end = Math.max(end, s + cycle * motionCommandRepeat(command3));
|
|
920
1012
|
});
|
|
921
1013
|
prevStart = begin;
|
|
922
1014
|
runEnd = Math.max(runEnd, end);
|
|
923
1015
|
});
|
|
924
1016
|
return { items, length: runEnd };
|
|
925
1017
|
}
|
|
926
|
-
function motionCycleDuration(
|
|
927
|
-
if (
|
|
928
|
-
if (
|
|
929
|
-
if (
|
|
930
|
-
if (
|
|
931
|
-
const ease = resolveEasing(
|
|
1018
|
+
function motionCycleDuration(command3, scene, target) {
|
|
1019
|
+
if (command3.type === "wait") return command3.duration;
|
|
1020
|
+
if (command3.type === "play") return motionClipLayout(scene, command3.clip, target ?? MOTION_CLIP_TARGET, command3.speed).length;
|
|
1021
|
+
if (command3.duration !== void 0) return command3.duration;
|
|
1022
|
+
if (command3.ease !== void 0) {
|
|
1023
|
+
const ease = resolveEasing(command3.ease);
|
|
932
1024
|
if (ease !== "hold" && ease.type === "spring") return springSettleMs(ease);
|
|
933
1025
|
}
|
|
934
|
-
if (
|
|
1026
|
+
if (command3.type === "draw" || command3.type === "enter" && command3.style === "draw" || command3.type === "exit" && command3.style === "undraw") {
|
|
935
1027
|
const entry = target ? indexMotionNodes(scene.artwork.nodes).get(target) : void 0;
|
|
936
1028
|
const length = entry ? motionPathLength(entry.node) : 0;
|
|
937
1029
|
const ms = length * MOTION_DRAW_MS_PER_UNIT;
|
|
938
1030
|
return Math.round(Math.min(MOTION_DRAW_RANGE[1], Math.max(MOTION_DRAW_RANGE[0], ms)));
|
|
939
1031
|
}
|
|
940
|
-
if (
|
|
941
|
-
return MOTION_DEFAULT_DURATIONS[
|
|
1032
|
+
if (command3.type === "move" && command3.via?.length) return MOTION_DEFAULT_DURATIONS.move + 400 * command3.via.length;
|
|
1033
|
+
return MOTION_DEFAULT_DURATIONS[command3.type];
|
|
942
1034
|
}
|
|
943
1035
|
var MOTION_EASE_OUT = { type: "cubic-bezier", x1: 1 / 3, y1: 1, x2: 2 / 3, y2: 1 };
|
|
944
1036
|
var MOTION_EASE_IN_OUT = { type: "cubic-bezier", x1: 0.65, y1: 0, x2: 0.35, y2: 1 };
|
|
@@ -974,14 +1066,15 @@ function compileMotionScene(scene) {
|
|
|
974
1066
|
const commandsStart = cursor;
|
|
975
1067
|
let runEnd = cursor;
|
|
976
1068
|
let prevStart = cursor;
|
|
977
|
-
shot.run.forEach((
|
|
978
|
-
const base =
|
|
979
|
-
const begin = base + (
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
const
|
|
983
|
-
const
|
|
984
|
-
|
|
1069
|
+
shot.run.forEach((command3, i) => {
|
|
1070
|
+
const base = command3.with === "previous" && i > 0 ? prevStart : runEnd;
|
|
1071
|
+
const begin = base + (command3.type === "wait" ? 0 : (command3.delay ?? 0) * f);
|
|
1072
|
+
const split = command3.type === "wait" ? null : motionCommandSlots(nodes, command3);
|
|
1073
|
+
const slots = split?.slots ?? [];
|
|
1074
|
+
const durations = command3.type === "wait" ? [command3.duration * f] : slots.map((s) => motionCommandDuration(command3, scene, s.id) * f);
|
|
1075
|
+
const stagger = (split?.stagger ?? 0) * f;
|
|
1076
|
+
const end2 = durations.reduce((m, d, k) => Math.max(m, begin + (slots[k]?.slot ?? k) * stagger + d), begin);
|
|
1077
|
+
commands.push({ shot: shot.id, shotIndex: index, command: i, type: command3.type, start: begin, end: end2, duration: durations[0] ?? 0 });
|
|
985
1078
|
prevStart = begin;
|
|
986
1079
|
runEnd = Math.max(runEnd, end2);
|
|
987
1080
|
});
|
|
@@ -1013,14 +1106,14 @@ function compileMotionScene(scene) {
|
|
|
1013
1106
|
}
|
|
1014
1107
|
for (const timing of commands) {
|
|
1015
1108
|
const shot = scene.shots[timing.shotIndex];
|
|
1016
|
-
const
|
|
1017
|
-
if (
|
|
1018
|
-
if (
|
|
1109
|
+
const command3 = shot.run[timing.command];
|
|
1110
|
+
if (command3.type === "wait") continue;
|
|
1111
|
+
if (command3.type === "play") {
|
|
1019
1112
|
const f = motionShotTimeScale(shot);
|
|
1020
|
-
|
|
1113
|
+
command3.targets.forEach((target, k) => {
|
|
1021
1114
|
if (!nodes.has(target)) return;
|
|
1022
|
-
const at = timing.start + k * (
|
|
1023
|
-
for (const item of motionClipLayout(scene,
|
|
1115
|
+
const at = timing.start + k * (command3.stagger ?? 0) * f;
|
|
1116
|
+
for (const item of motionClipLayout(scene, command3.clip, target, (command3.speed ?? 1) / f).items) {
|
|
1024
1117
|
const entry = nodes.get(item.target);
|
|
1025
1118
|
if (!entry) continue;
|
|
1026
1119
|
jobs.push({ start: at + item.start, order: order++, run: () => compileRepeated(item.command, item.target, entry, at + item.start, item.cycle, timing) });
|
|
@@ -1028,24 +1121,25 @@ function compileMotionScene(scene) {
|
|
|
1028
1121
|
});
|
|
1029
1122
|
continue;
|
|
1030
1123
|
}
|
|
1031
|
-
|
|
1124
|
+
const split = motionCommandSlots(nodes, command3);
|
|
1125
|
+
for (const { id: target, slot } of split.slots) {
|
|
1032
1126
|
const entry = nodes.get(target);
|
|
1033
|
-
if (!entry)
|
|
1127
|
+
if (!entry) continue;
|
|
1034
1128
|
const f = motionShotTimeScale(shot);
|
|
1035
|
-
const start = timing.start +
|
|
1036
|
-
const cycle = motionCycleDuration(
|
|
1037
|
-
jobs.push({ start, order: order++, run: () => compileRepeated(
|
|
1038
|
-
}
|
|
1129
|
+
const start = timing.start + slot * split.stagger * f;
|
|
1130
|
+
const cycle = motionCycleDuration(command3, scene, target) * f;
|
|
1131
|
+
jobs.push({ start, order: order++, run: () => compileRepeated(command3, target, entry, start, cycle, timing) });
|
|
1132
|
+
}
|
|
1039
1133
|
}
|
|
1040
1134
|
jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
|
|
1041
|
-
function compileRepeated(
|
|
1135
|
+
function compileRepeated(command3, node, entry, start, cycle, timing) {
|
|
1042
1136
|
const first = segments.length;
|
|
1043
|
-
compileCommand(
|
|
1137
|
+
compileCommand(command3, node, entry, start, cycle, timing);
|
|
1044
1138
|
const made = segments.slice(first);
|
|
1045
|
-
const steps =
|
|
1139
|
+
const steps = command3.steps ?? scene.steps;
|
|
1046
1140
|
if (steps && steps > 0) for (const s of made) s.steps = steps;
|
|
1047
|
-
const n = motionCommandRepeat(
|
|
1048
|
-
const mode =
|
|
1141
|
+
const n = motionCommandRepeat(command3);
|
|
1142
|
+
const mode = command3.repeatMode ?? "loop";
|
|
1049
1143
|
const shift = (v, by, k) => {
|
|
1050
1144
|
if (typeof v === "number" && typeof by === "number") return v + by * k;
|
|
1051
1145
|
if (Array.isArray(v) && Array.isArray(by) && v.length === by.length) return v.map((x, i) => x + by[i] * k);
|
|
@@ -1064,16 +1158,16 @@ function compileMotionScene(scene) {
|
|
|
1064
1158
|
}
|
|
1065
1159
|
}
|
|
1066
1160
|
}
|
|
1067
|
-
function compileCommand(
|
|
1161
|
+
function compileCommand(command3, node, entry, start, duration2, timing) {
|
|
1068
1162
|
const end = start + duration2;
|
|
1069
1163
|
const where = { shot: timing.shot, command: timing.command };
|
|
1070
1164
|
const seg = (property, to, ease2, s = start, e = end, extra = {}) => push({ node, property, start: s, end: e, from: extra.from ?? valueAt(node, property, s), to, ease: ease2, ...where, ...extra });
|
|
1071
1165
|
const rest = (p) => motionRestValue(entry, p, artwork);
|
|
1072
|
-
const
|
|
1166
|
+
const num2 = (p, time = start) => {
|
|
1073
1167
|
const v = valueAt(node, p, time);
|
|
1074
1168
|
return typeof v === "number" ? v : 0;
|
|
1075
1169
|
};
|
|
1076
|
-
const ease =
|
|
1170
|
+
const ease = command3.ease;
|
|
1077
1171
|
const drawWindows = (s, e, o) => {
|
|
1078
1172
|
const order2 = o.order ?? "together";
|
|
1079
1173
|
const pace = o.pace ?? "time";
|
|
@@ -1111,11 +1205,11 @@ function compileMotionScene(scene) {
|
|
|
1111
1205
|
return o.align === "end" ? { id: p.id, start: e - d, end: e } : { id: p.id, start: s, end: s + d };
|
|
1112
1206
|
});
|
|
1113
1207
|
};
|
|
1114
|
-
switch (
|
|
1208
|
+
switch (command3.type) {
|
|
1115
1209
|
case "enter": {
|
|
1116
|
-
const style =
|
|
1210
|
+
const style = command3.style ?? "fade";
|
|
1117
1211
|
const opacity = rest("opacity");
|
|
1118
|
-
const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction:
|
|
1212
|
+
const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command3.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command3.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command3.orientation }) : null;
|
|
1119
1213
|
for (const p of ["clipRect", "clipCircle"]) if (p !== reveal?.property && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
|
|
1120
1214
|
if (reveal) {
|
|
1121
1215
|
seg("opacity", opacity, "linear", start, start);
|
|
@@ -1123,30 +1217,30 @@ function compileMotionScene(scene) {
|
|
|
1123
1217
|
break;
|
|
1124
1218
|
}
|
|
1125
1219
|
if (style === "draw") {
|
|
1126
|
-
const windows = drawWindows(start, start + duration2 * 0.75,
|
|
1220
|
+
const windows = drawWindows(start, start + duration2 * 0.75, command3);
|
|
1127
1221
|
if (windows) for (const w of windows) push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: 0, to: 1, ease: ease ?? MOTION_EASE_IN_OUT, ...where });
|
|
1128
1222
|
else seg("drawEnd", 1, ease ?? MOTION_EASE_IN_OUT, start, start + duration2 * 0.75, { from: 0 });
|
|
1129
1223
|
seg("fillOpacity", 1, MOTION_EASE_OUT, start + duration2 * 0.6, end, { from: 0 });
|
|
1130
1224
|
break;
|
|
1131
1225
|
}
|
|
1132
1226
|
if (style === "pop" || style === "zoom") {
|
|
1133
|
-
const from =
|
|
1227
|
+
const from = command3.scale ?? (style === "pop" ? 0.5 : 1.6);
|
|
1134
1228
|
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * (style === "pop" ? 0.4 : 0.5), { from: 0 });
|
|
1135
1229
|
seg("scaleX", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
|
|
1136
1230
|
seg("scaleY", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
|
|
1137
1231
|
} else if (style === "rise") {
|
|
1138
|
-
const y =
|
|
1232
|
+
const y = num2("y");
|
|
1139
1233
|
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.5, { from: 0 });
|
|
1140
|
-
seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (
|
|
1234
|
+
seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (command3.distance ?? 20) });
|
|
1141
1235
|
} else if (style === "grow") {
|
|
1142
1236
|
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.3, { from: 0 });
|
|
1143
1237
|
seg("scaleX", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
|
|
1144
1238
|
seg("scaleY", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
|
|
1145
1239
|
} else if (style === "slide") {
|
|
1146
|
-
const dir = DIRECTION[
|
|
1147
|
-
const distance =
|
|
1148
|
-
const x =
|
|
1149
|
-
const y =
|
|
1240
|
+
const dir = DIRECTION[command3.direction ?? "up"];
|
|
1241
|
+
const distance = command3.distance ?? 40;
|
|
1242
|
+
const x = num2("x");
|
|
1243
|
+
const y = num2("y");
|
|
1150
1244
|
seg("opacity", opacity, MOTION_EASE_OUT, start, end, { from: 0 });
|
|
1151
1245
|
seg("x", x, ease ?? MOTION_EASE_OUT, start, end, { from: x - dir.x * distance });
|
|
1152
1246
|
seg("y", y, ease ?? MOTION_EASE_OUT, start, end, { from: y - dir.y * distance });
|
|
@@ -1154,12 +1248,12 @@ function compileMotionScene(scene) {
|
|
|
1154
1248
|
break;
|
|
1155
1249
|
}
|
|
1156
1250
|
case "exit": {
|
|
1157
|
-
const style =
|
|
1158
|
-
const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction:
|
|
1251
|
+
const style = command3.style ?? "fade";
|
|
1252
|
+
const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction: command3.direction, hide: true }) : null;
|
|
1159
1253
|
if (wipe) seg(wipe.property, wipe.to, ease ?? "ease-in", start, end, { from: wipe.from });
|
|
1160
1254
|
else if (style === "undraw") {
|
|
1161
1255
|
seg("fillOpacity", 0, "ease-in", start, start + duration2 * 0.4);
|
|
1162
|
-
const windows = drawWindows(start, end,
|
|
1256
|
+
const windows = drawWindows(start, end, command3);
|
|
1163
1257
|
if (windows) for (const w of windows) push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: valueAt(w.id, "drawEnd", w.start), to: 0, ease: ease ?? MOTION_EASE_IN_OUT, ...where });
|
|
1164
1258
|
else seg("drawEnd", 0, ease ?? MOTION_EASE_IN_OUT);
|
|
1165
1259
|
} else if (style === "shrink") {
|
|
@@ -1167,18 +1261,18 @@ function compileMotionScene(scene) {
|
|
|
1167
1261
|
seg("scaleY", 0, ease ?? "ease-in");
|
|
1168
1262
|
seg("opacity", 0, "ease-in", start + duration2 * 0.6, end);
|
|
1169
1263
|
} else if (style === "slide") {
|
|
1170
|
-
const dir = DIRECTION[
|
|
1171
|
-
const distance =
|
|
1172
|
-
seg("x",
|
|
1173
|
-
seg("y",
|
|
1264
|
+
const dir = DIRECTION[command3.direction ?? "down"];
|
|
1265
|
+
const distance = command3.distance ?? 40;
|
|
1266
|
+
seg("x", num2("x") + dir.x * distance, ease ?? "ease-in");
|
|
1267
|
+
seg("y", num2("y") + dir.y * distance, ease ?? "ease-in");
|
|
1174
1268
|
seg("opacity", 0, ease ?? "ease-in");
|
|
1175
1269
|
} else seg("opacity", 0, ease ?? "ease-in");
|
|
1176
1270
|
break;
|
|
1177
1271
|
}
|
|
1178
1272
|
case "draw": {
|
|
1179
1273
|
const e = ease ?? MOTION_EASE_IN_OUT;
|
|
1180
|
-
const comet =
|
|
1181
|
-
const windows = drawWindows(start, end,
|
|
1274
|
+
const comet = command3.comet !== void 0 ? Math.min(1, Math.max(0, command3.comet)) : void 0;
|
|
1275
|
+
const windows = drawWindows(start, end, command3) ?? [{ id: node, start, end }];
|
|
1182
1276
|
for (const w of windows) {
|
|
1183
1277
|
if (comet !== void 0) {
|
|
1184
1278
|
push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: 0, to: 1 + comet, ease: e, ...where });
|
|
@@ -1186,27 +1280,27 @@ function compileMotionScene(scene) {
|
|
|
1186
1280
|
continue;
|
|
1187
1281
|
}
|
|
1188
1282
|
const drawn = tracks.has(motionTrackKey(w.id, "drawEnd"));
|
|
1189
|
-
push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from:
|
|
1283
|
+
push({ node: w.id, property: "drawEnd", start: w.start, end: w.end, from: command3.from ?? (drawn ? valueAt(w.id, "drawEnd", w.start) : 0), to: command3.to ?? 1, ease: e, ...where });
|
|
1190
1284
|
}
|
|
1191
1285
|
break;
|
|
1192
1286
|
}
|
|
1193
1287
|
case "move": {
|
|
1194
1288
|
const e = ease ?? MOTION_EASE_IN_OUT;
|
|
1195
|
-
let dx =
|
|
1196
|
-
let dy =
|
|
1197
|
-
if (
|
|
1289
|
+
let dx = command3.by?.x ?? 0;
|
|
1290
|
+
let dy = command3.by?.y ?? 0;
|
|
1291
|
+
if (command3.to) {
|
|
1198
1292
|
const inv = invertMatrix(entry.parentWorld);
|
|
1199
|
-
const target = applyMatrix(inv,
|
|
1200
|
-
dx = target.x - (entry.center.x +
|
|
1201
|
-
dy = target.y - (entry.center.y +
|
|
1293
|
+
const target = applyMatrix(inv, command3.to);
|
|
1294
|
+
dx = target.x - (entry.center.x + num2("x"));
|
|
1295
|
+
dy = target.y - (entry.center.y + num2("y"));
|
|
1202
1296
|
}
|
|
1203
|
-
if (
|
|
1297
|
+
if (command3.via?.length) {
|
|
1204
1298
|
const inv = invertMatrix(entry.parentWorld);
|
|
1205
|
-
const x0 =
|
|
1206
|
-
const y0 =
|
|
1207
|
-
const add =
|
|
1208
|
-
const offsets = [{ x: 0, y: 0 }, ...
|
|
1209
|
-
const times = smoothMotionTimes(offsets, start, end - start,
|
|
1299
|
+
const x0 = num2("x");
|
|
1300
|
+
const y0 = num2("y");
|
|
1301
|
+
const add = command3.blend === "add";
|
|
1302
|
+
const offsets = [{ x: 0, y: 0 }, ...command3.via.map((p) => applyMatrix(inv, p)).map((p) => ({ x: p.x - entry.center.x - x0, y: p.y - entry.center.y - y0 })), { x: dx, y: dy }];
|
|
1303
|
+
const times = smoothMotionTimes(offsets, start, end - start, command3.pacing ?? "even");
|
|
1210
1304
|
for (const axis of ["x", "y"]) {
|
|
1211
1305
|
const base = add ? 0 : axis === "x" ? x0 : y0;
|
|
1212
1306
|
for (const leg of smoothMotionLegs(times, offsets.map((o) => o[axis])))
|
|
@@ -1214,42 +1308,42 @@ function compileMotionScene(scene) {
|
|
|
1214
1308
|
}
|
|
1215
1309
|
break;
|
|
1216
1310
|
}
|
|
1217
|
-
if (
|
|
1311
|
+
if (command3.blend === "add") {
|
|
1218
1312
|
seg("x", dx, e, start, end, { from: 0, blend: "add" });
|
|
1219
1313
|
seg("y", dy, e, start, end, { from: 0, blend: "add" });
|
|
1220
1314
|
} else {
|
|
1221
|
-
seg("x",
|
|
1222
|
-
seg("y",
|
|
1315
|
+
seg("x", num2("x") + dx, e);
|
|
1316
|
+
seg("y", num2("y") + dy, e);
|
|
1223
1317
|
}
|
|
1224
1318
|
break;
|
|
1225
1319
|
}
|
|
1226
1320
|
case "rotate": {
|
|
1227
1321
|
const e = ease ?? MOTION_EASE_IN_OUT;
|
|
1228
|
-
if (
|
|
1229
|
-
else seg("rotation",
|
|
1322
|
+
if (command3.blend === "add") seg("rotation", command3.to !== void 0 ? command3.to - num2("rotation") : command3.by ?? 0, e, start, end, { from: 0, blend: "add" });
|
|
1323
|
+
else seg("rotation", command3.to ?? num2("rotation") + (command3.by ?? 0), e);
|
|
1230
1324
|
break;
|
|
1231
1325
|
}
|
|
1232
1326
|
case "scale": {
|
|
1233
|
-
const to = typeof
|
|
1234
|
-
const extra = { ...
|
|
1327
|
+
const to = typeof command3.to === "number" ? { x: command3.to, y: command3.to } : command3.to;
|
|
1328
|
+
const extra = { ...command3.geometric ? { geometric: true } : {}, ...command3.blend === "add" ? { from: 1, blend: "add" } : {} };
|
|
1235
1329
|
seg("scaleX", to.x, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
|
|
1236
1330
|
seg("scaleY", to.y, ease ?? MOTION_EASE_IN_OUT, start, end, extra);
|
|
1237
1331
|
break;
|
|
1238
1332
|
}
|
|
1239
1333
|
case "fade":
|
|
1240
|
-
seg("opacity",
|
|
1334
|
+
seg("opacity", command3.to, ease ?? MOTION_EASE_IN_OUT);
|
|
1241
1335
|
break;
|
|
1242
1336
|
case "style":
|
|
1243
|
-
if (
|
|
1244
|
-
if (
|
|
1245
|
-
if (
|
|
1337
|
+
if (command3.fill !== void 0) seg("fill", command3.fill, ease ?? MOTION_EASE_IN_OUT);
|
|
1338
|
+
if (command3.stroke !== void 0) seg("stroke", command3.stroke, ease ?? MOTION_EASE_IN_OUT);
|
|
1339
|
+
if (command3.strokeWidth !== void 0) seg("strokeWidth", command3.strokeWidth, ease ?? MOTION_EASE_IN_OUT);
|
|
1246
1340
|
break;
|
|
1247
1341
|
case "morph":
|
|
1248
|
-
seg("shape",
|
|
1342
|
+
seg("shape", command3.into, ease ?? MOTION_EASE_IN_OUT);
|
|
1249
1343
|
break;
|
|
1250
1344
|
case "match": {
|
|
1251
|
-
const into = nodes.get(
|
|
1252
|
-
if (!into ||
|
|
1345
|
+
const into = nodes.get(command3.into);
|
|
1346
|
+
if (!into || command3.into === node) break;
|
|
1253
1347
|
const match = matchMotionNodes(entry.node, into.node);
|
|
1254
1348
|
const e = ease ?? MOTION_EASE_IN_OUT;
|
|
1255
1349
|
for (const [a, b] of match.pairs) push({ node: a, property: "shape", start, end, from: valueAt(a, "shape", start), to: b, ease: e, ...where });
|
|
@@ -1257,7 +1351,7 @@ function compileMotionScene(scene) {
|
|
|
1257
1351
|
break;
|
|
1258
1352
|
}
|
|
1259
1353
|
case "follow":
|
|
1260
|
-
seg("follow",
|
|
1354
|
+
seg("follow", command3.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command3.from ?? 0, path: command3.path, rotate: command3.rotate ?? false });
|
|
1261
1355
|
break;
|
|
1262
1356
|
// Emphasis: a change toward a peak that a behaviour swings round (as Flair's primitives do).
|
|
1263
1357
|
case "pulse":
|
|
@@ -1265,33 +1359,33 @@ function compileMotionScene(scene) {
|
|
|
1265
1359
|
case "teeter":
|
|
1266
1360
|
case "wave":
|
|
1267
1361
|
case "wiggle": {
|
|
1268
|
-
const add = (
|
|
1362
|
+
const add = (command3.blend ?? "add") === "add";
|
|
1269
1363
|
const swing = (p, by, extra) => {
|
|
1270
1364
|
const scale = p === "scaleX" || p === "scaleY";
|
|
1271
1365
|
if (add) seg(p, by, ease ?? "linear", start, end, { from: scale ? 1 : 0, blend: "add", ...extra });
|
|
1272
|
-
else seg(p, scale ?
|
|
1366
|
+
else seg(p, scale ? num2(p) * by : num2(p) + by, ease ?? "linear", start, end, extra);
|
|
1273
1367
|
};
|
|
1274
|
-
if (
|
|
1275
|
-
const behavior = { type: "pulse", count:
|
|
1276
|
-
swing("scaleX", 1 + (
|
|
1277
|
-
swing("scaleY", 1 + (
|
|
1278
|
-
} else if (
|
|
1279
|
-
else if (
|
|
1280
|
-
else if (
|
|
1368
|
+
if (command3.type === "pulse") {
|
|
1369
|
+
const behavior = { type: "pulse", count: command3.count ?? 1 };
|
|
1370
|
+
swing("scaleX", 1 + (command3.amount ?? 0.15), { behavior });
|
|
1371
|
+
swing("scaleY", 1 + (command3.amount ?? 0.15), { behavior });
|
|
1372
|
+
} else if (command3.type === "shake") swing(command3.axis === "vertical" ? "y" : "x", command3.amount ?? 8, { behavior: { type: "oscillate", count: command3.count ?? 3, decay: 1 } });
|
|
1373
|
+
else if (command3.type === "teeter") swing("rotation", command3.angle ?? 8, { behavior: { type: "oscillate", count: command3.count ?? 3, decay: 1 } });
|
|
1374
|
+
else if (command3.type === "wave") swing("y", -(command3.amount ?? 6), { behavior: { type: "oscillate", count: command3.count ?? 2 } });
|
|
1281
1375
|
else {
|
|
1282
|
-
const behavior = { type: "wriggle", frequency:
|
|
1283
|
-
const amount =
|
|
1376
|
+
const behavior = { type: "wriggle", frequency: command3.frequency ?? 3, octaves: command3.octaves ?? 2, seed: command3.seed || motionSeedFromId(node) };
|
|
1377
|
+
const amount = command3.amount ?? 6;
|
|
1284
1378
|
swing("x", amount, { behavior, index: 0 });
|
|
1285
1379
|
swing("y", amount, { behavior, index: 1 });
|
|
1286
|
-
if (
|
|
1380
|
+
if (command3.rotate) swing("rotation", command3.rotate, { behavior, index: 2 });
|
|
1287
1381
|
}
|
|
1288
1382
|
break;
|
|
1289
1383
|
}
|
|
1290
1384
|
case "color-pulse":
|
|
1291
|
-
seg(
|
|
1385
|
+
seg(command3.paint ?? "fill", command3.color ?? "#f59e0b", ease ?? "linear", start, end, { behavior: { type: "pulse", count: command3.count ?? 1 } });
|
|
1292
1386
|
break;
|
|
1293
1387
|
case "effect":
|
|
1294
|
-
seg(`effect:${
|
|
1388
|
+
seg(`effect:${command3.effect}:${command3.param}`, command3.to, ease ?? MOTION_EASE_IN_OUT);
|
|
1295
1389
|
break;
|
|
1296
1390
|
}
|
|
1297
1391
|
}
|
|
@@ -1682,24 +1776,24 @@ function usesColor(scene, color) {
|
|
|
1682
1776
|
return found;
|
|
1683
1777
|
}
|
|
1684
1778
|
function visibleAt(timeline, id, time, node) {
|
|
1685
|
-
const
|
|
1779
|
+
const num2 = (p, rest) => {
|
|
1686
1780
|
const v = trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
1687
1781
|
return typeof v === "number" ? v : rest;
|
|
1688
1782
|
};
|
|
1689
1783
|
if (node.visible === false) return false;
|
|
1690
|
-
if (
|
|
1691
|
-
if (Math.abs(
|
|
1784
|
+
if (num2("opacity", node.opacity ?? 1) <= 0.01) return false;
|
|
1785
|
+
if (Math.abs(num2("scaleX", 1) * num2("scaleY", 1)) <= 1e-4) return false;
|
|
1692
1786
|
const clip = (p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
1693
1787
|
const rect = clip("clipRect");
|
|
1694
1788
|
if (Array.isArray(rect) && (rect[2] <= 1e-4 || rect[3] <= 1e-4)) return false;
|
|
1695
1789
|
const circle = clip("clipCircle");
|
|
1696
1790
|
if (Array.isArray(circle) && circle[2] <= 1e-4) return false;
|
|
1697
1791
|
const hasFill = node.type === "path" ? !!node.fill && node.fill !== "none" : collectPathNodes(node).some((p) => p.fill && p.fill !== "none");
|
|
1698
|
-
const drawn =
|
|
1792
|
+
const drawn = num2("drawEnd", 1) > 0.01 && (node.type === "path" || collectPathNodes(node).length === 0 || collectPathNodes(node).some((p) => {
|
|
1699
1793
|
const v = trackValue(timeline.tracks.get(motionTrackKey(p.id, "drawEnd")), time);
|
|
1700
1794
|
return typeof v !== "number" || v > 0.01;
|
|
1701
1795
|
}));
|
|
1702
|
-
return drawn || hasFill &&
|
|
1796
|
+
return drawn || hasFill && num2("fillOpacity", 1) > 0.01;
|
|
1703
1797
|
}
|
|
1704
1798
|
function checkMotionScene(scene) {
|
|
1705
1799
|
const issues = [];
|
|
@@ -1727,48 +1821,53 @@ function checkMotionScene(scene) {
|
|
|
1727
1821
|
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 }));
|
|
1728
1822
|
}
|
|
1729
1823
|
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 }));
|
|
1730
|
-
shot.run.forEach((
|
|
1824
|
+
shot.run.forEach((command3, i) => {
|
|
1731
1825
|
const where = { shot: shot.id, command: i };
|
|
1732
|
-
if (
|
|
1733
|
-
const targets =
|
|
1826
|
+
if (command3.type === "wait") return;
|
|
1827
|
+
const targets = command3.targets.filter((id) => known(id, "unknown-target", "node", where));
|
|
1734
1828
|
const timing = timeline.commands.find((c) => c.shot === shot.id && c.command === i);
|
|
1735
1829
|
if (timing && timing.duration > 0 && timing.duration < MOTION_MIN_CHANGE_MS)
|
|
1736
|
-
issues.push(motionIssue("too-fast", `This ${
|
|
1737
|
-
if (
|
|
1738
|
-
const ease = resolveEasing(
|
|
1830
|
+
issues.push(motionIssue("too-fast", `This ${command3.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 }));
|
|
1831
|
+
if (command3.duration !== void 0 && command3.ease !== void 0) {
|
|
1832
|
+
const ease = resolveEasing(command3.ease);
|
|
1739
1833
|
if (ease !== "hold" && ease.type === "spring") {
|
|
1740
1834
|
const settle = springSettleMs(ease);
|
|
1741
|
-
if (
|
|
1742
|
-
issues.push(motionIssue("spring-cut-short", `This ${
|
|
1835
|
+
if (command3.duration < settle * 0.9)
|
|
1836
|
+
issues.push(motionIssue("spring-cut-short", `This ${command3.type}'s spring takes ${settle} ms to settle but lasts ${command3.duration} ms, so it jumps to its end; leave "duration" out (the spring sets it) or make it at least ${settle} ms.`, { ...where, measured: command3.duration, needed: settle, unit: "ms", ids: targets }));
|
|
1743
1837
|
}
|
|
1744
1838
|
}
|
|
1745
|
-
if (
|
|
1746
|
-
issues.push(motionIssue("not-a-path", `"${
|
|
1747
|
-
if (
|
|
1748
|
-
if (
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1839
|
+
if (command3.type === "follow" && known(command3.path, "unknown-path", "path", where) && nodes.get(command3.path).node.type !== "path")
|
|
1840
|
+
issues.push(motionIssue("not-a-path", `"${command3.path}" is a group; follow a single path node.`, { ...where, ids: [command3.path] }));
|
|
1841
|
+
if (command3.type === "morph") known(command3.into, "unknown-shape", "shape", where);
|
|
1842
|
+
if (command3.type !== "play" && command3.split) {
|
|
1843
|
+
for (const id of targets)
|
|
1844
|
+
if (!motionTextLayout(nodes, id))
|
|
1845
|
+
issues.push(motionIssue("split-not-text", `"${id}" is not a group of letter paths, so "split": "${command3.split}" animates it as a whole; convert the text to outlines and group the letters (get_text_layout shows how they read).`, { ...where, ids: [id] }));
|
|
1846
|
+
}
|
|
1847
|
+
if (command3.type === "play" && !scene.clips?.[command3.clip])
|
|
1848
|
+
issues.push(motionIssue("unknown-clip", `There is no clip ${namedWithSuggestion(command3.clip, Object.keys(scene.clips ?? {}))}; define it with set_animation_clip.`, where));
|
|
1849
|
+
if (command3.type === "match" && known(command3.into, "unknown-shape", "group", where)) {
|
|
1850
|
+
const into = nodes.get(command3.into);
|
|
1752
1851
|
for (const id of targets) {
|
|
1753
1852
|
const entry = nodes.get(id);
|
|
1754
|
-
if (id ===
|
|
1755
|
-
issues.push(motionIssue("match-self", `"${id}" cannot match "${
|
|
1853
|
+
if (id === command3.into || into.ancestors.includes(id) || entry.ancestors.includes(command3.into)) {
|
|
1854
|
+
issues.push(motionIssue("match-self", `"${id}" cannot match "${command3.into}": one is the other or holds it. Match two separate groups (the second usually hidden).`, { ...where, ids: [id, command3.into] }));
|
|
1756
1855
|
continue;
|
|
1757
1856
|
}
|
|
1758
1857
|
const m = matchMotionNodes(entry.node, into.node);
|
|
1759
1858
|
if (m.unpairedInto.length)
|
|
1760
|
-
issues.push(motionIssue("match-unpaired", `${m.unpairedInto.map((x) => `"${x}"`).join(", ")} in "${
|
|
1859
|
+
issues.push(motionIssue("match-unpaired", `${m.unpairedInto.map((x) => `"${x}"`).join(", ")} in "${command3.into}" ${m.unpairedInto.length > 1 ? "have" : "has"} no partner in "${id}", so ${m.unpairedInto.length > 1 ? "they do" : "it does"} not appear; add a part with the same name or id to "${id}" (it can start hidden or tiny).`, { ...where, ids: m.unpairedInto }));
|
|
1761
1860
|
}
|
|
1762
1861
|
}
|
|
1763
|
-
const style =
|
|
1862
|
+
const style = command3.type === "enter" || command3.type === "exit" ? command3.style : void 0;
|
|
1764
1863
|
if (style === "wipe" || style === "iris" || style === "split") {
|
|
1765
1864
|
for (const id of targets)
|
|
1766
1865
|
if (!motionRevealPadding(nodes.get(id)))
|
|
1767
|
-
issues.push(motionIssue("reveal-no-box", `"${id}" has no width or height (a straight line), so a ${style} cannot clip it and it fades instead; use style "${
|
|
1866
|
+
issues.push(motionIssue("reveal-no-box", `"${id}" has no width or height (a straight line), so a ${style} cannot clip it and it fades instead; use style "${command3.type === "enter" ? "draw" : "undraw"}" for a line.`, { ...where, ids: [id] }));
|
|
1768
1867
|
}
|
|
1769
|
-
if (
|
|
1868
|
+
if (command3.type === "style") {
|
|
1770
1869
|
for (const key of ["fill", "stroke"]) {
|
|
1771
|
-
const color =
|
|
1870
|
+
const color = command3[key];
|
|
1772
1871
|
if (color === void 0) continue;
|
|
1773
1872
|
if (normalizeTokenColor(color) === null) {
|
|
1774
1873
|
issues.push(motionIssue("invalid-color", `${key} "${color}" is not a colour this editor reads; use hex ("#0ea5e9"), rgb() or rgba().`, where));
|
|
@@ -1782,9 +1881,9 @@ function checkMotionScene(scene) {
|
|
|
1782
1881
|
}
|
|
1783
1882
|
}
|
|
1784
1883
|
}
|
|
1785
|
-
if (
|
|
1786
|
-
const key =
|
|
1787
|
-
const color =
|
|
1884
|
+
if (command3.type === "color-pulse") {
|
|
1885
|
+
const key = command3.paint ?? "fill";
|
|
1886
|
+
const color = command3.color ?? "#f59e0b";
|
|
1788
1887
|
if (normalizeTokenColor(color) === null) issues.push(motionIssue("invalid-color", `color "${color}" is not a colour this editor reads; use hex ("#0ea5e9"), rgb() or rgba().`, where));
|
|
1789
1888
|
else
|
|
1790
1889
|
for (const id of targets) {
|
|
@@ -1794,15 +1893,15 @@ function checkMotionScene(scene) {
|
|
|
1794
1893
|
issues.push(motionIssue("paint-not-color", `"${id}" has a gradient or no ${key}, so a colour pulse cannot tint it; give it a solid ${key} or pulse the ${key === "fill" ? "stroke" : "fill"}.`, { ...where, ids: [id] }));
|
|
1795
1894
|
}
|
|
1796
1895
|
}
|
|
1797
|
-
if (
|
|
1896
|
+
if (command3.type === "effect") {
|
|
1798
1897
|
for (const id of targets) {
|
|
1799
1898
|
const node = nodes.get(id).node;
|
|
1800
|
-
const params = NUMERIC_PARAMS(node,
|
|
1899
|
+
const params = NUMERIC_PARAMS(node, command3.effect);
|
|
1801
1900
|
const effects = (node.effects ?? []).map((e) => e.id);
|
|
1802
1901
|
if (!params)
|
|
1803
|
-
issues.push(motionIssue("unknown-effect", `"${id}" has no effect ${namedWithSuggestion(
|
|
1804
|
-
else if (!params.includes(
|
|
1805
|
-
issues.push(motionIssue("unknown-effect-param", `Effect "${
|
|
1902
|
+
issues.push(motionIssue("unknown-effect", `"${id}" has no effect ${namedWithSuggestion(command3.effect, effects)}${effects.length ? `; its effects are ${effects.map((e) => `"${e}"`).join(", ")}` : "; it has no live effects"}.`, { ...where, ids: [id] }));
|
|
1903
|
+
else if (!params.includes(command3.param))
|
|
1904
|
+
issues.push(motionIssue("unknown-effect-param", `Effect "${command3.effect}" has no numeric parameter ${namedWithSuggestion(command3.param, params)}; use one of ${params.join(", ")}.`, { ...where, ids: [id] }));
|
|
1806
1905
|
}
|
|
1807
1906
|
}
|
|
1808
1907
|
if (timing) {
|
|
@@ -1810,11 +1909,11 @@ function checkMotionScene(scene) {
|
|
|
1810
1909
|
const node = nodes.get(id).node;
|
|
1811
1910
|
const before = Math.max(0, timing.start - 0.5);
|
|
1812
1911
|
const visible = timing.start > 0 ? visibleAt(timeline, id, before, node) : node.visible !== false;
|
|
1813
|
-
if (
|
|
1912
|
+
if (command3.type === "enter" && visible && timing.start > 0)
|
|
1814
1913
|
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] }));
|
|
1815
|
-
const probe =
|
|
1816
|
-
if (
|
|
1817
|
-
issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${
|
|
1914
|
+
const probe = command3.type === "exit" ? timing.start + 0.5 : (timing.start + timing.end) / 2;
|
|
1915
|
+
if (command3.type !== "enter" && command3.type !== "draw" && !visibleAt(timeline, id, probe, node))
|
|
1916
|
+
issues.push(motionIssue("exit-hidden", `"${id}" is hidden when this ${command3.type} starts, so it is not seen; enter it first.`, { ...where, ids: [id] }));
|
|
1818
1917
|
}
|
|
1819
1918
|
}
|
|
1820
1919
|
});
|
|
@@ -1842,9 +1941,9 @@ function checkMotionScene(scene) {
|
|
|
1842
1941
|
if (loop.length) issues.push(motionIssue("driver-cycle", `Drivers follow each other round in a loop (${loop.join(" \u2190 ")}); remove one of them.`, { ids: [...new Set(loop.map((k) => k.split(".")[0]))] }));
|
|
1843
1942
|
}
|
|
1844
1943
|
for (const [name, clip] of Object.entries(scene.clips ?? {}))
|
|
1845
|
-
for (const
|
|
1846
|
-
if (
|
|
1847
|
-
for (const id of
|
|
1944
|
+
for (const command3 of clip.run)
|
|
1945
|
+
if (command3.type !== "wait") {
|
|
1946
|
+
for (const id of command3.targets)
|
|
1848
1947
|
if (id !== MOTION_CLIP_TARGET && !nodes.has(id)) issues.push(motionIssue("unknown-target", `Clip "${name}" names ${namedWithSuggestion(id, [...nodes.keys()])}, which the artwork does not have; use "$" for the node the clip is played on.`, { ids: [id] }));
|
|
1849
1948
|
}
|
|
1850
1949
|
if (scene.trigger) {
|
|
@@ -1887,18 +1986,18 @@ function checkMotionScene(scene) {
|
|
|
1887
1986
|
}
|
|
1888
1987
|
const reported = /* @__PURE__ */ new Set();
|
|
1889
1988
|
for (const timing of timeline.commands) {
|
|
1890
|
-
const
|
|
1891
|
-
if (
|
|
1989
|
+
const command3 = scene.shots[timing.shotIndex].run[timing.command];
|
|
1990
|
+
if (command3.type === "wait" || command3.type === "exit") continue;
|
|
1892
1991
|
const frame = motionFrameAt(scene, timing.end);
|
|
1893
1992
|
const cam = frame.camera;
|
|
1894
1993
|
const docIndex = indexMotionNodes(frame.document.nodes);
|
|
1895
|
-
for (const id of
|
|
1994
|
+
for (const id of command3.targets) {
|
|
1896
1995
|
const b = docIndex.get(id)?.bounds;
|
|
1897
1996
|
if (!b || reported.has(id) || !visibleAt(timeline, id, timing.end, nodes.get(id).node)) continue;
|
|
1898
1997
|
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;
|
|
1899
1998
|
if (outside) {
|
|
1900
1999
|
reported.add(id);
|
|
1901
|
-
issues.push(motionIssue("out-of-frame", `"${id}" is outside the camera when this ${
|
|
2000
|
+
issues.push(motionIssue("out-of-frame", `"${id}" is outside the camera when this ${command3.type} ends, so it is not seen; move it into frame or move the camera.`, { shot: timing.shot, command: timing.command, ids: [id] }));
|
|
1902
2001
|
}
|
|
1903
2002
|
}
|
|
1904
2003
|
}
|
|
@@ -2052,8 +2151,8 @@ function applyOne(scene, edit) {
|
|
|
2052
2151
|
case "add_commands": {
|
|
2053
2152
|
const i = shotIndex(scene, edit.shot);
|
|
2054
2153
|
if (!edit.commands.length) fail("Give at least one command.");
|
|
2055
|
-
edit.commands.forEach((
|
|
2056
|
-
const problems = commandShapeProblems(
|
|
2154
|
+
edit.commands.forEach((command3, k) => {
|
|
2155
|
+
const problems = commandShapeProblems(command3, `Command ${k + 1}`);
|
|
2057
2156
|
if (problems.length) fail(problems.join(" "));
|
|
2058
2157
|
});
|
|
2059
2158
|
const run = scene.shots[i].run;
|
|
@@ -2065,14 +2164,14 @@ function applyOne(scene, edit) {
|
|
|
2065
2164
|
const run = scene.shots[i].run;
|
|
2066
2165
|
if (!Number.isInteger(edit.command) || edit.command < 0 || edit.command >= run.length)
|
|
2067
2166
|
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}).`);
|
|
2068
|
-
const
|
|
2167
|
+
const command3 = { ...run[edit.command], ...edit.set ?? {} };
|
|
2069
2168
|
for (const key of edit.remove ?? []) {
|
|
2070
2169
|
if (key === "type") fail('"type" cannot be removed; replace the command instead.');
|
|
2071
|
-
delete
|
|
2170
|
+
delete command3[key];
|
|
2072
2171
|
}
|
|
2073
|
-
const problems = commandShapeProblems(
|
|
2172
|
+
const problems = commandShapeProblems(command3, `Command ${edit.command}`);
|
|
2074
2173
|
if (problems.length) fail(problems.join(" "));
|
|
2075
|
-
return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ?
|
|
2174
|
+
return replaceShot(scene, i, { ...scene.shots[i], run: run.map((c, k) => k === edit.command ? command3 : c) });
|
|
2076
2175
|
}
|
|
2077
2176
|
case "delete_commands": {
|
|
2078
2177
|
const i = shotIndex(scene, edit.shot);
|
|
@@ -2103,9 +2202,9 @@ function applyOne(scene, edit) {
|
|
|
2103
2202
|
else delete next.presets;
|
|
2104
2203
|
}
|
|
2105
2204
|
const names = new Set(edit.controls.map((c) => c.name));
|
|
2106
|
-
for (const
|
|
2107
|
-
for (const key of Object.keys(
|
|
2108
|
-
if (!names.has(key)) fail(`Preset "${
|
|
2205
|
+
for (const preset2 of next.presets ?? [])
|
|
2206
|
+
for (const key of Object.keys(preset2.values))
|
|
2207
|
+
if (!names.has(key)) fail(`Preset "${preset2.name}" sets ${namedWithSuggestion(key, names)}, which is not a control.`);
|
|
2109
2208
|
return next;
|
|
2110
2209
|
}
|
|
2111
2210
|
case "replace_scene":
|
|
@@ -2272,7 +2371,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2272
2371
|
const springs = flairAtLeast(flair, "0.39");
|
|
2273
2372
|
const strobes = flairAtLeast(flair, "0.39");
|
|
2274
2373
|
const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round2);
|
|
2275
|
-
const
|
|
2374
|
+
const easeOf2 = (segment) => {
|
|
2276
2375
|
const resolved = resolveEasing(segment.ease);
|
|
2277
2376
|
if (resolved !== "hold" && resolved.type === "spring") {
|
|
2278
2377
|
if (!springs || segment.reverse) return void 0;
|
|
@@ -2451,7 +2550,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2451
2550
|
const first = active[0];
|
|
2452
2551
|
const key = (x) => JSON.stringify([x.ease, x.behavior, x.reverse ?? false, x.steps ?? 0]);
|
|
2453
2552
|
const together = active.every((x) => x.start === a && x.end === b && channel.exact(x) && key(x) === key(first));
|
|
2454
|
-
const curve = together ?
|
|
2553
|
+
const curve = together ? easeOf2(first) : void 0;
|
|
2455
2554
|
const ease = curve && (!overshoots(curve) || channel.overshoot) ? curve : void 0;
|
|
2456
2555
|
if (ease) {
|
|
2457
2556
|
const from = channel.value(a);
|
|
@@ -2478,7 +2577,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2478
2577
|
for (const seg of follow.segments) {
|
|
2479
2578
|
const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
|
|
2480
2579
|
const progress = (t) => round2(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
|
|
2481
|
-
const curve = seg.end > seg.start ? plays(seg) ?
|
|
2580
|
+
const curve = seg.end > seg.start ? plays(seg) ? easeOf2(seg) : void 0 : "linear";
|
|
2482
2581
|
if (curve && !(overshoots(curve) && seg.rotate)) {
|
|
2483
2582
|
const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
|
|
2484
2583
|
animate.push({ ...along, ...seg.start > 0 ? { delay: round2(seg.start) } : {}, ms: round2(seg.end - seg.start), ease: writeEase(curve), ...seg.steps ? { strobe: seg.steps } : {}, at: [round2(ends[0]), round2(ends[1])] });
|
|
@@ -2722,9 +2821,9 @@ function fromFlairSvgMotion(input, options = {}) {
|
|
|
2722
2821
|
if (!anim.id) continue;
|
|
2723
2822
|
const at = Math.max(0, (anim.delay ?? 0) - cameraMs);
|
|
2724
2823
|
if (cameraMs && (anim.delay ?? 0) < cameraMs) notes.push(`Step ${step}: a change to "${anim.id}" during the camera move now starts after it.`);
|
|
2725
|
-
for (const
|
|
2824
|
+
for (const command3 of commandsFor(anim, nodes, entered, notes)) {
|
|
2726
2825
|
const first = run.length === 0;
|
|
2727
|
-
run.push({ ...
|
|
2826
|
+
run.push({ ...command3, ...first ? {} : { with: "previous" }, ...at - last > 0 || first ? { delay: first ? at : at - last } : {} });
|
|
2728
2827
|
if (first && at === 0) delete run[run.length - 1].delay;
|
|
2729
2828
|
last = at;
|
|
2730
2829
|
}
|
|
@@ -2974,11 +3073,11 @@ function toLottie(scene, options = {}) {
|
|
|
2974
3073
|
}
|
|
2975
3074
|
const stroke = paintOf(path.stroke, "stroke", path.id);
|
|
2976
3075
|
if (stroke || strokeFrom) {
|
|
2977
|
-
const
|
|
3076
|
+
const colour2 = strokeFrom ? track(strokeFrom, "stroke") : [];
|
|
2978
3077
|
items.push({
|
|
2979
3078
|
ty: "st",
|
|
2980
3079
|
nm: "Stroke",
|
|
2981
|
-
c:
|
|
3080
|
+
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? stroke ?? [0, 0, 0, 1]) : { a: 0, k: (stroke ?? [0, 0, 0, 1]).map(round2) },
|
|
2982
3081
|
o: { a: 0, k: 100 },
|
|
2983
3082
|
w: widthFrom ? animate(track(widthFrom, "strokeWidth"), (t) => [Math.max(0, Number(trackValue(track(widthFrom, "strokeWidth"), t) ?? path.strokeWidth ?? 1))]) : { a: 0, k: round2(path.strokeWidth ?? 1) },
|
|
2984
3083
|
lc: path.strokeLinecap === "round" ? 2 : path.strokeLinecap === "square" ? 3 : 1,
|
|
@@ -2990,12 +3089,12 @@ function toLottie(scene, options = {}) {
|
|
|
2990
3089
|
}
|
|
2991
3090
|
const fill = paintOf(path.fill, "fill", path.id);
|
|
2992
3091
|
if (fill || fillFrom) {
|
|
2993
|
-
const
|
|
3092
|
+
const colour2 = fillFrom ? track(fillFrom, "fill") : [];
|
|
2994
3093
|
const fade = fadeFrom ? track(fadeFrom, "fillOpacity") : [];
|
|
2995
3094
|
items.push({
|
|
2996
3095
|
ty: "fl",
|
|
2997
3096
|
nm: "Fill",
|
|
2998
|
-
c:
|
|
3097
|
+
c: colour2.length ? animate(colour2, (t) => rgba(trackValue(colour2, t)) ?? fill ?? [0, 0, 0, 1]) : { a: 0, k: (fill ?? [0, 0, 0, 1]).map(round2) },
|
|
2999
3098
|
o: fade.length ? animate(fade, (t) => [Math.min(1, Math.max(0, Number(trackValue(fade, t) ?? 1))) * 100]) : { a: 0, k: 100 },
|
|
3000
3099
|
r: path.fillRule === "evenodd" ? 2 : 1
|
|
3001
3100
|
});
|
|
@@ -3069,6 +3168,383 @@ function toLottie(scene, options = {}) {
|
|
|
3069
3168
|
return { lottie, sampled, notes: [...new Set(notes)] };
|
|
3070
3169
|
}
|
|
3071
3170
|
|
|
3171
|
+
// src/animation-editor/pptx.ts
|
|
3172
|
+
var EMU = 9525;
|
|
3173
|
+
var kids = (el, name) => el ? Array.from(el.children).filter((c) => c.localName === name) : [];
|
|
3174
|
+
var kid = (el, name) => kids(el, name)[0] ?? null;
|
|
3175
|
+
var deep = (el, name) => el ? Array.from(el.getElementsByTagNameNS("*", name)) : [];
|
|
3176
|
+
var num = (el, attr, fallback = 0) => {
|
|
3177
|
+
const v = el?.getAttribute(attr);
|
|
3178
|
+
const n = v === null || v === void 0 ? NaN : Number(v);
|
|
3179
|
+
return Number.isFinite(n) ? n : fallback;
|
|
3180
|
+
};
|
|
3181
|
+
var dirname = (p) => p.slice(0, p.lastIndexOf("/") + 1);
|
|
3182
|
+
function resolve(base, target) {
|
|
3183
|
+
const out = [];
|
|
3184
|
+
for (const seg of (target.startsWith("/") ? target.slice(1) : base + target).split("/")) {
|
|
3185
|
+
if (seg === "..") out.pop();
|
|
3186
|
+
else if (seg && seg !== ".") out.push(seg);
|
|
3187
|
+
}
|
|
3188
|
+
return out.join("/");
|
|
3189
|
+
}
|
|
3190
|
+
async function xml(ctx, path) {
|
|
3191
|
+
if (!ctx.zip.has(path)) return null;
|
|
3192
|
+
const text = new TextDecoder().decode(await ctx.zip.read(path));
|
|
3193
|
+
const doc = new ctx.Parser().parseFromString(text, "application/xml");
|
|
3194
|
+
return doc.getElementsByTagName("parsererror").length ? null : doc;
|
|
3195
|
+
}
|
|
3196
|
+
async function rels(ctx, part) {
|
|
3197
|
+
const doc = await xml(ctx, `${dirname(part)}_rels/${part.slice(part.lastIndexOf("/") + 1)}.rels`);
|
|
3198
|
+
const out = /* @__PURE__ */ new Map();
|
|
3199
|
+
for (const r of deep(doc, "Relationship")) if (r.getAttribute("TargetMode") !== "External") out.set(r.getAttribute("Id") ?? "", resolve(dirname(part), r.getAttribute("Target") ?? ""));
|
|
3200
|
+
return out;
|
|
3201
|
+
}
|
|
3202
|
+
function colour(ctx, holder) {
|
|
3203
|
+
if (!holder) return null;
|
|
3204
|
+
const c = holder.firstElementChild;
|
|
3205
|
+
if (!c) return null;
|
|
3206
|
+
let hex = null;
|
|
3207
|
+
if (c.localName === "srgbClr") hex = `#${(c.getAttribute("val") ?? "000000").toLowerCase()}`;
|
|
3208
|
+
else if (c.localName === "sysClr") hex = `#${(c.getAttribute("lastClr") ?? "000000").toLowerCase()}`;
|
|
3209
|
+
else if (c.localName === "schemeClr") {
|
|
3210
|
+
const name = c.getAttribute("val") ?? "";
|
|
3211
|
+
const alias = { tx1: "dk1", bg1: "lt1", tx2: "dk2", bg2: "lt2" };
|
|
3212
|
+
hex = ctx.theme.get(alias[name] ?? name) ?? null;
|
|
3213
|
+
} else if (c.localName === "prstClr") hex = { black: "#000000", white: "#ffffff", red: "#ff0000", green: "#008000", blue: "#0000ff" }[c.getAttribute("val") ?? ""] ?? null;
|
|
3214
|
+
if (!hex || !/^#[0-9a-f]{6}$/.test(hex)) return null;
|
|
3215
|
+
const lum = kid(c, "lumMod");
|
|
3216
|
+
if (lum) {
|
|
3217
|
+
const k = num(lum, "val", 1e5) / 1e5;
|
|
3218
|
+
const off = num(kid(c, "lumOff"), "val", 0) / 1e5;
|
|
3219
|
+
hex = `#${[1, 3, 5].map((i) => Math.round(Math.min(255, Math.max(0, parseInt(hex.slice(i, i + 2), 16) * k + 255 * off))).toString(16).padStart(2, "0")).join("")}`;
|
|
3220
|
+
}
|
|
3221
|
+
return hex;
|
|
3222
|
+
}
|
|
3223
|
+
function preset(prst, w, h) {
|
|
3224
|
+
const poly = (pts) => [{ closed: true, anchors: pts.map(([x, y]) => ({ x, y })) }];
|
|
3225
|
+
switch (prst) {
|
|
3226
|
+
case "rect":
|
|
3227
|
+
return [rectSubpath({ x: 0, y: 0, width: w, height: h })];
|
|
3228
|
+
case "roundRect":
|
|
3229
|
+
return [rectSubpath({ x: 0, y: 0, width: w, height: h }, Math.min(w, h) * 0.16667)];
|
|
3230
|
+
case "ellipse":
|
|
3231
|
+
return [ellipseSubpath({ x: 0, y: 0, width: w, height: h })];
|
|
3232
|
+
case "triangle":
|
|
3233
|
+
return poly([[w / 2, 0], [w, h], [0, h]]);
|
|
3234
|
+
case "rtTriangle":
|
|
3235
|
+
return poly([[0, 0], [w, h], [0, h]]);
|
|
3236
|
+
case "diamond":
|
|
3237
|
+
return poly([[w / 2, 0], [w, h / 2], [w / 2, h], [0, h / 2]]);
|
|
3238
|
+
case "parallelogram":
|
|
3239
|
+
return poly([[w * 0.25, 0], [w, 0], [w * 0.75, h], [0, h]]);
|
|
3240
|
+
case "hexagon":
|
|
3241
|
+
return poly([[w * 0.25, 0], [w * 0.75, 0], [w, h / 2], [w * 0.75, h], [w * 0.25, h], [0, h / 2]]);
|
|
3242
|
+
case "line":
|
|
3243
|
+
case "straightConnector1":
|
|
3244
|
+
return [{ closed: false, anchors: [{ x: 0, y: 0 }, { x: w, y: h }] }];
|
|
3245
|
+
default:
|
|
3246
|
+
return null;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
function custom(geom, w, h) {
|
|
3250
|
+
const out = [];
|
|
3251
|
+
for (const path of deep(geom, "path")) {
|
|
3252
|
+
const sx = w / (num(path, "w") || w || 1);
|
|
3253
|
+
const sy = h / (num(path, "h") || h || 1);
|
|
3254
|
+
const pt = (el) => ({ x: num(el, "x") * sx, y: num(el, "y") * sy });
|
|
3255
|
+
let cur = null;
|
|
3256
|
+
for (const cmd of Array.from(path.children)) {
|
|
3257
|
+
const pts = kids(cmd, "pt").map(pt);
|
|
3258
|
+
if (cmd.localName === "moveTo" && pts[0]) {
|
|
3259
|
+
if (cur) out.push(cur);
|
|
3260
|
+
cur = { closed: false, anchors: [{ x: pts[0].x, y: pts[0].y }] };
|
|
3261
|
+
} else if (!cur) continue;
|
|
3262
|
+
else if (cmd.localName === "lnTo" && pts[0]) cur.anchors.push({ x: pts[0].x, y: pts[0].y });
|
|
3263
|
+
else if (cmd.localName === "cubicBezTo" && pts.length === 3) {
|
|
3264
|
+
cur.anchors[cur.anchors.length - 1].out = pts[0];
|
|
3265
|
+
cur.anchors.push({ x: pts[2].x, y: pts[2].y, in: pts[1] });
|
|
3266
|
+
} else if (cmd.localName === "quadBezTo" && pts.length === 2) {
|
|
3267
|
+
const p0 = cur.anchors[cur.anchors.length - 1];
|
|
3268
|
+
cur.anchors[cur.anchors.length - 1].out = { x: p0.x + 2 / 3 * (pts[0].x - p0.x), y: p0.y + 2 / 3 * (pts[0].y - p0.y) };
|
|
3269
|
+
cur.anchors.push({ x: pts[1].x, y: pts[1].y, in: { x: pts[1].x + 2 / 3 * (pts[0].x - pts[1].x), y: pts[1].y + 2 / 3 * (pts[0].y - pts[1].y) } });
|
|
3270
|
+
} else if (cmd.localName === "close") {
|
|
3271
|
+
cur.closed = true;
|
|
3272
|
+
out.push(cur);
|
|
3273
|
+
cur = null;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
if (cur) out.push(cur);
|
|
3277
|
+
}
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3280
|
+
function placement(xfrm, map) {
|
|
3281
|
+
const off = kid(xfrm, "off");
|
|
3282
|
+
const ext = kid(xfrm, "ext");
|
|
3283
|
+
const [x, y, w, h] = map(num(off, "x"), num(off, "y"), num(ext, "cx"), num(ext, "cy"));
|
|
3284
|
+
const rot = num(xfrm, "rot") / 6e4 * (Math.PI / 180);
|
|
3285
|
+
const fx = xfrm?.getAttribute("flipH") === "1" ? -1 : 1;
|
|
3286
|
+
const fy = xfrm?.getAttribute("flipV") === "1" ? -1 : 1;
|
|
3287
|
+
const cx = x + w / 2;
|
|
3288
|
+
const cy = y + h / 2;
|
|
3289
|
+
const cos = Math.cos(rot);
|
|
3290
|
+
const sin = Math.sin(rot);
|
|
3291
|
+
const a = cos * fx;
|
|
3292
|
+
const b = sin * fx;
|
|
3293
|
+
const c = -sin * fy;
|
|
3294
|
+
const d = cos * fy;
|
|
3295
|
+
const e = cx - a * (w / 2) - c * (h / 2);
|
|
3296
|
+
const f = cy - b * (w / 2) - d * (h / 2);
|
|
3297
|
+
return { x, y, w, h, transform: [a, b, c, d, e, f].map((v) => v + 0) };
|
|
3298
|
+
}
|
|
3299
|
+
var idFor = (ctx, spid, name) => {
|
|
3300
|
+
const base = `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "shape"}-${spid}`;
|
|
3301
|
+
let id = base;
|
|
3302
|
+
for (let k = 2; ctx.used.has(id); k++) id = `${base}-${k}`;
|
|
3303
|
+
ctx.used.add(id);
|
|
3304
|
+
ctx.nodes.set(spid, id);
|
|
3305
|
+
return id;
|
|
3306
|
+
};
|
|
3307
|
+
function shapes(ctx, tree, map) {
|
|
3308
|
+
const out = [];
|
|
3309
|
+
for (const el of Array.from(tree.children)) {
|
|
3310
|
+
const nv = el.firstElementChild;
|
|
3311
|
+
const cNvPr = kid(nv, "cNvPr");
|
|
3312
|
+
const spid = num(cNvPr, "id", -1);
|
|
3313
|
+
const name = cNvPr?.getAttribute("name") ?? "";
|
|
3314
|
+
if (el.localName === "grpSp") {
|
|
3315
|
+
const xfrm = kid(kid(el, "grpSpPr"), "xfrm");
|
|
3316
|
+
const off = kid(xfrm, "off");
|
|
3317
|
+
const ext = kid(xfrm, "ext");
|
|
3318
|
+
const chOff = kid(xfrm, "chOff");
|
|
3319
|
+
const chExt = kid(xfrm, "chExt");
|
|
3320
|
+
const sx = num(ext, "cx") / (num(chExt, "cx") || num(ext, "cx") || 1);
|
|
3321
|
+
const sy = num(ext, "cy") / (num(chExt, "cy") || num(ext, "cy") || 1);
|
|
3322
|
+
const inner = (x, y, w2, h2) => map(num(off, "x") + (x - num(chOff, "x")) * sx, num(off, "y") + (y - num(chOff, "y")) * sy, w2 * sx, h2 * sy);
|
|
3323
|
+
const id = idFor(ctx, spid, name || "group");
|
|
3324
|
+
out.push({ id, type: "group", ...name ? { name } : {}, children: shapes(ctx, el, inner) });
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
if (el.localName === "pic" || el.localName === "graphicFrame") {
|
|
3328
|
+
ctx.notes.push(`"${name || `shape ${spid}`}" is a ${el.localName === "pic" ? "picture" : "chart, table or diagram"}, which the artwork cannot hold; it was left out.`);
|
|
3329
|
+
continue;
|
|
3330
|
+
}
|
|
3331
|
+
if (el.localName !== "sp" && el.localName !== "cxnSp") continue;
|
|
3332
|
+
const spPr = kid(el, "spPr");
|
|
3333
|
+
const { w, h, transform } = placement(kid(spPr, "xfrm"), map);
|
|
3334
|
+
const prst = kid(spPr, "prstGeom")?.getAttribute("prst");
|
|
3335
|
+
const cust = kid(spPr, "custGeom");
|
|
3336
|
+
let subpaths = cust ? custom(cust, w, h) : prst ? preset(prst, w, h) : null;
|
|
3337
|
+
if (!subpaths) {
|
|
3338
|
+
if (prst) ctx.notes.push(`"${name}" is a ${prst}, drawn as its bounding rectangle.`);
|
|
3339
|
+
subpaths = [rectSubpath({ x: 0, y: 0, width: w, height: h })];
|
|
3340
|
+
}
|
|
3341
|
+
const style = kid(el, "style");
|
|
3342
|
+
const fill = kid(spPr, "noFill") ? null : colour(ctx, kid(spPr, "solidFill")) ?? (el.localName === "cxnSp" ? null : colour(ctx, kid(style, "fillRef")));
|
|
3343
|
+
const ln = kid(spPr, "ln");
|
|
3344
|
+
const stroke = kid(ln, "noFill") ? null : colour(ctx, kid(ln, "solidFill")) ?? colour(ctx, kid(style, "lnRef"));
|
|
3345
|
+
if (kid(spPr, "gradFill")) ctx.notes.push(`"${name}" has a gradient fill, imported as its first colour or none.`);
|
|
3346
|
+
const text = deep(kid(el, "txBody"), "t").map((t) => t.textContent ?? "").join("").trim();
|
|
3347
|
+
if (text) ctx.notes.push(`"${name}" has text ("${text.slice(0, 40)}${text.length > 40 ? "\u2026" : ""}"), which the artwork cannot hold; only its shape was imported.`);
|
|
3348
|
+
const node = {
|
|
3349
|
+
id: idFor(ctx, spid, name),
|
|
3350
|
+
type: "path",
|
|
3351
|
+
...name ? { name } : {},
|
|
3352
|
+
transform,
|
|
3353
|
+
fill: fill ?? (kid(spPr, "gradFill") ? colour(ctx, kid(deep(spPr, "gs")[0], "srgbClr") ? deep(spPr, "gs")[0] : null) : null),
|
|
3354
|
+
stroke,
|
|
3355
|
+
strokeWidth: Math.max(0.5, (num(ln, "w", 12700) || 12700) / EMU),
|
|
3356
|
+
subpaths
|
|
3357
|
+
};
|
|
3358
|
+
out.push(node);
|
|
3359
|
+
}
|
|
3360
|
+
return out;
|
|
3361
|
+
}
|
|
3362
|
+
var DIRECTION_FROM = { 1: "down", 2: "left", 4: "up", 8: "right" };
|
|
3363
|
+
var DIRECTION_TO = { 1: "up", 2: "right", 4: "down", 8: "left" };
|
|
3364
|
+
function easeOf(ctn) {
|
|
3365
|
+
const accel = num(ctn, "accel") / 1e5;
|
|
3366
|
+
const decel = num(ctn, "decel") / 1e5;
|
|
3367
|
+
if (!accel && !decel) return void 0;
|
|
3368
|
+
return { type: "cubic-bezier", x1: Math.min(1, accel), y1: 0, x2: Math.max(0, 1 - decel), y2: 1 };
|
|
3369
|
+
}
|
|
3370
|
+
function command(ctx, ctn, scene, artwork, target, hidden) {
|
|
3371
|
+
const cls = ctn.getAttribute("presetClass");
|
|
3372
|
+
const id = num(ctn, "presetID");
|
|
3373
|
+
const sub = num(ctn, "presetSubtype");
|
|
3374
|
+
const durs = [ctn, ...deep(ctn, "cTn")].map((c) => num(c, "dur", NaN)).filter(Number.isFinite);
|
|
3375
|
+
const duration = durs.length ? Math.max(...durs) : void 0;
|
|
3376
|
+
const ease = easeOf(ctn);
|
|
3377
|
+
const base = { targets: [target], ...duration !== void 0 ? { duration } : {}, ...ease ? { ease } : {} };
|
|
3378
|
+
const repeat = num(ctn, "repeatCount", 1e3) / 1e3;
|
|
3379
|
+
const auto = ctn.getAttribute("autoRev") === "1";
|
|
3380
|
+
const timing = auto || repeat > 1 ? { repeat: Math.max(1, Math.round(repeat)) * (auto ? 2 : 1), ...auto ? { repeatMode: "mirror" } : {} } : {};
|
|
3381
|
+
const label = `${cls} effect ${id} on "${target}"`;
|
|
3382
|
+
if (cls === "entr") {
|
|
3383
|
+
const by = {
|
|
3384
|
+
1: { type: "enter", ...base, duration: 0 },
|
|
3385
|
+
2: { type: "enter", style: "slide", ...base, direction: DIRECTION_FROM[sub] ?? "up", distance: Math.round(Math.max(scene.width, scene.height) / 3) },
|
|
3386
|
+
10: { type: "enter", style: "fade", ...base },
|
|
3387
|
+
9: { type: "enter", style: "fade", ...base },
|
|
3388
|
+
22: { type: "enter", style: "wipe", ...base, direction: DIRECTION_FROM[sub] ?? "right" },
|
|
3389
|
+
16: { type: "enter", style: "split", ...base, orientation: sub === 26 || sub === 42 ? "vertical" : "horizontal" },
|
|
3390
|
+
6: { type: "enter", style: "iris", ...base, shape: "circle" },
|
|
3391
|
+
4: { type: "enter", style: "iris", ...base, shape: "box" },
|
|
3392
|
+
53: { type: "enter", style: "zoom", ...base },
|
|
3393
|
+
23: { type: "enter", style: "zoom", ...base },
|
|
3394
|
+
31: { type: "enter", style: "pop", ...base },
|
|
3395
|
+
26: { type: "enter", style: "pop", ...base },
|
|
3396
|
+
55: { type: "enter", style: "grow", ...base },
|
|
3397
|
+
37: { type: "enter", style: "rise", ...base },
|
|
3398
|
+
42: { type: "enter", style: "rise", ...base }
|
|
3399
|
+
};
|
|
3400
|
+
if (by[id]) return by[id];
|
|
3401
|
+
ctx.notes.push(`${label} has no match here; it fades in instead.`);
|
|
3402
|
+
return { type: "enter", style: "fade", ...base };
|
|
3403
|
+
}
|
|
3404
|
+
if (cls === "exit") {
|
|
3405
|
+
const by = {
|
|
3406
|
+
1: { type: "exit", ...base, duration: 0 },
|
|
3407
|
+
10: { type: "exit", style: "fade", ...base },
|
|
3408
|
+
9: { type: "exit", style: "fade", ...base },
|
|
3409
|
+
2: { type: "exit", style: "slide", ...base, direction: DIRECTION_TO[sub] ?? "down", distance: Math.round(Math.max(scene.width, scene.height) / 3) },
|
|
3410
|
+
22: { type: "exit", style: "wipe", ...base, direction: DIRECTION_TO[sub] ?? "right" },
|
|
3411
|
+
53: { type: "exit", style: "shrink", ...base },
|
|
3412
|
+
31: { type: "exit", style: "shrink", ...base }
|
|
3413
|
+
};
|
|
3414
|
+
if (by[id]) return by[id];
|
|
3415
|
+
ctx.notes.push(`${label} has no match here; it fades out instead.`);
|
|
3416
|
+
return { type: "exit", style: "fade", ...base };
|
|
3417
|
+
}
|
|
3418
|
+
if (cls === "emph") {
|
|
3419
|
+
const rot = deep(ctn, "animRot")[0];
|
|
3420
|
+
const scale = kid(deep(ctn, "animScale")[0], "by");
|
|
3421
|
+
const clr = colour(ctx, kid(deep(ctn, "animClr")[0], "to"));
|
|
3422
|
+
switch (id) {
|
|
3423
|
+
case 26:
|
|
3424
|
+
return { type: "pulse", ...base, ...timing };
|
|
3425
|
+
case 27:
|
|
3426
|
+
return { type: "color-pulse", ...base, ...clr ? { color: clr } : {}, ...timing };
|
|
3427
|
+
case 32:
|
|
3428
|
+
return { type: "teeter", ...base, ...timing };
|
|
3429
|
+
case 34:
|
|
3430
|
+
return { type: "wave", ...base, ...timing };
|
|
3431
|
+
case 8:
|
|
3432
|
+
return { type: "rotate", ...base, by: rot ? num(rot, "by") / 6e4 : 360, ...timing };
|
|
3433
|
+
case 6:
|
|
3434
|
+
return { type: "scale", ...base, to: scale ? { x: num(scale, "x", 1e5) / 1e5, y: num(scale, "y", 1e5) / 1e5 } : 1.5, ...timing };
|
|
3435
|
+
case 9:
|
|
3436
|
+
return { type: "fade", ...base, to: 0.5, ...timing };
|
|
3437
|
+
case 1:
|
|
3438
|
+
case 19:
|
|
3439
|
+
return clr ? { type: "style", ...base, fill: clr, ...timing } : null;
|
|
3440
|
+
case 7:
|
|
3441
|
+
return clr ? { type: "style", ...base, stroke: clr, ...timing } : null;
|
|
3442
|
+
}
|
|
3443
|
+
ctx.notes.push(`${label} has no match here and was skipped.`);
|
|
3444
|
+
return null;
|
|
3445
|
+
}
|
|
3446
|
+
if (cls === "path") {
|
|
3447
|
+
const motion = deep(ctn, "animMotion")[0];
|
|
3448
|
+
const d = motion?.getAttribute("path");
|
|
3449
|
+
if (!d) return null;
|
|
3450
|
+
const entry = artwork.flatMap(function all(n) {
|
|
3451
|
+
return n.type === "group" ? [n, ...n.children.flatMap(all)] : [n];
|
|
3452
|
+
}).find((n) => n.id === target);
|
|
3453
|
+
const t = entry?.transform ?? [1, 0, 0, 1, 0, 0];
|
|
3454
|
+
const p = entry?.type === "path" ? entry.subpaths[0]?.anchors : void 0;
|
|
3455
|
+
const xs = (p ?? []).map((a) => a.x);
|
|
3456
|
+
const ys = (p ?? []).map((a) => a.y);
|
|
3457
|
+
const lx = xs.length ? (Math.min(...xs) + Math.max(...xs)) / 2 : 0;
|
|
3458
|
+
const ly = ys.length ? (Math.min(...ys) + Math.max(...ys)) / 2 : 0;
|
|
3459
|
+
const cx = t[0] * lx + t[2] * ly + t[4];
|
|
3460
|
+
const cy = t[1] * lx + t[3] * ly + t[5];
|
|
3461
|
+
const nums = d.match(/[MLCZE]|-?[\d.]+(?:e-?\d+)?/gi) ?? [];
|
|
3462
|
+
const sp = { closed: false, anchors: [] };
|
|
3463
|
+
let i = 0;
|
|
3464
|
+
const read = () => ({ x: cx + Number(nums[i++]) * scene.width, y: cy + Number(nums[i++]) * scene.height });
|
|
3465
|
+
while (i < nums.length) {
|
|
3466
|
+
const c = nums[i++].toUpperCase();
|
|
3467
|
+
if (c === "M" || c === "L") sp.anchors.push(read());
|
|
3468
|
+
else if (c === "C") {
|
|
3469
|
+
const c1 = read();
|
|
3470
|
+
const c2 = read();
|
|
3471
|
+
const e = read();
|
|
3472
|
+
if (sp.anchors.length) sp.anchors[sp.anchors.length - 1].out = c1;
|
|
3473
|
+
sp.anchors.push({ ...e, in: c2 });
|
|
3474
|
+
} else if (c === "Z") sp.closed = true;
|
|
3475
|
+
}
|
|
3476
|
+
if (sp.anchors.length < 2) return null;
|
|
3477
|
+
const pathId = `${target}-path-${hidden.length + 1}`;
|
|
3478
|
+
hidden.push({ id: pathId, type: "path", name: "Motion path", visible: false, fill: null, stroke: "#94a3b8", subpaths: [sp] });
|
|
3479
|
+
return { type: "follow", ...base, path: pathId, ...timing };
|
|
3480
|
+
}
|
|
3481
|
+
return null;
|
|
3482
|
+
}
|
|
3483
|
+
async function fromPptxAnimation(data, options = {}) {
|
|
3484
|
+
const Parser = options.DOMParser ?? globalThis.DOMParser;
|
|
3485
|
+
if (!Parser) throw new Error("No DOMParser: pass one in options (jsdom's or xmldom's in Node).");
|
|
3486
|
+
let zip;
|
|
3487
|
+
try {
|
|
3488
|
+
zip = await readZip(data, options);
|
|
3489
|
+
} catch (error) {
|
|
3490
|
+
throw new Error(`This is not a PowerPoint file (.pptx): ${error.message}`);
|
|
3491
|
+
}
|
|
3492
|
+
const ctx = { zip, Parser, notes: [], theme: /* @__PURE__ */ new Map(), nodes: /* @__PURE__ */ new Map(), used: /* @__PURE__ */ new Set() };
|
|
3493
|
+
const pres = await xml(ctx, "ppt/presentation.xml");
|
|
3494
|
+
if (!pres) throw new Error("This is not a PowerPoint file (.pptx): it has no ppt/presentation.xml.");
|
|
3495
|
+
const presRels = await rels(ctx, "ppt/presentation.xml");
|
|
3496
|
+
const slidePaths = deep(pres, "sldId").map((s) => presRels.get(s.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? s.getAttribute("r:id") ?? "") ?? "").filter(Boolean);
|
|
3497
|
+
const n = options.slide ?? 1;
|
|
3498
|
+
if (!(Number.isInteger(n) && n >= 1 && n <= slidePaths.length)) throw new Error(`There is no slide ${n}; the file has ${slidePaths.length} slide${slidePaths.length === 1 ? "" : "s"}.`);
|
|
3499
|
+
const size = deep(pres, "sldSz")[0];
|
|
3500
|
+
const width = Math.round(num(size, "cx", 12192e3) / EMU);
|
|
3501
|
+
const height = Math.round(num(size, "cy", 6858e3) / EMU);
|
|
3502
|
+
for (const [, path] of presRels) {
|
|
3503
|
+
if (!path.includes("theme/")) continue;
|
|
3504
|
+
const theme = await xml(ctx, path);
|
|
3505
|
+
const scheme = deep(theme, "clrScheme")[0];
|
|
3506
|
+
for (const c of Array.from(scheme?.children ?? [])) {
|
|
3507
|
+
const v = c.firstElementChild;
|
|
3508
|
+
const hex = v?.localName === "srgbClr" ? v.getAttribute("val") : v?.getAttribute("lastClr");
|
|
3509
|
+
if (hex) ctx.theme.set(c.localName, `#${hex.toLowerCase()}`);
|
|
3510
|
+
}
|
|
3511
|
+
break;
|
|
3512
|
+
}
|
|
3513
|
+
const slide = await xml(ctx, slidePaths[n - 1]);
|
|
3514
|
+
if (!slide) throw new Error(`Slide ${n} cannot be read.`);
|
|
3515
|
+
const tree = deep(slide, "spTree")[0];
|
|
3516
|
+
const toPx = (x, y, w, h) => [x / EMU, y / EMU, w / EMU, h / EMU];
|
|
3517
|
+
const nodes = tree ? shapes(ctx, tree, toPx) : [];
|
|
3518
|
+
const bg = colour(ctx, deep(deep(slide, "bg")[0], "solidFill")[0] ?? null) ?? "#ffffff";
|
|
3519
|
+
const hidden = [];
|
|
3520
|
+
const shots = [];
|
|
3521
|
+
const effects = deep(deep(slide, "timing")[0], "cTn").filter((c) => c.hasAttribute("presetClass"));
|
|
3522
|
+
for (const ctn of effects) {
|
|
3523
|
+
const spid = num(deep(ctn, "spTgt")[0], "spid", -1);
|
|
3524
|
+
const target = ctx.nodes.get(spid);
|
|
3525
|
+
if (!target) {
|
|
3526
|
+
ctx.notes.push(`An effect targets shape ${spid}, which was not imported; it was skipped.`);
|
|
3527
|
+
continue;
|
|
3528
|
+
}
|
|
3529
|
+
if (deep(ctn, "txEl").length) ctx.notes.push(`An effect on "${target}" animates part of its text, which was not imported; it animates the whole shape.`);
|
|
3530
|
+
const cmd = command(ctx, ctn, { width, height }, nodes, target, hidden);
|
|
3531
|
+
if (!cmd) continue;
|
|
3532
|
+
const kind = ctn.getAttribute("nodeType");
|
|
3533
|
+
const delay = num(kid(kid(ctn, "stCondLst"), "cond"), "delay", 0);
|
|
3534
|
+
if (kind === "clickEffect" || !shots.length) shots.push({ id: `click-${shots.length + 1}`, run: [] });
|
|
3535
|
+
const run = shots[shots.length - 1].run;
|
|
3536
|
+
const placed = { ...cmd, ...delay > 0 ? { delay } : {}, ...kind === "withEffect" && run.length ? { with: "previous" } : {} };
|
|
3537
|
+
run.push(placed);
|
|
3538
|
+
}
|
|
3539
|
+
if (effects.length && !shots.length) ctx.notes.push("The slide's effects could not be read.");
|
|
3540
|
+
const artwork = { width, height, background: bg, nodes: [...nodes, ...hidden] };
|
|
3541
|
+
const scene = { version: 1, name: `slide-${n}`, artwork, shots, ...shots.length > 1 ? { trigger: "click" } : {} };
|
|
3542
|
+
const problems = motionSceneShapeProblems(scene);
|
|
3543
|
+
if (problems.length) throw new Error(`The imported scene is not well formed:
|
|
3544
|
+
${problems.join("\n")}`);
|
|
3545
|
+
return { scene, notes: [...new Set(ctx.notes)], issues: checkMotionScene(scene), slides: slidePaths.length };
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3072
3548
|
// src/animation-editor/controller.ts
|
|
3073
3549
|
var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
|
|
3074
3550
|
var AnimationEditorController = class {
|
|
@@ -3184,14 +3660,14 @@ var AnimationEditorController = class {
|
|
|
3184
3660
|
this.notify();
|
|
3185
3661
|
return;
|
|
3186
3662
|
}
|
|
3187
|
-
const
|
|
3663
|
+
const command3 = {
|
|
3188
3664
|
name: label,
|
|
3189
3665
|
tag: "animation-editor",
|
|
3190
3666
|
execute: () => this.setScene(next),
|
|
3191
3667
|
undo: () => this.setScene(before),
|
|
3192
3668
|
redo: () => this.setScene(next)
|
|
3193
3669
|
};
|
|
3194
|
-
this.history.execute(
|
|
3670
|
+
this.history.execute(command3);
|
|
3195
3671
|
}
|
|
3196
3672
|
refuse(message) {
|
|
3197
3673
|
this.error = message;
|
|
@@ -3348,6 +3824,8 @@ var BASE = {
|
|
|
3348
3824
|
with: { type: "string", enum: ["after", "previous"], description: '"after" (default) starts when the commands before it finish; "previous" starts together with the command before.' },
|
|
3349
3825
|
ease: EASE,
|
|
3350
3826
|
note: { type: "string", description: "Why this command is here, in a few words." },
|
|
3827
|
+
split: { type: "string", enum: ["letter", "word", "line"], description: "For outlined text (a group of letter paths): animate it a letter, word or line at a time in reading order, staggered (default 40 ms a letter, 120 a word, 300 a line). get_text_layout shows the units." },
|
|
3828
|
+
cascade: { type: "string", enum: ["forward", "backward", "center", "edges", "random"], description: "With split: the order units go in (default forward; random is the same every time)." },
|
|
3351
3829
|
repeat: { type: "integer", minimum: 1, maximum: 1e3, description: 'Play it this many times (default 1); "duration" is one cycle. Not for enter or exit.' },
|
|
3352
3830
|
repeatMode: { type: "string", enum: [...MOTION_REPEAT_MODES], description: "loop (default) restarts each cycle, mirror plays every other one backwards, offset carries on from where the last ended (rotate by 90 with repeat 4 makes a full turn)." },
|
|
3353
3831
|
steps: { type: "number", exclusiveMinimum: 0, maximum: 120, description: `Stepped time: hold on a grid of this many frames per second (12 = "on twos"). Default: the scene's steps, else smooth.` }
|
|
@@ -3364,11 +3842,11 @@ var DRAW_ORDER = {
|
|
|
3364
3842
|
sortBy: { type: "string", enum: ["document", "distance"], description: "With sequence: stacking order (default) or distance from origin (a radial reveal)." },
|
|
3365
3843
|
origin: object({ x: { type: "number", description: "Document units." }, y: { type: "number", description: "Document units." } }, ["x", "y"], "With sortBy distance: where strokes are drawn out from (default the target's centre).")
|
|
3366
3844
|
};
|
|
3367
|
-
var
|
|
3845
|
+
var command2 = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
|
|
3368
3846
|
var MOTION_COMMAND_SCHEMA = {
|
|
3369
3847
|
description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
|
|
3370
3848
|
oneOf: [
|
|
3371
|
-
|
|
3849
|
+
command2("enter", "Brings hidden targets in; they are hidden before this shot.", {
|
|
3372
3850
|
...DRAW_ORDER,
|
|
3373
3851
|
style: {
|
|
3374
3852
|
type: "string",
|
|
@@ -3381,18 +3859,18 @@ var MOTION_COMMAND_SCHEMA = {
|
|
|
3381
3859
|
shape: { type: "string", enum: [...MOTION_IRIS_SHAPES], description: "For iris: open through a circle (default; an ellipse on a box that is not square) or a box." },
|
|
3382
3860
|
orientation: { type: "string", enum: [...MOTION_SPLIT_ORIENTATIONS], description: "For split: horizontal (default) opens up and down from a horizontal line through the middle; vertical opens left and right." }
|
|
3383
3861
|
}),
|
|
3384
|
-
|
|
3862
|
+
command2("exit", "Takes targets out.", {
|
|
3385
3863
|
...DRAW_ORDER,
|
|
3386
3864
|
style: { type: "string", enum: [...MOTION_EXIT_STYLES], description: "fade (default), shrink, slide, undraw, or wipe (hides behind a travelling edge through a clip over the target's box: a wipe in, reversed)." },
|
|
3387
3865
|
direction: { type: "string", enum: [...MOTION_DIRECTIONS], description: "For slide: direction of travel (default down). For wipe: the way the edge travels (default right, hiding from the left)." },
|
|
3388
3866
|
distance: { type: "number", description: "For slide: document units (default 40)." }
|
|
3389
3867
|
}),
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3868
|
+
command2("draw", "Draws strokes on (or off) to a fraction of their length.", { ...DRAW_ORDER, comet: FRACTION("A travelling stroke this fraction of the length long: it runs along the path and off the end (leave out from and to)."), to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
|
|
3869
|
+
command2("move", "Moves targets by an offset or so their centre lands on a point.", { via: { type: "array", items: POINT("A point the centre passes through."), minItems: 1, maxItems: 64, description: 'Waypoints (document units) the centre passes through on the way to "to" or "by", smoothly (velocity carries through each) and without overshooting any. Default length 700 ms + 400 per waypoint.' }, pacing: { type: "string", enum: ["even", "equal"], description: "With via: even (default) keeps the speed steady along the path; equal gives each stretch the same time." }, blend: BLEND("replace"), by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
|
|
3870
|
+
command2("rotate", "Turns targets about their centre (degrees, clockwise).", { blend: BLEND("replace"), by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
|
|
3871
|
+
command2("scale", "Scales targets about their centre (1 = rest size).", { blend: BLEND("replace"), to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] }, geometric: { type: "boolean", description: "Grow by a steady factor, which reads as an even zoom (default false)." } }, ["to"]),
|
|
3872
|
+
command2("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
|
|
3873
|
+
command2("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
|
|
3396
3874
|
fill: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
|
|
3397
3875
|
stroke: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
|
|
3398
3876
|
strokeWidth: { type: "number", minimum: 0, description: "Document units." }
|
|
@@ -3411,24 +3889,24 @@ var MOTION_COMMAND_SCHEMA = {
|
|
|
3411
3889
|
["type", "targets", "clip"],
|
|
3412
3890
|
"Plays a clip on the targets."
|
|
3413
3891
|
),
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3892
|
+
command2("match", `Smart Animate: tweens a group into another group (usually hidden), part by part. Parts pair by name, then by id with the group ids taken out (card-a-title \u2194 card-b-title), then by kind in order; each takes its partner's shape, paint and place. Unpaired parts fade out; parts of "into" with no partner do not appear (check_animation lists them).`, { into: { type: "string", minLength: 1, description: "Id of the group to become." } }, ["into"]),
|
|
3893
|
+
command2("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"]),
|
|
3894
|
+
command2("follow", "Moves targets along a path node, centre on the path.", {
|
|
3417
3895
|
path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
|
|
3418
3896
|
rotate: { type: "boolean", description: "Turn with the path (default false); moves and shakes made meanwhile turn with it too." },
|
|
3419
3897
|
from: FRACTION("Where to start along the path (default 0)."),
|
|
3420
3898
|
to: FRACTION("Where to end along the path (default 1).")
|
|
3421
3899
|
}, ["path"]),
|
|
3422
|
-
|
|
3423
|
-
|
|
3900
|
+
command2("pulse", "Scales up and back to draw the eye.", { blend: BLEND("add"), amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
|
|
3901
|
+
command2("shake", "Shakes side to side (or up and down), settling.", {
|
|
3424
3902
|
blend: BLEND("add"),
|
|
3425
3903
|
amount: { type: "number", description: "Document units (default 8)." },
|
|
3426
3904
|
count: { type: "integer", minimum: 1, description: "Shakes (default 3)." },
|
|
3427
3905
|
axis: { type: "string", enum: ["horizontal", "vertical"], description: "Default horizontal." }
|
|
3428
3906
|
}),
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3907
|
+
command2("teeter", "Rocks about the centre either way, settling.", { blend: BLEND("add"), angle: { type: "number", description: "Degrees either way (default 8)." }, count: { type: "integer", minimum: 1, description: "Rocks (default 3)." } }),
|
|
3908
|
+
command2("wave", "Bobs up and down without settling, like something floating (default 1000 ms).", { blend: BLEND("add"), amount: { type: "number", description: "Height in document units (default 6)." }, count: { type: "integer", minimum: 1, description: "Bobs (default 2)." } }),
|
|
3909
|
+
command2("wiggle", "Wanders about its place as smooth seeded noise, easing out of and back to rest (default 1500 ms). The same seed always wanders the same way.", {
|
|
3432
3910
|
blend: BLEND("add"),
|
|
3433
3911
|
amount: { type: "number", description: "Up to about this far, in document units (default 6)." },
|
|
3434
3912
|
frequency: { type: "number", exclusiveMinimum: 0, description: "Wanders per second, in Hz (default 3)." },
|
|
@@ -3436,12 +3914,12 @@ var MOTION_COMMAND_SCHEMA = {
|
|
|
3436
3914
|
rotate: { type: "number", description: "Also turns about the centre by up to about this many degrees (default 0)." },
|
|
3437
3915
|
seed: { type: "integer", minimum: 0, description: "Default: one picked from the target id, so each target wanders its own way." }
|
|
3438
3916
|
}),
|
|
3439
|
-
|
|
3917
|
+
command2("color-pulse", "Tints solid paint toward a colour and back (mixed in OKLab). On a group it tints every path in it.", {
|
|
3440
3918
|
color: { type: "string", description: 'The tint, a CSS colour (default "#f59e0b").' },
|
|
3441
3919
|
paint: { type: "string", enum: ["fill", "stroke"], description: "Which paint (default fill)." },
|
|
3442
3920
|
count: { type: "integer", minimum: 1, description: "Pulses (default 1)." }
|
|
3443
3921
|
}),
|
|
3444
|
-
|
|
3922
|
+
command2("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
|
|
3445
3923
|
effect: { type: "string", minLength: 1, description: `Effect id from get_animation's element "effects".` },
|
|
3446
3924
|
param: { type: "string", minLength: 1, description: "A numeric parameter of that effect (listed with it)." },
|
|
3447
3925
|
to: { type: "number", description: "Value at the end." }
|
|
@@ -3763,6 +4241,21 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3763
4241
|
};
|
|
3764
4242
|
}
|
|
3765
4243
|
};
|
|
4244
|
+
const getTextLayout = {
|
|
4245
|
+
name: "get_text_layout",
|
|
4246
|
+
title: "Read outlined text as letters, words and lines",
|
|
4247
|
+
description: 'Returns how a group of letter paths (text converted to outlines) reads: its lines, each a list of words, each a list of letter ids, in reading order \u2014 the units a command with "split": "letter" | "word" | "line" animates. Errors if the element is not a group with paths.',
|
|
4248
|
+
inputSchema: object({ group: { type: "string", minLength: 1, description: "Element id of the group of letters." } }, ["group"]),
|
|
4249
|
+
annotations: { title: "Read outlined text as letters, words and lines", ...READ },
|
|
4250
|
+
handler: ({ group }) => {
|
|
4251
|
+
const s = scene();
|
|
4252
|
+
const nodes = indexMotionNodes(s.artwork.nodes);
|
|
4253
|
+
if (!nodes.has(group)) throw new Error(`There is no element ${namedWithSuggestion(group, [...nodes.keys()])}.`);
|
|
4254
|
+
const layout = motionTextLayout(nodes, group);
|
|
4255
|
+
if (!layout) throw new Error(`"${group}" is not a group of paths; convert the text to outlines (one path a letter) and group it.`);
|
|
4256
|
+
return { group, letters: layout.lines.reduce((n, l) => n + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
|
|
4257
|
+
}
|
|
4258
|
+
};
|
|
3766
4259
|
const getMotionPath = {
|
|
3767
4260
|
name: "get_motion_path",
|
|
3768
4261
|
title: "Read where a node travels",
|
|
@@ -3835,6 +4328,33 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
3835
4328
|
return apply({ op: "set_artwork", artwork }, "Replace artwork", (s) => ({ elements: describeMotionElements(s) }));
|
|
3836
4329
|
}
|
|
3837
4330
|
};
|
|
4331
|
+
const importPptx = {
|
|
4332
|
+
name: "import_pptx_animation",
|
|
4333
|
+
title: "Import a PowerPoint slide's animation",
|
|
4334
|
+
description: "Replaces the whole animation with a PowerPoint slide (one undo step): its shapes become the artwork and its animation pane becomes shots, a shot per click (with previous / after previous, delays, repeats and acceleration kept; the scene plays on click). Text, pictures and charts are left out and effects with no match fade, with notes. Returns the shots, notes and issues. With dry_run it only reports them.",
|
|
4335
|
+
inputSchema: object(
|
|
4336
|
+
{
|
|
4337
|
+
data: { type: "string", minLength: 1, description: "The .pptx file, base64-encoded." },
|
|
4338
|
+
slide: { type: "integer", minimum: 1, description: "Which slide (1 = first, default 1)." },
|
|
4339
|
+
dry_run: { type: "boolean", description: "Report without replacing anything (default false)." }
|
|
4340
|
+
},
|
|
4341
|
+
["data"]
|
|
4342
|
+
),
|
|
4343
|
+
annotations: { title: "Import a PowerPoint slide's animation", ...DESTRUCTIVE },
|
|
4344
|
+
handler: async ({ data, slide, dry_run = false }) => {
|
|
4345
|
+
let result;
|
|
4346
|
+
try {
|
|
4347
|
+
const bytes = Uint8Array.from(atob(data.replace(/\s+/g, "")), (c) => c.charCodeAt(0));
|
|
4348
|
+
result = await fromPptxAnimation(bytes, slide ? { slide } : {});
|
|
4349
|
+
} catch (error) {
|
|
4350
|
+
throw new Error(`The slide could not be imported: ${error.message} Nothing was changed.`);
|
|
4351
|
+
}
|
|
4352
|
+
const report = { slides: result.slides, notes: result.notes, issues: issueSummary(result.issues), shots: describeMotionShots(result.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
|
|
4353
|
+
if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
|
|
4354
|
+
const applied = controller.apply({ op: "replace_scene", scene: result.scene }, "Import PowerPoint slide");
|
|
4355
|
+
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
|
|
4356
|
+
}
|
|
4357
|
+
};
|
|
3838
4358
|
const importFlairEffect = {
|
|
3839
4359
|
name: "import_flair_effect",
|
|
3840
4360
|
title: "Import a Flair effect",
|
|
@@ -4045,7 +4565,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
4045
4565
|
["shot", "command"]
|
|
4046
4566
|
),
|
|
4047
4567
|
annotations: { title: "Change a command", ...WRITE_IDEMPOTENT },
|
|
4048
|
-
handler: ({ shot, command:
|
|
4568
|
+
handler: ({ shot, command: command3, set, remove }) => apply({ op: "update_command", shot, command: command3, set, remove }, "Change command", (s) => ({ command: describeMotionShots(s).find((x) => x.id === shot).commands[command3] }))
|
|
4049
4569
|
};
|
|
4050
4570
|
const deleteCommands = {
|
|
4051
4571
|
name: "delete_commands",
|
|
@@ -4068,7 +4588,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
4068
4588
|
["shot", "command", "to"]
|
|
4069
4589
|
),
|
|
4070
4590
|
annotations: { title: "Reorder a command", ...WRITE },
|
|
4071
|
-
handler: ({ shot, command:
|
|
4591
|
+
handler: ({ shot, command: command3, to }) => apply({ op: "move_command", shot, command: command3, to }, "Move command", (s) => ({ shot, command: to, from: command3, commands: describeMotionShots(s).find((x) => x.id === shot).commands.map((c) => ({ index: c.index, type: c.type, start: c.start, end: c.end })) }))
|
|
4072
4592
|
};
|
|
4073
4593
|
const setSettings = {
|
|
4074
4594
|
name: "set_animation_settings",
|
|
@@ -4153,8 +4673,8 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
4153
4673
|
return { redone, ...redone ? { label } : {} };
|
|
4154
4674
|
}
|
|
4155
4675
|
};
|
|
4156
|
-
const reads = [getAnimation, getShot, findElements, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
|
|
4157
|
-
const writes = [setArtworkSvg, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
4676
|
+
const reads = [getAnimation, getShot, findElements, getTextLayout, getMotionPath, getTimingGrid, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, exportLottie, seekAnimation];
|
|
4677
|
+
const writes = [setArtworkSvg, importPptx, setDrivers, setClip, copyAnimation, fitAnimation, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, moveCommand, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
4158
4678
|
return {
|
|
4159
4679
|
id: options.id ?? "animation",
|
|
4160
4680
|
namespace: options.namespace,
|
|
@@ -4186,6 +4706,7 @@ Commands
|
|
|
4186
4706
|
- enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
|
|
4187
4707
|
- enter also reveals through a clip over the target's box: wipe (behind an edge travelling "direction", default right), iris (from the centre; "shape": circle or box) and split (from a line through the middle; "orientation": horizontal or vertical). exit wipe hides the same way. Reveals need a width and a height: use draw / undraw for a straight line.
|
|
4188
4708
|
- 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.
|
|
4709
|
+
- Text: convert it to outlines (a group with a path per letter) and give a command "split": "letter", "word" or "line" (and "cascade") to animate it a unit at a time; get_text_layout shows the units.
|
|
4189
4710
|
- Drawing a group of strokes: "order": "sequence" draws them one after another (a signature, a diagram), "sortBy": "distance" from "origin" reveals outward, "pace": "speed" keeps one speed; draw with "comet": 0.2 sends a short stroke along a path and off the end.
|
|
4190
4711
|
- Fast motion reads better with blur: set_animation_settings { "motionBlur": { "shutter": 180 } } (stage and rendered frames only).
|
|
4191
4712
|
- Tie properties together with set_animation_drivers: parallax layers, a wheel that turns as it rolls, a shadow that fades as something rises. Drivers add to the target's own commands.
|
|
@@ -4203,7 +4724,7 @@ Commands
|
|
|
4203
4724
|
- effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
|
|
4204
4725
|
|
|
4205
4726
|
Working method
|
|
4206
|
-
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; or import_flair_effect to start from a Flair effect
|
|
4727
|
+
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; or import_flair_effect / import_pptx_animation to start from a Flair effect or a PowerPoint slide).
|
|
4207
4728
|
2. Plan a few shots, each one idea. Add them with insert_shots (add_commands, update_command, move_command and delete_commands refine a shot), or edit_animation to make several edits as one step (dry_run first when unsure).
|
|
4208
4729
|
- In edit_animation, a later op can use what an earlier one made: "$0.indexes[0]" (the first command op 1 added), "$last.shot".
|
|
4209
4730
|
3. To hit a length, fit_animation (keep the shots whose pace matters). check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result, or with "times" for a contact sheet of several moments; add onion_skin or motion_paths to see the motion in one picture, or get_motion_path for the numbers.
|
|
@@ -4299,11 +4820,11 @@ function parseMotionFieldInput(field, raw) {
|
|
|
4299
4820
|
if (field.kind === "enum" && field.options && !field.options.includes(text)) return { error: `${field.label} must be one of ${field.options.join(", ")}.` };
|
|
4300
4821
|
return { set: text };
|
|
4301
4822
|
}
|
|
4302
|
-
function motionFieldEdit(shot,
|
|
4823
|
+
function motionFieldEdit(shot, command3, field, raw) {
|
|
4303
4824
|
const input = parseMotionFieldInput(field, raw);
|
|
4304
4825
|
if ("error" in input) return input;
|
|
4305
|
-
if ("remove" in input) return { op: "update_command", shot, command:
|
|
4306
|
-
return { op: "update_command", shot, command:
|
|
4826
|
+
if ("remove" in input) return { op: "update_command", shot, command: command3, remove: [field.name] };
|
|
4827
|
+
return { op: "update_command", shot, command: command3, set: { [field.name]: input.set } };
|
|
4307
4828
|
}
|
|
4308
4829
|
function defaultMotionCommand(type, targets, scene) {
|
|
4309
4830
|
if (type === "wait") return { type: "wait", duration: 500 };
|
|
@@ -4352,26 +4873,26 @@ function defaultMotionCommand(type, targets, scene) {
|
|
|
4352
4873
|
return { type, ...base };
|
|
4353
4874
|
}
|
|
4354
4875
|
}
|
|
4355
|
-
function motionCommandSummary(
|
|
4356
|
-
const c =
|
|
4357
|
-
const what = [
|
|
4358
|
-
if (
|
|
4876
|
+
function motionCommandSummary(command3) {
|
|
4877
|
+
const c = command3;
|
|
4878
|
+
const what = [command3.type, typeof c.style === "string" ? c.style : ""].filter(Boolean).join(" ");
|
|
4879
|
+
if (command3.type === "wait") return `wait ${command3.duration} ms`;
|
|
4359
4880
|
const extra = typeof c.into === "string" ? ` \u2192 ${c.into}` : typeof c.path === "string" ? ` along ${c.path}` : "";
|
|
4360
|
-
return `${what} \xB7 ${(
|
|
4881
|
+
return `${what} \xB7 ${(command3.targets ?? []).join(", ")}${extra}`;
|
|
4361
4882
|
}
|
|
4362
4883
|
function motionCommandRows(scene, shotId) {
|
|
4363
4884
|
const index = scene.shots.findIndex((s) => s.id === shotId);
|
|
4364
4885
|
if (index < 0) return [];
|
|
4365
4886
|
const timeline = compileMotionScene(scene);
|
|
4366
|
-
return scene.shots[index].run.map((
|
|
4887
|
+
return scene.shots[index].run.map((command3, i) => {
|
|
4367
4888
|
const t = timeline.commands.find((c) => c.shotIndex === index && c.command === i);
|
|
4368
|
-
const primitive = motionFlairPrimitive(
|
|
4369
|
-
return { index: i, command:
|
|
4889
|
+
const primitive = motionFlairPrimitive(command3);
|
|
4890
|
+
return { index: i, command: command3, summary: motionCommandSummary(command3), start: t?.start ?? 0, end: t?.end ?? 0, ...primitive ? { flairPrimitive: primitive } : {} };
|
|
4370
4891
|
});
|
|
4371
4892
|
}
|
|
4372
|
-
function duplicateMotionCommandEdit(scene, shot,
|
|
4893
|
+
function duplicateMotionCommandEdit(scene, shot, command3) {
|
|
4373
4894
|
const run = scene.shots.find((s) => s.id === shot)?.run ?? [];
|
|
4374
|
-
return { op: "add_commands", shot, commands: run[
|
|
4895
|
+
return { op: "add_commands", shot, commands: run[command3] ? [structuredClone(run[command3])] : [], at: command3 + 1 };
|
|
4375
4896
|
}
|
|
4376
4897
|
var MOTION_EASE_CHOICES = [
|
|
4377
4898
|
{ value: "", label: "Default" },
|
|
@@ -4518,6 +5039,7 @@ export {
|
|
|
4518
5039
|
MOTION_ADDITIVE_PROPERTIES,
|
|
4519
5040
|
MOTION_BLENDS,
|
|
4520
5041
|
MOTION_CAMERA_MS,
|
|
5042
|
+
MOTION_CASCADES,
|
|
4521
5043
|
MOTION_CLIP_TARGET,
|
|
4522
5044
|
MOTION_COMMAND_SCHEMA,
|
|
4523
5045
|
MOTION_COMMAND_TYPES,
|
|
@@ -4540,6 +5062,8 @@ export {
|
|
|
4540
5062
|
MOTION_REPEAT_MODES,
|
|
4541
5063
|
MOTION_SPEED_RANGE,
|
|
4542
5064
|
MOTION_SPLIT_ORIENTATIONS,
|
|
5065
|
+
MOTION_TEXT_STAGGER,
|
|
5066
|
+
MOTION_TEXT_UNITS,
|
|
4543
5067
|
MOTION_TRIGGERS,
|
|
4544
5068
|
MotionSceneFormatError,
|
|
4545
5069
|
activeSegment,
|
|
@@ -4568,6 +5092,7 @@ export {
|
|
|
4568
5092
|
freshShotId,
|
|
4569
5093
|
fromFlairSvgMotion,
|
|
4570
5094
|
fromFlairVectorEffect,
|
|
5095
|
+
fromPptxAnimation,
|
|
4571
5096
|
highlightMotionSvg,
|
|
4572
5097
|
indexMotionNodes,
|
|
4573
5098
|
interpolateMotionValue,
|
|
@@ -4578,12 +5103,14 @@ export {
|
|
|
4578
5103
|
motionBeatAt,
|
|
4579
5104
|
motionBeatTime,
|
|
4580
5105
|
motionBlurTimes,
|
|
5106
|
+
motionCascade,
|
|
4581
5107
|
motionClipAttrs,
|
|
4582
5108
|
motionClipLayout,
|
|
4583
5109
|
motionCommandDuration,
|
|
4584
5110
|
motionCommandFields,
|
|
4585
5111
|
motionCommandRepeat,
|
|
4586
5112
|
motionCommandRows,
|
|
5113
|
+
motionCommandSlots,
|
|
4587
5114
|
motionCommandSummary,
|
|
4588
5115
|
motionContactSheetSvg,
|
|
4589
5116
|
motionCueChoices,
|
|
@@ -4625,6 +5152,8 @@ export {
|
|
|
4625
5152
|
motionShotCueTime,
|
|
4626
5153
|
motionShotTimeScale,
|
|
4627
5154
|
motionStageSvg,
|
|
5155
|
+
motionTextLayout,
|
|
5156
|
+
motionTextUnits,
|
|
4628
5157
|
motionTrackKey,
|
|
4629
5158
|
namedWithSuggestion,
|
|
4630
5159
|
parseMotionDrivers,
|