@pixerate/editor 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation/index.cjs +23 -3
- package/dist/animation/index.cjs.map +1 -1
- package/dist/animation/index.d.cts +3 -2
- package/dist/animation/index.d.ts +3 -2
- package/dist/animation/index.js +23 -3
- package/dist/animation-editor/index.cjs +836 -158
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +454 -40
- package/dist/animation-editor/index.d.ts +454 -40
- package/dist/animation-editor/index.js +790 -112
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/behavior-Ymbld6jL.d.cts +60 -0
- package/dist/behavior-Ymbld6jL.d.ts +60 -0
- package/dist/canvas/index.cjs +35 -29
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +4 -4
- package/dist/canvas/index.d.ts +4 -4
- package/dist/canvas/index.js +9 -3
- package/dist/canvas/index.js.map +1 -1
- package/dist/{chunk-O4ONBPZW.cjs → chunk-2FGRTYJN.cjs} +4 -4
- package/dist/{chunk-O4ONBPZW.cjs.map → chunk-2FGRTYJN.cjs.map} +1 -1
- package/dist/{chunk-XZ4L3R4K.cjs → chunk-2FHLHZAL.cjs} +39 -3
- package/dist/chunk-2FHLHZAL.cjs.map +1 -0
- package/dist/chunk-3NYXDBBW.js +122 -0
- package/dist/chunk-3NYXDBBW.js.map +1 -0
- package/dist/{chunk-NONDTTH7.js → chunk-3TLL5OID.js} +17 -2
- package/dist/{chunk-NONDTTH7.js.map → chunk-3TLL5OID.js.map} +1 -1
- package/dist/{chunk-UB2OY7FD.js → chunk-AGR4P4UZ.js} +38 -2
- package/dist/chunk-AGR4P4UZ.js.map +1 -0
- package/dist/{chunk-SHOVAHUB.js → chunk-JK6M6V7G.js} +2 -2
- package/dist/{chunk-XCXQDPID.cjs → chunk-YJLCBA7V.cjs} +25 -10
- package/dist/chunk-YJLCBA7V.cjs.map +1 -0
- package/dist/chunk-ZCNLBRT6.cjs +122 -0
- package/dist/chunk-ZCNLBRT6.cjs.map +1 -0
- package/dist/{easing-CNGk-W6d.d.ts → easing-lM0Xo9Ni.d.cts} +14 -2
- package/dist/{easing-CNGk-W6d.d.cts → easing-lM0Xo9Ni.d.ts} +14 -2
- package/dist/parameters/index.cjs +2 -2
- package/dist/parameters/index.d.cts +1 -1
- package/dist/parameters/index.d.ts +1 -1
- package/dist/parameters/index.js +1 -1
- package/dist/{player-W-5DX3Go.d.ts → player-DV4Jujdq.d.ts} +1 -1
- package/dist/{player-Dg0FT0UI.d.cts → player-DrcP2xqj.d.cts} +1 -1
- package/dist/{svg-CPK82TtU.d.ts → svg-Dh17jCyA.d.ts} +22 -1
- package/dist/{svg-CLg2zqZA.d.cts → svg-ymShkNtu.d.cts} +22 -1
- package/dist/vector/index.cjs +472 -472
- package/dist/vector/index.d.cts +3 -3
- package/dist/vector/index.d.ts +3 -3
- package/dist/vector/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-UB2OY7FD.js.map +0 -1
- package/dist/chunk-XCXQDPID.cjs.map +0 -1
- package/dist/chunk-XZ4L3R4K.cjs.map +0 -1
- /package/dist/{chunk-SHOVAHUB.js.map → chunk-JK6M6V7G.js.map} +0 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isIdentityMatrix,
|
|
11
11
|
multiplyMatrices,
|
|
12
12
|
nodeMatrix,
|
|
13
|
+
parseSvgTransform,
|
|
13
14
|
parseVectorSvg,
|
|
14
15
|
rotateMatrix,
|
|
15
16
|
scaleMatrix,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
vectorDocumentToSvg,
|
|
18
19
|
vectorNodeBounds,
|
|
19
20
|
vectorPathWorldBezier
|
|
20
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-3TLL5OID.js";
|
|
21
22
|
import {
|
|
22
23
|
bezierPathLength,
|
|
23
24
|
bezierPathPointAtLength,
|
|
@@ -27,9 +28,13 @@ import {
|
|
|
27
28
|
import {
|
|
28
29
|
HistoryManager
|
|
29
30
|
} from "../chunk-QVZZGT4X.js";
|
|
31
|
+
import {
|
|
32
|
+
behaviorProgress,
|
|
33
|
+
motionSeedFromId
|
|
34
|
+
} from "../chunk-3NYXDBBW.js";
|
|
30
35
|
import {
|
|
31
36
|
AnimationPlayer
|
|
32
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-JK6M6V7G.js";
|
|
33
38
|
import {
|
|
34
39
|
mixColorsOklab
|
|
35
40
|
} from "../chunk-ZRJMJIQU.js";
|
|
@@ -42,8 +47,10 @@ import {
|
|
|
42
47
|
ANIMATION_EASING_PRESETS,
|
|
43
48
|
evaluateEasing,
|
|
44
49
|
isAnimationEasing,
|
|
45
|
-
resolveEasing
|
|
46
|
-
|
|
50
|
+
resolveEasing,
|
|
51
|
+
reverseCubicBezier,
|
|
52
|
+
springSettleMs
|
|
53
|
+
} from "../chunk-AGR4P4UZ.js";
|
|
47
54
|
|
|
48
55
|
// src/animation-editor/types.ts
|
|
49
56
|
var MOTION_COMMAND_TYPES = [
|
|
@@ -59,11 +66,18 @@ var MOTION_COMMAND_TYPES = [
|
|
|
59
66
|
"follow",
|
|
60
67
|
"pulse",
|
|
61
68
|
"shake",
|
|
69
|
+
"teeter",
|
|
70
|
+
"wave",
|
|
71
|
+
"wiggle",
|
|
72
|
+
"color-pulse",
|
|
62
73
|
"effect",
|
|
63
74
|
"wait"
|
|
64
75
|
];
|
|
65
|
-
var MOTION_ENTER_STYLES = ["fade", "pop", "grow", "slide", "draw"];
|
|
66
|
-
var
|
|
76
|
+
var MOTION_ENTER_STYLES = ["fade", "pop", "grow", "slide", "draw", "zoom", "rise", "wipe", "iris", "split"];
|
|
77
|
+
var MOTION_REPEAT_MODES = ["loop", "mirror", "offset"];
|
|
78
|
+
var MOTION_EXIT_STYLES = ["fade", "shrink", "slide", "undraw", "wipe"];
|
|
79
|
+
var MOTION_IRIS_SHAPES = ["circle", "box"];
|
|
80
|
+
var MOTION_SPLIT_ORIENTATIONS = ["horizontal", "vertical"];
|
|
67
81
|
var MOTION_DIRECTIONS = ["up", "down", "left", "right"];
|
|
68
82
|
var MOTION_DEFAULT_DURATIONS = {
|
|
69
83
|
enter: 600,
|
|
@@ -77,12 +91,37 @@ var MOTION_DEFAULT_DURATIONS = {
|
|
|
77
91
|
follow: 1400,
|
|
78
92
|
pulse: 600,
|
|
79
93
|
shake: 500,
|
|
94
|
+
teeter: 600,
|
|
95
|
+
wave: 1e3,
|
|
96
|
+
wiggle: 1500,
|
|
97
|
+
"color-pulse": 600,
|
|
80
98
|
effect: 900
|
|
81
99
|
};
|
|
82
100
|
var MOTION_DEFAULT_HOLD = 600;
|
|
83
101
|
var MOTION_CAMERA_MS = 1e3;
|
|
84
102
|
var MOTION_DRAW_MS_PER_UNIT = 1.5;
|
|
85
103
|
var MOTION_DRAW_RANGE = [500, 2500];
|
|
104
|
+
function motionFlairPrimitive(command2) {
|
|
105
|
+
switch (command2.type) {
|
|
106
|
+
case "enter":
|
|
107
|
+
return `entrance.${command2.style ?? "fade"}`;
|
|
108
|
+
case "exit":
|
|
109
|
+
return `exit.${command2.style ?? "fade"}`;
|
|
110
|
+
case "pulse":
|
|
111
|
+
case "shake":
|
|
112
|
+
case "teeter":
|
|
113
|
+
case "wave":
|
|
114
|
+
case "wiggle":
|
|
115
|
+
case "color-pulse":
|
|
116
|
+
return `emphasis.${command2.type}`;
|
|
117
|
+
case "rotate":
|
|
118
|
+
return command2.by !== void 0 ? "emphasis.spin" : void 0;
|
|
119
|
+
case "follow":
|
|
120
|
+
return "path.follow";
|
|
121
|
+
default:
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
86
125
|
|
|
87
126
|
// src/animation-editor/issues.ts
|
|
88
127
|
var MOTION_ISSUE_CODES = {
|
|
@@ -91,18 +130,20 @@ var MOTION_ISSUE_CODES = {
|
|
|
91
130
|
"unknown-target": { severity: "error", wording: "A command names a node the artwork does not have." },
|
|
92
131
|
"unknown-path": { severity: "error", wording: "A follow command names a path the artwork does not have." },
|
|
93
132
|
"not-a-path": { severity: "error", wording: "A follow path must be a path node, not a group." },
|
|
133
|
+
"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." },
|
|
94
134
|
"unknown-shape": { severity: "error", wording: "A morph names a shape the artwork does not have." },
|
|
95
135
|
"unknown-effect": { severity: "error", wording: "An effect command names an effect the node does not have." },
|
|
96
136
|
"unknown-effect-param": { severity: "error", wording: "An effect command names a parameter that is not a number of that effect." },
|
|
97
137
|
"unknown-frame": { severity: "error", wording: "A camera frames a node the artwork does not have." },
|
|
98
138
|
"invalid-camera": { severity: "error", wording: "A camera rectangle has no area." },
|
|
99
139
|
"invalid-color": { severity: "error", wording: "A colour cannot be read." },
|
|
100
|
-
"paint-not-color": { severity: "warning", wording: "A style command recolours a gradient or empty paint, which only switches at the end." },
|
|
140
|
+
"paint-not-color": { severity: "warning", wording: "A style command recolours a gradient or empty paint, which only switches at the end, or a colour pulse has no solid paint to tint." },
|
|
101
141
|
"overlap": { severity: "warning", wording: "Two commands change the same property of a node at the same time; the later one wins." },
|
|
102
142
|
"enter-visible": { severity: "warning", wording: "A node enters while it is already showing, so it blinks out first." },
|
|
103
143
|
"exit-hidden": { severity: "warning", wording: "A node exits or animates while it is hidden." },
|
|
104
144
|
"out-of-frame": { severity: "warning", wording: "A node a command animates ends up outside the camera." },
|
|
105
145
|
"too-fast": { severity: "warning", wording: "A change is too quick to follow." },
|
|
146
|
+
"spring-cut-short": { severity: "warning", wording: "A spring is given a duration shorter than it takes to settle, so it jumps to its end." },
|
|
106
147
|
"scene-too-long": { severity: "warning", wording: "The scene runs past its target length." },
|
|
107
148
|
"scene-too-short": { severity: "warning", wording: "The scene ends before its target length." },
|
|
108
149
|
"unknown-control-color": { severity: "warning", wording: "A control stands for a colour the scene never uses." },
|
|
@@ -160,23 +201,27 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
160
201
|
this.problems = problems;
|
|
161
202
|
}
|
|
162
203
|
};
|
|
163
|
-
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "target", "controls", "presets"];
|
|
204
|
+
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "loop", "fps", "steps", "target", "controls", "presets"];
|
|
164
205
|
var SHOT_KEYS = ["id", "label", "camera", "run", "hold"];
|
|
165
|
-
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms"];
|
|
166
|
-
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note"];
|
|
206
|
+
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
|
|
207
|
+
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps"];
|
|
167
208
|
var COMMAND_KEYS = {
|
|
168
|
-
enter: [...BASE_KEYS, "style", "direction", "distance"],
|
|
209
|
+
enter: [...BASE_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
|
|
169
210
|
exit: [...BASE_KEYS, "style", "direction", "distance"],
|
|
170
211
|
draw: [...BASE_KEYS, "from", "to"],
|
|
171
212
|
move: [...BASE_KEYS, "by", "to"],
|
|
172
213
|
rotate: [...BASE_KEYS, "by", "to"],
|
|
173
|
-
scale: [...BASE_KEYS, "to"],
|
|
214
|
+
scale: [...BASE_KEYS, "to", "geometric"],
|
|
174
215
|
fade: [...BASE_KEYS, "to"],
|
|
175
216
|
style: [...BASE_KEYS, "fill", "stroke", "strokeWidth"],
|
|
176
217
|
morph: [...BASE_KEYS, "into"],
|
|
177
218
|
follow: [...BASE_KEYS, "path", "rotate", "from", "to"],
|
|
178
219
|
pulse: [...BASE_KEYS, "amount", "count"],
|
|
179
|
-
shake: [...BASE_KEYS, "amount", "count"],
|
|
220
|
+
shake: [...BASE_KEYS, "amount", "count", "axis"],
|
|
221
|
+
teeter: [...BASE_KEYS, "angle", "count"],
|
|
222
|
+
wave: [...BASE_KEYS, "amount", "count"],
|
|
223
|
+
wiggle: [...BASE_KEYS, "amount", "frequency", "octaves", "rotate", "seed"],
|
|
224
|
+
"color-pulse": [...BASE_KEYS, "color", "paint", "count"],
|
|
180
225
|
effect: [...BASE_KEYS, "effect", "param", "to"],
|
|
181
226
|
wait: ["type", "duration", "with", "note"]
|
|
182
227
|
};
|
|
@@ -214,7 +259,13 @@ function commandShapeProblems(command2, where) {
|
|
|
214
259
|
nonNegative("duration");
|
|
215
260
|
nonNegative("delay");
|
|
216
261
|
nonNegative("stagger");
|
|
217
|
-
if (command2.ease !== void 0 && !isAnimationEasing(command2.ease)) p(`"ease" is not an easing; use a preset such as "ease-in-out" or { "type": "cubic-bezier", \u2026 }.`);
|
|
262
|
+
if (command2.ease !== void 0 && !isAnimationEasing(command2.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 }.`);
|
|
263
|
+
if (command2.repeat !== void 0) {
|
|
264
|
+
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.`);
|
|
265
|
+
else if (!(Number.isInteger(command2.repeat) && command2.repeat >= 1 && command2.repeat <= 1e3)) p(`"repeat" must be a whole number from 1 to 1000.`);
|
|
266
|
+
}
|
|
267
|
+
if (command2.repeatMode !== void 0 && !MOTION_REPEAT_MODES.includes(command2.repeatMode)) p(`"repeatMode" must be one of ${MOTION_REPEAT_MODES.join(", ")}.`);
|
|
268
|
+
if (command2.steps !== void 0 && !(isNum(command2.steps) && command2.steps > 0 && command2.steps <= 120)) p(`"steps" must be frames per second from above 0 to 120 (12 holds each pose for two frames at 24 fps).`);
|
|
218
269
|
switch (type) {
|
|
219
270
|
case "enter":
|
|
220
271
|
case "exit": {
|
|
@@ -223,6 +274,9 @@ function commandShapeProblems(command2, where) {
|
|
|
223
274
|
p(`"style" must be one of ${styles.join(", ")}${typeof command2.style === "string" ? ` (got ${namedWithSuggestion(command2.style, styles)})` : ""}.`);
|
|
224
275
|
if (command2.direction !== void 0 && !MOTION_DIRECTIONS.includes(command2.direction)) p(`"direction" must be one of ${MOTION_DIRECTIONS.join(", ")}.`);
|
|
225
276
|
optionalNumber("distance");
|
|
277
|
+
if (command2.scale !== void 0 && !(isNum(command2.scale) && command2.scale >= 0)) p(`"scale" must be a number \u2265 0 (the starting scale for pop and zoom).`);
|
|
278
|
+
if (command2.shape !== void 0 && !MOTION_IRIS_SHAPES.includes(command2.shape)) p(`"shape" must be one of ${MOTION_IRIS_SHAPES.join(", ")} (the opening of an iris).`);
|
|
279
|
+
if (command2.orientation !== void 0 && !MOTION_SPLIT_ORIENTATIONS.includes(command2.orientation)) p(`"orientation" must be one of ${MOTION_SPLIT_ORIENTATIONS.join(", ")} (the line a split opens from).`);
|
|
226
280
|
break;
|
|
227
281
|
}
|
|
228
282
|
case "draw":
|
|
@@ -243,6 +297,7 @@ function commandShapeProblems(command2, where) {
|
|
|
243
297
|
break;
|
|
244
298
|
case "scale":
|
|
245
299
|
if (!(isNum(command2.to) || isPoint(command2.to))) p(`"to" must be a number or { "x": number, "y": number }.`);
|
|
300
|
+
if (command2.geometric !== void 0 && typeof command2.geometric !== "boolean") p(`"geometric" must be true or false.`);
|
|
246
301
|
break;
|
|
247
302
|
case "fade":
|
|
248
303
|
if (!(isNum(command2.to) && command2.to >= 0 && command2.to <= 1)) p(`"to" must be an opacity from 0 to 1.`);
|
|
@@ -264,8 +319,22 @@ function commandShapeProblems(command2, where) {
|
|
|
264
319
|
break;
|
|
265
320
|
case "pulse":
|
|
266
321
|
case "shake":
|
|
322
|
+
case "teeter":
|
|
323
|
+
case "wave":
|
|
324
|
+
case "color-pulse":
|
|
267
325
|
optionalNumber("amount");
|
|
326
|
+
optionalNumber("angle");
|
|
268
327
|
if (command2.count !== void 0 && !(Number.isInteger(command2.count) && command2.count >= 1)) p(`"count" must be a whole number \u2265 1.`);
|
|
328
|
+
if (command2.axis !== void 0 && command2.axis !== "horizontal" && command2.axis !== "vertical") p(`"axis" must be "horizontal" or "vertical".`);
|
|
329
|
+
if (command2.color !== void 0 && typeof command2.color !== "string") p(`"color" must be a CSS colour.`);
|
|
330
|
+
if (command2.paint !== void 0 && command2.paint !== "fill" && command2.paint !== "stroke") p(`"paint" must be "fill" or "stroke".`);
|
|
331
|
+
break;
|
|
332
|
+
case "wiggle":
|
|
333
|
+
optionalNumber("amount");
|
|
334
|
+
optionalNumber("rotate");
|
|
335
|
+
if (command2.frequency !== void 0 && !(isNum(command2.frequency) && command2.frequency > 0)) p(`"frequency" must be a number of Hz > 0.`);
|
|
336
|
+
if (command2.octaves !== void 0 && !(Number.isInteger(command2.octaves) && command2.octaves >= 1 && command2.octaves <= 8)) p(`"octaves" must be a whole number from 1 to 8.`);
|
|
337
|
+
if (command2.seed !== void 0 && !(Number.isInteger(command2.seed) && command2.seed >= 0)) p(`"seed" must be a whole number \u2265 0.`);
|
|
269
338
|
break;
|
|
270
339
|
case "effect":
|
|
271
340
|
if (typeof command2.effect !== "string" || !command2.effect) p(`"effect" must be the id of one of the node's effects.`);
|
|
@@ -294,6 +363,7 @@ function shotShapeProblems(shot, where) {
|
|
|
294
363
|
} else if (!isRect) problems.push(`${name} camera: give { x, y, width, height } or { frame: "<node id>" }.`);
|
|
295
364
|
if (c.ease !== void 0 && !isAnimationEasing(c.ease)) problems.push(`${name} camera: "ease" is not an easing.`);
|
|
296
365
|
if (c.ms !== void 0 && !(isNum(c.ms) && c.ms >= 0)) problems.push(`${name} camera: "ms" must be a number \u2265 0.`);
|
|
366
|
+
if (c.geometric !== void 0 && typeof c.geometric !== "boolean") problems.push(`${name} camera: "geometric" must be true or false.`);
|
|
297
367
|
}
|
|
298
368
|
}
|
|
299
369
|
if (!Array.isArray(shot.run)) problems.push(`${name}: "run" must be a list of commands (it may be empty).`);
|
|
@@ -312,6 +382,7 @@ function motionSceneShapeProblems(input) {
|
|
|
312
382
|
problems.push('Scene: "artwork" must be a vector document { width, height, nodes }.');
|
|
313
383
|
if (input.loop !== void 0 && !["none", "loop", "ping-pong"].includes(input.loop)) problems.push('Scene: "loop" must be "none", "loop" or "ping-pong".');
|
|
314
384
|
if (input.fps !== void 0 && !(isNum(input.fps) && input.fps > 0 && input.fps <= 120)) problems.push('Scene: "fps" must be a number from 1 to 120.');
|
|
385
|
+
if (input.steps !== void 0 && !(isNum(input.steps) && input.steps > 0 && input.steps <= 120)) problems.push('Scene: "steps" must be frames per second from above 0 to 120.');
|
|
315
386
|
if (input.target !== void 0 && !(Array.isArray(input.target) && input.target.length === 2 && input.target.every(isNum) && input.target[0] <= input.target[1]))
|
|
316
387
|
problems.push('Scene: "target" must be [min, max] seconds.');
|
|
317
388
|
if (input.controls !== void 0) {
|
|
@@ -358,7 +429,7 @@ function indexMotionNodes(nodes) {
|
|
|
358
429
|
const local = vectorNodeBounds(node);
|
|
359
430
|
const bounds = vectorNodeBounds(node, parentWorld);
|
|
360
431
|
const center = local ? { x: local.x + local.width / 2, y: local.y + local.height / 2 } : { x: 0, y: 0 };
|
|
361
|
-
if (!out.has(node.id)) out.set(node.id, { node, ancestors, parentWorld, center, bounds });
|
|
432
|
+
if (!out.has(node.id)) out.set(node.id, { node, ancestors, parentWorld, center, bounds, box: local });
|
|
362
433
|
if (node.type === "group") walk(node.children, multiplyMatrices(nodeMatrix(node), parentWorld), [...ancestors, node.id]);
|
|
363
434
|
}
|
|
364
435
|
};
|
|
@@ -375,6 +446,56 @@ function firstPaint(node, key) {
|
|
|
375
446
|
}
|
|
376
447
|
return void 0;
|
|
377
448
|
}
|
|
449
|
+
function strokeReach(node) {
|
|
450
|
+
let reach = 0;
|
|
451
|
+
for (const p of node.type === "path" ? [node] : collectPathNodes(node)) {
|
|
452
|
+
if (p.stroke == null || p.stroke === "none") continue;
|
|
453
|
+
const width = p.strokeWidth ?? 1;
|
|
454
|
+
reach = Math.max(reach, p.strokeAlign === "outside" ? width : p.strokeAlign === "inside" ? 0 : width / 2);
|
|
455
|
+
}
|
|
456
|
+
return reach;
|
|
457
|
+
}
|
|
458
|
+
function motionRevealPadding(entry) {
|
|
459
|
+
const box = entry.box;
|
|
460
|
+
if (!box || !(box.width > 0) || !(box.height > 0)) return null;
|
|
461
|
+
const s = strokeReach(entry.node);
|
|
462
|
+
return [Math.min(1, s / box.width + 0.02), Math.min(1, s / box.height + 0.02)];
|
|
463
|
+
}
|
|
464
|
+
var clipRound = (n) => Math.round(n * 1e4) / 1e4 || 0;
|
|
465
|
+
function revealFull(property, [px, py]) {
|
|
466
|
+
return (property === "clipRect" ? [-px, -py, 1 + 2 * px, 1 + 2 * py] : [0.5, 0.5, Math.hypot(0.5 + px, 0.5 + py)]).map(clipRound);
|
|
467
|
+
}
|
|
468
|
+
var OPPOSITE = { up: "down", down: "up", left: "right", right: "left" };
|
|
469
|
+
function motionRevealClip(entry, reveal) {
|
|
470
|
+
const pad = motionRevealPadding(entry);
|
|
471
|
+
if (!pad) return null;
|
|
472
|
+
const [px, py] = pad;
|
|
473
|
+
const [w, h] = [1 + 2 * px, 1 + 2 * py];
|
|
474
|
+
const rect = (from, to) => ({ property: "clipRect", from: from.map(clipRound), to: to.map(clipRound) });
|
|
475
|
+
if (reveal.style === "iris") {
|
|
476
|
+
if (reveal.shape === "box") return rect([0.5, 0.5, 0, 0], [-px, -py, w, h]);
|
|
477
|
+
return { property: "clipCircle", from: [0.5, 0.5, 0], to: revealFull("clipCircle", pad) };
|
|
478
|
+
}
|
|
479
|
+
if (reveal.style === "split") {
|
|
480
|
+
if (reveal.orientation === "vertical") return rect([0.5, -py, 0, h], [-px, -py, w, h]);
|
|
481
|
+
return rect([-px, 0.5, w, 0], [-px, -py, w, h]);
|
|
482
|
+
}
|
|
483
|
+
const direction = reveal.direction ?? "right";
|
|
484
|
+
if (reveal.hide) {
|
|
485
|
+
const shown = motionRevealClip(entry, { style: "wipe", direction: OPPOSITE[direction] });
|
|
486
|
+
return { ...shown, from: shown.to, to: shown.from };
|
|
487
|
+
}
|
|
488
|
+
switch (direction) {
|
|
489
|
+
case "right":
|
|
490
|
+
return rect([-px, -py, 0, h], [-px, -py, w, h]);
|
|
491
|
+
case "left":
|
|
492
|
+
return rect([1 + px, -py, 0, h], [-px, -py, w, h]);
|
|
493
|
+
case "down":
|
|
494
|
+
return rect([-px, -py, w, 0], [-px, -py, w, h]);
|
|
495
|
+
case "up":
|
|
496
|
+
return rect([-px, 1 + py, w, 0], [-px, -py, w, h]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
378
499
|
function motionRestValue(entry, property, artwork) {
|
|
379
500
|
const node = entry?.node;
|
|
380
501
|
switch (property) {
|
|
@@ -402,6 +523,11 @@ function motionRestValue(entry, property, artwork) {
|
|
|
402
523
|
}
|
|
403
524
|
case "shape":
|
|
404
525
|
return node?.id ?? "";
|
|
526
|
+
case "clipRect":
|
|
527
|
+
case "clipCircle": {
|
|
528
|
+
const pad = entry && motionRevealPadding(entry);
|
|
529
|
+
return pad ? revealFull(property, pad) : [];
|
|
530
|
+
}
|
|
405
531
|
case "camera":
|
|
406
532
|
return [0, 0, artwork.width, artwork.height];
|
|
407
533
|
}
|
|
@@ -423,16 +549,36 @@ function interpolateMotionValue(from, to, u) {
|
|
|
423
549
|
}
|
|
424
550
|
function segmentProgress(segment, time) {
|
|
425
551
|
const span = segment.end - segment.start;
|
|
426
|
-
|
|
427
|
-
|
|
552
|
+
let elapsed = time - segment.start;
|
|
553
|
+
if (segment.steps && segment.steps > 0 && elapsed < span) elapsed = Math.floor(elapsed * segment.steps / 1e3 + 1e-9) / segment.steps * 1e3;
|
|
554
|
+
const u = span <= 0 ? 1 : Math.min(1, Math.max(0, elapsed / span));
|
|
555
|
+
return { u, eased: evaluateEasing(segment.ease, segment.reverse ? 1 - u : u, span) };
|
|
428
556
|
}
|
|
429
|
-
function
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
557
|
+
function geometricValue(from, to, e) {
|
|
558
|
+
const grow = (a, b) => a > 0 && b > 0 ? a * (b / a) ** e : void 0;
|
|
559
|
+
if (typeof from === "number" && typeof to === "number") return grow(from, to);
|
|
560
|
+
if (Array.isArray(from) && Array.isArray(to) && from.length === 4 && to.length === 4) {
|
|
561
|
+
const [x0, y0, w0, h0] = from;
|
|
562
|
+
const [x1, y1, w1, h1] = to;
|
|
563
|
+
const w = grow(w0, w1);
|
|
564
|
+
const h = grow(h0, h1);
|
|
565
|
+
if (w === void 0 || h === void 0) return void 0;
|
|
566
|
+
const f = Math.abs(w1 - w0) > 1e-9 ? (w - w0) / (w1 - w0) : e;
|
|
567
|
+
const cx = x0 + w0 / 2 + (x1 + w1 / 2 - x0 - w0 / 2) * f;
|
|
568
|
+
const cy = y0 + h0 / 2 + (y1 + h1 / 2 - y0 - h0 / 2) * f;
|
|
569
|
+
return [cx - w / 2, cy - h / 2, w, h];
|
|
570
|
+
}
|
|
571
|
+
return void 0;
|
|
572
|
+
}
|
|
573
|
+
function segmentValue(segment, time, raw = false) {
|
|
574
|
+
const { eased } = segmentProgress(segment, time);
|
|
575
|
+
if (segment.geometric && !segment.behavior) {
|
|
576
|
+
const g = geometricValue(segment.from, segment.to, eased);
|
|
577
|
+
if (g !== void 0) return g;
|
|
578
|
+
}
|
|
579
|
+
if (!segment.behavior || raw) return interpolateMotionValue(segment.from, segment.to, eased);
|
|
580
|
+
const p = behaviorProgress(segment.behavior, eased, segment.index ?? 0, segment.end - segment.start);
|
|
581
|
+
return typeof segment.from === "string" ? interpolateMotionValue(segment.from, segment.to, Math.min(1, Math.max(0, p))) : interpolateMotionValue(segment.from, segment.to, p);
|
|
436
582
|
}
|
|
437
583
|
function activeSegment(track, time) {
|
|
438
584
|
if (!track?.length) return void 0;
|
|
@@ -440,10 +586,10 @@ function activeSegment(track, time) {
|
|
|
440
586
|
for (const s of track) if (s.start <= time && (!found || s.start >= found.start)) found = s;
|
|
441
587
|
return found;
|
|
442
588
|
}
|
|
443
|
-
function trackValue(track, time) {
|
|
589
|
+
function trackValue(track, time, raw = false) {
|
|
444
590
|
if (!track?.length) return void 0;
|
|
445
591
|
const s = activeSegment(track, time);
|
|
446
|
-
if (s) return segmentValue(s, time);
|
|
592
|
+
if (s) return segmentValue(s, time, raw);
|
|
447
593
|
let first = track[0];
|
|
448
594
|
for (const t of track) if (t.start < first.start) first = t;
|
|
449
595
|
return first.from;
|
|
@@ -480,9 +626,21 @@ function motionPathLength(node, parentWorld = IDENTITY_MATRIX) {
|
|
|
480
626
|
}
|
|
481
627
|
return longest;
|
|
482
628
|
}
|
|
629
|
+
function motionCommandRepeat(command2) {
|
|
630
|
+
if (command2.type === "wait" || command2.type === "enter" || command2.type === "exit") return 1;
|
|
631
|
+
const n = command2.repeat ?? 1;
|
|
632
|
+
return Number.isInteger(n) && n >= 1 ? n : 1;
|
|
633
|
+
}
|
|
483
634
|
function motionCommandDuration(command2, scene, target) {
|
|
635
|
+
return motionCycleDuration(command2, scene, target) * motionCommandRepeat(command2);
|
|
636
|
+
}
|
|
637
|
+
function motionCycleDuration(command2, scene, target) {
|
|
484
638
|
if (command2.type === "wait") return command2.duration;
|
|
485
639
|
if (command2.duration !== void 0) return command2.duration;
|
|
640
|
+
if (command2.ease !== void 0) {
|
|
641
|
+
const ease = resolveEasing(command2.ease);
|
|
642
|
+
if (ease !== "hold" && ease.type === "spring") return springSettleMs(ease);
|
|
643
|
+
}
|
|
486
644
|
if (command2.type === "draw" || command2.type === "enter" && command2.style === "draw" || command2.type === "exit" && command2.style === "undraw") {
|
|
487
645
|
const entry = target ? indexMotionNodes(scene.artwork.nodes).get(target) : void 0;
|
|
488
646
|
const length = entry ? motionPathLength(entry.node) : 0;
|
|
@@ -511,7 +669,7 @@ function compileMotionScene(scene) {
|
|
|
511
669
|
const rect = resolveMotionCamera(shot.camera, scene);
|
|
512
670
|
if (rect && rect.width > 0 && rect.height > 0) {
|
|
513
671
|
const ms = index === 0 ? 0 : shot.camera.ms ?? MOTION_CAMERA_MS;
|
|
514
|
-
cameraMoves.push({ shot: shot.id, start, end: start + ms, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT });
|
|
672
|
+
cameraMoves.push({ shot: shot.id, start, end: start + ms, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
|
|
515
673
|
cursor = start + ms;
|
|
516
674
|
}
|
|
517
675
|
}
|
|
@@ -552,7 +710,7 @@ function compileMotionScene(scene) {
|
|
|
552
710
|
jobs.push({
|
|
553
711
|
start: move.start,
|
|
554
712
|
order: order++,
|
|
555
|
-
run: () => push({ node: "", property: "camera", start: move.start, end: move.end, from: valueAt("", "camera", move.start), to: [move.to.x, move.to.y, move.to.width, move.to.height], ease: move.ease, shot: move.shot, command: -1 })
|
|
713
|
+
run: () => push({ node: "", property: "camera", start: move.start, end: move.end, from: valueAt("", "camera", move.start), to: [move.to.x, move.to.y, move.to.width, move.to.height], ease: move.ease, shot: move.shot, command: -1, ...move.geometric ? { geometric: true } : {} })
|
|
556
714
|
});
|
|
557
715
|
}
|
|
558
716
|
for (const timing of commands) {
|
|
@@ -563,11 +721,37 @@ function compileMotionScene(scene) {
|
|
|
563
721
|
const entry = nodes.get(target);
|
|
564
722
|
if (!entry) return;
|
|
565
723
|
const start = timing.start + k * (command2.stagger ?? 0);
|
|
566
|
-
const
|
|
567
|
-
jobs.push({ start, order: order++, run: () =>
|
|
724
|
+
const cycle = motionCycleDuration(command2, scene, target);
|
|
725
|
+
jobs.push({ start, order: order++, run: () => compileRepeated(command2, target, entry, start, cycle, timing) });
|
|
568
726
|
});
|
|
569
727
|
}
|
|
570
728
|
jobs.sort((a, b) => a.start - b.start || a.order - b.order).forEach((job) => job.run());
|
|
729
|
+
function compileRepeated(command2, node, entry, start, cycle, timing) {
|
|
730
|
+
const first = segments.length;
|
|
731
|
+
compileCommand(command2, node, entry, start, cycle, timing);
|
|
732
|
+
const made = segments.slice(first);
|
|
733
|
+
const steps = command2.steps ?? scene.steps;
|
|
734
|
+
if (steps && steps > 0) for (const s of made) s.steps = steps;
|
|
735
|
+
const n = motionCommandRepeat(command2);
|
|
736
|
+
const mode = command2.repeatMode ?? "loop";
|
|
737
|
+
const shift = (v, by, k) => {
|
|
738
|
+
if (typeof v === "number" && typeof by === "number") return v + by * k;
|
|
739
|
+
if (Array.isArray(v) && Array.isArray(by) && v.length === by.length) return v.map((x, i) => x + by[i] * k);
|
|
740
|
+
return v;
|
|
741
|
+
};
|
|
742
|
+
for (let k = 1; k < n; k++) {
|
|
743
|
+
const at = start + k * cycle;
|
|
744
|
+
for (const s of made) {
|
|
745
|
+
if (mode === "mirror" && k % 2 === 1) {
|
|
746
|
+
push({ ...s, start: at + (start + cycle - s.end), end: at + (start + cycle - s.start), reverse: !s.reverse });
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
const delta = typeof s.from === "number" && typeof s.to === "number" ? s.to - s.from : Array.isArray(s.from) && Array.isArray(s.to) && s.from.length === s.to.length ? s.to.map((x, i) => x - s.from[i]) : 0;
|
|
750
|
+
const by = mode === "offset" ? delta : 0;
|
|
751
|
+
push({ ...s, start: at + (s.start - start), end: at + (s.end - start), from: shift(s.from, by, k), to: shift(s.to, by, k) });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
571
755
|
function compileCommand(command2, node, entry, start, duration2, timing) {
|
|
572
756
|
const end = start + duration2;
|
|
573
757
|
const where = { shot: timing.shot, command: timing.command };
|
|
@@ -582,15 +766,27 @@ function compileMotionScene(scene) {
|
|
|
582
766
|
case "enter": {
|
|
583
767
|
const style = command2.style ?? "fade";
|
|
584
768
|
const opacity = rest("opacity");
|
|
769
|
+
const reveal = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction }) : style === "iris" ? motionRevealClip(entry, { style, shape: command2.shape }) : style === "split" ? motionRevealClip(entry, { style, orientation: command2.orientation }) : null;
|
|
770
|
+
for (const p of ["clipRect", "clipCircle"]) if (p !== reveal?.property && tracks.has(motionTrackKey(node, p))) seg(p, rest(p), "linear", start, start);
|
|
771
|
+
if (reveal) {
|
|
772
|
+
seg("opacity", opacity, "linear", start, start);
|
|
773
|
+
seg(reveal.property, reveal.to, ease ?? MOTION_EASE_OUT, start, end, { from: reveal.from });
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
585
776
|
if (style === "draw") {
|
|
586
777
|
seg("drawEnd", 1, ease ?? MOTION_EASE_IN_OUT, start, start + duration2 * 0.75, { from: 0 });
|
|
587
778
|
seg("fillOpacity", 1, MOTION_EASE_OUT, start + duration2 * 0.6, end, { from: 0 });
|
|
588
779
|
break;
|
|
589
780
|
}
|
|
590
|
-
if (style === "pop") {
|
|
591
|
-
|
|
592
|
-
seg("
|
|
593
|
-
seg("
|
|
781
|
+
if (style === "pop" || style === "zoom") {
|
|
782
|
+
const from = command2.scale ?? (style === "pop" ? 0.5 : 1.6);
|
|
783
|
+
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * (style === "pop" ? 0.4 : 0.5), { from: 0 });
|
|
784
|
+
seg("scaleX", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
|
|
785
|
+
seg("scaleY", 1, ease ?? (style === "pop" ? "ease-out-back" : MOTION_EASE_OUT), start, end, { from });
|
|
786
|
+
} else if (style === "rise") {
|
|
787
|
+
const y = num("y");
|
|
788
|
+
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.5, { from: 0 });
|
|
789
|
+
seg("y", y, ease ?? "ease-out-back", start, end, { from: y + (command2.distance ?? 20) });
|
|
594
790
|
} else if (style === "grow") {
|
|
595
791
|
seg("opacity", opacity, MOTION_EASE_OUT, start, start + duration2 * 0.3, { from: 0 });
|
|
596
792
|
seg("scaleX", 1, ease ?? MOTION_EASE_OUT, start, end, { from: 0 });
|
|
@@ -608,7 +804,9 @@ function compileMotionScene(scene) {
|
|
|
608
804
|
}
|
|
609
805
|
case "exit": {
|
|
610
806
|
const style = command2.style ?? "fade";
|
|
611
|
-
|
|
807
|
+
const wipe = style === "wipe" ? motionRevealClip(entry, { style, direction: command2.direction, hide: true }) : null;
|
|
808
|
+
if (wipe) seg(wipe.property, wipe.to, ease ?? "ease-in", start, end, { from: wipe.from });
|
|
809
|
+
else if (style === "undraw") {
|
|
612
810
|
seg("fillOpacity", 0, "ease-in", start, start + duration2 * 0.4);
|
|
613
811
|
seg("drawEnd", 0, ease ?? MOTION_EASE_IN_OUT);
|
|
614
812
|
} else if (style === "shrink") {
|
|
@@ -624,13 +822,11 @@ function compileMotionScene(scene) {
|
|
|
624
822
|
} else seg("opacity", 0, ease ?? "ease-in");
|
|
625
823
|
break;
|
|
626
824
|
}
|
|
627
|
-
case "draw":
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
seg("drawEnd", command2.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, command2.from !== void 0 ? { from: command2.from } : drawn ? {} : { from: 0 });
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
825
|
+
case "draw": {
|
|
826
|
+
const drawn = tracks.has(motionTrackKey(node, "drawEnd"));
|
|
827
|
+
seg("drawEnd", command2.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, command2.from !== void 0 ? { from: command2.from } : drawn ? {} : { from: 0 });
|
|
633
828
|
break;
|
|
829
|
+
}
|
|
634
830
|
case "move": {
|
|
635
831
|
const e = ease ?? MOTION_EASE_IN_OUT;
|
|
636
832
|
let dx = command2.by?.x ?? 0;
|
|
@@ -650,8 +846,9 @@ function compileMotionScene(scene) {
|
|
|
650
846
|
break;
|
|
651
847
|
case "scale": {
|
|
652
848
|
const to = typeof command2.to === "number" ? { x: command2.to, y: command2.to } : command2.to;
|
|
653
|
-
|
|
654
|
-
seg("
|
|
849
|
+
const geometric = command2.geometric ? { geometric: true } : {};
|
|
850
|
+
seg("scaleX", to.x, ease ?? MOTION_EASE_IN_OUT, start, end, geometric);
|
|
851
|
+
seg("scaleY", to.y, ease ?? MOTION_EASE_IN_OUT, start, end, geometric);
|
|
655
852
|
break;
|
|
656
853
|
}
|
|
657
854
|
case "fade":
|
|
@@ -668,14 +865,35 @@ function compileMotionScene(scene) {
|
|
|
668
865
|
case "follow":
|
|
669
866
|
seg("follow", command2.to ?? 1, ease ?? MOTION_EASE_IN_OUT, start, end, { from: command2.from ?? 0, path: command2.path, rotate: command2.rotate ?? false });
|
|
670
867
|
break;
|
|
868
|
+
// Emphasis: a change toward a peak that a behaviour swings round (as Flair's primitives do).
|
|
671
869
|
case "pulse": {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
seg("
|
|
870
|
+
const behavior = { type: "pulse", count: command2.count ?? 1 };
|
|
871
|
+
const k = 1 + (command2.amount ?? 0.15);
|
|
872
|
+
seg("scaleX", num("scaleX") * k, ease ?? "linear", start, end, { behavior });
|
|
873
|
+
seg("scaleY", num("scaleY") * k, ease ?? "linear", start, end, { behavior });
|
|
675
874
|
break;
|
|
676
875
|
}
|
|
677
|
-
case "shake":
|
|
678
|
-
|
|
876
|
+
case "shake": {
|
|
877
|
+
const p = command2.axis === "vertical" ? "y" : "x";
|
|
878
|
+
seg(p, num(p) + (command2.amount ?? 8), ease ?? "linear", start, end, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
case "teeter":
|
|
882
|
+
seg("rotation", num("rotation") + (command2.angle ?? 8), ease ?? "linear", start, end, { behavior: { type: "oscillate", count: command2.count ?? 3, decay: 1 } });
|
|
883
|
+
break;
|
|
884
|
+
case "wave":
|
|
885
|
+
seg("y", num("y") - (command2.amount ?? 6), ease ?? "linear", start, end, { behavior: { type: "oscillate", count: command2.count ?? 2 } });
|
|
886
|
+
break;
|
|
887
|
+
case "wiggle": {
|
|
888
|
+
const behavior = { type: "wriggle", frequency: command2.frequency ?? 3, octaves: command2.octaves ?? 2, seed: command2.seed || motionSeedFromId(node) };
|
|
889
|
+
const amount = command2.amount ?? 6;
|
|
890
|
+
seg("x", num("x") + amount, ease ?? "linear", start, end, { behavior, index: 0 });
|
|
891
|
+
seg("y", num("y") + amount, ease ?? "linear", start, end, { behavior, index: 1 });
|
|
892
|
+
if (command2.rotate) seg("rotation", num("rotation") + command2.rotate, ease ?? "linear", start, end, { behavior, index: 2 });
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
case "color-pulse":
|
|
896
|
+
seg(command2.paint ?? "fill", command2.color ?? "#f59e0b", ease ?? "linear", start, end, { behavior: { type: "pulse", count: command2.count ?? 1 } });
|
|
679
897
|
break;
|
|
680
898
|
case "effect":
|
|
681
899
|
seg(`effect:${command2.effect}:${command2.param}`, command2.to, ease ?? MOTION_EASE_IN_OUT);
|
|
@@ -757,44 +975,53 @@ function shapeAt(entry, timeline, nodes, time) {
|
|
|
757
975
|
if (u >= 1) return b === entry ? null : shapeIn(entry, b);
|
|
758
976
|
return morphBetween(shapeIn(entry, a), shapeIn(entry, b)).at(u);
|
|
759
977
|
}
|
|
978
|
+
function motionFollowRoute(entry, path) {
|
|
979
|
+
if (!path || path.node.type !== "path") return null;
|
|
980
|
+
try {
|
|
981
|
+
return transformBezierPath(vectorPathWorldBezier(path.node, path.parentWorld, { effects: false }), invertMatrix(entry.parentWorld));
|
|
982
|
+
} catch {
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
760
986
|
function followAt(entry, timeline, nodes, time) {
|
|
761
987
|
const track = timeline.tracks.get(motionTrackKey(entry.node.id, "follow"));
|
|
762
988
|
const s = activeSegment(track, time) ?? track?.[0];
|
|
763
989
|
if (!s?.path) return null;
|
|
764
|
-
const
|
|
765
|
-
if (!path || path.node.type !== "path") return null;
|
|
990
|
+
const route = motionFollowRoute(entry, nodes.get(s.path));
|
|
766
991
|
const progress = Number(trackValue(track, time));
|
|
767
|
-
if (!(progress >= 0)) return null;
|
|
992
|
+
if (!route || !(progress >= 0)) return null;
|
|
768
993
|
try {
|
|
769
|
-
const
|
|
770
|
-
const at = bezierPathPointAtLength(bezier, Math.min(1, Math.max(0, progress)) * bezierPathLength(bezier));
|
|
994
|
+
const at = bezierPathPointAtLength(route, Math.min(1, Math.max(0, progress)) * bezierPathLength(route));
|
|
771
995
|
if (!at) return null;
|
|
772
|
-
|
|
773
|
-
return { x: p.x, y: p.y, angle: s.rotate ? at.angle * 180 / Math.PI : 0 };
|
|
996
|
+
return { x: at.point[0], y: at.point[1], angle: s.rotate ? at.angle * 180 / Math.PI : 0 };
|
|
774
997
|
} catch {
|
|
775
998
|
return null;
|
|
776
999
|
}
|
|
777
1000
|
}
|
|
778
|
-
function motionNodeOffset(entry, timeline, time, nodes = indexMotionNodes([entry.node])) {
|
|
1001
|
+
function motionNodeOffset(entry, timeline, time, nodes = indexMotionNodes([entry.node]), raw = false, follow = true) {
|
|
779
1002
|
const read = (p, rest) => {
|
|
780
|
-
const v = trackValue(timeline.tracks.get(motionTrackKey(entry.node.id, p)), time);
|
|
1003
|
+
const v = trackValue(timeline.tracks.get(motionTrackKey(entry.node.id, p)), time, raw);
|
|
781
1004
|
return typeof v === "number" ? v : rest;
|
|
782
1005
|
};
|
|
783
|
-
if (!TRANSFORM_PROPS.some((p) => timeline.tracks.has(motionTrackKey(entry.node.id, p)))) return null;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1006
|
+
if (!TRANSFORM_PROPS.some((p) => (follow || p !== "follow") && timeline.tracks.has(motionTrackKey(entry.node.id, p)))) return null;
|
|
1007
|
+
const dx = read("x", 0);
|
|
1008
|
+
const dy = read("y", 0);
|
|
1009
|
+
const rotation = read("rotation", 0);
|
|
787
1010
|
const scaleX = read("scaleX", 1);
|
|
788
1011
|
const scaleY = read("scaleY", 1);
|
|
789
|
-
const follow = followAt(entry, timeline, nodes, time);
|
|
790
|
-
if (follow) {
|
|
791
|
-
dx += follow.x - entry.center.x;
|
|
792
|
-
dy += follow.y - entry.center.y;
|
|
793
|
-
rotation += follow.angle;
|
|
794
|
-
}
|
|
795
1012
|
const c = entry.center;
|
|
796
|
-
|
|
797
|
-
|
|
1013
|
+
let matrix = multiplyMatrices(multiplyMatrices(scaleMatrix(scaleX, scaleY, c), rotateMatrix(rotation, c)), translateMatrix(dx, dy));
|
|
1014
|
+
const along = follow ? followAt(entry, timeline, nodes, time) : null;
|
|
1015
|
+
if (along) matrix = multiplyMatrices(matrix, multiplyMatrices(multiplyMatrices(translateMatrix(-c.x, -c.y), rotateMatrix(along.angle)), translateMatrix(along.x, along.y)));
|
|
1016
|
+
return { matrix, dx, dy, rotation, scaleX, scaleY, follow: along };
|
|
1017
|
+
}
|
|
1018
|
+
function motionNodeClips(id, timeline, time) {
|
|
1019
|
+
const out = [];
|
|
1020
|
+
const rect = trackValue(timeline.tracks.get(motionTrackKey(id, "clipRect")), time);
|
|
1021
|
+
if (Array.isArray(rect) && rect.length === 4) out.push({ shape: "rect", x: tidy(rect[0]), y: tidy(rect[1]), width: Math.max(0, tidy(rect[2])), height: Math.max(0, tidy(rect[3])) });
|
|
1022
|
+
const circle = trackValue(timeline.tracks.get(motionTrackKey(id, "clipCircle")), time);
|
|
1023
|
+
if (Array.isArray(circle) && circle.length === 3) out.push({ shape: "circle", cx: tidy(circle[0]), cy: tidy(circle[1]), r: Math.max(0, tidy(circle[2])) });
|
|
1024
|
+
return out;
|
|
798
1025
|
}
|
|
799
1026
|
function motionFrameAt(scene, time, options = {}) {
|
|
800
1027
|
const s = applyMotionControlValues(scene, options.values);
|
|
@@ -803,12 +1030,15 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
803
1030
|
const drawOn = options.draw !== false;
|
|
804
1031
|
const value = (id, p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
805
1032
|
const animated = new Set(timeline.segments.map((seg) => seg.node));
|
|
1033
|
+
const clips = {};
|
|
806
1034
|
const visit = (node) => {
|
|
807
1035
|
let out = node.type === "group" ? { ...node, children: node.children.map(visit) } : node;
|
|
808
1036
|
if (!animated.has(node.id)) return out;
|
|
809
1037
|
const entry = nodes.get(node.id);
|
|
810
1038
|
if (!entry) return out;
|
|
811
1039
|
const id = node.id;
|
|
1040
|
+
const clip = motionNodeClips(id, timeline, time);
|
|
1041
|
+
if (clip.length) clips[id] = clip;
|
|
812
1042
|
const shape = shapeAt(entry, timeline, nodes, time);
|
|
813
1043
|
if (shape) {
|
|
814
1044
|
const { name, visible, opacity: opacity2, locked } = node;
|
|
@@ -872,11 +1102,19 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
872
1102
|
const document = { ...s.artwork, nodes: s.artwork.nodes.map(visit) };
|
|
873
1103
|
const cam = trackValue(timeline.tracks.get(motionTrackKey("", "camera")), time);
|
|
874
1104
|
const box = Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
|
|
875
|
-
return { time, document, camera: { x: tidy(box[0]), y: tidy(box[1]), width: tidy(box[2]), height: tidy(box[3]) } };
|
|
1105
|
+
return { time, document, camera: { x: tidy(box[0]), y: tidy(box[1]), width: tidy(box[2]), height: tidy(box[3]) }, clips };
|
|
1106
|
+
}
|
|
1107
|
+
function motionClipAttrs(clip) {
|
|
1108
|
+
const { shape: _shape, ...attrs } = clip;
|
|
1109
|
+
return attrs;
|
|
876
1110
|
}
|
|
877
1111
|
function motionFrameToSvg(scene, time, options = {}) {
|
|
878
1112
|
const frame = motionFrameAt(scene, time, options);
|
|
879
|
-
|
|
1113
|
+
const prefix = options.idPrefix ?? "vg-";
|
|
1114
|
+
const wrap = { ...options.wrap };
|
|
1115
|
+
for (const [id, clips] of Object.entries(frame.clips))
|
|
1116
|
+
wrap[id] = [...wrap[id] ?? [], ...clips.map((clip, i) => ({ clip: { id: `${prefix}reveal-${id}-${i}`, shape: clip.shape, attrs: motionClipAttrs(clip) } }))];
|
|
1117
|
+
return withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera);
|
|
880
1118
|
}
|
|
881
1119
|
function withViewBox(svg, box) {
|
|
882
1120
|
const f = (n) => String(Math.round(n * 1e3) / 1e3);
|
|
@@ -911,6 +1149,11 @@ function visibleAt(timeline, id, time, node) {
|
|
|
911
1149
|
if (node.visible === false) return false;
|
|
912
1150
|
if (num("opacity", node.opacity ?? 1) <= 0.01) return false;
|
|
913
1151
|
if (Math.abs(num("scaleX", 1) * num("scaleY", 1)) <= 1e-4) return false;
|
|
1152
|
+
const clip = (p) => trackValue(timeline.tracks.get(motionTrackKey(id, p)), time);
|
|
1153
|
+
const rect = clip("clipRect");
|
|
1154
|
+
if (Array.isArray(rect) && (rect[2] <= 1e-4 || rect[3] <= 1e-4)) return false;
|
|
1155
|
+
const circle = clip("clipCircle");
|
|
1156
|
+
if (Array.isArray(circle) && circle[2] <= 1e-4) return false;
|
|
914
1157
|
const hasFill = node.type === "path" ? !!node.fill && node.fill !== "none" : collectPathNodes(node).some((p) => p.fill && p.fill !== "none");
|
|
915
1158
|
return num("drawEnd", 1) > 0.01 || hasFill && num("fillOpacity", 1) > 0.01;
|
|
916
1159
|
}
|
|
@@ -947,9 +1190,23 @@ function checkMotionScene(scene) {
|
|
|
947
1190
|
const timing = timeline.commands.find((c) => c.shot === shot.id && c.command === i);
|
|
948
1191
|
if (timing && timing.duration > 0 && timing.duration < MOTION_MIN_CHANGE_MS)
|
|
949
1192
|
issues.push(motionIssue("too-fast", `This ${command2.type} lasts ${timing.duration} ms, too quick to follow; make it at least ${MOTION_MIN_CHANGE_MS} ms.`, { ...where, measured: timing.duration, needed: MOTION_MIN_CHANGE_MS, unit: "ms", ids: targets }));
|
|
1193
|
+
if (command2.duration !== void 0 && command2.ease !== void 0) {
|
|
1194
|
+
const ease = resolveEasing(command2.ease);
|
|
1195
|
+
if (ease !== "hold" && ease.type === "spring") {
|
|
1196
|
+
const settle = springSettleMs(ease);
|
|
1197
|
+
if (command2.duration < settle * 0.9)
|
|
1198
|
+
issues.push(motionIssue("spring-cut-short", `This ${command2.type}'s spring takes ${settle} ms to settle but lasts ${command2.duration} ms, so it jumps to its end; leave "duration" out (the spring sets it) or make it at least ${settle} ms.`, { ...where, measured: command2.duration, needed: settle, unit: "ms", ids: targets }));
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
950
1201
|
if (command2.type === "follow" && known(command2.path, "unknown-path", "path", where) && nodes.get(command2.path).node.type !== "path")
|
|
951
1202
|
issues.push(motionIssue("not-a-path", `"${command2.path}" is a group; follow a single path node.`, { ...where, ids: [command2.path] }));
|
|
952
1203
|
if (command2.type === "morph") known(command2.into, "unknown-shape", "shape", where);
|
|
1204
|
+
const style = command2.type === "enter" || command2.type === "exit" ? command2.style : void 0;
|
|
1205
|
+
if (style === "wipe" || style === "iris" || style === "split") {
|
|
1206
|
+
for (const id of targets)
|
|
1207
|
+
if (!motionRevealPadding(nodes.get(id)))
|
|
1208
|
+
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 "${command2.type === "enter" ? "draw" : "undraw"}" for a line.`, { ...where, ids: [id] }));
|
|
1209
|
+
}
|
|
953
1210
|
if (command2.type === "style") {
|
|
954
1211
|
for (const key of ["fill", "stroke"]) {
|
|
955
1212
|
const color = command2[key];
|
|
@@ -966,6 +1223,18 @@ function checkMotionScene(scene) {
|
|
|
966
1223
|
}
|
|
967
1224
|
}
|
|
968
1225
|
}
|
|
1226
|
+
if (command2.type === "color-pulse") {
|
|
1227
|
+
const key = command2.paint ?? "fill";
|
|
1228
|
+
const color = command2.color ?? "#f59e0b";
|
|
1229
|
+
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));
|
|
1230
|
+
else
|
|
1231
|
+
for (const id of targets) {
|
|
1232
|
+
const node = nodes.get(id).node;
|
|
1233
|
+
const paths = node.type === "path" ? [node] : collectPathNodes(node);
|
|
1234
|
+
if (paths.some((p) => typeof p[key] !== "string"))
|
|
1235
|
+
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] }));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
969
1238
|
if (command2.type === "effect") {
|
|
970
1239
|
for (const id of targets) {
|
|
971
1240
|
const node = nodes.get(id).node;
|
|
@@ -1086,6 +1355,11 @@ function applyOne(scene, edit) {
|
|
|
1086
1355
|
else if (edit.description !== void 0) next.description = edit.description;
|
|
1087
1356
|
if (edit.loop !== void 0) next.loop = edit.loop;
|
|
1088
1357
|
if (edit.fps !== void 0) next.fps = edit.fps;
|
|
1358
|
+
if (edit.steps === null) delete next.steps;
|
|
1359
|
+
else if (edit.steps !== void 0) {
|
|
1360
|
+
if (!(typeof edit.steps === "number" && edit.steps > 0 && edit.steps <= 120)) fail('"steps" must be frames per second from above 0 to 120 (12 is "on twos" at 24 fps), or null for smooth.');
|
|
1361
|
+
next.steps = edit.steps;
|
|
1362
|
+
}
|
|
1089
1363
|
if (edit.target === null) delete next.target;
|
|
1090
1364
|
else if (edit.target !== void 0) next.target = edit.target;
|
|
1091
1365
|
return next;
|
|
@@ -1221,6 +1495,13 @@ var FLAIR_EASES = {
|
|
|
1221
1495
|
"in-out": (t) => t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2,
|
|
1222
1496
|
out: (t) => 1 - (1 - t) ** 3
|
|
1223
1497
|
};
|
|
1498
|
+
var FLAIR_EXPORT_VERSION = "0.39";
|
|
1499
|
+
function flairAtLeast(version, min) {
|
|
1500
|
+
const a = version.split(".").map((n) => parseInt(n, 10) || 0);
|
|
1501
|
+
const b = min.split(".").map((n) => parseInt(n, 10) || 0);
|
|
1502
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1224
1505
|
function flairEaseFor(ease) {
|
|
1225
1506
|
for (const name of ["linear", "out", "in-out"]) {
|
|
1226
1507
|
let worst = 0;
|
|
@@ -1239,7 +1520,7 @@ function flairCurveFor(ease, curves = true) {
|
|
|
1239
1520
|
if (resolved === "hold" || resolved.type !== "cubic-bezier") return void 0;
|
|
1240
1521
|
return [resolved.x1, resolved.y1, resolved.x2, resolved.y2];
|
|
1241
1522
|
}
|
|
1242
|
-
var overshoots = (ease) => typeof ease !== "string" && (ease[1] < 0 || ease[1] > 1 || ease[3] < 0 || ease[3] > 1);
|
|
1523
|
+
var overshoots = (ease) => typeof ease !== "string" && ("spring" in ease || ease[1] < 0 || ease[1] > 1 || ease[3] < 0 || ease[3] > 1);
|
|
1243
1524
|
var TRANSFORM_PROPS2 = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
|
|
1244
1525
|
var LINEAR_TRANSFORM = /* @__PURE__ */ new Set(["x", "y", "rotation", "scaleX", "scaleY"]);
|
|
1245
1526
|
function hex6(color) {
|
|
@@ -1249,7 +1530,24 @@ function hex6(color) {
|
|
|
1249
1530
|
function toFlairSvgMotion(scene, options = {}) {
|
|
1250
1531
|
const fps = options.fps ?? scene.fps ?? 30;
|
|
1251
1532
|
const precision = options.precision ?? 3;
|
|
1252
|
-
const
|
|
1533
|
+
const flair = options.flair ?? FLAIR_EXPORT_VERSION;
|
|
1534
|
+
const curves = options.curves ?? flairAtLeast(flair, "0.36");
|
|
1535
|
+
const behaviors = options.behaviors ?? flairAtLeast(flair, "0.38");
|
|
1536
|
+
const springs = flairAtLeast(flair, "0.39");
|
|
1537
|
+
const strobes = flairAtLeast(flair, "0.39");
|
|
1538
|
+
const writeEase = (ease) => typeof ease === "string" || "spring" in ease ? ease : ease.map(round2);
|
|
1539
|
+
const easeOf = (segment) => {
|
|
1540
|
+
const resolved = resolveEasing(segment.ease);
|
|
1541
|
+
if (resolved !== "hold" && resolved.type === "spring") {
|
|
1542
|
+
if (!springs || segment.reverse) return void 0;
|
|
1543
|
+
const { mass, stiffness, damping, velocity } = resolved;
|
|
1544
|
+
return { spring: { mass, stiffness, damping, velocity } };
|
|
1545
|
+
}
|
|
1546
|
+
if (!segment.reverse) return flairCurveFor(segment.ease, curves);
|
|
1547
|
+
const back = reverseCubicBezier(segment.ease);
|
|
1548
|
+
return back && curves ? flairCurveFor(back, curves) : void 0;
|
|
1549
|
+
};
|
|
1550
|
+
const plays = (s) => (!s.behavior || behaviors) && (!s.steps || strobes) && !s.geometric;
|
|
1253
1551
|
const round2 = (n) => Math.round(n * 10 ** precision) / 10 ** precision;
|
|
1254
1552
|
const f = (n) => String(round2(n));
|
|
1255
1553
|
const timeline = compileMotionScene(scene);
|
|
@@ -1264,26 +1562,72 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1264
1562
|
};
|
|
1265
1563
|
const animatedNodes = new Set(timeline.segments.map((s) => s.node).filter(Boolean));
|
|
1266
1564
|
const pathsSeen = /* @__PURE__ */ new Set();
|
|
1565
|
+
const wrap = {};
|
|
1566
|
+
const routes = [];
|
|
1567
|
+
const follows = [];
|
|
1568
|
+
const usedIds = new Set(nodes.keys());
|
|
1569
|
+
const freshId = (base) => {
|
|
1570
|
+
let id = base;
|
|
1571
|
+
for (let n = 2; usedIds.has(id); n++) id = `${base}-${n}`;
|
|
1572
|
+
usedIds.add(id);
|
|
1573
|
+
return id;
|
|
1574
|
+
};
|
|
1267
1575
|
for (const id of animatedNodes) {
|
|
1268
1576
|
const entry = nodes.get(id);
|
|
1269
1577
|
const node = entry.node;
|
|
1578
|
+
const drawn = [...entry.ancestors.map((a) => nodes.get(a).node), node].every((n) => n.visible !== false);
|
|
1579
|
+
for (const [p, name] of [["clipRect", "reveal"], ["clipCircle", "iris"]]) {
|
|
1580
|
+
if (!drawn || !timeline.tracks.has(motionTrackKey(id, p))) continue;
|
|
1581
|
+
const wrapper = freshId(`${id}--${name}`);
|
|
1582
|
+
const clipId = freshId(`${wrapper}-clip`);
|
|
1583
|
+
const shapeId = freshId(`${wrapper}-clip-shape`);
|
|
1584
|
+
const clipAt = (t) => motionNodeClips(id, timeline, t).find((c) => c.shape === (p === "clipRect" ? "rect" : "circle"));
|
|
1585
|
+
const rest = clipAt(0);
|
|
1586
|
+
(wrap[id] ??= []).push({ id: wrapper, clip: { id: clipId, shapeId, shape: rest.shape, attrs: motionClipAttrs(rest) } });
|
|
1587
|
+
channels.push({ id: shapeId, kind: "attrs", attrs: Object.keys(motionClipAttrs(rest)), segments: [...track(id, p)], exact: () => true, value: (t) => Object.values(motionClipAttrs(clipAt(t))).map((v) => Math.round(v * 1e4) / 1e4) });
|
|
1588
|
+
}
|
|
1589
|
+
const followSegments = track(id, "follow");
|
|
1590
|
+
if (drawn && followSegments.length) {
|
|
1591
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1592
|
+
for (const seg of followSegments) {
|
|
1593
|
+
if (!seg.path || ids.has(seg.path)) continue;
|
|
1594
|
+
const route = motionFollowRoute(entry, nodes.get(seg.path));
|
|
1595
|
+
if (!route) continue;
|
|
1596
|
+
const routeId = freshId(`${id}--follow-route`);
|
|
1597
|
+
ids.set(seg.path, routeId);
|
|
1598
|
+
routes.push(`<path id="${routeId}" d="${bezierPathToSvg(route, { precision })}" fill="none"/>`);
|
|
1599
|
+
}
|
|
1600
|
+
if (ids.size) {
|
|
1601
|
+
const wrapper = freshId(`${id}--follow`);
|
|
1602
|
+
const centre = freshId(`${id}--follow-centre`);
|
|
1603
|
+
const c = entry.center;
|
|
1604
|
+
const at = motionNodeOffset(entry, timeline, 0, nodes)?.follow;
|
|
1605
|
+
const placed = at ? `translate(${f(at.x)} ${f(at.y)})${at.angle ? ` rotate(${f(at.angle)})` : ""}` : void 0;
|
|
1606
|
+
(wrap[id] ??= []).push({ id: wrapper, ...placed ? { transform: placed } : {} }, { id: centre, transform: `translate(${f(-c.x)} ${f(-c.y)})` });
|
|
1607
|
+
follows.push({ id, wrapper, routes: ids, segments: followSegments.filter((seg) => seg.path && ids.has(seg.path)) });
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1270
1610
|
if (timeline.tracks.has(motionTrackKey(id, "opacity")))
|
|
1271
|
-
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity")], exact:
|
|
1272
|
-
const
|
|
1611
|
+
channels.push({ id, kind: "attr", attr: "opacity", segments: [...track(id, "opacity")], exact: plays, value: (t) => round2(Math.min(1, Math.max(0, Number(trackValue(track(id, "opacity"), t))))) });
|
|
1612
|
+
const followed = follows.some((x) => x.id === id);
|
|
1613
|
+
const transformSegments = TRANSFORM_PROPS2.flatMap((p) => followed && p === "follow" ? [] : track(id, p));
|
|
1273
1614
|
if (transformSegments.length) {
|
|
1274
1615
|
const base = node.transform ? ` matrix(${node.transform.map(f).join(" ")})` : "";
|
|
1275
1616
|
const c = entry.center;
|
|
1617
|
+
const transformAt = (t, raw) => {
|
|
1618
|
+
const o = motionNodeOffset(entry, timeline, t, nodes, raw, !followed);
|
|
1619
|
+
return `translate(${f(o.dx)} ${f(o.dy)}) rotate(${f(o.rotation)} ${f(c.x)} ${f(c.y)}) translate(${f(c.x)} ${f(c.y)}) scale(${f(o.scaleX)} ${f(o.scaleY)}) translate(${f(-c.x)} ${f(-c.y)})${base}`;
|
|
1620
|
+
};
|
|
1276
1621
|
channels.push({
|
|
1277
1622
|
id,
|
|
1278
1623
|
kind: "attr",
|
|
1279
1624
|
attr: "transform",
|
|
1280
1625
|
segments: transformSegments,
|
|
1281
|
-
exact: (s) => LINEAR_TRANSFORM.has(s.property) &&
|
|
1626
|
+
exact: (s) => LINEAR_TRANSFORM.has(s.property) && plays(s),
|
|
1282
1627
|
overshoot: true,
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
}
|
|
1628
|
+
// x, y and the angle are the transform's numbers 0, 1 and 2 (`MotionSegment.index`).
|
|
1629
|
+
value: (t) => transformAt(t, false),
|
|
1630
|
+
raw: (t) => transformAt(t, true)
|
|
1287
1631
|
});
|
|
1288
1632
|
}
|
|
1289
1633
|
const paths = node.type === "path" ? [node] : collectPathNodes(node);
|
|
@@ -1292,7 +1636,7 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1292
1636
|
pathsSeen.add(path.id);
|
|
1293
1637
|
const paint = (p, attr) => {
|
|
1294
1638
|
const from = owner(path, p);
|
|
1295
|
-
if (from) channels.push({ id: path.id, kind: "attr", attr, segments: [...track(from, p)], exact: () =>
|
|
1639
|
+
if (from) channels.push({ id: path.id, kind: "attr", attr, segments: [...track(from, p)], exact: plays, value: (t) => hex6(trackValue(track(from, p), t)), raw: (t) => hex6(trackValue(track(from, p), t, true)) });
|
|
1296
1640
|
};
|
|
1297
1641
|
paint("fill", "fill");
|
|
1298
1642
|
paint("stroke", "stroke");
|
|
@@ -1331,15 +1675,30 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1331
1675
|
const animate = [];
|
|
1332
1676
|
let sampled = 0;
|
|
1333
1677
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
1334
|
-
const emit = (channel, delay, ms, from, to, ease) => {
|
|
1335
|
-
const
|
|
1678
|
+
const emit = (channel, delay, ms, from, to, ease, segment) => {
|
|
1679
|
+
const written = writeEase(ease);
|
|
1680
|
+
const base = {
|
|
1681
|
+
...channel.id ? { id: channel.id } : {},
|
|
1682
|
+
...delay > 0 ? { delay: round2(delay) } : {},
|
|
1683
|
+
ms: round2(ms),
|
|
1684
|
+
ease: written,
|
|
1685
|
+
...segment?.behavior ? { behavior: segment.behavior } : {},
|
|
1686
|
+
...segment?.steps ? { strobe: segment.steps } : {}
|
|
1687
|
+
};
|
|
1336
1688
|
if (channel.kind === "view") animate.push({ ...base, view: to });
|
|
1337
1689
|
else if (channel.kind === "draw") animate.push({ ...base, draw: [from, to] });
|
|
1338
|
-
else
|
|
1690
|
+
else if (channel.kind === "attrs") {
|
|
1691
|
+
const [a, b] = [from, to];
|
|
1692
|
+
const attrs = Object.fromEntries(channel.attrs.flatMap((name, k) => a[k] !== b[k] ? [[name, [a[k], b[k]]]] : []));
|
|
1693
|
+
animate.push({ ...base, attrs });
|
|
1694
|
+
} else animate.push({ ...base, attrs: { [channel.attr]: [from, to] } });
|
|
1339
1695
|
};
|
|
1340
1696
|
for (const channel of channels) {
|
|
1341
1697
|
const cuts = channel.segments.filter((s) => s.end <= s.start);
|
|
1342
|
-
for (const cut of cuts)
|
|
1698
|
+
for (const cut of cuts) {
|
|
1699
|
+
const [from, to] = [channel.value(Math.max(0, cut.start - 1e-3)), channel.value(cut.start)];
|
|
1700
|
+
if (cut.start <= 0 || !same(from, to) || channel.kind === "view") emit(channel, cut.start, 0, from, to, "linear");
|
|
1701
|
+
}
|
|
1343
1702
|
const times = [...new Set(channel.segments.flatMap((s) => [s.start, s.end]))].sort((a, b) => a - b);
|
|
1344
1703
|
for (let i = 0; i + 1 < times.length; i++) {
|
|
1345
1704
|
const a = times[i];
|
|
@@ -1347,13 +1706,14 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1347
1706
|
const active = channel.segments.filter((s) => s.start < b && s.end > a);
|
|
1348
1707
|
if (!active.length) continue;
|
|
1349
1708
|
const first = active[0];
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1709
|
+
const key = (x) => JSON.stringify([x.ease, x.behavior, x.reverse ?? false, x.steps ?? 0]);
|
|
1710
|
+
const together = active.every((x) => x.start === a && x.end === b && channel.exact(x) && key(x) === key(first));
|
|
1711
|
+
const curve = together ? easeOf(first) : void 0;
|
|
1352
1712
|
const ease = curve && (!overshoots(curve) || channel.overshoot) ? curve : void 0;
|
|
1353
1713
|
if (ease) {
|
|
1354
1714
|
const from = channel.value(a);
|
|
1355
|
-
const to = channel.value(b);
|
|
1356
|
-
if (!same(from, to) || channel.kind === "view") emit(channel, a, b - a, from, to, ease);
|
|
1715
|
+
const to = first.behavior ? (channel.raw ?? channel.value)(b) : channel.value(b);
|
|
1716
|
+
if (!same(from, to) || channel.kind === "view") emit(channel, a, b - a, from, to, ease, first);
|
|
1357
1717
|
continue;
|
|
1358
1718
|
}
|
|
1359
1719
|
const n = Math.max(1, Math.ceil((b - a) * fps / 1e3));
|
|
@@ -1370,8 +1730,30 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
1370
1730
|
}
|
|
1371
1731
|
}
|
|
1372
1732
|
}
|
|
1733
|
+
for (const follow of follows) {
|
|
1734
|
+
const starts = follow.segments.map((seg) => seg.start);
|
|
1735
|
+
for (const seg of follow.segments) {
|
|
1736
|
+
const along = { id: follow.wrapper, along: follow.routes.get(seg.path), ...seg.rotate ? { orient: true } : {} };
|
|
1737
|
+
const progress = (t) => round2(Math.min(1, Math.max(0, Number(segmentValue(seg, t)))));
|
|
1738
|
+
const curve = seg.end > seg.start ? plays(seg) ? easeOf(seg) : void 0 : "linear";
|
|
1739
|
+
if (curve && !(overshoots(curve) && seg.rotate)) {
|
|
1740
|
+
const ends = seg.reverse ? [Number(seg.to), Number(seg.from)] : [Number(seg.from), Number(seg.to)];
|
|
1741
|
+
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])] });
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const until = Math.min(seg.end, ...starts.filter((t) => t > seg.start));
|
|
1745
|
+
const n = Math.max(1, Math.ceil((until - seg.start) * fps / 1e3));
|
|
1746
|
+
for (let k = 1; k <= n; k++) {
|
|
1747
|
+
const t0 = seg.start + (until - seg.start) * (k - 1) / n;
|
|
1748
|
+
const t1 = seg.start + (until - seg.start) * k / n;
|
|
1749
|
+
animate.push({ ...along, ...t0 > 0 ? { delay: round2(t0) } : {}, ms: round2(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
|
|
1750
|
+
sampled++;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1373
1754
|
animate.sort((x, y) => (x.delay ?? 0) - (y.delay ?? 0));
|
|
1374
|
-
let svg = vectorDocumentToSvg({ ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) }, { pretty: false, precision });
|
|
1755
|
+
let svg = vectorDocumentToSvg({ ...scene.artwork, nodes: expandVectorAppearances(scene.artwork.nodes) }, { pretty: false, precision, wrap });
|
|
1756
|
+
if (routes.length) svg = svg.replace(/^(<svg[^>]*>)/, `$1<defs>${routes.join("")}</defs>`);
|
|
1375
1757
|
const lastChange = animate.reduce((m, a) => Math.max(m, (a.delay ?? 0) + (a.ms ?? 0)), 0);
|
|
1376
1758
|
let motion = { svg, animate };
|
|
1377
1759
|
if (scene.controls?.length) {
|
|
@@ -1414,6 +1796,196 @@ function toFlairEffectDocument(scene, options = {}) {
|
|
|
1414
1796
|
};
|
|
1415
1797
|
}
|
|
1416
1798
|
|
|
1799
|
+
// src/animation-editor/flair-import.ts
|
|
1800
|
+
var PLACEHOLDER = /\{\{\s*([A-Za-z_]\w*)\s*(?::\s*([A-Za-z0-9]*)([^}]*))?\}\}/g;
|
|
1801
|
+
function attributeOf(attrs, name) {
|
|
1802
|
+
const m = new RegExp(`(?:^|\\s)${name}=(?:'([^']*)'|(\\S+))`).exec(attrs);
|
|
1803
|
+
return m ? m[1] ?? m[2] : void 0;
|
|
1804
|
+
}
|
|
1805
|
+
function resolvePlaceholders(motion, notes) {
|
|
1806
|
+
const text = JSON.stringify(motion);
|
|
1807
|
+
const declared = /* @__PURE__ */ new Map();
|
|
1808
|
+
for (const m of text.matchAll(PLACEHOLDER)) {
|
|
1809
|
+
if (!m[2] || declared.has(m[1])) continue;
|
|
1810
|
+
const attrs = m[3] ?? "";
|
|
1811
|
+
const value = attributeOf(attrs, "default");
|
|
1812
|
+
const fallback = m[2] === "color" ? "#000000" : "0";
|
|
1813
|
+
declared.set(m[1], { type: m[2], value: m[2] === "color" ? (value ?? fallback).toLowerCase() : value ?? fallback, ...attributeOf(attrs, "label") ? { label: attributeOf(attrs, "label") } : {} });
|
|
1814
|
+
}
|
|
1815
|
+
const controls = [];
|
|
1816
|
+
for (const [name, d] of declared) {
|
|
1817
|
+
if (d.type === "color" && /^#[0-9a-fA-F]{6}$/.test(d.value)) controls.push({ name, type: "color", default: d.value.toLowerCase(), ...d.label ? { label: d.label } : {} });
|
|
1818
|
+
else notes.push(`Control "${name}" (${d.type}) is fixed at its default ${d.value}: scenes take colour controls only.`);
|
|
1819
|
+
}
|
|
1820
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
1821
|
+
const filled = text.replace(PLACEHOLDER, (_, name) => {
|
|
1822
|
+
const d = declared.get(name);
|
|
1823
|
+
if (!d) unknown.add(name);
|
|
1824
|
+
return (d?.value ?? "").replace(/"/g, '\\"');
|
|
1825
|
+
});
|
|
1826
|
+
for (const name of unknown) notes.push(`Placeholder "{{${name}}}" has no declaration, so it was left empty.`);
|
|
1827
|
+
return { motion: JSON.parse(filled), controls };
|
|
1828
|
+
}
|
|
1829
|
+
function easingOf(ease, fallback) {
|
|
1830
|
+
if (ease === void 0) return fallback;
|
|
1831
|
+
if (ease === "linear") return "linear";
|
|
1832
|
+
if (ease === "out") return MOTION_EASE_OUT;
|
|
1833
|
+
if (ease === "in-out") return MOTION_EASE_IN_OUT;
|
|
1834
|
+
if (Array.isArray(ease)) return { type: "cubic-bezier", x1: ease[0], y1: ease[1], x2: ease[2], y2: ease[3] };
|
|
1835
|
+
if (typeof ease === "object" && "spring" in ease) return { type: "spring", ...ease.spring };
|
|
1836
|
+
return fallback;
|
|
1837
|
+
}
|
|
1838
|
+
function poseOf(value, entry) {
|
|
1839
|
+
const t = parseSvgTransform(String(value));
|
|
1840
|
+
const o = multiplyMatrices(invertMatrix(nodeMatrix(entry.node)) ?? [1, 0, 0, 1, 0, 0], t);
|
|
1841
|
+
const sx = Math.hypot(o[0], o[1]);
|
|
1842
|
+
if (!(sx > 1e-9)) return null;
|
|
1843
|
+
const sy = (o[0] * o[3] - o[1] * o[2]) / sx;
|
|
1844
|
+
if (Math.abs(o[2] + o[1] / sx * sy) > 1e-6 || Math.abs(o[3] - o[0] / sx * sy) > 1e-6) return null;
|
|
1845
|
+
const c = entry.center;
|
|
1846
|
+
const moved = applyMatrix(o, c);
|
|
1847
|
+
return { dx: moved.x - c.x, dy: moved.y - c.y, rotation: Math.atan2(o[1], o[0]) * 180 / Math.PI, sx, sy };
|
|
1848
|
+
}
|
|
1849
|
+
var near = (a, b, eps = 1e-6) => Math.abs(a - b) < eps;
|
|
1850
|
+
var r3 = (n) => Math.round(n * 1e3) / 1e3;
|
|
1851
|
+
function commandsFor(anim, nodes, entered, notes) {
|
|
1852
|
+
const id = anim.id;
|
|
1853
|
+
const entry = nodes.get(id);
|
|
1854
|
+
const where = `"${id}"`;
|
|
1855
|
+
if (!entry) {
|
|
1856
|
+
notes.push(`A change to ${where} was skipped: the artwork has no element with that id.`);
|
|
1857
|
+
return [];
|
|
1858
|
+
}
|
|
1859
|
+
const targets = [id];
|
|
1860
|
+
const duration = anim.ms ?? 800;
|
|
1861
|
+
const timing = {
|
|
1862
|
+
duration,
|
|
1863
|
+
...anim.strobe ? { steps: anim.strobe } : {},
|
|
1864
|
+
...anim.repeat && anim.repeat > 1 ? { repeat: anim.repeat, ...anim.alternate ? { repeatMode: "mirror" } : {} } : {}
|
|
1865
|
+
};
|
|
1866
|
+
const ease = (fallback) => ({ ease: easingOf(anim.ease, fallback) });
|
|
1867
|
+
const out = [];
|
|
1868
|
+
const behavior = anim.behavior;
|
|
1869
|
+
const unsupported = ["count", "write", "scramble", "solo"].filter((k) => anim[k] !== void 0);
|
|
1870
|
+
if (unsupported.length) notes.push(`${where}: ${unsupported.join(", ")} ${unsupported.length > 1 ? "are" : "is"} not part of scenes and was skipped.`);
|
|
1871
|
+
if (anim.draw !== void 0 && anim.draw !== false) {
|
|
1872
|
+
const [from, to] = anim.draw === true ? [0, 1] : anim.draw;
|
|
1873
|
+
out.push({ type: "draw", targets, from, to, ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1874
|
+
}
|
|
1875
|
+
if (anim.along) {
|
|
1876
|
+
const path = nodes.get(anim.along);
|
|
1877
|
+
if (!path || path.node.type !== "path") notes.push(`${where} follows "${anim.along}", which is not a path in the artwork; the follow was skipped.`);
|
|
1878
|
+
else out.push({ type: "follow", targets, path: anim.along, from: anim.at?.[0] ?? 0, to: anim.at?.[1] ?? 1, ...anim.orient ? { rotate: true } : {}, ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1879
|
+
}
|
|
1880
|
+
for (const [attr, [a, b]] of Object.entries(anim.attrs ?? {})) {
|
|
1881
|
+
if (attr === "opacity") {
|
|
1882
|
+
const [from, to] = [Number(a), Number(b)];
|
|
1883
|
+
if (behavior) notes.push(`${where}: an opacity ${behavior.type} became a plain fade.`);
|
|
1884
|
+
if (from === 0 && to > 0 && !entered.has(id)) {
|
|
1885
|
+
entered.add(id);
|
|
1886
|
+
out.push({ type: "enter", targets, style: "fade", ...timing, ...ease(MOTION_EASE_OUT) });
|
|
1887
|
+
} else out.push({ type: "fade", targets, to: Math.min(1, Math.max(0, to)), ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1888
|
+
} else if (attr === "fill" || attr === "stroke") {
|
|
1889
|
+
if (behavior?.type === "pulse") out.push({ type: "color-pulse", targets, color: String(b), paint: attr, count: behavior.count ?? 1, ...timing, ...ease("linear") });
|
|
1890
|
+
else {
|
|
1891
|
+
if (behavior) notes.push(`${where}: a ${attr} ${behavior.type} became a plain colour change.`);
|
|
1892
|
+
out.push({ type: "style", targets, [attr]: String(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1893
|
+
}
|
|
1894
|
+
} else if (attr === "stroke-width") {
|
|
1895
|
+
out.push({ type: "style", targets, strokeWidth: Number(b), ...timing, ...ease(MOTION_EASE_IN_OUT) });
|
|
1896
|
+
} else if (attr === "transform") {
|
|
1897
|
+
const p0 = poseOf(a, entry);
|
|
1898
|
+
const p1 = poseOf(b, entry);
|
|
1899
|
+
if (!p0 || !p1) {
|
|
1900
|
+
notes.push(`${where}: a transform that skews was skipped (scenes move, turn and scale).`);
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const d = { x: r3(p1.dx - p0.dx), y: r3(p1.dy - p0.dy), r: r3(p1.rotation - p0.rotation), sx: p1.sx / p0.sx, sy: p1.sy / p0.sy };
|
|
1904
|
+
if (behavior) {
|
|
1905
|
+
const linear = ease("linear");
|
|
1906
|
+
const moves = d.x !== 0 || d.y !== 0;
|
|
1907
|
+
const turns = d.r !== 0;
|
|
1908
|
+
const scales = !near(d.sx, 1) || !near(d.sy, 1);
|
|
1909
|
+
if (behavior.type === "pulse" && scales && !moves && !turns) out.push({ type: "pulse", targets, amount: r3(d.sx - 1), count: behavior.count ?? 1, ...timing, ...linear });
|
|
1910
|
+
else if (behavior.type === "oscillate" && (behavior.decay ?? 0) > 0 && turns && !moves && !scales) out.push({ type: "teeter", targets, angle: d.r, count: behavior.count ?? 1, ...timing, ...linear });
|
|
1911
|
+
else if (behavior.type === "oscillate" && (behavior.decay ?? 0) > 0 && moves && !turns && !scales && (d.x === 0 || d.y === 0))
|
|
1912
|
+
out.push({ type: "shake", targets, amount: d.x || d.y, count: behavior.count ?? 1, ...d.x === 0 ? { axis: "vertical" } : {}, ...timing, ...linear });
|
|
1913
|
+
else if (behavior.type === "oscillate" && !behavior.decay && d.x === 0 && d.y !== 0 && !turns && !scales) out.push({ type: "wave", targets, amount: -d.y, count: behavior.count ?? 1, ...timing, ...linear });
|
|
1914
|
+
else if (behavior.type === "wriggle" && !scales)
|
|
1915
|
+
out.push({ type: "wiggle", targets, amount: Math.max(Math.abs(d.x), Math.abs(d.y)), ...turns ? { rotate: Math.abs(d.r) } : {}, frequency: behavior.frequency ?? 2, octaves: behavior.octaves ?? 2, seed: behavior.seed ?? 42, ...timing, ...linear });
|
|
1916
|
+
else notes.push(`${where}: a transform ${behavior.type} that is not a pulse, shake, teeter, wave or wiggle was skipped.`);
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
const e = ease(MOTION_EASE_IN_OUT);
|
|
1920
|
+
if (d.x !== 0 || d.y !== 0) out.push({ type: "move", targets, by: { x: d.x, y: d.y }, ...timing, ...e });
|
|
1921
|
+
if (d.r !== 0) out.push({ type: "rotate", targets, to: r3(p1.rotation), ...timing, ...e });
|
|
1922
|
+
if (!near(d.sx, 1) || !near(d.sy, 1)) out.push({ type: "scale", targets, to: near(p1.sx, p1.sy) ? r3(p1.sx) : { x: r3(p1.sx), y: r3(p1.sy) }, ...timing, ...e });
|
|
1923
|
+
} else notes.push(`${where}: changing "${attr}" is not part of scenes and was skipped.`);
|
|
1924
|
+
}
|
|
1925
|
+
return out;
|
|
1926
|
+
}
|
|
1927
|
+
function fromFlairSvgMotion(input, options = {}) {
|
|
1928
|
+
const notes = [];
|
|
1929
|
+
if (!input || typeof input.svg !== "string") throw new Error('Give a Flair SvgMotion: { svg: "<svg \u2026>", animate: [ \u2026 ] }.');
|
|
1930
|
+
const { motion, controls } = resolvePlaceholders(input, notes);
|
|
1931
|
+
const artwork = parseVectorSvg(motion.svg);
|
|
1932
|
+
const nodes = indexMotionNodes(artwork.nodes);
|
|
1933
|
+
const byStep = /* @__PURE__ */ new Map();
|
|
1934
|
+
for (const anim of motion.animate ?? []) {
|
|
1935
|
+
const step = Math.max(0, Math.floor(anim.step ?? 0));
|
|
1936
|
+
if (!byStep.has(step)) byStep.set(step, []);
|
|
1937
|
+
byStep.get(step).push(anim);
|
|
1938
|
+
}
|
|
1939
|
+
const steps = [...byStep.keys()].sort((a, b) => a - b);
|
|
1940
|
+
if (steps.length > 1) notes.push("Each Flair step became a shot played after the one before (Flair steps wait for clicks or set times).");
|
|
1941
|
+
const entered = /* @__PURE__ */ new Set();
|
|
1942
|
+
const shots = steps.map((step, index) => {
|
|
1943
|
+
const anims = [...byStep.get(step)].sort((a, b) => (a.delay ?? 0) - (b.delay ?? 0));
|
|
1944
|
+
let camera;
|
|
1945
|
+
let cameraMs = 0;
|
|
1946
|
+
const run = [];
|
|
1947
|
+
let last = 0;
|
|
1948
|
+
for (const anim of anims) {
|
|
1949
|
+
if (anim.view !== void 0) {
|
|
1950
|
+
if (camera || (anim.delay ?? 0) > 0) {
|
|
1951
|
+
notes.push(`Step ${step}: a camera move ${camera ? "after the first" : `${anim.delay} ms in`} was dropped; a shot frames the camera once, at its start.`);
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const ms = anim.ms ?? 800;
|
|
1955
|
+
const ease = easingOf(anim.ease, MOTION_EASE_IN_OUT);
|
|
1956
|
+
camera = typeof anim.view === "string" ? { frame: anim.view, padding: 0, ms, ease } : { x: anim.view[0], y: anim.view[1], width: anim.view[2], height: anim.view[3], ms, ease };
|
|
1957
|
+
cameraMs = index === 0 ? 0 : ms;
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
if (!anim.id) continue;
|
|
1961
|
+
const at = Math.max(0, (anim.delay ?? 0) - cameraMs);
|
|
1962
|
+
if (cameraMs && (anim.delay ?? 0) < cameraMs) notes.push(`Step ${step}: a change to "${anim.id}" during the camera move now starts after it.`);
|
|
1963
|
+
for (const command2 of commandsFor(anim, nodes, entered, notes)) {
|
|
1964
|
+
const first = run.length === 0;
|
|
1965
|
+
run.push({ ...command2, ...first ? {} : { with: "previous" }, ...at - last > 0 || first ? { delay: first ? at : at - last } : {} });
|
|
1966
|
+
if (first && at === 0) delete run[run.length - 1].delay;
|
|
1967
|
+
last = at;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return { id: `step-${step}`, ...camera ? { camera } : {}, run, ...index === steps.length - 1 ? { hold: options.hold ?? 600 } : {} };
|
|
1971
|
+
});
|
|
1972
|
+
const scene = { version: 1, name: options.name ?? "from-flair", artwork, shots, ...controls.length ? { controls } : {} };
|
|
1973
|
+
const problems = motionSceneShapeProblems(scene);
|
|
1974
|
+
if (problems.length) throw new Error(`The imported scene is not well formed:
|
|
1975
|
+
${problems.join("\n")}`);
|
|
1976
|
+
return { scene, notes: [...new Set(notes)], issues: checkMotionScene(scene) };
|
|
1977
|
+
}
|
|
1978
|
+
function fromFlairVectorEffect(effect, options = {}) {
|
|
1979
|
+
if (!effect || effect.kind !== "vector" || !effect.motion) throw new Error('Give a Flair vector effect: { kind: "vector", id, name, motion: { svg, animate } }.');
|
|
1980
|
+
const result = fromFlairSvgMotion(effect.motion, { name: options.name ?? effect.id ?? effect.name, hold: options.hold ?? effect.hold });
|
|
1981
|
+
const names = new Set((result.scene.controls ?? []).map((c) => c.name));
|
|
1982
|
+
const presets = (effect.presets ?? []).map((p) => ({ name: p.name, values: Object.fromEntries(Object.entries(p.values).filter(([k]) => names.has(k))) })).filter((p) => Object.keys(p.values).length);
|
|
1983
|
+
const scene = { ...result.scene, ...effect.description ? { description: effect.description } : {}, ...presets.length ? { presets } : {} };
|
|
1984
|
+
const notes = [...result.notes];
|
|
1985
|
+
if (effect.trigger) notes.push(`The effect plays on ${effect.trigger}; scenes play through, so the trigger is not kept (pass it back to toFlairVectorEffect).`);
|
|
1986
|
+
return { scene, notes, issues: checkMotionScene(scene) };
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1417
1989
|
// src/animation-editor/controller.ts
|
|
1418
1990
|
var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
|
|
1419
1991
|
var AnimationEditorController = class {
|
|
@@ -1660,10 +2232,29 @@ var MS = (what) => ({ type: "number", minimum: 0, description: `${what} in milli
|
|
|
1660
2232
|
var FRACTION = (what) => ({ type: "number", minimum: 0, maximum: 1, description: what });
|
|
1661
2233
|
var POINT = (what) => object({ x: { type: "number", description: "Document units." }, y: { type: "number", description: "Document units." } }, ["x", "y"], what);
|
|
1662
2234
|
var EASE = {
|
|
1663
|
-
description: `Easing: a preset (${ANIMATION_EASING_PRESETS.join(", ")})
|
|
2235
|
+
description: `Easing: a preset (${ANIMATION_EASING_PRESETS.join(", ")}), { "type": "cubic-bezier", "x1", "y1", "x2", "y2" }, { "type": "spring", \u2026 } or { "type": "elastic", \u2026 }. The spring presets are Figma's: gentle, quick, bouncy, slow. A command with a spring and no "duration" lasts until the spring settles. Default: a cubic ease-out for entrances, cubic ease-in-out for moves.`,
|
|
1664
2236
|
anyOf: [
|
|
1665
2237
|
{ type: "string", enum: [...ANIMATION_EASING_PRESETS] },
|
|
1666
|
-
object({ type: { const: "cubic-bezier", description: "Curve kind." }, x1: { type: "number", description: "Out handle x (0\u20131)." }, y1: { type: "number", description: "Out handle y." }, x2: { type: "number", description: "In handle x (0\u20131)." }, y2: { type: "number", description: "In handle y." } }, ["type", "x1", "y1", "x2", "y2"])
|
|
2238
|
+
object({ type: { const: "cubic-bezier", description: "Curve kind." }, x1: { type: "number", description: "Out handle x (0\u20131)." }, y1: { type: "number", description: "Out handle y." }, x2: { type: "number", description: "In handle x (0\u20131)." }, y2: { type: "number", description: "In handle y." } }, ["type", "x1", "y1", "x2", "y2"]),
|
|
2239
|
+
object(
|
|
2240
|
+
{
|
|
2241
|
+
type: { const: "spring", description: "A damped spring (may overshoot)." },
|
|
2242
|
+
stiffness: { type: "number", exclusiveMinimum: 0, description: "Stiffness (default 170; quick 300, bouncy 600)." },
|
|
2243
|
+
damping: { type: "number", minimum: 0, description: "Damping (default 26; lower bounces more)." },
|
|
2244
|
+
mass: { type: "number", exclusiveMinimum: 0, description: "Mass (default 1)." },
|
|
2245
|
+
velocity: { type: "number", description: "Starting speed, in changes per second (default 0)." }
|
|
2246
|
+
},
|
|
2247
|
+
["type"]
|
|
2248
|
+
),
|
|
2249
|
+
object(
|
|
2250
|
+
{
|
|
2251
|
+
type: { const: "elastic", description: "A decaying wobble." },
|
|
2252
|
+
amplitude: { type: "number", minimum: 0, description: "Overshoot factor (default 1)." },
|
|
2253
|
+
period: { type: "number", exclusiveMinimum: 0, description: "Wobble period as a fraction of the length (default 0.3)." },
|
|
2254
|
+
mode: { type: "string", enum: ["out", "in", "in-out"], description: "Where it wobbles (default out)." }
|
|
2255
|
+
},
|
|
2256
|
+
["type"]
|
|
2257
|
+
)
|
|
1667
2258
|
]
|
|
1668
2259
|
};
|
|
1669
2260
|
var BASE = {
|
|
@@ -1673,26 +2264,36 @@ var BASE = {
|
|
|
1673
2264
|
stagger: MS("Start each further target this much later (default 0)"),
|
|
1674
2265
|
with: { type: "string", enum: ["after", "previous"], description: '"after" (default) starts when the commands before it finish; "previous" starts together with the command before.' },
|
|
1675
2266
|
ease: EASE,
|
|
1676
|
-
note: { type: "string", description: "Why this command is here, in a few words." }
|
|
2267
|
+
note: { type: "string", description: "Why this command is here, in a few words." },
|
|
2268
|
+
repeat: { type: "integer", minimum: 1, maximum: 1e3, description: 'Play it this many times (default 1); "duration" is one cycle. Not for enter or exit.' },
|
|
2269
|
+
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)." },
|
|
2270
|
+
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.` }
|
|
1677
2271
|
};
|
|
1678
2272
|
var command = (type, description, extra = {}, required = []) => object({ type: { const: type, description }, ...BASE, ...extra }, ["type", "targets", ...required], description);
|
|
1679
2273
|
var MOTION_COMMAND_SCHEMA = {
|
|
1680
2274
|
description: `A command (one of ${MOTION_COMMAND_TYPES.join(", ")}). Commands in a shot play one after another unless "with": "previous".`,
|
|
1681
2275
|
oneOf: [
|
|
1682
2276
|
command("enter", "Brings hidden targets in; they are hidden before this shot.", {
|
|
1683
|
-
style: {
|
|
1684
|
-
|
|
1685
|
-
|
|
2277
|
+
style: {
|
|
2278
|
+
type: "string",
|
|
2279
|
+
enum: [...MOTION_ENTER_STYLES],
|
|
2280
|
+
description: "fade (default), pop (scales up past full size and settles), grow (from nothing), slide, draw (outline drawn on, then the fill fades in), zoom (settles from larger), rise (rises a short way, overshooting a touch), or a reveal through a clip over the target's box: wipe (behind a travelling edge), iris (from the centre outwards) or split (from a line through the middle). Reveals need a box with a width and a height (not a lone straight line)."
|
|
2281
|
+
},
|
|
2282
|
+
direction: { type: "string", enum: [...MOTION_DIRECTIONS], description: "For slide: direction of travel (default up). For wipe: the way the edge travels (default right, revealing from the left)." },
|
|
2283
|
+
distance: { type: "number", description: "For slide (default 40) and rise (default 20): document units." },
|
|
2284
|
+
scale: { type: "number", minimum: 0, description: "For pop (default 0.5) and zoom (default 1.6): the starting scale (1 = rest size)." },
|
|
2285
|
+
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." },
|
|
2286
|
+
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." }
|
|
1686
2287
|
}),
|
|
1687
2288
|
command("exit", "Takes targets out.", {
|
|
1688
|
-
style: { type: "string", enum: [...MOTION_EXIT_STYLES], description: "fade (default), shrink, slide or
|
|
1689
|
-
direction: { type: "string", enum: [...MOTION_DIRECTIONS], description: "For slide: direction of travel (default down)." },
|
|
2289
|
+
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)." },
|
|
2290
|
+
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)." },
|
|
1690
2291
|
distance: { type: "number", description: "For slide: document units (default 40)." }
|
|
1691
2292
|
}),
|
|
1692
2293
|
command("draw", "Draws strokes on (or off) to a fraction of their length.", { to: FRACTION("Fraction drawn at the end (default 1)."), from: FRACTION("Fraction drawn at the start (default: what is drawn now).") }),
|
|
1693
2294
|
command("move", "Moves targets by an offset or so their centre lands on a point.", { by: POINT("Offset in document units."), to: POINT("Point (document coordinates) for the centre.") }),
|
|
1694
2295
|
command("rotate", "Turns targets about their centre (degrees, clockwise).", { by: { type: "number", description: "Degrees to turn by." }, to: { type: "number", description: "Angle to turn to, in degrees from rest." } }),
|
|
1695
|
-
command("scale", "Scales targets about their centre (1 = rest size).", { to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] } }, ["to"]),
|
|
2296
|
+
command("scale", "Scales targets about their centre (1 = rest size).", { to: { description: "A factor, or { x, y } factors.", anyOf: [{ type: "number" }, POINT("Factors per axis.")] }, geometric: { type: "boolean", description: "Grow by a steady factor, which reads as an even zoom (default false)." } }, ["to"]),
|
|
1696
2297
|
command("fade", "Changes opacity.", { to: FRACTION("Opacity at the end (0\u20131).") }, ["to"]),
|
|
1697
2298
|
command("style", "Blends solid fill / stroke colours (in OKLab) and stroke width. On a group it changes every path in it.", {
|
|
1698
2299
|
fill: { type: "string", description: "CSS colour (hex, rgb(), rgba())." },
|
|
@@ -1702,12 +2303,30 @@ var MOTION_COMMAND_SCHEMA = {
|
|
|
1702
2303
|
command("morph", "Morphs each target into the shape (and paint) of another node, which usually stays hidden.", { into: { type: "string", minLength: 1, description: "Id of the node whose shape to take." } }, ["into"]),
|
|
1703
2304
|
command("follow", "Moves targets along a path node, centre on the path.", {
|
|
1704
2305
|
path: { type: "string", minLength: 1, description: "Id of a path node (it can be hidden)." },
|
|
1705
|
-
rotate: { type: "boolean", description: "Turn with the path (default false)." },
|
|
2306
|
+
rotate: { type: "boolean", description: "Turn with the path (default false); moves and shakes made meanwhile turn with it too." },
|
|
1706
2307
|
from: FRACTION("Where to start along the path (default 0)."),
|
|
1707
2308
|
to: FRACTION("Where to end along the path (default 1).")
|
|
1708
2309
|
}, ["path"]),
|
|
1709
2310
|
command("pulse", "Scales up and back to draw the eye.", { amount: { type: "number", description: "Extra scale at the peak (default 0.15)." }, count: { type: "integer", minimum: 1, description: "Pulses (default 1)." } }),
|
|
1710
|
-
command("shake", "Shakes side to side
|
|
2311
|
+
command("shake", "Shakes side to side (or up and down), settling.", {
|
|
2312
|
+
amount: { type: "number", description: "Document units (default 8)." },
|
|
2313
|
+
count: { type: "integer", minimum: 1, description: "Shakes (default 3)." },
|
|
2314
|
+
axis: { type: "string", enum: ["horizontal", "vertical"], description: "Default horizontal." }
|
|
2315
|
+
}),
|
|
2316
|
+
command("teeter", "Rocks about the centre either way, settling.", { angle: { type: "number", description: "Degrees either way (default 8)." }, count: { type: "integer", minimum: 1, description: "Rocks (default 3)." } }),
|
|
2317
|
+
command("wave", "Bobs up and down without settling, like something floating (default 1000 ms).", { amount: { type: "number", description: "Height in document units (default 6)." }, count: { type: "integer", minimum: 1, description: "Bobs (default 2)." } }),
|
|
2318
|
+
command("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.", {
|
|
2319
|
+
amount: { type: "number", description: "Up to about this far, in document units (default 6)." },
|
|
2320
|
+
frequency: { type: "number", exclusiveMinimum: 0, description: "Wanders per second, in Hz (default 3)." },
|
|
2321
|
+
octaves: { type: "integer", minimum: 1, maximum: 8, description: "Layers of finer detail (default 2)." },
|
|
2322
|
+
rotate: { type: "number", description: "Also turns about the centre by up to about this many degrees (default 0)." },
|
|
2323
|
+
seed: { type: "integer", minimum: 0, description: "Default: one picked from the target id, so each target wanders its own way." }
|
|
2324
|
+
}),
|
|
2325
|
+
command("color-pulse", "Tints solid paint toward a colour and back (mixed in OKLab). On a group it tints every path in it.", {
|
|
2326
|
+
color: { type: "string", description: 'The tint, a CSS colour (default "#f59e0b").' },
|
|
2327
|
+
paint: { type: "string", enum: ["fill", "stroke"], description: "Which paint (default fill)." },
|
|
2328
|
+
count: { type: "integer", minimum: 1, description: "Pulses (default 1)." }
|
|
2329
|
+
}),
|
|
1711
2330
|
command("effect", "Tweens a numeric parameter of a live path effect on the targets (twist angle, wiggle time, zig zag size, trim offset\u2026).", {
|
|
1712
2331
|
effect: { type: "string", minLength: 1, description: `Effect id from get_animation's element "effects".` },
|
|
1713
2332
|
param: { type: "string", minLength: 1, description: "A numeric parameter of that effect (listed with it)." },
|
|
@@ -1720,11 +2339,12 @@ var MOTION_COMMAND_SCHEMA = {
|
|
|
1720
2339
|
)
|
|
1721
2340
|
]
|
|
1722
2341
|
};
|
|
2342
|
+
var GEOMETRIC_CAMERA = { type: "boolean", description: "Zoom at a steady rate, so a big zoom does not rush at the start (default false)." };
|
|
1723
2343
|
var CAMERA = {
|
|
1724
2344
|
description: `Where the camera looks: a rectangle { x, y, width, height } in document units, or { frame: "<node id>", padding } to frame a node (fitted to the artwork's shape). It eases there over "ms" (default 1000); the first shot cuts.`,
|
|
1725
2345
|
anyOf: [
|
|
1726
|
-
object({ x: { type: "number", description: "Left." }, y: { type: "number", description: "Top." }, width: { type: "number", exclusiveMinimum: 0, description: "Width." }, height: { type: "number", exclusiveMinimum: 0, description: "Height." }, ease: EASE, ms: MS("Camera move length") }, ["x", "y", "width", "height"]),
|
|
1727
|
-
object({ frame: { type: "string", minLength: 1, description: "Node id to frame." }, padding: { type: "number", description: "Margin around it (default 24)." }, ease: EASE, ms: MS("Camera move length") }, ["frame"])
|
|
2346
|
+
object({ x: { type: "number", description: "Left." }, y: { type: "number", description: "Top." }, width: { type: "number", exclusiveMinimum: 0, description: "Width." }, height: { type: "number", exclusiveMinimum: 0, description: "Height." }, ease: EASE, ms: MS("Camera move length"), geometric: GEOMETRIC_CAMERA }, ["x", "y", "width", "height"]),
|
|
2347
|
+
object({ frame: { type: "string", minLength: 1, description: "Node id to frame." }, padding: { type: "number", description: "Margin around it (default 24)." }, ease: EASE, ms: MS("Camera move length"), geometric: GEOMETRIC_CAMERA }, ["frame"])
|
|
1728
2348
|
]
|
|
1729
2349
|
};
|
|
1730
2350
|
var SHOT_FIELDS = {
|
|
@@ -1750,6 +2370,7 @@ var SETTINGS = {
|
|
|
1750
2370
|
description: { type: ["string", "null"], description: "What it shows; null clears." },
|
|
1751
2371
|
loop: { type: "string", enum: ["none", "loop", "ping-pong"], description: "Playback past the end (default none)." },
|
|
1752
2372
|
fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for exports (default 30)." },
|
|
2373
|
+
steps: { anyOf: [{ type: "number", exclusiveMinimum: 0, maximum: 120 }, { type: "null" }], description: 'Stepped time for every command without its own: changes hold on a grid of this many frames per second (12 = "on twos" at 24 fps) for a hand-drawn look; null makes it smooth again (default).' },
|
|
1753
2374
|
target: { anyOf: [{ type: "array", items: { type: "number", minimum: 0 }, minItems: 2, maxItems: 2 }, { type: "null" }], description: "Wanted length [min, max] in seconds; null clears." }
|
|
1754
2375
|
};
|
|
1755
2376
|
var EDIT_OP = {
|
|
@@ -1804,7 +2425,8 @@ function describeMotionShots(scene) {
|
|
|
1804
2425
|
...shot.hold !== void 0 ? { hold: shot.hold } : {},
|
|
1805
2426
|
commands: shot.run.map((c, index) => {
|
|
1806
2427
|
const ct = timeline.commands.find((x) => x.shotIndex === i && x.command === index);
|
|
1807
|
-
|
|
2428
|
+
const primitive = motionFlairPrimitive(c);
|
|
2429
|
+
return { index, ...c, start: ct.start, end: ct.end, ...primitive ? { flairPrimitive: primitive } : {} };
|
|
1808
2430
|
})
|
|
1809
2431
|
};
|
|
1810
2432
|
});
|
|
@@ -1840,7 +2462,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
1840
2462
|
const getAnimation = {
|
|
1841
2463
|
name: "get_animation",
|
|
1842
2464
|
title: "Read the animation",
|
|
1843
|
-
description:
|
|
2465
|
+
description: `Returns the vector animation: its name, artwork size, length (ms), loop mode, the artwork's elements (id, type, name, parent, bounds, solid paints, live effects with their numeric parameters), every shot with its commands and their start/end times (and, where Flair has one, the Flair motion primitive a command plays as "flairPrimitive", e.g. "entrance.pop"), Flair controls, and a summary of issues. Use the ids it returns with the write tools.`,
|
|
1844
2466
|
inputSchema: object({ includeElements: { type: "boolean", description: "Include the artwork's elements (default true)." } }),
|
|
1845
2467
|
annotations: { title: "Read the animation", ...READ },
|
|
1846
2468
|
handler: ({ includeElements = true }) => {
|
|
@@ -1927,16 +2549,20 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
1927
2549
|
}
|
|
1928
2550
|
};
|
|
1929
2551
|
const CURVES_DESCRIPTION = "Write cubic-bezier easings (ease, ease-in, ease-out, ease-in-out, back eases, custom curves) as one change with ease: [x1, y1, x2, y2] (default true). Needs Flair 0.36 or later; false samples them into linear steps for older Flair.";
|
|
2552
|
+
const FLAIR_DESCRIPTION = "The Flair version to export for (default 0.39): springs and stepped time need 0.39, behaviours 0.38, cubic-bezier curves 0.36; older versions get those sampled into linear steps.";
|
|
2553
|
+
const BEHAVIORS_DESCRIPTION = "Write pulses, shakes, teeters, waves, wiggles and colour pulses as one change with a Flair behavior, so they play exactly (default true; needs Flair 0.38 or later). False samples them into linear steps for older Flair.";
|
|
1930
2554
|
const exportFlairMotion = {
|
|
1931
2555
|
name: "export_flair_motion",
|
|
1932
2556
|
title: "Export as Flair vector motion",
|
|
1933
2557
|
description: "Returns the animation as a Flair SvgMotion ({ svg, animate }) that Flair's SvgPlayer plays, plus its hold, how many changes were sampled into linear steps, and notes on anything not carried over.",
|
|
1934
2558
|
inputSchema: object({
|
|
1935
2559
|
fps: { type: "number", minimum: 1, maximum: 120, description: "Sampling rate for changes Flair cannot express exactly (default: the scene's fps, else 30)." },
|
|
1936
|
-
curves: { type: "boolean", description: CURVES_DESCRIPTION }
|
|
2560
|
+
curves: { type: "boolean", description: CURVES_DESCRIPTION },
|
|
2561
|
+
behaviors: { type: "boolean", description: BEHAVIORS_DESCRIPTION },
|
|
2562
|
+
flair: { type: "string", pattern: "^\\d+\\.\\d+(\\.\\d+)?$", description: FLAIR_DESCRIPTION }
|
|
1937
2563
|
}),
|
|
1938
2564
|
annotations: { title: "Export as Flair vector motion", ...READ },
|
|
1939
|
-
handler: ({ fps, curves }) => controller.toFlairMotion({ ...fps ? { fps } : {}, ...curves === void 0 ? {} : { curves } })
|
|
2565
|
+
handler: ({ fps, curves, behaviors, flair }) => controller.toFlairMotion({ ...fps ? { fps } : {}, ...curves === void 0 ? {} : { curves }, ...behaviors === void 0 ? {} : { behaviors }, ...flair ? { flair } : {} })
|
|
1940
2566
|
};
|
|
1941
2567
|
const exportFlairEffect = {
|
|
1942
2568
|
name: "export_flair_effect",
|
|
@@ -1948,7 +2574,9 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
1948
2574
|
description: { type: "string", description: "One sentence (default: the scene's description)." },
|
|
1949
2575
|
category: { type: "string", minLength: 1, description: "Flair catalog category, e.g. icon, loader, ui, data, text, morph (default icon)." },
|
|
1950
2576
|
tags: { type: "array", items: { type: "string" }, description: "Search tags (default none)." },
|
|
1951
|
-
curves: { type: "boolean", description: CURVES_DESCRIPTION }
|
|
2577
|
+
curves: { type: "boolean", description: CURVES_DESCRIPTION },
|
|
2578
|
+
behaviors: { type: "boolean", description: BEHAVIORS_DESCRIPTION },
|
|
2579
|
+
flair: { type: "string", pattern: "^\\d+\\.\\d+(\\.\\d+)?$", description: FLAIR_DESCRIPTION }
|
|
1952
2580
|
}),
|
|
1953
2581
|
annotations: { title: "Export as a Flair effect", ...READ },
|
|
1954
2582
|
handler: (args) => controller.toFlairEffect(args)
|
|
@@ -1969,6 +2597,34 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
1969
2597
|
return apply({ op: "set_artwork", artwork }, "Replace artwork", (s) => ({ elements: describeMotionElements(s) }));
|
|
1970
2598
|
}
|
|
1971
2599
|
};
|
|
2600
|
+
const importFlairEffect = {
|
|
2601
|
+
name: "import_flair_effect",
|
|
2602
|
+
title: "Import a Flair effect",
|
|
2603
|
+
description: "Replaces the whole animation with a Flair vector effect or SvgMotion (one undo step): its SVG becomes the artwork, {{name: color}} placeholders become colour controls, and each Flair step becomes a shot of commands (fade, enter, style, move, rotate, scale, draw, follow, pulse, shake, teeter, wave, wiggle, color-pulse). Returns the shots, the elements, notes on anything not carried over and the issues. With dry_run it only reports what the import would give.",
|
|
2604
|
+
inputSchema: object(
|
|
2605
|
+
{
|
|
2606
|
+
effect: { type: "object", description: 'A Flair catalog vector effect { kind: "vector", id, name, motion: { svg, animate }, hold?, presets? }. Give this or "motion".' },
|
|
2607
|
+
motion: { type: "object", description: 'A Flair SvgMotion { svg, animate }. Give this or "effect".' },
|
|
2608
|
+
name: { type: "string", minLength: 1, description: `Name for the scene (default: the effect's id, else "from-flair").` },
|
|
2609
|
+
dry_run: { type: "boolean", description: "Report without replacing anything (default false)." }
|
|
2610
|
+
},
|
|
2611
|
+
[]
|
|
2612
|
+
),
|
|
2613
|
+
annotations: { title: "Import a Flair effect", ...DESTRUCTIVE },
|
|
2614
|
+
handler: ({ effect, motion, name, dry_run = false }) => {
|
|
2615
|
+
if (effect === void 0 === (motion === void 0)) throw new Error('Give exactly one of "effect" (a Flair vector effect) or "motion" (a Flair SvgMotion). Nothing was changed.');
|
|
2616
|
+
let result;
|
|
2617
|
+
try {
|
|
2618
|
+
result = effect !== void 0 ? fromFlairVectorEffect(effect, name ? { name } : {}) : fromFlairSvgMotion(motion, name ? { name } : {});
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
throw new Error(`The Flair effect could not be imported: ${error.message} Nothing was changed.`);
|
|
2621
|
+
}
|
|
2622
|
+
const report = { notes: result.notes, issues: issueSummary(result.issues), shots: describeMotionShots(result.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
|
|
2623
|
+
if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
|
|
2624
|
+
const applied = controller.apply({ op: "replace_scene", scene: result.scene }, "Import Flair effect");
|
|
2625
|
+
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result.scene) } : {});
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
1972
2628
|
const insertShots = {
|
|
1973
2629
|
name: "insert_shots",
|
|
1974
2630
|
title: "Add shots",
|
|
@@ -2051,7 +2707,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2051
2707
|
const setSettings = {
|
|
2052
2708
|
name: "set_animation_settings",
|
|
2053
2709
|
title: "Change animation settings",
|
|
2054
|
-
description: "Sets the name, description, loop mode, export frame rate or target length. Returns the timing.",
|
|
2710
|
+
description: "Sets the name, description, loop mode, export frame rate, stepped time or target length. Returns the timing.",
|
|
2055
2711
|
inputSchema: object(SETTINGS),
|
|
2056
2712
|
annotations: { title: "Change animation settings", ...WRITE_IDEMPOTENT },
|
|
2057
2713
|
handler: (args) => apply({ op: "set_settings", ...args }, "Change settings")
|
|
@@ -2130,7 +2786,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
2130
2786
|
}
|
|
2131
2787
|
};
|
|
2132
2788
|
const reads = [getAnimation, getShot, findElements, checkAnimation, renderFrame, exportFlairMotion, exportFlairEffect, seekAnimation];
|
|
2133
|
-
const writes = [setArtworkSvg, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
2789
|
+
const writes = [setArtworkSvg, importFlairEffect, insertShots, updateShot, deleteShots, moveShot, addCommands, updateCommand, deleteCommands, setSettings, setControls, editAnimation, writeAnimation, undo, redo];
|
|
2134
2790
|
return {
|
|
2135
2791
|
id: options.id ?? "animation",
|
|
2136
2792
|
namespace: options.namespace,
|
|
@@ -2160,12 +2816,19 @@ How it plays
|
|
|
2160
2816
|
|
|
2161
2817
|
Commands
|
|
2162
2818
|
- enter (fade, pop, grow, slide, draw) and exit (fade, shrink, slide, undraw) bring things in and out.
|
|
2819
|
+
- 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.
|
|
2163
2820
|
- 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.
|
|
2164
|
-
-
|
|
2821
|
+
- enter also has zoom (settles from larger) and rise (rises a short way, overshooting a touch); pop and zoom take a starting "scale".
|
|
2822
|
+
- morph turns a shape into another node's shape (keep the target hidden); follow moves along a path node (with "rotate", moves made meanwhile turn with the path).
|
|
2823
|
+
- Emphasis draws the eye and returns to rest: pulse (swell), shake (side to side, or "axis": "vertical"), teeter (rock), wave (bob, no settling), wiggle (seeded wander; the same seed wanders the same way) and color-pulse (tint and back).
|
|
2824
|
+
- Any command but enter and exit can "repeat" (loop, mirror back and forth, or offset to carry on: rotate by 90, repeat 4 is a full turn); "duration" is one cycle.
|
|
2825
|
+
- Springs ("spring-gentle", "spring-quick", "spring-bouncy", "spring-slow" or { "type": "spring", \u2026 }) feel physical; leave "duration" out and the spring sets it.
|
|
2826
|
+
- "steps": 12 (on a command, or for the whole scene with set_animation_settings) holds poses "on twos" for a hand-drawn look. "geometric": true on scale or a camera zooms at an even rate.
|
|
2827
|
+
- Commands share their names and defaults with Flair's motion primitives (get_animation shows each one's "flairPrimitive"), and emphasis, reveals and follows export to Flair exactly.
|
|
2165
2828
|
- effect tweens a numeric parameter of a live path effect already on the node (twist angle, wiggle time, zig zag size).
|
|
2166
2829
|
|
|
2167
2830
|
Working method
|
|
2168
|
-
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).
|
|
2831
|
+
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 and edit it).
|
|
2169
2832
|
2. Plan a few shots, each one idea. Add them with insert_shots, or edit_animation to make several edits as one step (dry_run first when unsure).
|
|
2170
2833
|
3. check_animation and fix every error and warning; render_animation_frame (shot + "end") to look at the result.
|
|
2171
2834
|
4. To make a reusable Flair effect, set_animation_controls (colour controls stand for every use of their default colour) and export_flair_effect.
|
|
@@ -2175,6 +2838,7 @@ export {
|
|
|
2175
2838
|
ANIMATION_EDITOR_AGENT_INSTRUCTIONS,
|
|
2176
2839
|
AnimationEditorController,
|
|
2177
2840
|
FLAIR_EASES,
|
|
2841
|
+
FLAIR_EXPORT_VERSION,
|
|
2178
2842
|
MOTION_CAMERA_MS,
|
|
2179
2843
|
MOTION_COMMAND_SCHEMA,
|
|
2180
2844
|
MOTION_COMMAND_TYPES,
|
|
@@ -2189,8 +2853,11 @@ export {
|
|
|
2189
2853
|
MOTION_EDIT_OPS,
|
|
2190
2854
|
MOTION_ENTER_STYLES,
|
|
2191
2855
|
MOTION_EXIT_STYLES,
|
|
2856
|
+
MOTION_IRIS_SHAPES,
|
|
2192
2857
|
MOTION_ISSUE_CODES,
|
|
2193
2858
|
MOTION_MIN_CHANGE_MS,
|
|
2859
|
+
MOTION_REPEAT_MODES,
|
|
2860
|
+
MOTION_SPLIT_ORIENTATIONS,
|
|
2194
2861
|
MotionSceneFormatError,
|
|
2195
2862
|
activeSegment,
|
|
2196
2863
|
applyMotionControlValues,
|
|
@@ -2203,22 +2870,33 @@ export {
|
|
|
2203
2870
|
describeMotionElements,
|
|
2204
2871
|
describeMotionShots,
|
|
2205
2872
|
fitMotionRect,
|
|
2873
|
+
flairAtLeast,
|
|
2206
2874
|
flairCurveFor,
|
|
2207
2875
|
flairEaseFor,
|
|
2208
2876
|
freshShotId,
|
|
2877
|
+
fromFlairSvgMotion,
|
|
2878
|
+
fromFlairVectorEffect,
|
|
2209
2879
|
indexMotionNodes,
|
|
2210
2880
|
interpolateMotionValue,
|
|
2211
2881
|
isMotionCommand,
|
|
2212
2882
|
isMotionShot,
|
|
2883
|
+
motionClipAttrs,
|
|
2213
2884
|
motionCommandDuration,
|
|
2885
|
+
motionCommandRepeat,
|
|
2886
|
+
motionCycleDuration,
|
|
2887
|
+
motionFlairPrimitive,
|
|
2888
|
+
motionFollowRoute,
|
|
2214
2889
|
motionFrameAt,
|
|
2215
2890
|
motionFrameToSvg,
|
|
2216
2891
|
motionIssue,
|
|
2217
2892
|
motionIssueKey,
|
|
2218
2893
|
motionIssueLine,
|
|
2894
|
+
motionNodeClips,
|
|
2219
2895
|
motionNodeOffset,
|
|
2220
2896
|
motionPathLength,
|
|
2221
2897
|
motionRestValue,
|
|
2898
|
+
motionRevealClip,
|
|
2899
|
+
motionRevealPadding,
|
|
2222
2900
|
motionSceneShapeProblems,
|
|
2223
2901
|
motionSceneToJson,
|
|
2224
2902
|
motionShotAt,
|