@pixerate/editor 0.232.0 → 0.233.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-editor/compile.d.cts +3 -0
- package/dist/animation-editor/compile.d.ts +3 -0
- package/dist/animation-editor/controller.d.cts +67 -2
- package/dist/animation-editor/controller.d.ts +67 -2
- package/dist/animation-editor/edit.d.cts +1 -1
- package/dist/animation-editor/edit.d.ts +1 -1
- package/dist/animation-editor/evaluate.d.cts +8 -1
- package/dist/animation-editor/evaluate.d.ts +8 -1
- package/dist/animation-editor/index.cjs +1202 -371
- package/dist/animation-editor/index.d.cts +3 -0
- package/dist/animation-editor/index.d.ts +3 -0
- package/dist/animation-editor/index.js +1032 -201
- package/dist/animation-editor/issues.d.cts +28 -0
- package/dist/animation-editor/issues.d.ts +28 -0
- package/dist/animation-editor/layer-edit.d.cts +55 -0
- package/dist/animation-editor/layer-edit.d.ts +55 -0
- package/dist/animation-editor/layer-tools.d.cts +11 -0
- package/dist/animation-editor/layer-tools.d.ts +11 -0
- package/dist/animation-editor/layers.d.cts +166 -0
- package/dist/animation-editor/layers.d.ts +166 -0
- package/dist/animation-editor/parse.d.cts +2 -0
- package/dist/animation-editor/parse.d.ts +2 -0
- package/dist/animation-editor/types.d.cts +44 -1
- package/dist/animation-editor/types.d.ts +44 -1
- package/dist/canvas/index.cjs +10 -10
- package/dist/canvas/index.js +4 -4
- package/dist/chunk-6RZVFYGJ.cjs +255 -0
- package/dist/{chunk-RG4O6K2A.cjs → chunk-A7PQNIBX.cjs} +14 -14
- package/dist/{chunk-ERJA5CGT.cjs → chunk-DGWOGQJ5.cjs} +8 -1
- package/dist/{chunk-TYCJGFCF.js → chunk-EUKY4KFX.js} +8 -1
- package/dist/chunk-I277KJLQ.js +255 -0
- package/dist/{chunk-2K5TELHV.cjs → chunk-JAOSL334.cjs} +10 -10
- package/dist/{chunk-MUSZYMBX.js → chunk-TEFQISBY.js} +1 -1
- package/dist/{chunk-6QLTUGMO.js → chunk-WDDUPWVG.js} +1 -1
- package/dist/{chunk-A653N65Q.cjs → chunk-ZQIWBJGH.cjs} +7 -7
- package/dist/effects/index.cjs +114 -322
- package/dist/effects/index.js +23 -231
- package/dist/image-editor/index.cjs +16 -16
- package/dist/image-editor/index.js +4 -4
- package/dist/paint-editor/index.cjs +10 -10
- package/dist/paint-editor/index.js +5 -5
- package/dist/vector/index.cjs +6 -6
- package/dist/vector/index.js +1 -1
- package/dist/video-editor/index.cjs +339 -339
- package/dist/video-editor/index.js +5 -5
- package/package.json +1 -1
- package/dist/{chunk-PQYDJ2AV.js → chunk-SIK3LT6M.js} +16 -16
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
effectLayerInput,
|
|
3
|
+
effectLayerOffered,
|
|
4
|
+
effectLayerRole,
|
|
5
|
+
effectSubjectKind
|
|
6
|
+
} from "../chunk-I277KJLQ.js";
|
|
1
7
|
import {
|
|
2
8
|
createVectorMorph
|
|
3
9
|
} from "../chunk-YMREOA67.js";
|
|
@@ -8,7 +14,7 @@ import {
|
|
|
8
14
|
motionIssueLine,
|
|
9
15
|
namedWithSuggestion,
|
|
10
16
|
suggestName
|
|
11
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-EUKY4KFX.js";
|
|
12
18
|
import {
|
|
13
19
|
IDENTITY_MATRIX,
|
|
14
20
|
applyMatrix,
|
|
@@ -42,10 +48,16 @@ import {
|
|
|
42
48
|
transformBezierPath
|
|
43
49
|
} from "../chunk-WEIK3UYB.js";
|
|
44
50
|
import "../chunk-PWBZH3FK.js";
|
|
51
|
+
import {
|
|
52
|
+
EffectRendererPool
|
|
53
|
+
} from "../chunk-EHLOCVWB.js";
|
|
45
54
|
import {
|
|
46
55
|
CAMERA_MOVE_MAX_MS,
|
|
56
|
+
EFFECT_LAYER_ROLES,
|
|
57
|
+
EFFECT_TRANSITION_MS,
|
|
47
58
|
EffectUnsupportedError,
|
|
48
59
|
EffectsSlot,
|
|
60
|
+
NoEffectsProviderError,
|
|
49
61
|
cameraBoxOfView,
|
|
50
62
|
cameraMoveField,
|
|
51
63
|
cameraMoveParts,
|
|
@@ -54,7 +66,7 @@ import {
|
|
|
54
66
|
isEffectError,
|
|
55
67
|
resolveCameraMoveSpec,
|
|
56
68
|
resolveEffectValues
|
|
57
|
-
} from "../chunk-
|
|
69
|
+
} from "../chunk-SIK3LT6M.js";
|
|
58
70
|
import "../chunk-V2ZCJRDM.js";
|
|
59
71
|
import "../chunk-2PBZBRP5.js";
|
|
60
72
|
import "../chunk-ODBT55Q7.js";
|
|
@@ -196,7 +208,10 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
|
|
|
196
208
|
}
|
|
197
209
|
};
|
|
198
210
|
var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
|
|
199
|
-
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
|
|
211
|
+
var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed", "layers", "transition"];
|
|
212
|
+
var LAYER_KEYS = ["id", "effect", "preset", "values", "subject", "role", "rect", "from", "until", "opacity", "fade", "provider", "name"];
|
|
213
|
+
var TRANSITION_KEYS = ["effect", "ms", "preset", "values", "provider", "name"];
|
|
214
|
+
var LAYER_ID = /^[A-Za-z_][\w.-]*$/;
|
|
200
215
|
var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric", "in", "during"];
|
|
201
216
|
var CAMERA_MOVE_KEYS = ["move", "ms", "preset", "values"];
|
|
202
217
|
var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade", "source"];
|
|
@@ -466,6 +481,50 @@ function shotShapeProblems(shot, where) {
|
|
|
466
481
|
}
|
|
467
482
|
if (!Array.isArray(shot.run)) problems.push(`${name}: "run" must be a list of commands (it may be empty).`);
|
|
468
483
|
else shot.run.forEach((command3, i) => problems.push(...commandShapeProblems(command3, `${name}, command ${i + 1}`)));
|
|
484
|
+
problems.push(...shotLayersShapeProblems(shot, name));
|
|
485
|
+
return problems;
|
|
486
|
+
}
|
|
487
|
+
var isLayerTime = (t) => isNum(t) && t >= 0 || isObject(t) && Object.keys(t).length === 1 && Number.isInteger(t.shots) && t.shots >= 1;
|
|
488
|
+
function shotLayersShapeProblems(shot, name) {
|
|
489
|
+
const problems = [];
|
|
490
|
+
if (shot.layers !== void 0) {
|
|
491
|
+
if (!Array.isArray(shot.layers)) problems.push(`${name}: "layers" must be a list of layers.`);
|
|
492
|
+
else
|
|
493
|
+
shot.layers.forEach((layer, i) => {
|
|
494
|
+
const where = `${name}, layer ${i + 1}`;
|
|
495
|
+
if (!isObject(layer)) return void problems.push(`${where} must be an object.`);
|
|
496
|
+
unknownKeys(layer, LAYER_KEYS, where, problems);
|
|
497
|
+
if (typeof layer.id !== "string" || !LAYER_ID.test(layer.id)) problems.push(`${where}: "id" must be letters, digits, "_", "-" or "." (e.g. "layer1").`);
|
|
498
|
+
if (layer.effect === void 0 && layer.subject === void 0) problems.push(`${where} needs an "effect", a "subject" or both.`);
|
|
499
|
+
if (layer.effect !== void 0 && (typeof layer.effect !== "string" || !layer.effect)) problems.push(`${where}: "effect" must be an effect id.`);
|
|
500
|
+
if (layer.subject !== void 0) {
|
|
501
|
+
const kind = effectSubjectKind(layer.subject);
|
|
502
|
+
if (kind === "video") problems.push(`${where}: an animation layer cannot play a video; use { "image" }, { "svg" }, { "text" } or { "slot" } (the video editor plays video).`);
|
|
503
|
+
else if (!kind || !String(layer.subject[kind]).trim()) problems.push(`${where}: "subject" must be { "image": url }, { "svg": markup }, { "text": words } or { "slot": name }.`);
|
|
504
|
+
}
|
|
505
|
+
if (layer.role !== void 0 && !EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: "role" must be one of ${EFFECT_LAYER_ROLES.join(", ")}.`);
|
|
506
|
+
if (layer.rect !== void 0) {
|
|
507
|
+
const r = layer.rect;
|
|
508
|
+
if (!isObject(r) || !["x", "y", "width", "height"].every((k) => isNum(r[k])) || !(r.width > 0 && r.height > 0)) problems.push(`${where}: "rect" must be { x, y, width, height } in artwork units, width and height > 0.`);
|
|
509
|
+
}
|
|
510
|
+
for (const k of ["from", "until"]) if (layer[k] !== void 0 && !isLayerTime(layer[k])) problems.push(`${where}: "${k}" must be ms \u2265 0 after the shot starts, or { "shots": n } (n \u2265 1: when the n-th shot after this one starts).`);
|
|
511
|
+
if (layer.opacity !== void 0 && !(isNum(layer.opacity) && layer.opacity >= 0 && layer.opacity <= 1)) problems.push(`${where}: "opacity" must be a number from 0 to 1.`);
|
|
512
|
+
if (layer.fade !== void 0 && !(isNum(layer.fade) && layer.fade >= 0)) problems.push(`${where}: "fade" must be ms \u2265 0.`);
|
|
513
|
+
if (layer.preset !== void 0 && typeof layer.preset !== "string") problems.push(`${where}: "preset" is a preset's name.`);
|
|
514
|
+
if (layer.values !== void 0 && !isObject(layer.values)) problems.push(`${where}: "values" is an object of the effect's control values.`);
|
|
515
|
+
for (const k of ["provider", "name"]) if (layer[k] !== void 0 && typeof layer[k] !== "string") problems.push(`${where}: "${k}" must be a string.`);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (shot.transition !== void 0) {
|
|
519
|
+
const t = shot.transition;
|
|
520
|
+
if (!isObject(t) || typeof t.effect !== "string" || !t.effect) problems.push(`${name}: "transition" is { "effect": id, "ms"?, "preset"?, "values"? }.`);
|
|
521
|
+
else {
|
|
522
|
+
unknownKeys(t, TRANSITION_KEYS, `${name} transition`, problems);
|
|
523
|
+
if (t.ms !== void 0 && !(isNum(t.ms) && t.ms > 0 && t.ms <= 6e4)) problems.push(`${name} transition: "ms" must be a number from 1 to 60000.`);
|
|
524
|
+
if (t.preset !== void 0 && typeof t.preset !== "string") problems.push(`${name} transition: "preset" is a preset's name.`);
|
|
525
|
+
if (t.values !== void 0 && !isObject(t.values)) problems.push(`${name} transition: "values" is an object of the effect's control values.`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
469
528
|
return problems;
|
|
470
529
|
}
|
|
471
530
|
function motionSceneShapeProblems(input) {
|
|
@@ -494,8 +553,8 @@ function motionSceneShapeProblems(input) {
|
|
|
494
553
|
problems.push('Scene: "beat" must be { "bpm": 1\u20131000, "offset"?: ms, "meter"?: beats a bar }.');
|
|
495
554
|
}
|
|
496
555
|
if (input.narration !== void 0) {
|
|
497
|
-
const
|
|
498
|
-
if (!isObject(
|
|
556
|
+
const n2 = input.narration;
|
|
557
|
+
if (!isObject(n2) || !Array.isArray(n2.words) || !n2.words.every((w, i, all) => isObject(w) && typeof w.text === "string" && isNum(w.start) && w.start >= 0 && (w.end === void 0 || isNum(w.end) && w.end >= w.start) && (i === 0 || w.start >= (all[i - 1].start ?? 0))))
|
|
499
558
|
problems.push('Scene: "narration" must be { "words": [{ "text", "start": ms, "end"?: ms }, \u2026] } in time order.');
|
|
500
559
|
}
|
|
501
560
|
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.');
|
|
@@ -513,6 +572,17 @@ function motionSceneShapeProblems(input) {
|
|
|
513
572
|
problems.push('Scene: "presets" must be a list of { name, values }.');
|
|
514
573
|
if (!Array.isArray(input.shots)) problems.push('Scene: "shots" must be a list.');
|
|
515
574
|
else input.shots.forEach((shot, i) => problems.push(...shotShapeProblems(shot, `Shot ${i + 1}`)));
|
|
575
|
+
if (Array.isArray(input.shots)) {
|
|
576
|
+
const seen = /* @__PURE__ */ new Set();
|
|
577
|
+
for (const shot of input.shots)
|
|
578
|
+
if (isObject(shot) && Array.isArray(shot.layers)) {
|
|
579
|
+
for (const layer of shot.layers)
|
|
580
|
+
if (isObject(layer) && typeof layer.id === "string") {
|
|
581
|
+
if (seen.has(layer.id)) problems.push(`Layer id "${layer.id}" is used twice; layer ids are unique in the scene.`);
|
|
582
|
+
seen.add(layer.id);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
516
586
|
return problems;
|
|
517
587
|
}
|
|
518
588
|
function parseMotionScene(input) {
|
|
@@ -556,7 +626,7 @@ function matchMotionNodes(from, into) {
|
|
|
556
626
|
for (const x of [...left]) {
|
|
557
627
|
const k = key(x, a);
|
|
558
628
|
if (!k) continue;
|
|
559
|
-
const y = [...right].find((
|
|
629
|
+
const y = [...right].find((n2) => key(n2, b) === k);
|
|
560
630
|
if (!y) continue;
|
|
561
631
|
left.delete(x);
|
|
562
632
|
right.delete(y);
|
|
@@ -564,18 +634,18 @@ function matchMotionNodes(from, into) {
|
|
|
564
634
|
else out.pairs.push([x.id, y.id]);
|
|
565
635
|
}
|
|
566
636
|
};
|
|
567
|
-
pair((
|
|
568
|
-
pair((
|
|
637
|
+
pair((n2) => n2.name?.trim() || void 0);
|
|
638
|
+
pair((n2, g) => localId(n2.id, g.id) || void 0);
|
|
569
639
|
for (const x of [...left]) {
|
|
570
|
-
const y = [...right].find((
|
|
640
|
+
const y = [...right].find((n2) => n2.type === x.type);
|
|
571
641
|
if (!y) continue;
|
|
572
642
|
left.delete(x);
|
|
573
643
|
right.delete(y);
|
|
574
644
|
if (x.type === "group" && y.type === "group") visit(x, y);
|
|
575
645
|
else out.pairs.push([x.id, y.id]);
|
|
576
646
|
}
|
|
577
|
-
out.unpairedFrom.push(...[...left].map((
|
|
578
|
-
out.unpairedInto.push(...[...right].map((
|
|
647
|
+
out.unpairedFrom.push(...[...left].map((n2) => n2.id));
|
|
648
|
+
out.unpairedInto.push(...[...right].map((n2) => n2.id));
|
|
579
649
|
};
|
|
580
650
|
visit(from, into);
|
|
581
651
|
return out;
|
|
@@ -583,10 +653,10 @@ function matchMotionNodes(from, into) {
|
|
|
583
653
|
|
|
584
654
|
// src/animation-editor/smooth.ts
|
|
585
655
|
function smoothMotionLegs(times, values) {
|
|
586
|
-
const
|
|
587
|
-
const slope = new Array(
|
|
656
|
+
const n2 = values.length;
|
|
657
|
+
const slope = new Array(n2).fill(0);
|
|
588
658
|
const secant = (i) => (values[i + 1] - values[i]) / Math.max(1e-9, times[i + 1] - times[i]);
|
|
589
|
-
for (let i = 1; i <
|
|
659
|
+
for (let i = 1; i < n2 - 1; i++) {
|
|
590
660
|
const a = secant(i - 1);
|
|
591
661
|
const b = secant(i);
|
|
592
662
|
if (a * b <= 0) continue;
|
|
@@ -595,7 +665,7 @@ function smoothMotionLegs(times, values) {
|
|
|
595
665
|
slope[i] = 3 * (ha + hb) / ((2 * hb + ha) / a + (hb + 2 * ha) / b);
|
|
596
666
|
}
|
|
597
667
|
const legs = [];
|
|
598
|
-
for (let i = 0; i + 1 <
|
|
668
|
+
for (let i = 0; i + 1 < n2; i++) {
|
|
599
669
|
const dt = times[i + 1] - times[i];
|
|
600
670
|
const dv = values[i + 1] - values[i];
|
|
601
671
|
if (Math.abs(dv) < 1e-9) {
|
|
@@ -658,8 +728,8 @@ function motionTextUnits(layout, by) {
|
|
|
658
728
|
return layout.lines.flatMap((l) => l.words.flatMap((w) => w.letters.map((id) => [id])));
|
|
659
729
|
}
|
|
660
730
|
function motionCascade(units, cascade = "forward", seed = 1) {
|
|
661
|
-
const
|
|
662
|
-
const mid = (
|
|
731
|
+
const n2 = units.length;
|
|
732
|
+
const mid = (n2 - 1) / 2;
|
|
663
733
|
const indexed = units.map((u, i) => ({ u, i }));
|
|
664
734
|
if (cascade === "backward") return [...units].reverse();
|
|
665
735
|
if (cascade === "center") return indexed.sort((a, b) => Math.abs(a.i - mid) - Math.abs(b.i - mid) || a.i - b.i).map((x) => x.u);
|
|
@@ -674,7 +744,7 @@ function motionCascade(units, cascade = "forward", seed = 1) {
|
|
|
674
744
|
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
675
745
|
};
|
|
676
746
|
const out = [...units];
|
|
677
|
-
for (let i =
|
|
747
|
+
for (let i = n2 - 1; i > 0; i--) {
|
|
678
748
|
const j = Math.floor(rand() * (i + 1));
|
|
679
749
|
[out[i], out[j]] = [out[j], out[i]];
|
|
680
750
|
}
|
|
@@ -739,12 +809,12 @@ function strokeReach(node) {
|
|
|
739
809
|
return reach;
|
|
740
810
|
}
|
|
741
811
|
function motionRevealPadding(entry) {
|
|
742
|
-
const
|
|
743
|
-
if (!
|
|
812
|
+
const box2 = entry.box;
|
|
813
|
+
if (!box2 || !(box2.width > 0) || !(box2.height > 0)) return null;
|
|
744
814
|
const s = strokeReach(entry.node);
|
|
745
|
-
return [Math.min(1, s /
|
|
815
|
+
return [Math.min(1, s / box2.width + 0.02), Math.min(1, s / box2.height + 0.02)];
|
|
746
816
|
}
|
|
747
|
-
var clipRound = (
|
|
817
|
+
var clipRound = (n2) => Math.round(n2 * 1e4) / 1e4 || 0;
|
|
748
818
|
function revealFull(property, [px, py]) {
|
|
749
819
|
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);
|
|
750
820
|
}
|
|
@@ -978,8 +1048,8 @@ function motionDriverSources(timeline, node, properties, seen = /* @__PURE__ */
|
|
|
978
1048
|
}
|
|
979
1049
|
function motionCommandRepeat(command3) {
|
|
980
1050
|
if (command3.type === "wait" || command3.type === "enter" || command3.type === "exit") return 1;
|
|
981
|
-
const
|
|
982
|
-
return Number.isInteger(
|
|
1051
|
+
const n2 = command3.repeat ?? 1;
|
|
1052
|
+
return Number.isInteger(n2) && n2 >= 1 ? n2 : 1;
|
|
983
1053
|
}
|
|
984
1054
|
function motionCommandDuration(command3, scene, target) {
|
|
985
1055
|
return motionCycleDuration(command3, scene, target) * motionCommandRepeat(command3);
|
|
@@ -1054,13 +1124,14 @@ function compileMotionScene(scene) {
|
|
|
1054
1124
|
let cursor = start;
|
|
1055
1125
|
const f = motionShotTimeScale(shot);
|
|
1056
1126
|
const moveIn = shot.camera ? cameraMoveParts(shot.camera.in) : void 0;
|
|
1127
|
+
const transitionMs = index > 0 && shot.transition ? (shot.transition.ms ?? EFFECT_TRANSITION_MS) * f : 0;
|
|
1057
1128
|
const moveDuring = shot.camera ? cameraMoveParts(shot.camera.during) : void 0;
|
|
1058
1129
|
let playsIn = false;
|
|
1059
1130
|
if (shot.camera) {
|
|
1060
1131
|
const framed = motionCameraFramed(shot.camera);
|
|
1061
1132
|
const rect = framed ? resolveMotionCamera(shot.camera, scene) : null;
|
|
1062
1133
|
if (rect && rect.width > 0 && rect.height > 0 || !framed && moveIn) {
|
|
1063
|
-
const ms2 = index === 0 ? 0 : (moveIn?.ms ?? shot.camera.ms ?? MOTION_CAMERA_MS) * f;
|
|
1134
|
+
const ms2 = index === 0 || transitionMs > 0 && !moveIn ? 0 : (moveIn?.ms ?? shot.camera.ms ?? MOTION_CAMERA_MS) * f;
|
|
1064
1135
|
cameraMoves.push({ shot: shot.id, start, end: start + ms2, to: rect, ease: shot.camera.ease ?? MOTION_EASE_IN_OUT, geometric: shot.camera.geometric === true });
|
|
1065
1136
|
cursor = start + ms2;
|
|
1066
1137
|
playsIn = !!moveIn && index > 0;
|
|
@@ -1081,7 +1152,7 @@ function compileMotionScene(scene) {
|
|
|
1081
1152
|
prevStart = begin;
|
|
1082
1153
|
runEnd = Math.max(runEnd, end2);
|
|
1083
1154
|
});
|
|
1084
|
-
const end = runEnd + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
|
|
1155
|
+
const end = Math.max(runEnd, start + transitionMs) + (shot.hold ?? MOTION_DEFAULT_HOLD) * f;
|
|
1085
1156
|
shots.push({
|
|
1086
1157
|
id: shot.id,
|
|
1087
1158
|
index,
|
|
@@ -1093,7 +1164,8 @@ function compileMotionScene(scene) {
|
|
|
1093
1164
|
...cue !== void 0 ? { cue } : {},
|
|
1094
1165
|
...missedBy !== void 0 ? { missedBy } : {},
|
|
1095
1166
|
...playsIn ? { cameraIn: moveIn.move } : {},
|
|
1096
|
-
...moveDuring ? { cameraDuring: moveDuring.move } : {}
|
|
1167
|
+
...moveDuring ? { cameraDuring: moveDuring.move } : {},
|
|
1168
|
+
...transitionMs > 0 ? { transition: shot.transition.effect, transitionEnd: start + transitionMs } : {}
|
|
1097
1169
|
});
|
|
1098
1170
|
t = end;
|
|
1099
1171
|
});
|
|
@@ -1157,14 +1229,14 @@ function compileMotionScene(scene) {
|
|
|
1157
1229
|
const made = segments.slice(first);
|
|
1158
1230
|
const steps = command3.steps ?? scene.steps;
|
|
1159
1231
|
if (steps && steps > 0) for (const s of made) s.steps = steps;
|
|
1160
|
-
const
|
|
1232
|
+
const n2 = motionCommandRepeat(command3);
|
|
1161
1233
|
const mode = command3.repeatMode ?? "loop";
|
|
1162
1234
|
const shift = (v, by, k) => {
|
|
1163
1235
|
if (typeof v === "number" && typeof by === "number") return v + by * k;
|
|
1164
1236
|
if (Array.isArray(v) && Array.isArray(by) && v.length === by.length) return v.map((x, i) => x + by[i] * k);
|
|
1165
1237
|
return v;
|
|
1166
1238
|
};
|
|
1167
|
-
for (let k = 1; k <
|
|
1239
|
+
for (let k = 1; k < n2; k++) {
|
|
1168
1240
|
const at = start + k * cycle;
|
|
1169
1241
|
for (const s of made) {
|
|
1170
1242
|
if (mode === "mirror" && k % 2 === 1) {
|
|
@@ -1529,8 +1601,8 @@ function motionCameraTransform(view, width) {
|
|
|
1529
1601
|
const distance = (view.perspective ?? 2) * width;
|
|
1530
1602
|
return `perspective(${Math.round(distance)}px)${view.tilt ? ` rotateX(${round(view.tilt)}deg)` : ""}${view.turn ? ` rotateY(${round(-view.turn)}deg)` : ""}`;
|
|
1531
1603
|
}
|
|
1532
|
-
var round = (
|
|
1533
|
-
function withCameraViewEffects(svg, view,
|
|
1604
|
+
var round = (n2) => Math.round(n2 * 1e3) / 1e3;
|
|
1605
|
+
function withCameraViewEffects(svg, view, box2, idPrefix = "vg-") {
|
|
1534
1606
|
if (!view || !view.roll && !(view.blur && view.blur > 0)) return svg;
|
|
1535
1607
|
const open = svg.match(/<svg\b[^>]*>/);
|
|
1536
1608
|
const close = svg.lastIndexOf("</svg>");
|
|
@@ -1538,7 +1610,7 @@ function withCameraViewEffects(svg, view, box, idPrefix = "vg-") {
|
|
|
1538
1610
|
const startBody = open.index + open[0].length;
|
|
1539
1611
|
const attrs = [];
|
|
1540
1612
|
let defs = "";
|
|
1541
|
-
if (view.roll) attrs.push(`transform="rotate(${round(view.roll)} ${round(
|
|
1613
|
+
if (view.roll) attrs.push(`transform="rotate(${round(view.roll)} ${round(box2.x + box2.width / 2)} ${round(box2.y + box2.height / 2)})"`);
|
|
1542
1614
|
if (view.blur && view.blur > 0) {
|
|
1543
1615
|
const s = view.blur / view.zoom / 2;
|
|
1544
1616
|
const a = (view.blurAngle ?? 0) * Math.PI / 180;
|
|
@@ -1573,6 +1645,329 @@ async function checkMotionCameraMoves(scene, provider) {
|
|
|
1573
1645
|
return issues;
|
|
1574
1646
|
}
|
|
1575
1647
|
|
|
1648
|
+
// src/animation-editor/layers.ts
|
|
1649
|
+
var spanCache = /* @__PURE__ */ new WeakMap();
|
|
1650
|
+
function motionLayerSpans(scene) {
|
|
1651
|
+
const hit = spanCache.get(scene);
|
|
1652
|
+
if (hit) return hit;
|
|
1653
|
+
const timeline = compileMotionScene(scene);
|
|
1654
|
+
const shots = timeline.shots;
|
|
1655
|
+
const total = timeline.duration;
|
|
1656
|
+
const at = (shotIndex2, t, fallback) => {
|
|
1657
|
+
if (t === void 0) return fallback;
|
|
1658
|
+
if (typeof t === "number") return shots[shotIndex2].start + t;
|
|
1659
|
+
return shots[shotIndex2 + t.shots]?.start ?? total;
|
|
1660
|
+
};
|
|
1661
|
+
const out = [];
|
|
1662
|
+
scene.shots.forEach((shot, shotIndex2) => {
|
|
1663
|
+
(shot.layers ?? []).forEach((layer, index) => {
|
|
1664
|
+
const start = at(shotIndex2, layer.from, shots[shotIndex2].start);
|
|
1665
|
+
const end = Math.max(start, at(shotIndex2, layer.until ?? { shots: 1 }, total));
|
|
1666
|
+
out.push({ layer, shot: shot.id, shotIndex: shotIndex2, index, role: effectLayerRole(layer), start, end });
|
|
1667
|
+
});
|
|
1668
|
+
});
|
|
1669
|
+
spanCache.set(scene, out);
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
function motionLayersAt(scene, time) {
|
|
1673
|
+
if (!scene.shots.some((s) => s.layers?.length)) return [];
|
|
1674
|
+
const total = compileMotionScene(scene).duration;
|
|
1675
|
+
const out = [];
|
|
1676
|
+
for (const span of motionLayerSpans(scene)) {
|
|
1677
|
+
if (time < span.start || time > span.end || time === span.end && span.end < total) continue;
|
|
1678
|
+
const fade = span.layer.fade ?? 0;
|
|
1679
|
+
const k = fade > 0 ? Math.min(1, (time - span.start) / fade, (span.end - time) / fade) : 1;
|
|
1680
|
+
const opacity = Math.max(0, Math.min(1, (span.layer.opacity ?? 1) * k));
|
|
1681
|
+
if (opacity <= 0) continue;
|
|
1682
|
+
out.push({ ...span, time: time - span.start, opacity });
|
|
1683
|
+
}
|
|
1684
|
+
return out;
|
|
1685
|
+
}
|
|
1686
|
+
function motionTransitionAt(scene, time) {
|
|
1687
|
+
if (!scene.shots.some((s) => s.transition)) return null;
|
|
1688
|
+
for (const s of compileMotionScene(scene).shots) {
|
|
1689
|
+
if (s.transition && s.transitionEnd !== void 0 && time >= s.start && time < s.transitionEnd)
|
|
1690
|
+
return { shot: s.id, shotIndex: s.index, effect: s.transition, start: s.start, end: s.transitionEnd, progress: (time - s.start) / (s.transitionEnd - s.start) };
|
|
1691
|
+
}
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
var motionSceneHasLayers = (scene) => scene.shots.some((s) => s.layers?.length || s.transition);
|
|
1695
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1696
|
+
var n = (v) => String(Math.round(v * 1e3) / 1e3);
|
|
1697
|
+
var box = (b) => `x="${n(b.x)}" y="${n(b.y)}" width="${n(b.width)}" height="${n(b.height)}"`;
|
|
1698
|
+
var svgDataUrl = (svg) => `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
1699
|
+
function subjectSvg(subject, b, opacity, pictures) {
|
|
1700
|
+
const o = opacity < 1 ? ` opacity="${n(opacity)}"` : "";
|
|
1701
|
+
if ("image" in subject) {
|
|
1702
|
+
const href = pictures?.subject(subject) ?? subject.image;
|
|
1703
|
+
return `<image href="${esc(href)}" ${box(b)} preserveAspectRatio="xMidYMid meet"${o}/>`;
|
|
1704
|
+
}
|
|
1705
|
+
if ("svg" in subject) {
|
|
1706
|
+
const href = subject.svg.trimStart().startsWith("<") ? svgDataUrl(subject.svg) : subject.svg;
|
|
1707
|
+
return `<image href="${esc(href)}" ${box(b)} preserveAspectRatio="xMidYMid meet"${o}/>`;
|
|
1708
|
+
}
|
|
1709
|
+
if ("text" in subject) {
|
|
1710
|
+
const size = Math.max(1, Math.min(b.height * 0.6, b.width / Math.max(1, subject.text.length) * 1.6));
|
|
1711
|
+
return `<text x="${n(b.x + b.width / 2)}" y="${n(b.y + b.height / 2)}" text-anchor="middle" dominant-baseline="central" font-family="${esc(subject.font ?? "system-ui, sans-serif")}" font-weight="${esc(String(subject.weight ?? 700))}" font-size="${n(size)}" fill="#ffffff"${o}>${esc(subject.text)}</text>`;
|
|
1712
|
+
}
|
|
1713
|
+
return "";
|
|
1714
|
+
}
|
|
1715
|
+
function layerSvg(scene, active, camera, pictures) {
|
|
1716
|
+
const b = active.layer.rect ?? camera;
|
|
1717
|
+
if (!active.layer.effect) return active.layer.subject ? subjectSvg(active.layer.subject, b, active.opacity, pictures) : "";
|
|
1718
|
+
const href = pictures?.layer(scene, active, b);
|
|
1719
|
+
if (!href) return "";
|
|
1720
|
+
return `<image href="${esc(href)}" ${box(b)} preserveAspectRatio="none" data-layer="${esc(active.layer.id)}"${active.opacity < 1 ? ` opacity="${n(active.opacity)}"` : ""}/>`;
|
|
1721
|
+
}
|
|
1722
|
+
function withMotionShotLayers(svg, scene, time, camera, pictures, background) {
|
|
1723
|
+
const active = motionLayersAt(scene, time);
|
|
1724
|
+
const open = svg.indexOf(">") + 1;
|
|
1725
|
+
let body = svg.slice(open, svg.lastIndexOf("</svg>"));
|
|
1726
|
+
const defs = /^\s*<defs>[\s\S]*?<\/defs>/.exec(body)?.[0] ?? "";
|
|
1727
|
+
body = body.slice(defs.length);
|
|
1728
|
+
const bg = background && scene.artwork.background ? `<rect width="${n(scene.artwork.width)}" height="${n(scene.artwork.height)}" fill="${esc(scene.artwork.background)}"/>` : "";
|
|
1729
|
+
const draw = (roles) => active.filter((a) => roles.includes(a.role)).map((a) => layerSvg(scene, a, camera, pictures)).join("");
|
|
1730
|
+
return `${svg.slice(0, open)}${defs}${bg}${draw(["background"])}${body}${draw(["subject"])}${draw(["overlay"])}</svg>`;
|
|
1731
|
+
}
|
|
1732
|
+
function motionTransitionSvg(svg, from, active, camera, picture) {
|
|
1733
|
+
const open = svg.indexOf(">") + 1;
|
|
1734
|
+
if (picture) return `${svg.slice(0, open)}<image href="${esc(picture)}" ${box(camera)} preserveAspectRatio="none" data-transition="${esc(active.effect)}"/></svg>`;
|
|
1735
|
+
const inner = svg.slice(open, svg.lastIndexOf("</svg>"));
|
|
1736
|
+
const fromView = /viewBox="([^"]*)"/.exec(from)?.[1] ?? "";
|
|
1737
|
+
const fromInner = from.slice(from.indexOf(">") + 1, from.lastIndexOf("</svg>"));
|
|
1738
|
+
return `${svg.slice(0, open)}<svg ${box(camera)} viewBox="${fromView}" preserveAspectRatio="none" data-transition-from="">${fromInner}</svg><g opacity="${n(active.progress)}" data-transition-to="">${inner}</g></svg>`;
|
|
1739
|
+
}
|
|
1740
|
+
var sceneIds = /* @__PURE__ */ new WeakMap();
|
|
1741
|
+
var nextSceneId = 1;
|
|
1742
|
+
var sceneId = (scene) => {
|
|
1743
|
+
let id = sceneIds.get(scene);
|
|
1744
|
+
if (!id) sceneIds.set(scene, id = nextSceneId++);
|
|
1745
|
+
return id;
|
|
1746
|
+
};
|
|
1747
|
+
var MotionLayersEngine = class {
|
|
1748
|
+
slot;
|
|
1749
|
+
host;
|
|
1750
|
+
frameSvg;
|
|
1751
|
+
cache = /* @__PURE__ */ new Map();
|
|
1752
|
+
pending = /* @__PURE__ */ new Map();
|
|
1753
|
+
failures = /* @__PURE__ */ new Map();
|
|
1754
|
+
descriptions = /* @__PURE__ */ new Map();
|
|
1755
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1756
|
+
pool;
|
|
1757
|
+
options;
|
|
1758
|
+
generation = 0;
|
|
1759
|
+
detach;
|
|
1760
|
+
constructor(options = {}) {
|
|
1761
|
+
this.slot = options.slot ?? new EffectsSlot();
|
|
1762
|
+
this.host = options.host ?? null;
|
|
1763
|
+
this.frameSvg = options.frameSvg;
|
|
1764
|
+
this.options = { scale: options.scale ?? 1, maxSize: options.maxSize ?? 1280, fps: options.fps ?? 30, maxEntries: options.maxEntries ?? 160 };
|
|
1765
|
+
this.pool = new EffectRendererPool({ maxRenderers: options.maxRenderers });
|
|
1766
|
+
this.detach = this.slot.subscribe(() => {
|
|
1767
|
+
this.reset();
|
|
1768
|
+
this.notify();
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
get provider() {
|
|
1772
|
+
return this.slot.provider;
|
|
1773
|
+
}
|
|
1774
|
+
/** Called when a picture is ready (draw the frame again). */
|
|
1775
|
+
subscribe(listener) {
|
|
1776
|
+
this.listeners.add(listener);
|
|
1777
|
+
return () => this.listeners.delete(listener);
|
|
1778
|
+
}
|
|
1779
|
+
/** Why pictures failed, newest last. */
|
|
1780
|
+
failuresList() {
|
|
1781
|
+
return [...this.failures.values()];
|
|
1782
|
+
}
|
|
1783
|
+
/** True while any picture is being made. */
|
|
1784
|
+
get busy() {
|
|
1785
|
+
return this.pending.size > 0;
|
|
1786
|
+
}
|
|
1787
|
+
/** Resolves once every picture asked for so far is made (or failed). */
|
|
1788
|
+
async idle() {
|
|
1789
|
+
while (this.pending.size) await Promise.all([...this.pending.values()]);
|
|
1790
|
+
}
|
|
1791
|
+
/** Forgets every picture and failure (a new provider, or the host changed). */
|
|
1792
|
+
reset() {
|
|
1793
|
+
this.generation++;
|
|
1794
|
+
this.cache.clear();
|
|
1795
|
+
this.pending.clear();
|
|
1796
|
+
this.failures.clear();
|
|
1797
|
+
this.descriptions.clear();
|
|
1798
|
+
this.pool.clear();
|
|
1799
|
+
}
|
|
1800
|
+
dispose() {
|
|
1801
|
+
this.detach();
|
|
1802
|
+
this.reset();
|
|
1803
|
+
this.listeners.clear();
|
|
1804
|
+
}
|
|
1805
|
+
notify() {
|
|
1806
|
+
this.listeners.forEach((l) => l());
|
|
1807
|
+
}
|
|
1808
|
+
size(b) {
|
|
1809
|
+
const k = Math.min(this.options.scale, this.options.maxSize / Math.max(b.width, b.height));
|
|
1810
|
+
return { width: Math.max(1, Math.round(b.width * k)), height: Math.max(1, Math.round(b.height * k)) };
|
|
1811
|
+
}
|
|
1812
|
+
snap(t) {
|
|
1813
|
+
const step = 1e3 / this.options.fps;
|
|
1814
|
+
return Math.round(Math.round(t / step) * step * 1e3) / 1e3;
|
|
1815
|
+
}
|
|
1816
|
+
get(key) {
|
|
1817
|
+
const v = this.cache.get(key);
|
|
1818
|
+
if (v === void 0) return void 0;
|
|
1819
|
+
this.cache.delete(key);
|
|
1820
|
+
this.cache.set(key, v);
|
|
1821
|
+
return v;
|
|
1822
|
+
}
|
|
1823
|
+
request(key, info2, work) {
|
|
1824
|
+
if (this.cache.has(key) || this.pending.has(key) || this.failures.has(key)) return;
|
|
1825
|
+
const generation = this.generation;
|
|
1826
|
+
const run = work().then((url) => {
|
|
1827
|
+
if (generation !== this.generation) return;
|
|
1828
|
+
this.cache.set(key, url);
|
|
1829
|
+
while (this.cache.size > this.options.maxEntries) this.cache.delete(this.cache.keys().next().value);
|
|
1830
|
+
}).catch((error) => {
|
|
1831
|
+
if (generation !== this.generation) return;
|
|
1832
|
+
this.failures.set(key, { key, ...info2, error: error instanceof Error ? error : new Error(String(error)) });
|
|
1833
|
+
}).finally(() => {
|
|
1834
|
+
if (generation !== this.generation) return;
|
|
1835
|
+
this.pending.delete(key);
|
|
1836
|
+
this.notify();
|
|
1837
|
+
});
|
|
1838
|
+
this.pending.set(key, run);
|
|
1839
|
+
}
|
|
1840
|
+
describe(id) {
|
|
1841
|
+
const provider = this.slot.require();
|
|
1842
|
+
let d = this.descriptions.get(id);
|
|
1843
|
+
if (!d) {
|
|
1844
|
+
d = Promise.resolve(provider.describe(id));
|
|
1845
|
+
this.descriptions.set(id, d);
|
|
1846
|
+
d.catch(() => this.descriptions.delete(id));
|
|
1847
|
+
}
|
|
1848
|
+
return d;
|
|
1849
|
+
}
|
|
1850
|
+
subject(subject) {
|
|
1851
|
+
if (subject.image.startsWith("data:")) return subject.image;
|
|
1852
|
+
const host = this.host;
|
|
1853
|
+
if (!host) return void 0;
|
|
1854
|
+
const key = `img:${subject.image}`;
|
|
1855
|
+
this.request(key, {}, () => host.loadImage(subject.image));
|
|
1856
|
+
return this.get(key);
|
|
1857
|
+
}
|
|
1858
|
+
/** The pixels a layer's subject feeds its effect, at `size`. */
|
|
1859
|
+
async subjectPixels(subject, size) {
|
|
1860
|
+
const host = this.host;
|
|
1861
|
+
const b = { x: 0, y: 0, ...size };
|
|
1862
|
+
let s = subject;
|
|
1863
|
+
if ("image" in s && !s.image.startsWith("data:")) s = { image: await host.loadImage(s.image) };
|
|
1864
|
+
return host.rasterize(`<svg xmlns="http://www.w3.org/2000/svg" width="${size.width}" height="${size.height}" viewBox="0 0 ${size.width} ${size.height}">${subjectSvg(s, b, 1, void 0)}</svg>`, size.width, size.height);
|
|
1865
|
+
}
|
|
1866
|
+
layer(scene, active, b) {
|
|
1867
|
+
const provider = this.slot.provider;
|
|
1868
|
+
const host = this.host;
|
|
1869
|
+
const layer = active.layer;
|
|
1870
|
+
if (!provider || !host || !layer.effect) return void 0;
|
|
1871
|
+
if (layer.subject && "slot" in layer.subject) return void 0;
|
|
1872
|
+
const size = this.size(b);
|
|
1873
|
+
const time = this.snap(active.time);
|
|
1874
|
+
const key = `layer:${provider.id}:${layer.effect}:${JSON.stringify(layer.values ?? {})}:${JSON.stringify(layer.subject ?? null)}:${size.width}x${size.height}@${time}`;
|
|
1875
|
+
const effect = layer.effect;
|
|
1876
|
+
this.request(key, { layer: layer.id }, async () => {
|
|
1877
|
+
const d = await this.describe(effect);
|
|
1878
|
+
const values = resolveEffectValues(d, layer.values);
|
|
1879
|
+
const input = effectLayerInput(d);
|
|
1880
|
+
const inputs = layer.subject && input ? { [input]: await this.subjectPixels(layer.subject, size) } : void 0;
|
|
1881
|
+
const pixels = await this.pool.render(provider, effect, size, { time, values, ...inputs ? { inputs } : {} });
|
|
1882
|
+
return host.encode(pixels);
|
|
1883
|
+
});
|
|
1884
|
+
return this.get(key);
|
|
1885
|
+
}
|
|
1886
|
+
transition(scene, time, active) {
|
|
1887
|
+
const provider = this.slot.provider;
|
|
1888
|
+
const host = this.host;
|
|
1889
|
+
const frameSvg = this.frameSvg;
|
|
1890
|
+
if (!provider || !host || !frameSvg) return void 0;
|
|
1891
|
+
const shot = scene.shots[active.shotIndex];
|
|
1892
|
+
const t = shot.transition;
|
|
1893
|
+
const size = this.size({ x: 0, y: 0, width: scene.artwork.width, height: scene.artwork.height });
|
|
1894
|
+
const at = Math.min(active.end - 1e-3, this.snap(time));
|
|
1895
|
+
const progress = (at - active.start) / (active.end - active.start);
|
|
1896
|
+
const key = `transition:${sceneId(scene)}:${provider.id}:${t.effect}:${JSON.stringify(t.values ?? {})}:${size.width}x${size.height}@${at}`;
|
|
1897
|
+
const fromKey = `from:${sceneId(scene)}:${active.shot}:${size.width}x${size.height}`;
|
|
1898
|
+
this.request(key, { transition: active.shot }, async () => {
|
|
1899
|
+
const d = await this.describe(t.effect);
|
|
1900
|
+
const values = resolveEffectValues(d, t.values);
|
|
1901
|
+
frameSvg(scene, Math.max(0, active.start - 1e-3));
|
|
1902
|
+
frameSvg(scene, at);
|
|
1903
|
+
await this.idleExcept(key);
|
|
1904
|
+
const fromSvg = frameSvg(scene, Math.max(0, active.start - 1e-3));
|
|
1905
|
+
const toSvg = frameSvg(scene, at);
|
|
1906
|
+
let from = this.fromPixels.get(fromKey);
|
|
1907
|
+
if (!from) {
|
|
1908
|
+
from = host.rasterize(fromSvg, size.width, size.height);
|
|
1909
|
+
this.fromPixels.set(fromKey, from);
|
|
1910
|
+
if (this.fromPixels.size > 8) this.fromPixels.delete(this.fromPixels.keys().next().value);
|
|
1911
|
+
}
|
|
1912
|
+
const to = await host.rasterize(toSvg, size.width, size.height);
|
|
1913
|
+
const pixels = await this.pool.render(provider, t.effect, size, { time: at - active.start, progress, values, inputs: { from: await from, to } });
|
|
1914
|
+
return host.encode(pixels);
|
|
1915
|
+
});
|
|
1916
|
+
return this.get(key);
|
|
1917
|
+
}
|
|
1918
|
+
fromPixels = /* @__PURE__ */ new Map();
|
|
1919
|
+
async idleExcept(key) {
|
|
1920
|
+
for (; ; ) {
|
|
1921
|
+
const others = [...this.pending].filter(([k]) => k !== key && !k.startsWith("transition:")).map(([, p]) => p);
|
|
1922
|
+
if (!others.length) return;
|
|
1923
|
+
await Promise.all(others);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
function browserMotionLayerHost(doc = globalThis.document) {
|
|
1928
|
+
const view = doc?.defaultView;
|
|
1929
|
+
if (!doc || !view?.Image || typeof doc.createElement !== "function") return null;
|
|
1930
|
+
const canvas = (width, height) => {
|
|
1931
|
+
const c = doc.createElement("canvas");
|
|
1932
|
+
c.width = width;
|
|
1933
|
+
c.height = height;
|
|
1934
|
+
const ctx = c.getContext("2d");
|
|
1935
|
+
if (!ctx) throw new Error("This browser cannot draw layer pictures (no 2D canvas).");
|
|
1936
|
+
return { c, ctx };
|
|
1937
|
+
};
|
|
1938
|
+
const load = (src, cors) => new Promise((resolve2, reject) => {
|
|
1939
|
+
const image = new view.Image();
|
|
1940
|
+
if (cors) image.crossOrigin = "anonymous";
|
|
1941
|
+
image.onload = () => resolve2(image);
|
|
1942
|
+
image.onerror = () => reject(new Error(`The picture ${src.startsWith("data:") ? "(a data: URL)" : `"${src}"`} could not be loaded; check the URL and that its server allows cross-origin use.`));
|
|
1943
|
+
image.src = src;
|
|
1944
|
+
});
|
|
1945
|
+
return {
|
|
1946
|
+
async rasterize(svg, width, height) {
|
|
1947
|
+
const image = await load(svgDataUrl(svg), false);
|
|
1948
|
+
const { ctx } = canvas(width, height);
|
|
1949
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
1950
|
+
const data = ctx.getImageData(0, 0, width, height);
|
|
1951
|
+
return { width, height, data: data.data };
|
|
1952
|
+
},
|
|
1953
|
+
encode(pixels) {
|
|
1954
|
+
const { c, ctx } = canvas(pixels.width, pixels.height);
|
|
1955
|
+
const image = ctx.createImageData(pixels.width, pixels.height);
|
|
1956
|
+
image.data.set(pixels.data);
|
|
1957
|
+
ctx.putImageData(image, 0, 0);
|
|
1958
|
+
return c.toDataURL("image/png");
|
|
1959
|
+
},
|
|
1960
|
+
async loadImage(url) {
|
|
1961
|
+
if (url.startsWith("data:")) return url;
|
|
1962
|
+
const image = await load(url, true);
|
|
1963
|
+
const { c, ctx } = canvas(image.naturalWidth || 1, image.naturalHeight || 1);
|
|
1964
|
+
ctx.drawImage(image, 0, 0);
|
|
1965
|
+
return c.toDataURL("image/png");
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
var motionLayerSlot = (layer) => layer.subject && "slot" in layer.subject ? layer.subject.slot : void 0;
|
|
1970
|
+
|
|
1576
1971
|
// src/animation-editor/evaluate.ts
|
|
1577
1972
|
var MOTION_DRAW_EFFECT_ID = "~draw";
|
|
1578
1973
|
var TRANSFORM_PROPS = ["x", "y", "rotation", "scaleX", "scaleY", "follow"];
|
|
@@ -1767,8 +2162,8 @@ function motionFrameAt(scene, time, options = {}) {
|
|
|
1767
2162
|
const document = { ...s.artwork, nodes: s.artwork.nodes.map(visit) };
|
|
1768
2163
|
const cam = trackValue(timeline.tracks.get(motionTrackKey("", "camera")), time);
|
|
1769
2164
|
const view = motionCameraViewAt(s, time, options.effects);
|
|
1770
|
-
const
|
|
1771
|
-
return { time, document, camera: { x: tidy(
|
|
2165
|
+
const box2 = view ? (({ x, y, width, height }) => [x, y, width, height])(motionCameraBox(view, s)) : Array.isArray(cam) ? cam : [0, 0, s.artwork.width, s.artwork.height];
|
|
2166
|
+
return { time, document, camera: { x: tidy(box2[0]), y: tidy(box2[1]), width: tidy(box2[2]), height: tidy(box2[3]) }, ...view ? { view } : {}, clips };
|
|
1772
2167
|
}
|
|
1773
2168
|
function motionClipAttrs(clip) {
|
|
1774
2169
|
const { shape: _shape, ...attrs } = clip;
|
|
@@ -1780,11 +2175,20 @@ function motionFrameToSvg(scene, time, options = {}) {
|
|
|
1780
2175
|
const wrap = { ...options.wrap };
|
|
1781
2176
|
for (const [id, clips] of Object.entries(frame.clips))
|
|
1782
2177
|
wrap[id] = [...wrap[id] ?? [], ...clips.map((clip, i) => ({ clip: { id: `${prefix}reveal-${id}-${i}`, shape: clip.shape, attrs: motionClipAttrs(clip) } }))];
|
|
1783
|
-
return withCameraViewEffects(withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera), frame.view, frame.camera, prefix);
|
|
2178
|
+
if (!motionSceneHasLayers(scene)) return withCameraViewEffects(withViewBox(vectorDocumentToSvg(frame.document, { ...options, wrap }), frame.camera), frame.view, frame.camera, prefix);
|
|
2179
|
+
const art = vectorDocumentToSvg(frame.document, { ...options, wrap, background: false });
|
|
2180
|
+
let svg = withViewBox(withMotionShotLayers(art, scene, time, frame.camera, options.layers, options.background !== false), frame.camera);
|
|
2181
|
+
const transition = options.transitions === false ? null : motionTransitionAt(scene, time);
|
|
2182
|
+
if (transition) {
|
|
2183
|
+
const picture = options.layers?.transition(scene, time, transition);
|
|
2184
|
+
const from = picture ? "" : motionFrameToSvg(scene, Math.max(0, transition.start - 1e-3), { ...options, transitions: false, idPrefix: `${prefix}from-` });
|
|
2185
|
+
svg = motionTransitionSvg(svg, from, transition, frame.camera, picture);
|
|
2186
|
+
}
|
|
2187
|
+
return withCameraViewEffects(svg, frame.view, frame.camera, prefix);
|
|
1784
2188
|
}
|
|
1785
|
-
function withViewBox(svg,
|
|
1786
|
-
const f = (
|
|
1787
|
-
return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(
|
|
2189
|
+
function withViewBox(svg, box2) {
|
|
2190
|
+
const f = (n2) => String(Math.round(n2 * 1e3) / 1e3);
|
|
2191
|
+
return svg.replace(/viewBox="[^"]*"/, `viewBox="${f(box2.x)} ${f(box2.y)} ${f(box2.width)} ${f(box2.height)}"`);
|
|
1788
2192
|
}
|
|
1789
2193
|
|
|
1790
2194
|
// src/animation-editor/onion.ts
|
|
@@ -1822,7 +2226,7 @@ function motionPath(scene, id, options = {}) {
|
|
|
1822
2226
|
const to = Math.min(timeline.duration, options.to ?? timeline.duration);
|
|
1823
2227
|
let step = options.step && options.step > 0 ? options.step : 1e3 / 30;
|
|
1824
2228
|
if ((to - from) / step > 400) step = (to - from) / 400;
|
|
1825
|
-
const r = (
|
|
2229
|
+
const r = (n2) => Math.round(n2 * 100) / 100;
|
|
1826
2230
|
const centreAt = (t) => {
|
|
1827
2231
|
const node = indexMotionNodes(motionFrameAtCache(scene, t)).get(id);
|
|
1828
2232
|
const b = node?.bounds;
|
|
@@ -1843,7 +2247,7 @@ function motionFrameAtCache(scene, t) {
|
|
|
1843
2247
|
}
|
|
1844
2248
|
function motionPathsSvg(paths, camera, color = "#f43f5e") {
|
|
1845
2249
|
const w = Math.max(camera.width, camera.height) / 400;
|
|
1846
|
-
const f = (
|
|
2250
|
+
const f = (n2) => String(Math.round(n2 * 100) / 100);
|
|
1847
2251
|
return paths.filter((p) => p.points.length > 1).map(
|
|
1848
2252
|
(p) => `<g data-motion-path="${p.id.replace(/"/g, """)}" fill="none" stroke="${color}" stroke-width="${f(w)}" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"><polyline points="${p.points.map((q) => `${f(q.x)},${f(q.y)}`).join(" ")}" stroke-dasharray="${f(w * 3)} ${f(w * 2)}"/>${p.keys.map((k) => `<circle cx="${f(k.x)}" cy="${f(k.y)}" r="${f(w * 2.5)}" fill="${color}" stroke="none"/>`).join("")}</g>`
|
|
1849
2253
|
).join("");
|
|
@@ -1863,18 +2267,18 @@ function motionBlurTimes(scene, time, blur = {}) {
|
|
|
1863
2267
|
const q = b.get(id)?.bounds;
|
|
1864
2268
|
if (p && q) travel = Math.max(travel, Math.abs(p.x - q.x), Math.abs(p.y - q.y), Math.abs(p.x + p.width - q.x - q.width), Math.abs(p.y + p.height - q.y - q.height));
|
|
1865
2269
|
}
|
|
1866
|
-
const
|
|
1867
|
-
if (
|
|
1868
|
-
return Array.from({ length:
|
|
2270
|
+
const n2 = Math.min(most, Math.ceil(travel / 1.5));
|
|
2271
|
+
if (n2 <= 1) return [time];
|
|
2272
|
+
return Array.from({ length: n2 }, (_, k) => time - span / 2 + span * (k + 0.5) / n2);
|
|
1869
2273
|
}
|
|
1870
2274
|
function averagedFrames(scene, times, options, prefix) {
|
|
1871
|
-
const
|
|
1872
|
-
return `<g data-motion-blur="${
|
|
2275
|
+
const n2 = times.length;
|
|
2276
|
+
return `<g data-motion-blur="${n2}" style="isolation:isolate">${times.map((t, k) => {
|
|
1873
2277
|
const own = `${prefix}blur${k}-`;
|
|
1874
2278
|
const frame = motionFrameToSvg(scene, t, { ...options, idPrefix: own, background: false });
|
|
1875
2279
|
const view = /viewBox="([^"]*)"/.exec(frame)?.[1] ?? "";
|
|
1876
|
-
const inner = k ===
|
|
1877
|
-
return `<svg viewBox="${view}" preserveAspectRatio="none" style="mix-blend-mode:plus-lighter" opacity="${Math.round(1e4 /
|
|
2280
|
+
const inner = k === n2 - 1 ? innerSvg(frame) : innerSvg(frame).replace(new RegExp(` id="(?!${own.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})[^"]*"`, "g"), "");
|
|
2281
|
+
return `<svg viewBox="${view}" preserveAspectRatio="none" style="mix-blend-mode:plus-lighter" opacity="${Math.round(1e4 / n2) / 1e4}">${inner}</svg>`;
|
|
1878
2282
|
}).join("")}</g>`;
|
|
1879
2283
|
}
|
|
1880
2284
|
var innerSvg = (svg) => svg.slice(svg.indexOf(">") + 1, svg.lastIndexOf("</svg>"));
|
|
@@ -1907,9 +2311,9 @@ function motionStageSvg(scene, time, options = {}) {
|
|
|
1907
2311
|
}
|
|
1908
2312
|
function motionContactSheetSvg(scene, times, options = {}) {
|
|
1909
2313
|
const { columns, labels = true, ...frameOptions } = options;
|
|
1910
|
-
const
|
|
1911
|
-
const cols = Math.max(1, Math.min(
|
|
1912
|
-
const rows = Math.ceil(
|
|
2314
|
+
const n2 = Math.max(1, times.length);
|
|
2315
|
+
const cols = Math.max(1, Math.min(n2, Math.floor(columns ?? Math.ceil(Math.sqrt(n2)))));
|
|
2316
|
+
const rows = Math.ceil(n2 / cols);
|
|
1913
2317
|
const { width: w, height: h } = scene.artwork;
|
|
1914
2318
|
const gap = Math.round(Math.max(w, h) * 0.04);
|
|
1915
2319
|
const label = labels ? Math.round(Math.max(12, h * 0.08)) : 0;
|
|
@@ -2100,7 +2504,7 @@ function checkMotionScene(scene) {
|
|
|
2100
2504
|
if (!nodes.has(id)) issues.push(motionIssue("unknown-target", `Driver ${i + 1} names ${namedWithSuggestion(id, [...nodes.keys()])}, which the artwork does not have.`, { ids: [id] }));
|
|
2101
2505
|
});
|
|
2102
2506
|
{
|
|
2103
|
-
const key = (
|
|
2507
|
+
const key = (n2, p) => `${n2}.${p}`;
|
|
2104
2508
|
const edges = /* @__PURE__ */ new Map();
|
|
2105
2509
|
for (const d of drivers) edges.set(key(d.target, d.property), [...edges.get(key(d.target, d.property)) ?? [], key(d.source.node, d.source.property)]);
|
|
2106
2510
|
const state = /* @__PURE__ */ new Map();
|
|
@@ -2109,7 +2513,7 @@ function checkMotionScene(scene) {
|
|
|
2109
2513
|
if (state.get(k) === 2) return false;
|
|
2110
2514
|
if (state.get(k) === 1) return loop.push(...trail.slice(trail.indexOf(k)), k), true;
|
|
2111
2515
|
state.set(k, 1);
|
|
2112
|
-
for (const
|
|
2516
|
+
for (const n2 of edges.get(k) ?? []) if (visit(n2, [...trail, k])) return true;
|
|
2113
2517
|
state.set(k, 2);
|
|
2114
2518
|
return false;
|
|
2115
2519
|
};
|
|
@@ -2228,9 +2632,9 @@ var replaceShot = (scene, index, shot) => ({ ...scene, shots: scene.shots.map((s
|
|
|
2228
2632
|
function freshShotId(scene, base = "shot") {
|
|
2229
2633
|
const used = new Set(scene.shots.map((s) => s.id));
|
|
2230
2634
|
if (!used.has(base)) return base;
|
|
2231
|
-
let
|
|
2232
|
-
while (used.has(`${base}-${
|
|
2233
|
-
return `${base}-${
|
|
2635
|
+
let n2 = 2;
|
|
2636
|
+
while (used.has(`${base}-${n2}`)) n2++;
|
|
2637
|
+
return `${base}-${n2}`;
|
|
2234
2638
|
}
|
|
2235
2639
|
function applyOne(scene, edit) {
|
|
2236
2640
|
switch (edit.op) {
|
|
@@ -2431,14 +2835,14 @@ function resolveMotionEditRefs(scene, edits) {
|
|
|
2431
2835
|
if (v.startsWith("$$")) return v.slice(1);
|
|
2432
2836
|
const m = REF.exec(v);
|
|
2433
2837
|
if (!m) return v;
|
|
2434
|
-
const
|
|
2435
|
-
if (!(
|
|
2436
|
-
problem = `${where}: "${v}" refers to op ${
|
|
2838
|
+
const n2 = m[1] === "last" ? i - 1 : Number(m[1]);
|
|
2839
|
+
if (!(n2 >= 0 && n2 < i)) {
|
|
2840
|
+
problem = `${where}: "${v}" refers to op ${n2 + 1}, but only ops before this one (1 to ${i}) can be referred to.`;
|
|
2437
2841
|
return v;
|
|
2438
2842
|
}
|
|
2439
|
-
let value = outcomes[
|
|
2843
|
+
let value = outcomes[n2];
|
|
2440
2844
|
for (const part of m[2].match(/\.[A-Za-z_]\w*|\[\d+\]/g) ?? []) value = value == null ? void 0 : value[part.startsWith(".") ? part.slice(1) : part.slice(1, -1)];
|
|
2441
|
-
if (value === void 0) problem = `${where}: "${v}" has no value; op ${
|
|
2845
|
+
if (value === void 0) problem = `${where}: "${v}" has no value; op ${n2 + 1} made ${JSON.stringify(outcomes[n2])}.`;
|
|
2442
2846
|
return value;
|
|
2443
2847
|
}
|
|
2444
2848
|
if (Array.isArray(v)) return v.map(walk);
|
|
@@ -2528,11 +2932,11 @@ function motionNodePose(scene, id, time) {
|
|
|
2528
2932
|
};
|
|
2529
2933
|
}
|
|
2530
2934
|
function simplifyTimedSamples(times, values, tol) {
|
|
2531
|
-
const
|
|
2532
|
-
if (
|
|
2533
|
-
const keep = new Uint8Array(
|
|
2534
|
-
keep[0] = keep[
|
|
2535
|
-
const stack = [[0,
|
|
2935
|
+
const n2 = times.length;
|
|
2936
|
+
if (n2 <= 2) return [...Array(n2).keys()];
|
|
2937
|
+
const keep = new Uint8Array(n2);
|
|
2938
|
+
keep[0] = keep[n2 - 1] = 1;
|
|
2939
|
+
const stack = [[0, n2 - 1]];
|
|
2536
2940
|
while (stack.length) {
|
|
2537
2941
|
const [a, b] = stack.pop();
|
|
2538
2942
|
let worst = -1;
|
|
@@ -2559,8 +2963,8 @@ function simplifyTimedSamples(times, values, tol) {
|
|
|
2559
2963
|
keep.forEach((k, i) => k && out.push(i));
|
|
2560
2964
|
return out;
|
|
2561
2965
|
}
|
|
2562
|
-
var round2 = (
|
|
2563
|
-
var ms = (
|
|
2966
|
+
var round2 = (n2, places = 2) => Math.round(n2 * 10 ** places) / 10 ** places || 0;
|
|
2967
|
+
var ms = (n2) => Math.max(0, Math.round(n2));
|
|
2564
2968
|
function motionTakeProblems(scene, take) {
|
|
2565
2969
|
const problems = [];
|
|
2566
2970
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
@@ -2701,10 +3105,10 @@ function motionTakeShot(scene, take, options = {}) {
|
|
|
2701
3105
|
if (!run.length) Object.assign(shot, { run: [{ type: "wait", duration: ms(Math.max(1, length)) }], hold: 0 });
|
|
2702
3106
|
return { shot, at, start, samples: { given: take.samples.length, kept: kept.n } };
|
|
2703
3107
|
}
|
|
2704
|
-
function motionClientToDocument(camera,
|
|
2705
|
-
const k = Math.min(
|
|
2706
|
-
const ox =
|
|
2707
|
-
const oy =
|
|
3108
|
+
function motionClientToDocument(camera, box2, clientX, clientY) {
|
|
3109
|
+
const k = Math.min(box2.width / camera.width, box2.height / camera.height) || 1;
|
|
3110
|
+
const ox = box2.left + (box2.width - camera.width * k) / 2;
|
|
3111
|
+
const oy = box2.top + (box2.height - camera.height * k) / 2;
|
|
2708
3112
|
return { x: camera.x + (clientX - ox) / k, y: camera.y + (clientY - oy) / k };
|
|
2709
3113
|
}
|
|
2710
3114
|
|
|
@@ -2716,8 +3120,8 @@ var FLAIR_EASES = {
|
|
|
2716
3120
|
};
|
|
2717
3121
|
var FLAIR_EXPORT_VERSION = "0.39";
|
|
2718
3122
|
function flairAtLeast(version, min) {
|
|
2719
|
-
const a = version.split(".").map((
|
|
2720
|
-
const b = min.split(".").map((
|
|
3123
|
+
const a = version.split(".").map((n2) => parseInt(n2, 10) || 0);
|
|
3124
|
+
const b = min.split(".").map((n2) => parseInt(n2, 10) || 0);
|
|
2721
3125
|
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);
|
|
2722
3126
|
return true;
|
|
2723
3127
|
}
|
|
@@ -2767,8 +3171,8 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2767
3171
|
return back && curves ? flairCurveFor(back, curves) : void 0;
|
|
2768
3172
|
};
|
|
2769
3173
|
const plays = (s) => (!s.behavior || behaviors) && (!s.steps || strobes) && !s.geometric;
|
|
2770
|
-
const round4 = (
|
|
2771
|
-
const f = (
|
|
3174
|
+
const round4 = (n2) => Math.round(n2 * 10 ** precision) / 10 ** precision;
|
|
3175
|
+
const f = (n2) => String(round4(n2));
|
|
2772
3176
|
const timeline = compileMotionScene(scene);
|
|
2773
3177
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
2774
3178
|
const notes = [];
|
|
@@ -2793,14 +3197,14 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2793
3197
|
const usedIds = new Set(nodes.keys());
|
|
2794
3198
|
const freshId = (base) => {
|
|
2795
3199
|
let id = base;
|
|
2796
|
-
for (let
|
|
3200
|
+
for (let n2 = 2; usedIds.has(id); n2++) id = `${base}-${n2}`;
|
|
2797
3201
|
usedIds.add(id);
|
|
2798
3202
|
return id;
|
|
2799
3203
|
};
|
|
2800
3204
|
for (const id of animatedNodes) {
|
|
2801
3205
|
const entry = nodes.get(id);
|
|
2802
3206
|
const node = entry.node;
|
|
2803
|
-
const drawn = [...entry.ancestors.map((a) => nodes.get(a).node), node].every((
|
|
3207
|
+
const drawn = [...entry.ancestors.map((a) => nodes.get(a).node), node].every((n2) => n2.visible !== false);
|
|
2804
3208
|
for (const [p, name] of [["clipRect", "reveal"], ["clipCircle", "iris"]]) {
|
|
2805
3209
|
if (!drawn || !timeline.tracks.has(motionTrackKey(id, p))) continue;
|
|
2806
3210
|
const wrapper = freshId(`${id}--${name}`);
|
|
@@ -2901,8 +3305,8 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2901
3305
|
const moveSegments = windows.map((w) => ({ node: "", property: "camera", start: w.start, end: w.end, from: [], to: [], ease: "linear", shot: w.shot, command: -1 }));
|
|
2902
3306
|
const sampledMove = new WeakSet(moveSegments);
|
|
2903
3307
|
const cameraValue = windows.length ? (t) => {
|
|
2904
|
-
const
|
|
2905
|
-
return [
|
|
3308
|
+
const box2 = motionFrameAt(scene, t, { draw: false, offsets: false, effects: options.effects }).camera;
|
|
3309
|
+
return [box2.x, box2.y, box2.width, box2.height].map(round4);
|
|
2906
3310
|
} : (t) => trackValue(camera, t).map(round4);
|
|
2907
3311
|
if (camera.length || windows.length) channels.push({ id: "", kind: "view", segments: [...camera, ...moveSegments].sort((a, b) => a.start - b.start), exact: (seg) => !sampledMove.has(seg), overshoot: true, value: cameraValue });
|
|
2908
3312
|
const cameraTag = (t) => {
|
|
@@ -2965,11 +3369,11 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
2965
3369
|
if (!same(from, to) || channel.kind === "view") emit(channel, a, b - a, from, to, ease, first);
|
|
2966
3370
|
continue;
|
|
2967
3371
|
}
|
|
2968
|
-
const
|
|
3372
|
+
const n2 = Math.max(1, Math.ceil((b - a) * fps / 1e3));
|
|
2969
3373
|
let prev = channel.value(a);
|
|
2970
|
-
for (let k = 1; k <=
|
|
2971
|
-
const t0 = a + (b - a) * (k - 1) /
|
|
2972
|
-
const t1 = a + (b - a) * k /
|
|
3374
|
+
for (let k = 1; k <= n2; k++) {
|
|
3375
|
+
const t0 = a + (b - a) * (k - 1) / n2;
|
|
3376
|
+
const t1 = a + (b - a) * k / n2;
|
|
2973
3377
|
const next = channel.value(t1);
|
|
2974
3378
|
const jump = channel.kind === "view" && active.some((x) => sampledMove.has(x)) ? viewJump(channel.value, t0, t1) : void 0;
|
|
2975
3379
|
if (jump) {
|
|
@@ -3001,10 +3405,10 @@ function toFlairSvgMotion(scene, options = {}) {
|
|
|
3001
3405
|
continue;
|
|
3002
3406
|
}
|
|
3003
3407
|
const until = Math.min(seg.end, ...starts.filter((t) => t > seg.start));
|
|
3004
|
-
const
|
|
3005
|
-
for (let k = 1; k <=
|
|
3006
|
-
const t0 = seg.start + (until - seg.start) * (k - 1) /
|
|
3007
|
-
const t1 = seg.start + (until - seg.start) * k /
|
|
3408
|
+
const n2 = Math.max(1, Math.ceil((until - seg.start) * fps / 1e3));
|
|
3409
|
+
for (let k = 1; k <= n2; k++) {
|
|
3410
|
+
const t0 = seg.start + (until - seg.start) * (k - 1) / n2;
|
|
3411
|
+
const t1 = seg.start + (until - seg.start) * k / n2;
|
|
3008
3412
|
animate.push({ ...along, ...t0 > 0 ? { delay: round4(t0) } : {}, ms: round4(t1 - t0), ease: "linear", at: [progress(t0), progress(t1)] });
|
|
3009
3413
|
sampled++;
|
|
3010
3414
|
}
|
|
@@ -3073,17 +3477,17 @@ function toFlairEffectDocument(scene, options = {}) {
|
|
|
3073
3477
|
};
|
|
3074
3478
|
}
|
|
3075
3479
|
function viewJump(value, t0, t1) {
|
|
3076
|
-
const
|
|
3480
|
+
const box2 = (t) => value(t);
|
|
3077
3481
|
const apart = (a2, b2) => Math.hypot(a2[0] + a2[2] / 2 - b2[0] - b2[2] / 2, a2[1] + a2[3] / 2 - b2[1] - b2[3] / 2, a2[2] - b2[2]);
|
|
3078
3482
|
let lo = t0;
|
|
3079
3483
|
let hi = t1;
|
|
3080
3484
|
for (let i = 0; i < 40 && hi - lo > 1e-4; i++) {
|
|
3081
3485
|
const mid = (lo + hi) / 2;
|
|
3082
|
-
if (apart(
|
|
3486
|
+
if (apart(box2(lo), box2(mid)) >= apart(box2(mid), box2(hi))) hi = mid;
|
|
3083
3487
|
else lo = mid;
|
|
3084
3488
|
}
|
|
3085
|
-
const a =
|
|
3086
|
-
const b =
|
|
3489
|
+
const a = box2(lo);
|
|
3490
|
+
const b = box2(hi);
|
|
3087
3491
|
return apart(a, b) > Math.min(a[2], b[2]) / 20 ? hi : void 0;
|
|
3088
3492
|
}
|
|
3089
3493
|
|
|
@@ -3138,7 +3542,7 @@ function poseOf(value, entry) {
|
|
|
3138
3542
|
return { dx: moved.x - c.x, dy: moved.y - c.y, rotation: Math.atan2(o[1], o[0]) * 180 / Math.PI, sx, sy };
|
|
3139
3543
|
}
|
|
3140
3544
|
var near = (a, b, eps = 1e-6) => Math.abs(a - b) < eps;
|
|
3141
|
-
var r3 = (
|
|
3545
|
+
var r3 = (n2) => Math.round(n2 * 1e3) / 1e3;
|
|
3142
3546
|
function commandsFor(anim, nodes, entered, notes) {
|
|
3143
3547
|
const id = anim.id;
|
|
3144
3548
|
const entry = nodes.get(id);
|
|
@@ -3269,17 +3673,17 @@ ${problems.join("\n")}`);
|
|
|
3269
3673
|
}
|
|
3270
3674
|
function presetColor(value) {
|
|
3271
3675
|
if (typeof value === "string") return /^#[0-9a-f]{6}$/i.test(value) ? value.toLowerCase() : value;
|
|
3272
|
-
if (Array.isArray(value) && value.length >= 3 && value.slice(0, 3).every((
|
|
3273
|
-
return `#${value.slice(0, 3).map((
|
|
3676
|
+
if (Array.isArray(value) && value.length >= 3 && value.slice(0, 3).every((n2) => typeof n2 === "number" && Number.isFinite(n2)))
|
|
3677
|
+
return `#${value.slice(0, 3).map((n2) => Math.round(Math.min(1, Math.max(0, n2)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
3274
3678
|
return void 0;
|
|
3275
3679
|
}
|
|
3276
3680
|
function fromFlairVectorEffect(effect, options = {}) {
|
|
3277
3681
|
if (!effect || effect.kind !== "vector" || !effect.motion) throw new Error('Give a Flair vector effect: { kind: "vector", id, name, motion: { svg, animate } }.');
|
|
3278
|
-
const
|
|
3279
|
-
const names = new Set((
|
|
3682
|
+
const result2 = fromFlairSvgMotion(effect.motion, { name: options.name ?? effect.id ?? effect.name, hold: options.hold ?? effect.hold, ...effect.trigger === "click" ? { clickSteps: true } : {} });
|
|
3683
|
+
const names = new Set((result2.scene.controls ?? []).map((c) => c.name));
|
|
3280
3684
|
const presets = (effect.presets ?? []).map((p) => ({ name: p.name, values: Object.fromEntries(Object.entries(p.values).flatMap(([k, v]) => names.has(k) && presetColor(v) ? [[k, presetColor(v)]] : [])) })).filter((p) => Object.keys(p.values).length);
|
|
3281
|
-
const scene = { ...
|
|
3282
|
-
const notes = [...
|
|
3685
|
+
const scene = { ...result2.scene, ...effect.description ? { description: effect.description } : {}, ...presets.length ? { presets } : {}, ...effect.trigger ? { trigger: effect.trigger } : {} };
|
|
3686
|
+
const notes = [...result2.notes];
|
|
3283
3687
|
return { scene, notes, issues: checkMotionScene(scene) };
|
|
3284
3688
|
}
|
|
3285
3689
|
|
|
@@ -3297,8 +3701,8 @@ var STATIC_TR = () => ({
|
|
|
3297
3701
|
function rgba(color) {
|
|
3298
3702
|
const hex = typeof color === "string" ? normalizeTokenColor(color) : null;
|
|
3299
3703
|
if (!hex) return null;
|
|
3300
|
-
const
|
|
3301
|
-
return [
|
|
3704
|
+
const n2 = (i) => parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
3705
|
+
return [n2(1), n2(3), n2(5), hex.length >= 9 ? parseInt(hex.slice(7, 9), 16) / 255 : 1];
|
|
3302
3706
|
}
|
|
3303
3707
|
function lottieContours(path, round4) {
|
|
3304
3708
|
const out = [];
|
|
@@ -3329,9 +3733,9 @@ function lottieContours(path, round4) {
|
|
|
3329
3733
|
add(cmd.x, cmd.y, cmd.x2, cmd.y2);
|
|
3330
3734
|
} else if (cmd.type === "Z") {
|
|
3331
3735
|
cur.c = true;
|
|
3332
|
-
const
|
|
3333
|
-
if (
|
|
3334
|
-
cur.i[0] = cur.i[
|
|
3736
|
+
const n2 = cur.v.length;
|
|
3737
|
+
if (n2 > 1 && Math.abs(cur.v[n2 - 1][0] - cur.v[0][0]) < 1e-9 && Math.abs(cur.v[n2 - 1][1] - cur.v[0][1]) < 1e-9) {
|
|
3738
|
+
cur.i[0] = cur.i[n2 - 1];
|
|
3335
3739
|
cur.v.pop();
|
|
3336
3740
|
cur.i.pop();
|
|
3337
3741
|
cur.o.pop();
|
|
@@ -3353,7 +3757,7 @@ function staticParts(m) {
|
|
|
3353
3757
|
function toLottie(scene, options = {}) {
|
|
3354
3758
|
const fps = options.fps ?? scene.fps ?? 30;
|
|
3355
3759
|
const precision = options.precision ?? 3;
|
|
3356
|
-
const round4 = (
|
|
3760
|
+
const round4 = (n2) => Math.round(n2 * 10 ** precision) / 10 ** precision;
|
|
3357
3761
|
const frame = (ms2) => round4(ms2 * fps / 1e3);
|
|
3358
3762
|
const timeline = compileMotionScene(scene);
|
|
3359
3763
|
const nodes = indexMotionNodes(scene.artwork.nodes);
|
|
@@ -3386,9 +3790,9 @@ function toLottie(scene, options = {}) {
|
|
|
3386
3790
|
else keys.push(k);
|
|
3387
3791
|
};
|
|
3388
3792
|
const times = [.../* @__PURE__ */ new Set([0, ...segments.flatMap((s) => [s.start, s.end]), end])].filter((t) => t >= 0 && t <= end).sort((a, b) => a - b);
|
|
3389
|
-
for (let
|
|
3390
|
-
const a = times[
|
|
3391
|
-
const b = times[
|
|
3793
|
+
for (let n2 = 0; n2 + 1 < times.length; n2++) {
|
|
3794
|
+
const a = times[n2];
|
|
3795
|
+
const b = times[n2 + 1];
|
|
3392
3796
|
const active = segments.filter((s) => s.start < b && s.end > a);
|
|
3393
3797
|
if (!active.length) {
|
|
3394
3798
|
key(a, value(a), "hold");
|
|
@@ -3471,7 +3875,7 @@ function toLottie(scene, options = {}) {
|
|
|
3471
3875
|
return now && now.type === "path" ? vectorPathWorldBezier(expandVectorAppearances([now])[0]) : vectorPathWorldBezier(path);
|
|
3472
3876
|
};
|
|
3473
3877
|
const rest = lottieContours(toBezierPath(geometryAt(0)), round4);
|
|
3474
|
-
rest.forEach((contour,
|
|
3878
|
+
rest.forEach((contour, n2) => {
|
|
3475
3879
|
let ks = { a: 0, k: contour };
|
|
3476
3880
|
if (shapeSegments.length) {
|
|
3477
3881
|
const keys = [];
|
|
@@ -3479,7 +3883,7 @@ function toLottie(scene, options = {}) {
|
|
|
3479
3883
|
const count = Math.max(1, Math.ceil(end * fps / 1e3));
|
|
3480
3884
|
for (let k = 0; k <= count && fits; k++) {
|
|
3481
3885
|
const t = end * k / count;
|
|
3482
|
-
const c = lottieContours(toBezierPath(geometryAt(t)), round4)[
|
|
3886
|
+
const c = lottieContours(toBezierPath(geometryAt(t)), round4)[n2];
|
|
3483
3887
|
if (!c || c.v.length !== contour.v.length || c.c !== contour.c) fits = false;
|
|
3484
3888
|
else keys.push({ t: frame(t), s: [c], ...k < count ? { o: { x: [0], y: [0] }, i: { x: [1], y: [1] } } : {} });
|
|
3485
3889
|
}
|
|
@@ -3488,7 +3892,7 @@ function toLottie(scene, options = {}) {
|
|
|
3488
3892
|
sampled += keys.length;
|
|
3489
3893
|
} else notes.push(`"${path.id}": its morph changes how many points the outline has, which Lottie cannot tween; it shows its starting shape.`);
|
|
3490
3894
|
}
|
|
3491
|
-
items.push({ ty: "sh", nm: `${path.id} ${
|
|
3895
|
+
items.push({ ty: "sh", nm: `${path.id} ${n2 + 1}`, ks });
|
|
3492
3896
|
});
|
|
3493
3897
|
const fillFrom = owner(path, "fill");
|
|
3494
3898
|
const strokeFrom = owner(path, "stroke");
|
|
@@ -3532,7 +3936,7 @@ function toLottie(scene, options = {}) {
|
|
|
3532
3936
|
}
|
|
3533
3937
|
return items;
|
|
3534
3938
|
};
|
|
3535
|
-
const shown = (list) => list.filter((
|
|
3939
|
+
const shown = (list) => list.filter((n2) => n2.visible !== false);
|
|
3536
3940
|
const nodeGroup = (node) => {
|
|
3537
3941
|
const entry = nodes.get(node.id);
|
|
3538
3942
|
if (timeline.tracks.has(motionTrackKey(node.id, "clipRect")) || timeline.tracks.has(motionTrackKey(node.id, "clipCircle")))
|
|
@@ -3567,9 +3971,9 @@ function toLottie(scene, options = {}) {
|
|
|
3567
3971
|
const camera = track("", "camera");
|
|
3568
3972
|
const root = STATIC_TR();
|
|
3569
3973
|
if (camera.length) {
|
|
3570
|
-
const
|
|
3571
|
-
root.a = animate(camera, (t) =>
|
|
3572
|
-
root.s = animate(camera, (t) => [artwork.width /
|
|
3974
|
+
const box2 = (t) => trackValue(camera, t) ?? [0, 0, artwork.width, artwork.height];
|
|
3975
|
+
root.a = animate(camera, (t) => box2(t).slice(0, 2), false);
|
|
3976
|
+
root.s = animate(camera, (t) => [artwork.width / box2(t)[2] * 100, artwork.height / box2(t)[3] * 100], false);
|
|
3573
3977
|
}
|
|
3574
3978
|
const op = Math.max(1, Math.ceil(end * fps / 1e3));
|
|
3575
3979
|
const lottie = {
|
|
@@ -3610,8 +4014,8 @@ var kid = (el, name) => kids(el, name)[0] ?? null;
|
|
|
3610
4014
|
var deep = (el, name) => el ? Array.from(el.getElementsByTagNameNS("*", name)) : [];
|
|
3611
4015
|
var num = (el, attr, fallback = 0) => {
|
|
3612
4016
|
const v = el?.getAttribute(attr);
|
|
3613
|
-
const
|
|
3614
|
-
return Number.isFinite(
|
|
4017
|
+
const n2 = v === null || v === void 0 ? NaN : Number(v);
|
|
4018
|
+
return Number.isFinite(n2) ? n2 : fallback;
|
|
3615
4019
|
};
|
|
3616
4020
|
var dirname = (p) => p.slice(0, p.lastIndexOf("/") + 1);
|
|
3617
4021
|
function resolve(base, target) {
|
|
@@ -3882,9 +4286,9 @@ function command(ctx, ctn, scene, artwork, target, hidden) {
|
|
|
3882
4286
|
const motion = deep(ctn, "animMotion")[0];
|
|
3883
4287
|
const d = motion?.getAttribute("path");
|
|
3884
4288
|
if (!d) return null;
|
|
3885
|
-
const entry = artwork.flatMap(function all(
|
|
3886
|
-
return
|
|
3887
|
-
}).find((
|
|
4289
|
+
const entry = artwork.flatMap(function all(n2) {
|
|
4290
|
+
return n2.type === "group" ? [n2, ...n2.children.flatMap(all)] : [n2];
|
|
4291
|
+
}).find((n2) => n2.id === target);
|
|
3888
4292
|
const t = entry?.transform ?? [1, 0, 0, 1, 0, 0];
|
|
3889
4293
|
const p = entry?.type === "path" ? entry.subpaths[0]?.anchors : void 0;
|
|
3890
4294
|
const xs = (p ?? []).map((a) => a.x);
|
|
@@ -3929,8 +4333,8 @@ async function fromPptxAnimation(data, options = {}) {
|
|
|
3929
4333
|
if (!pres) throw new Error("This is not a PowerPoint file (.pptx): it has no ppt/presentation.xml.");
|
|
3930
4334
|
const presRels = await rels(ctx, "ppt/presentation.xml");
|
|
3931
4335
|
const slidePaths = deep(pres, "sldId").map((s) => presRels.get(s.getAttributeNS("http://schemas.openxmlformats.org/officeDocument/2006/relationships", "id") ?? s.getAttribute("r:id") ?? "") ?? "").filter(Boolean);
|
|
3932
|
-
const
|
|
3933
|
-
if (!(Number.isInteger(
|
|
4336
|
+
const n2 = options.slide ?? 1;
|
|
4337
|
+
if (!(Number.isInteger(n2) && n2 >= 1 && n2 <= slidePaths.length)) throw new Error(`There is no slide ${n2}; the file has ${slidePaths.length} slide${slidePaths.length === 1 ? "" : "s"}.`);
|
|
3934
4338
|
const size = deep(pres, "sldSz")[0];
|
|
3935
4339
|
const width = Math.round(num(size, "cx", 12192e3) / EMU);
|
|
3936
4340
|
const height = Math.round(num(size, "cy", 6858e3) / EMU);
|
|
@@ -3945,8 +4349,8 @@ async function fromPptxAnimation(data, options = {}) {
|
|
|
3945
4349
|
}
|
|
3946
4350
|
break;
|
|
3947
4351
|
}
|
|
3948
|
-
const slide = await xml(ctx, slidePaths[
|
|
3949
|
-
if (!slide) throw new Error(`Slide ${
|
|
4352
|
+
const slide = await xml(ctx, slidePaths[n2 - 1]);
|
|
4353
|
+
if (!slide) throw new Error(`Slide ${n2} cannot be read.`);
|
|
3950
4354
|
const tree = deep(slide, "spTree")[0];
|
|
3951
4355
|
const toPx = (x, y, w, h) => [x / EMU, y / EMU, w / EMU, h / EMU];
|
|
3952
4356
|
const nodes = tree ? shapes(ctx, tree, toPx) : [];
|
|
@@ -3973,18 +4377,118 @@ async function fromPptxAnimation(data, options = {}) {
|
|
|
3973
4377
|
}
|
|
3974
4378
|
if (effects.length && !shots.length) ctx.notes.push("The slide's effects could not be read.");
|
|
3975
4379
|
const artwork = { width, height, background: bg, nodes: [...nodes, ...hidden] };
|
|
3976
|
-
const scene = { version: 1, name: `slide-${
|
|
4380
|
+
const scene = { version: 1, name: `slide-${n2}`, artwork, shots, ...shots.length > 1 ? { trigger: "click" } : {} };
|
|
3977
4381
|
const problems = motionSceneShapeProblems(scene);
|
|
3978
4382
|
if (problems.length) throw new Error(`The imported scene is not well formed:
|
|
3979
4383
|
${problems.join("\n")}`);
|
|
3980
4384
|
return { scene, notes: [...new Set(ctx.notes)], issues: checkMotionScene(scene), slides: slidePaths.length };
|
|
3981
4385
|
}
|
|
3982
4386
|
|
|
4387
|
+
// src/animation-editor/layer-edit.ts
|
|
4388
|
+
function motionShotLayersInfo(scene, shot) {
|
|
4389
|
+
const spans = motionLayerSpans(scene);
|
|
4390
|
+
const timings = compileMotionScene(scene).shots;
|
|
4391
|
+
return scene.shots.filter((s) => shot === void 0 || s.id === shot).map((s) => {
|
|
4392
|
+
const timing = timings.find((t) => t.id === s.id);
|
|
4393
|
+
return {
|
|
4394
|
+
shot: s.id,
|
|
4395
|
+
layers: spans.filter((sp) => sp.shot === s.id).map((sp) => ({ ...sp.layer, shot: s.id, index: sp.index, role: sp.role, start: sp.start, end: sp.end })),
|
|
4396
|
+
transition: s.transition && timing.transitionEnd !== void 0 ? { ...s.transition, start: timing.start, end: timing.transitionEnd } : null
|
|
4397
|
+
};
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
function findMotionShotLayer(scene, id) {
|
|
4401
|
+
for (let shot = 0; shot < scene.shots.length; shot++) {
|
|
4402
|
+
const index = (scene.shots[shot].layers ?? []).findIndex((l) => l.id === id);
|
|
4403
|
+
if (index >= 0) return { shot, index };
|
|
4404
|
+
}
|
|
4405
|
+
return null;
|
|
4406
|
+
}
|
|
4407
|
+
var motionLayerIds = (scene) => scene.shots.flatMap((s) => (s.layers ?? []).map((l) => l.id));
|
|
4408
|
+
function freshMotionLayerId(scene, base = "layer") {
|
|
4409
|
+
const used = new Set(motionLayerIds(scene));
|
|
4410
|
+
for (let i = 1; ; i++) if (!used.has(`${base}${i}`)) return `${base}${i}`;
|
|
4411
|
+
}
|
|
4412
|
+
var kindHint = (d) => {
|
|
4413
|
+
if (d.uses.includes("transition")) return "it is a transition: set it between shots with set_shot_transition";
|
|
4414
|
+
if (d.kind === "camera") return "it is a camera move: set it with set_shot_camera_move";
|
|
4415
|
+
if (d.kind === "motion") return "it animates artwork nodes: apply it with apply_effect";
|
|
4416
|
+
return `its uses are ${d.uses.join(", ")}`;
|
|
4417
|
+
};
|
|
4418
|
+
function withPreset(d, preset2, values) {
|
|
4419
|
+
if (preset2 === void 0) return values;
|
|
4420
|
+
const p = d.presets.find((x) => x.name === preset2);
|
|
4421
|
+
if (!p) throw new EffectUnsupportedError(`"${d.id}" has no preset "${preset2}"; ${d.presets.length ? `its presets are ${d.presets.map((x) => `"${x.name}"`).join(", ")}` : "it has no presets"}.`, d.id);
|
|
4422
|
+
return { ...p.values, ...values ?? {} };
|
|
4423
|
+
}
|
|
4424
|
+
async function resolveMotionShotLayer(provider, layer) {
|
|
4425
|
+
if (!layer.effect) {
|
|
4426
|
+
if (!layer.subject) throw new Error('A layer needs an "effect", a "subject" or both.');
|
|
4427
|
+
if (layer.preset !== void 0 || layer.values !== void 0) throw new Error('A bare subject has no controls; leave out "preset" and "values", or give an "effect".');
|
|
4428
|
+
const { provider: _p, name: _n, ...rest } = layer;
|
|
4429
|
+
return rest;
|
|
4430
|
+
}
|
|
4431
|
+
if (!provider) throw new NoEffectsProviderError();
|
|
4432
|
+
const d = await provider.describe(layer.effect);
|
|
4433
|
+
if (!effectLayerOffered(d)) throw new EffectUnsupportedError(`"${d.id}" (${d.name}) cannot be a layer; ${kindHint(d)}. A layer's effect is a generator, filter, overlay or title (search_effects with use "generator", "filter", "overlay" or "title").`, d.id);
|
|
4434
|
+
const input = effectLayerInput(d);
|
|
4435
|
+
if (layer.subject && !input) throw new EffectUnsupportedError(`"${d.id}" (${d.name}) draws its own picture and takes no subject; leave "subject" out, or pick a filter to change the subject.`, d.id);
|
|
4436
|
+
if (!layer.subject && input && d.inputs.find((i) => i.name === input)?.required) throw new EffectUnsupportedError(`"${d.id}" (${d.name}) changes a picture; give a "subject": { "image": url }, { "svg": markup }, { "text": words } or { "slot": name }.`, d.id);
|
|
4437
|
+
const values = withPreset(d, layer.preset, layer.values);
|
|
4438
|
+
resolveEffectValues(d, values);
|
|
4439
|
+
const out = { ...layer, provider: provider.id, name: d.name };
|
|
4440
|
+
if (values && Object.keys(values).length) out.values = values;
|
|
4441
|
+
else delete out.values;
|
|
4442
|
+
return out;
|
|
4443
|
+
}
|
|
4444
|
+
async function resolveMotionShotTransition(provider, transition) {
|
|
4445
|
+
if (!provider) throw new NoEffectsProviderError();
|
|
4446
|
+
const d = await provider.describe(transition.effect);
|
|
4447
|
+
if (!d.uses.includes("transition") || d.kind === "camera") throw new EffectUnsupportedError(`"${d.id}" (${d.name}) is not a transition (${kindHint(d)}); search_effects with use "transition" lists them.`, d.id);
|
|
4448
|
+
if (transition.ms !== void 0 && !(transition.ms > 0 && transition.ms <= 6e4)) throw new Error(`A transition's "ms" is from 1 to 60000; got ${transition.ms}.`);
|
|
4449
|
+
const values = withPreset(d, transition.preset, transition.values);
|
|
4450
|
+
resolveEffectValues(d, values);
|
|
4451
|
+
const out = { ...transition, ms: transition.ms ?? EFFECT_TRANSITION_MS, provider: provider.id, name: d.name };
|
|
4452
|
+
if (values && Object.keys(values).length) out.values = values;
|
|
4453
|
+
else delete out.values;
|
|
4454
|
+
return out;
|
|
4455
|
+
}
|
|
4456
|
+
async function checkMotionShotLayers(scene, provider) {
|
|
4457
|
+
const issues = [];
|
|
4458
|
+
if (scene.shots[0]?.transition) issues.push(motionIssue("transition-first-shot", `Shot "${scene.shots[0].id}" is the first shot, so its transition does not play; move it to a later shot or clear it.`, { shot: scene.shots[0].id }));
|
|
4459
|
+
for (const shot of scene.shots)
|
|
4460
|
+
for (const layer of shot.layers ?? [])
|
|
4461
|
+
if (layer.subject && "slot" in layer.subject) issues.push(motionIssue("layer-slot-empty", `Layer "${layer.id}" shows slot "${layer.subject.slot}", which is empty; set its "subject" to a picture with update_shot_layer.`, { shot: shot.id, ids: [layer.id] }));
|
|
4462
|
+
const effectLayers = scene.shots.flatMap((s) => (s.layers ?? []).filter((l) => l.effect).map((l) => ({ shot: s.id, layer: l })));
|
|
4463
|
+
const transitions = scene.shots.slice(1).filter((s) => s.transition);
|
|
4464
|
+
if (!provider) {
|
|
4465
|
+
if (effectLayers.length) issues.push(motionIssue("layer-not-drawn", `Layers ${effectLayers.map((e) => `"${e.layer.id}"`).join(", ")} name provider effects, but no effects provider is set, so they are left out of frames; set a provider to draw them.`, { ids: effectLayers.map((e) => e.layer.id) }));
|
|
4466
|
+
if (transitions.length) issues.push(motionIssue("transition-not-played", `Shots ${transitions.map((s) => `"${s.id}"`).join(", ")} name transitions, but no effects provider is set, so they crossfade; set a provider to play them.`));
|
|
4467
|
+
return issues;
|
|
4468
|
+
}
|
|
4469
|
+
for (const { shot, layer } of effectLayers) {
|
|
4470
|
+
try {
|
|
4471
|
+
await resolveMotionShotLayer(provider, layer);
|
|
4472
|
+
} catch (error) {
|
|
4473
|
+
const code = error.code === "invalid_values" || /preset/.test(error.message) ? "layer-effect-values" : "layer-effect-unknown";
|
|
4474
|
+
issues.push(motionIssue(code, `Layer "${layer.id}": ${error.message}`, { shot, ids: [layer.id] }));
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
for (const shot of transitions) {
|
|
4478
|
+
try {
|
|
4479
|
+
await resolveMotionShotTransition(provider, shot.transition);
|
|
4480
|
+
} catch (error) {
|
|
4481
|
+
issues.push(motionIssue(error.code === "invalid_values" ? "layer-effect-values" : "transition-unknown", error.message, { shot: shot.id }));
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
return issues;
|
|
4485
|
+
}
|
|
4486
|
+
|
|
3983
4487
|
// src/animation-editor/effects.ts
|
|
3984
4488
|
var sourceOf = (c) => c.source;
|
|
3985
4489
|
function motionSceneEffects(scene) {
|
|
3986
4490
|
const out = /* @__PURE__ */ new Map();
|
|
3987
|
-
const groups = new Set(scene.artwork.nodes.filter((
|
|
4491
|
+
const groups = new Set(scene.artwork.nodes.filter((n2) => n2.type === "group").map((n2) => n2.id));
|
|
3988
4492
|
for (const shot of scene.shots) {
|
|
3989
4493
|
for (const c of shot.run) {
|
|
3990
4494
|
const s = sourceOf(c);
|
|
@@ -4005,7 +4509,7 @@ function motionSceneEffects(scene) {
|
|
|
4005
4509
|
}
|
|
4006
4510
|
function nextMotionEffectInstance(scene) {
|
|
4007
4511
|
const used = /* @__PURE__ */ new Set([...motionSceneEffects(scene).map((e) => e.instance), ...scene.shots.map((s) => s.id)]);
|
|
4008
|
-
const walk = (nodes) => nodes.forEach((
|
|
4512
|
+
const walk = (nodes) => nodes.forEach((n2) => (used.add(n2.id), n2.type === "group" && walk(n2.children)));
|
|
4009
4513
|
walk(scene.artwork.nodes);
|
|
4010
4514
|
for (let i = 1; ; i++) if (!used.has(`fx${i}`)) return `fx${i}`;
|
|
4011
4515
|
}
|
|
@@ -4021,10 +4525,10 @@ function motionCommandsFromEffect(artwork, motion, source) {
|
|
|
4021
4525
|
}
|
|
4022
4526
|
function prefixed(nodes, shots, prefix) {
|
|
4023
4527
|
const map = /* @__PURE__ */ new Map();
|
|
4024
|
-
const rename = (list) => list.map((
|
|
4025
|
-
map.set(
|
|
4026
|
-
const next = { ...
|
|
4027
|
-
if (next.type === "group") next.children = rename(
|
|
4528
|
+
const rename = (list) => list.map((n2) => {
|
|
4529
|
+
map.set(n2.id, prefix + n2.id);
|
|
4530
|
+
const next = { ...n2, id: prefix + n2.id };
|
|
4531
|
+
if (next.type === "group") next.children = rename(n2.children);
|
|
4028
4532
|
return next;
|
|
4029
4533
|
});
|
|
4030
4534
|
const out = rename(nodes);
|
|
@@ -4040,14 +4544,14 @@ function prefixed(nodes, shots, prefix) {
|
|
|
4040
4544
|
}));
|
|
4041
4545
|
return { nodes: out, shots: remapped };
|
|
4042
4546
|
}
|
|
4043
|
-
function motionPartsFromVectorEffect(artwork, motion, source,
|
|
4547
|
+
function motionPartsFromVectorEffect(artwork, motion, source, box2 = { x: 0, y: 0, width: artwork.width, height: artwork.height }) {
|
|
4044
4548
|
const imported = fromFlairSvgMotion({ svg: motion.svg, animate: [...motion.animate ?? []] }, { hold: 0 });
|
|
4045
4549
|
const art = imported.scene.artwork;
|
|
4046
4550
|
const prefix = `${source.instance}-`;
|
|
4047
4551
|
const { nodes, shots } = prefixed(art.nodes, imported.scene.shots, prefix);
|
|
4048
|
-
const k = Math.min(
|
|
4049
|
-
const dx =
|
|
4050
|
-
const dy =
|
|
4552
|
+
const k = Math.min(box2.width / art.width, box2.height / art.height);
|
|
4553
|
+
const dx = box2.x + (box2.width - art.width * k) / 2;
|
|
4554
|
+
const dy = box2.y + (box2.height - art.height * k) / 2;
|
|
4051
4555
|
const group = { type: "group", id: source.instance, name: source.name ?? source.effect, children: nodes, transform: [k, 0, 0, k, dx, dy] };
|
|
4052
4556
|
const tagged = shots.map((s, i) => ({ ...s, id: i === 0 ? source.instance + "-shot" : `${source.instance}-shot-${i + 1}`, run: tag(s.run, source) }));
|
|
4053
4557
|
return { group, shots: tagged.map((s) => s.hold === 0 ? (({ hold: _h, ...rest }) => rest)(s) : s), notes: imported.notes };
|
|
@@ -4066,7 +4570,7 @@ function withoutMotionEffect(scene, instance) {
|
|
|
4066
4570
|
else if (run.length === 0) removedShots.push(shot.id);
|
|
4067
4571
|
else shots.push({ ...shot, run });
|
|
4068
4572
|
}
|
|
4069
|
-
const artwork = { ...scene.artwork, nodes: scene.artwork.nodes.filter((
|
|
4573
|
+
const artwork = { ...scene.artwork, nodes: scene.artwork.nodes.filter((n2) => n2.id !== instance) };
|
|
4070
4574
|
return { scene: { ...scene, shots, artwork }, place, removedShots };
|
|
4071
4575
|
}
|
|
4072
4576
|
|
|
@@ -4079,6 +4583,8 @@ var AnimationEditorController = class {
|
|
|
4079
4583
|
player;
|
|
4080
4584
|
/** The effects provider slot (see `setEffectsProvider`). */
|
|
4081
4585
|
effects = new EffectsSlot();
|
|
4586
|
+
/** Renders shot layer and transition pictures for frames (see `MotionLayersEngine`). */
|
|
4587
|
+
layers;
|
|
4082
4588
|
scene;
|
|
4083
4589
|
issues;
|
|
4084
4590
|
selection = null;
|
|
@@ -4098,6 +4604,11 @@ var AnimationEditorController = class {
|
|
|
4098
4604
|
this.readOnly = options.readOnly ?? false;
|
|
4099
4605
|
this.onChange = options.onChange;
|
|
4100
4606
|
if (options.effects) this.effects.set(options.effects);
|
|
4607
|
+
this.layers = new MotionLayersEngine({
|
|
4608
|
+
slot: this.effects,
|
|
4609
|
+
host: options.layerHost === void 0 ? browserMotionLayerHost() : options.layerHost,
|
|
4610
|
+
frameSvg: (scene, time) => motionFrameToSvg(scene, time, { effects: this.effects.provider, layers: this.layers, transitions: false, pretty: false })
|
|
4611
|
+
});
|
|
4101
4612
|
this.player = new AnimationPlayer({ clock: options.clock, duration: () => compileMotionScene(this.scene).duration, loop: this.scene.loop ?? "none" });
|
|
4102
4613
|
this.state = this.computeState();
|
|
4103
4614
|
this.connect();
|
|
@@ -4109,7 +4620,8 @@ var AnimationEditorController = class {
|
|
|
4109
4620
|
this.history.subscribe(() => {
|
|
4110
4621
|
if (!this.applying) this.notify();
|
|
4111
4622
|
}),
|
|
4112
|
-
this.effects.subscribe(() => this.notify())
|
|
4623
|
+
this.effects.subscribe(() => this.notify()),
|
|
4624
|
+
this.layers.subscribe(() => this.notify())
|
|
4113
4625
|
);
|
|
4114
4626
|
}
|
|
4115
4627
|
/**
|
|
@@ -4228,10 +4740,10 @@ var AnimationEditorController = class {
|
|
|
4228
4740
|
apply(edits, label = "Edit animation", options = {}) {
|
|
4229
4741
|
const reason = this.readOnlyReason() ?? (this.recording ? "A take is being recorded; stop or cancel the recording first." : null);
|
|
4230
4742
|
if (reason) return this.refuse(reason);
|
|
4231
|
-
const
|
|
4232
|
-
if (!
|
|
4233
|
-
if (!options.dryRun) this.commit(label,
|
|
4234
|
-
return
|
|
4743
|
+
const result2 = applyMotionEdits(this.scene, edits);
|
|
4744
|
+
if (!result2.ok) return options.dryRun ? result2 : this.refuse(result2.error);
|
|
4745
|
+
if (!options.dryRun) this.commit(label, result2.scene, options.merge);
|
|
4746
|
+
return result2;
|
|
4235
4747
|
}
|
|
4236
4748
|
/** Replaces the artwork (e.g. from a vector editor's `onChange`), as one undo step. */
|
|
4237
4749
|
setArtwork(artwork, label = "Change artwork") {
|
|
@@ -4284,7 +4796,7 @@ var AnimationEditorController = class {
|
|
|
4284
4796
|
}
|
|
4285
4797
|
knownNodes(ids) {
|
|
4286
4798
|
const all = /* @__PURE__ */ new Set();
|
|
4287
|
-
const walk = (nodes) => nodes.forEach((
|
|
4799
|
+
const walk = (nodes) => nodes.forEach((n2) => (all.add(n2.id), n2.type === "group" && walk(n2.children)));
|
|
4288
4800
|
walk(this.scene.artwork.nodes);
|
|
4289
4801
|
const missing = ids.filter((id) => !all.has(id));
|
|
4290
4802
|
if (missing.length) throw new Error(`The artwork has no node ${missing.map((m) => `"${m}"`).join(", ")}; get_animation lists the node ids.`);
|
|
@@ -4350,10 +4862,10 @@ var AnimationEditorController = class {
|
|
|
4350
4862
|
const provider = this.effects.require();
|
|
4351
4863
|
const source = { instance, effect: d.id, provider: provider.id, name: d.name, values: merged };
|
|
4352
4864
|
if (was.kind === "vector") {
|
|
4353
|
-
const old = this.scene.artwork.nodes.find((
|
|
4865
|
+
const old = this.scene.artwork.nodes.find((n2) => n2.id === instance);
|
|
4354
4866
|
const motion2 = await provider.applyVectorEffect(d.id, motionArtworkSvg(bare.artwork), merged);
|
|
4355
4867
|
const { group, shots: shots2, notes: notes2 } = motionPartsFromVectorEffect(bare.artwork, motion2, source);
|
|
4356
|
-
const index = this.scene.artwork.nodes.findIndex((
|
|
4868
|
+
const index = this.scene.artwork.nodes.findIndex((n2) => n2.id === instance);
|
|
4357
4869
|
const nodes = [...bare.artwork.nodes];
|
|
4358
4870
|
nodes.splice(Math.max(0, index), 0, old?.transform ? { ...group, transform: old.transform } : group);
|
|
4359
4871
|
const at = this.scene.shots.findIndex((s) => was.shots.includes(s.id));
|
|
@@ -4517,9 +5029,151 @@ var AnimationEditorController = class {
|
|
|
4517
5029
|
frame(time = this.player.currentTime, options = {}) {
|
|
4518
5030
|
return motionFrameAt(this.viewScene(), time, { effects: this.effects.provider, ...options });
|
|
4519
5031
|
}
|
|
4520
|
-
/**
|
|
5032
|
+
/**
|
|
5033
|
+
* The scene at `time` (default: the playhead) as SVG. Shot layer and transition pictures that
|
|
5034
|
+
* are not rendered yet are left out (a transition crossfades) and come in on a later frame;
|
|
5035
|
+
* `toSvgExact` waits for them.
|
|
5036
|
+
*/
|
|
4521
5037
|
toSvg(time = this.player.currentTime, options = {}) {
|
|
4522
|
-
return motionStageSvg(this.viewScene(), time, { effects: this.effects.provider, ...options });
|
|
5038
|
+
return motionStageSvg(this.viewScene(), time, { effects: this.effects.provider, layers: this.layers, ...options });
|
|
5039
|
+
}
|
|
5040
|
+
/** Like `toSvg`, once every shot layer and transition picture of the frame is rendered (or has failed). */
|
|
5041
|
+
async toSvgExact(time = this.player.currentTime, options = {}) {
|
|
5042
|
+
let svg = this.toSvg(time, options);
|
|
5043
|
+
for (let pass = 0; pass < 8 && this.layers.busy; pass++) {
|
|
5044
|
+
await this.layers.idle();
|
|
5045
|
+
svg = this.toSvg(time, options);
|
|
5046
|
+
}
|
|
5047
|
+
return svg;
|
|
5048
|
+
}
|
|
5049
|
+
// ---------------------------------------------------------------------------------------
|
|
5050
|
+
// Shot layers and transitions
|
|
5051
|
+
// ---------------------------------------------------------------------------------------
|
|
5052
|
+
/** Every shot's layers and transition (or one shot's). Throws for an unknown shot, naming the shots. */
|
|
5053
|
+
shotLayers(shot) {
|
|
5054
|
+
if (shot !== void 0 && !this.scene.shots.some((s) => s.id === shot)) throw new Error(this.noShot(shot));
|
|
5055
|
+
return motionShotLayersInfo(this.scene, shot);
|
|
5056
|
+
}
|
|
5057
|
+
noShot(shot) {
|
|
5058
|
+
return `There is no shot "${shot}"; the shots are ${this.scene.shots.map((s) => `"${s.id}"`).join(", ") || "none yet"}.`;
|
|
5059
|
+
}
|
|
5060
|
+
noLayer(layer) {
|
|
5061
|
+
const ids = this.scene.shots.flatMap((s) => (s.layers ?? []).map((l) => `"${l.id}"`));
|
|
5062
|
+
return `There is no layer "${layer}"; ${ids.length ? `the layers are ${ids.join(", ")}` : "the scene has no layers yet (add_shot_layer adds one)"}.`;
|
|
5063
|
+
}
|
|
5064
|
+
layerResult(shot, layerId, notes = []) {
|
|
5065
|
+
const info2 = motionShotLayersInfo(this.scene, shot)[0];
|
|
5066
|
+
const layer = layerId ? info2.layers.find((l) => l.id === layerId) : void 0;
|
|
5067
|
+
return { ok: true, ...info2, ...layer ? { layer } : {}, notes };
|
|
5068
|
+
}
|
|
5069
|
+
async layerEdit(work, merge) {
|
|
5070
|
+
const reason = this.readOnlyReason();
|
|
5071
|
+
if (reason) return this.refuse(reason);
|
|
5072
|
+
let plan;
|
|
5073
|
+
try {
|
|
5074
|
+
plan = await work();
|
|
5075
|
+
} catch (error) {
|
|
5076
|
+
if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
|
|
5077
|
+
throw error;
|
|
5078
|
+
}
|
|
5079
|
+
const id = this.scene.shots[plan.shot].id;
|
|
5080
|
+
const r = this.apply(plan.layers.length ? { op: "update_shot", shot: id, set: { layers: plan.layers } } : { op: "update_shot", shot: id, remove: ["layers"] }, plan.label, { merge });
|
|
5081
|
+
if (!r.ok) return r;
|
|
5082
|
+
return this.layerResult(id, plan.layer, plan.notes);
|
|
5083
|
+
}
|
|
5084
|
+
/**
|
|
5085
|
+
* Adds a layer to a shot: a provider effect (a generator, filter, overlay or title), optionally
|
|
5086
|
+
* applied to a `subject`, or a bare subject. Checked against the provider first; at `index`
|
|
5087
|
+
* (default the top). One undo step. Returns the layer (with its id) and the shot's layers.
|
|
5088
|
+
*/
|
|
5089
|
+
addShotLayer(shot, layer, options = {}) {
|
|
5090
|
+
return this.layerEdit(async () => {
|
|
5091
|
+
const at = this.scene.shots.findIndex((s) => s.id === shot);
|
|
5092
|
+
if (at < 0) throw new Error(this.noShot(shot));
|
|
5093
|
+
const id = layer.id ?? freshMotionLayerId(this.scene);
|
|
5094
|
+
if (findMotionShotLayer(this.scene, id)) throw new Error(`Layer id "${id}" is already used; choose another (e.g. "${freshMotionLayerId(this.scene)}").`);
|
|
5095
|
+
const resolved = await resolveMotionShotLayer(this.effects.provider, { ...layer, id });
|
|
5096
|
+
const layers = [...this.scene.shots[at].layers ?? []];
|
|
5097
|
+
const index = options.index ?? layers.length;
|
|
5098
|
+
if (!Number.isInteger(index) || index < 0 || index > layers.length) throw new Error(`"index" must be a whole number from 0 to ${layers.length} (got ${index}).`);
|
|
5099
|
+
layers.splice(index, 0, resolved);
|
|
5100
|
+
const notes = resolved.subject && "slot" in resolved.subject ? [`The subject is slot "${resolved.subject.slot}", which draws nothing until it is filled; set "subject" with update_shot_layer.`] : [];
|
|
5101
|
+
return { shot: at, layers, label: `Add layer ${resolved.name ?? id}`, layer: id, notes };
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
/** Changes a layer's fields (`null` clears an optional one); the result is checked against the provider. One undo step (`merge` folds a slider drag into one). */
|
|
5105
|
+
updateShotLayer(layer, changes, options = {}) {
|
|
5106
|
+
return this.layerEdit(async () => {
|
|
5107
|
+
const found = findMotionShotLayer(this.scene, layer);
|
|
5108
|
+
if (!found) throw new Error(this.noLayer(layer));
|
|
5109
|
+
const was = this.scene.shots[found.shot].layers[found.index];
|
|
5110
|
+
const next = { ...was };
|
|
5111
|
+
for (const [k, v] of Object.entries(changes)) {
|
|
5112
|
+
if (v === null) delete next[k];
|
|
5113
|
+
else if (v !== void 0) next[k] = v;
|
|
5114
|
+
}
|
|
5115
|
+
if (changes.effect !== void 0 && changes.effect !== was.effect && changes.values === void 0) delete next.values;
|
|
5116
|
+
const resolved = await resolveMotionShotLayer(this.effects.provider, next);
|
|
5117
|
+
const layers = this.scene.shots[found.shot].layers.map((l, i) => i === found.index ? resolved : l);
|
|
5118
|
+
return { shot: found.shot, layers, label: `Change layer ${resolved.name ?? layer}`, layer };
|
|
5119
|
+
}, options.merge);
|
|
5120
|
+
}
|
|
5121
|
+
/** Removes a layer. One undo step. Works without a provider. */
|
|
5122
|
+
removeShotLayer(layer) {
|
|
5123
|
+
return this.layerEdit(async () => {
|
|
5124
|
+
const found = findMotionShotLayer(this.scene, layer);
|
|
5125
|
+
if (!found) throw new Error(this.noLayer(layer));
|
|
5126
|
+
const layers = this.scene.shots[found.shot].layers.filter((_, i) => i !== found.index);
|
|
5127
|
+
return { shot: found.shot, layers, label: "Remove layer" };
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
/** Moves a layer to `index` in its shot (0 = bottom). One undo step. */
|
|
5131
|
+
moveShotLayer(layer, index) {
|
|
5132
|
+
return this.layerEdit(async () => {
|
|
5133
|
+
const found = findMotionShotLayer(this.scene, layer);
|
|
5134
|
+
if (!found) throw new Error(this.noLayer(layer));
|
|
5135
|
+
const layers = [...this.scene.shots[found.shot].layers];
|
|
5136
|
+
if (!Number.isInteger(index) || index < 0 || index >= layers.length) throw new Error(`"index" must be a whole number from 0 to ${layers.length - 1} (got ${index}).`);
|
|
5137
|
+
const [moved] = layers.splice(found.index, 1);
|
|
5138
|
+
layers.splice(index, 0, moved);
|
|
5139
|
+
return { shot: found.shot, layers, label: "Move layer", layer };
|
|
5140
|
+
});
|
|
5141
|
+
}
|
|
5142
|
+
/**
|
|
5143
|
+
* Sets the provider transition into a shot from the shot before (not the first shot). Checked
|
|
5144
|
+
* against the provider (its use, preset and values; its length is written out). With no camera
|
|
5145
|
+
* move in, the camera cuts. One undo step (`merge` folds a slider drag into one).
|
|
5146
|
+
*/
|
|
5147
|
+
async setShotTransition(shot, transition, options = {}) {
|
|
5148
|
+
const reason = this.readOnlyReason();
|
|
5149
|
+
if (reason) return this.refuse(reason);
|
|
5150
|
+
const at = this.scene.shots.findIndex((s) => s.id === shot);
|
|
5151
|
+
if (at < 0) return this.refuse(this.noShot(shot));
|
|
5152
|
+
if (at === 0) return this.refuse(`Shot "${shot}" is the first shot, so there is no shot before it to transition from; set the transition on a later shot${this.scene.shots[1] ? ` (e.g. "${this.scene.shots[1].id}")` : ""}.`);
|
|
5153
|
+
let resolved;
|
|
5154
|
+
try {
|
|
5155
|
+
const { provider: _p, name: _n, ...given } = transition;
|
|
5156
|
+
resolved = await resolveMotionShotTransition(this.effects.provider, given);
|
|
5157
|
+
} catch (error) {
|
|
5158
|
+
if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
|
|
5159
|
+
throw error;
|
|
5160
|
+
}
|
|
5161
|
+
if (!this.scene.shots.some((s) => s.id === shot)) return this.refuse(`Shot "${shot}" was removed while the transition was checked.`);
|
|
5162
|
+
const r = this.apply({ op: "update_shot", shot, set: { transition: resolved } }, `Transition ${resolved.name ?? resolved.effect}`, { merge: options.merge });
|
|
5163
|
+
if (!r.ok) return r;
|
|
5164
|
+
const camera = this.scene.shots.find((s) => s.id === shot).camera;
|
|
5165
|
+
return this.layerResult(shot, void 0, camera?.in === void 0 ? ["With no camera move in, the camera cuts to this shot's framing and the transition does the move."] : []);
|
|
5166
|
+
}
|
|
5167
|
+
/** Removes a shot's transition. One undo step. Works without a provider. */
|
|
5168
|
+
clearShotTransition(shot) {
|
|
5169
|
+
const reason = this.readOnlyReason();
|
|
5170
|
+
if (reason) return this.refuse(reason);
|
|
5171
|
+
const found = this.scene.shots.find((s) => s.id === shot);
|
|
5172
|
+
if (!found) return this.refuse(this.noShot(shot));
|
|
5173
|
+
if (!found.transition) return this.refuse(`Shot "${shot}" has no transition to clear.`);
|
|
5174
|
+
const r = this.apply({ op: "update_shot", shot, remove: ["transition"] }, "Clear transition");
|
|
5175
|
+
if (!r.ok) return r;
|
|
5176
|
+
return this.layerResult(shot, void 0);
|
|
4523
5177
|
}
|
|
4524
5178
|
// ---------------------------------------------------------------------------------------
|
|
4525
5179
|
// Recording
|
|
@@ -4568,8 +5222,8 @@ var AnimationEditorController = class {
|
|
|
4568
5222
|
return motionNodePose(this.viewScene(), node, time);
|
|
4569
5223
|
}
|
|
4570
5224
|
/** A point on a stage element (client pixels, e.g. a pointer event) in document units, through the camera now. */
|
|
4571
|
-
clientToDocument(
|
|
4572
|
-
return motionClientToDocument(this.frame().camera,
|
|
5225
|
+
clientToDocument(box2, clientX, clientY) {
|
|
5226
|
+
return motionClientToDocument(this.frame().camera, box2, clientX, clientY);
|
|
4573
5227
|
}
|
|
4574
5228
|
/**
|
|
4575
5229
|
* Starts a stage drag while recording: from `point` (document units), `move` drags the node,
|
|
@@ -4633,9 +5287,9 @@ var AnimationEditorController = class {
|
|
|
4633
5287
|
this.player.seek(r.start);
|
|
4634
5288
|
return this.refuse("Nothing was recorded, so no shot was added: drag an element on the stage (or change its paint) while recording.");
|
|
4635
5289
|
}
|
|
4636
|
-
const
|
|
5290
|
+
const result2 = this.recordTake({ samples: r.samples, length }, r.options);
|
|
4637
5291
|
this.player.seek(r.start);
|
|
4638
|
-
return
|
|
5292
|
+
return result2;
|
|
4639
5293
|
}
|
|
4640
5294
|
/** Stops recording and throws the take away. */
|
|
4641
5295
|
cancelRecording() {
|
|
@@ -4655,10 +5309,10 @@ var AnimationEditorController = class {
|
|
|
4655
5309
|
} catch (error) {
|
|
4656
5310
|
return options.dryRun ? { ok: false, error: error.message } : this.refuse(error.message);
|
|
4657
5311
|
}
|
|
4658
|
-
const
|
|
4659
|
-
if (!
|
|
5312
|
+
const result2 = this.apply({ op: "insert_shots", shots: [made.shot], at: made.at }, "Record take", { dryRun: options.dryRun });
|
|
5313
|
+
if (!result2.ok) return result2;
|
|
4660
5314
|
if (!options.dryRun) this.select({ shot: made.shot.id });
|
|
4661
|
-
return { ok: true, ...made, issues:
|
|
5315
|
+
return { ok: true, ...made, issues: result2.issues };
|
|
4662
5316
|
}
|
|
4663
5317
|
/** The scene as a Flair `SvgMotion`. */
|
|
4664
5318
|
toFlairMotion(options = {}) {
|
|
@@ -5019,7 +5673,7 @@ function withDefs(schema) {
|
|
|
5019
5673
|
};
|
|
5020
5674
|
visit(schema);
|
|
5021
5675
|
if (!used.size) return schema;
|
|
5022
|
-
return { ...schema, $defs: Object.fromEntries(Object.keys(DEFS).filter((
|
|
5676
|
+
return { ...schema, $defs: Object.fromEntries(Object.keys(DEFS).filter((n2) => used.has(n2)).map((n2) => [n2, DEFS[n2]])) };
|
|
5023
5677
|
}
|
|
5024
5678
|
var SETTINGS = {
|
|
5025
5679
|
name: { type: "string", minLength: 1, description: 'Short name, e.g. "logo-reveal".' },
|
|
@@ -5084,7 +5738,7 @@ function describeElement(node, ancestors) {
|
|
|
5084
5738
|
...effects.length ? { effects } : {}
|
|
5085
5739
|
};
|
|
5086
5740
|
}
|
|
5087
|
-
var round3 = (
|
|
5741
|
+
var round3 = (n2) => Math.round(n2 * 10) / 10;
|
|
5088
5742
|
function describeMotionElements(scene) {
|
|
5089
5743
|
return [...indexMotionNodes(scene.artwork.nodes).values()].map((e) => ({
|
|
5090
5744
|
...describeElement(e.node, e.ancestors),
|
|
@@ -5125,20 +5779,20 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5125
5779
|
return controller.readOnlyReason();
|
|
5126
5780
|
};
|
|
5127
5781
|
const scene = () => controller.getScene();
|
|
5128
|
-
const outcome = (
|
|
5129
|
-
if (!
|
|
5130
|
-
const timeline = compileMotionScene(
|
|
5782
|
+
const outcome = (result2, extra = {}) => {
|
|
5783
|
+
if (!result2.ok) throw new Error(result2.error);
|
|
5784
|
+
const timeline = compileMotionScene(result2.scene);
|
|
5131
5785
|
return {
|
|
5132
5786
|
...extra,
|
|
5133
5787
|
duration: timeline.duration,
|
|
5134
5788
|
shots: timeline.shots.map((s) => ({ id: s.id, start: s.start, end: s.end })),
|
|
5135
|
-
issues: issueSummary(
|
|
5136
|
-
...
|
|
5789
|
+
issues: issueSummary(result2.issues),
|
|
5790
|
+
...result2.hints.length ? { hints: result2.hints } : {}
|
|
5137
5791
|
};
|
|
5138
5792
|
};
|
|
5139
5793
|
const apply = (edits, label, extra) => {
|
|
5140
|
-
const
|
|
5141
|
-
return outcome(
|
|
5794
|
+
const result2 = controller.apply(edits, label);
|
|
5795
|
+
return outcome(result2, result2.ok && extra ? extra(result2.scene) : {});
|
|
5142
5796
|
};
|
|
5143
5797
|
const getAnimation = {
|
|
5144
5798
|
name: "get_animation",
|
|
@@ -5266,7 +5920,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5266
5920
|
}
|
|
5267
5921
|
return { times, svg: sheet };
|
|
5268
5922
|
}
|
|
5269
|
-
const svg = controller.
|
|
5923
|
+
const svg = await controller.toSvgExact(t, { pretty: false, ...overlays });
|
|
5270
5924
|
if (format === "png") {
|
|
5271
5925
|
if (!options.rasterize) throw new Error('This host cannot make PNG images; call render_animation_frame with format "svg".');
|
|
5272
5926
|
const image = await options.rasterize(svg, { width: s.artwork.width, height: s.artwork.height });
|
|
@@ -5326,7 +5980,7 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5326
5980
|
if (!nodes.has(group)) throw new Error(`There is no element ${namedWithSuggestion(group, [...nodes.keys()])}.`);
|
|
5327
5981
|
const layout = motionTextLayout(nodes, group);
|
|
5328
5982
|
if (!layout) throw new Error(`"${group}" is not a group of paths; convert the text to outlines (one path a letter) and group it.`);
|
|
5329
|
-
return { group, letters: layout.lines.reduce((
|
|
5983
|
+
return { group, letters: layout.lines.reduce((n2, l) => n2 + l.words.reduce((m, w) => m + w.letters.length, 0), 0), ...layout };
|
|
5330
5984
|
}
|
|
5331
5985
|
};
|
|
5332
5986
|
const getMotionPath = {
|
|
@@ -5415,17 +6069,17 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5415
6069
|
),
|
|
5416
6070
|
annotations: { title: "Import a PowerPoint slide's animation", ...DESTRUCTIVE },
|
|
5417
6071
|
handler: async ({ data, slide, dry_run = false }) => {
|
|
5418
|
-
let
|
|
6072
|
+
let result2;
|
|
5419
6073
|
try {
|
|
5420
6074
|
const bytes = Uint8Array.from(atob(data.replace(/\s+/g, "")), (c) => c.charCodeAt(0));
|
|
5421
|
-
|
|
6075
|
+
result2 = await fromPptxAnimation(bytes, slide ? { slide } : {});
|
|
5422
6076
|
} catch (error) {
|
|
5423
6077
|
throw new Error(`The slide could not be imported: ${error.message} Nothing was changed.`);
|
|
5424
6078
|
}
|
|
5425
|
-
const report = { slides:
|
|
6079
|
+
const report = { slides: result2.slides, notes: result2.notes, issues: issueSummary(result2.issues), shots: describeMotionShots(result2.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
|
|
5426
6080
|
if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
|
|
5427
|
-
const applied = controller.apply({ op: "replace_scene", scene:
|
|
5428
|
-
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(
|
|
6081
|
+
const applied = controller.apply({ op: "replace_scene", scene: result2.scene }, "Import PowerPoint slide");
|
|
6082
|
+
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result2.scene) } : {});
|
|
5429
6083
|
}
|
|
5430
6084
|
};
|
|
5431
6085
|
const importFlairEffect = {
|
|
@@ -5444,16 +6098,16 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5444
6098
|
annotations: { title: "Import a Flair effect", ...DESTRUCTIVE },
|
|
5445
6099
|
handler: ({ effect, motion, name, dry_run = false }) => {
|
|
5446
6100
|
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.');
|
|
5447
|
-
let
|
|
6101
|
+
let result2;
|
|
5448
6102
|
try {
|
|
5449
|
-
|
|
6103
|
+
result2 = effect !== void 0 ? fromFlairVectorEffect(effect, name ? { name } : {}) : fromFlairSvgMotion(motion, name ? { name } : {});
|
|
5450
6104
|
} catch (error) {
|
|
5451
6105
|
throw new Error(`The Flair effect could not be imported: ${error.message} Nothing was changed.`);
|
|
5452
6106
|
}
|
|
5453
|
-
const report = { notes:
|
|
6107
|
+
const report = { notes: result2.notes, issues: issueSummary(result2.issues), shots: describeMotionShots(result2.scene).map(({ id, start, end, commands }) => ({ id, start, end, commands: commands.length })) };
|
|
5454
6108
|
if (dry_run) return { dry_run: true, ...report, hint: "Nothing was replaced; call again without dry_run to import." };
|
|
5455
|
-
const applied = controller.apply({ op: "replace_scene", scene:
|
|
5456
|
-
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(
|
|
6109
|
+
const applied = controller.apply({ op: "replace_scene", scene: result2.scene }, "Import Flair effect");
|
|
6110
|
+
return outcome(applied, applied.ok ? { ...report, elements: describeMotionElements(result2.scene) } : {});
|
|
5457
6111
|
}
|
|
5458
6112
|
};
|
|
5459
6113
|
const fitAnimation = {
|
|
@@ -5516,18 +6170,18 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5516
6170
|
),
|
|
5517
6171
|
annotations: { title: "Record a performed take", ...WRITE },
|
|
5518
6172
|
handler: ({ samples, length, id, label, at, tolerance, dry_run = false }) => {
|
|
5519
|
-
const
|
|
5520
|
-
if (!
|
|
6173
|
+
const result2 = controller.recordTake({ samples, length }, { id, label, at, tolerance, dryRun: dry_run });
|
|
6174
|
+
if (!result2.ok) throw new Error(result2.error);
|
|
5521
6175
|
const s = dry_run ? scene() : controller.getScene();
|
|
5522
|
-
const report = { shot:
|
|
5523
|
-
if (dry_run) return { dry_run: true, ...report, run:
|
|
6176
|
+
const report = { shot: result2.shot.id, at: result2.at, start: Math.round(result2.start), samples: result2.samples };
|
|
6177
|
+
if (dry_run) return { dry_run: true, ...report, run: result2.shot.run, hold: result2.shot.hold, hint: "Nothing was changed; call again without dry_run to add the shot." };
|
|
5524
6178
|
const timeline = compileMotionScene(s);
|
|
5525
|
-
const described = describeMotionShots(s).find((x) => x.id ===
|
|
6179
|
+
const described = describeMotionShots(s).find((x) => x.id === result2.shot.id);
|
|
5526
6180
|
return {
|
|
5527
6181
|
...report,
|
|
5528
6182
|
commands: described.commands.map((c) => ({ index: c.index, type: c.type, targets: "targets" in c ? c.targets : [], start: c.start, end: c.end })),
|
|
5529
6183
|
duration: timeline.duration,
|
|
5530
|
-
issues: issueSummary(
|
|
6184
|
+
issues: issueSummary(result2.issues)
|
|
5531
6185
|
};
|
|
5532
6186
|
}
|
|
5533
6187
|
};
|
|
@@ -5606,16 +6260,16 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5606
6260
|
for (const shot of s.shots) {
|
|
5607
6261
|
if (only && !only.has(shot.id)) continue;
|
|
5608
6262
|
const run = [];
|
|
5609
|
-
let
|
|
6263
|
+
let n2 = 0;
|
|
5610
6264
|
for (const c of shot.run) {
|
|
5611
6265
|
run.push(c);
|
|
5612
6266
|
if (c.type === "wait" || !c.targets.includes(from)) continue;
|
|
5613
6267
|
const { delay: _delay, ...rest } = c;
|
|
5614
6268
|
run.push({ ...rest, targets: to, with: "previous", ...stagger ? { stagger } : {} });
|
|
5615
|
-
|
|
6269
|
+
n2++;
|
|
5616
6270
|
}
|
|
5617
|
-
if (
|
|
5618
|
-
copied[shot.id] =
|
|
6271
|
+
if (n2) {
|
|
6272
|
+
copied[shot.id] = n2;
|
|
5619
6273
|
edits.push({ op: "update_shot", shot: shot.id, set: { run } });
|
|
5620
6274
|
}
|
|
5621
6275
|
}
|
|
@@ -5742,8 +6396,8 @@ function createAnimationEditorToolProvider(controller, options = {}) {
|
|
|
5742
6396
|
handler: ({ ops, dry_run = false }) => {
|
|
5743
6397
|
const refs = resolveMotionEditRefs(controller.getScene(), ops);
|
|
5744
6398
|
if (!refs.ok) throw new Error(refs.error);
|
|
5745
|
-
const
|
|
5746
|
-
return outcome(
|
|
6399
|
+
const result2 = controller.apply(refs.edits, "Edit animation", { dryRun: dry_run });
|
|
6400
|
+
return outcome(result2, { outcomes: refs.outcomes, ...dry_run ? { dry_run: true, hint: "Nothing was applied; call again without dry_run to apply." } : {} });
|
|
5747
6401
|
}
|
|
5748
6402
|
};
|
|
5749
6403
|
const writeAnimation = {
|
|
@@ -5907,7 +6561,7 @@ function motionCommandFields(type) {
|
|
|
5907
6561
|
const schema = branch(type);
|
|
5908
6562
|
if (!schema?.properties) return [];
|
|
5909
6563
|
const required = new Set(schema.required ?? []);
|
|
5910
|
-
const
|
|
6564
|
+
const fields2 = Object.entries(schema.properties).filter(([name]) => name !== "type").map(([name, s]) => {
|
|
5911
6565
|
const kind = kindOf(name, s);
|
|
5912
6566
|
const min = typeof s.minimum === "number" ? s.minimum : typeof s.exclusiveMinimum === "number" ? s.exclusiveMinimum : void 0;
|
|
5913
6567
|
return {
|
|
@@ -5923,7 +6577,7 @@ function motionCommandFields(type) {
|
|
|
5923
6577
|
group: TIMING.has(name) ? "timing" : "main"
|
|
5924
6578
|
};
|
|
5925
6579
|
});
|
|
5926
|
-
return [...
|
|
6580
|
+
return [...fields2.filter((f) => f.group === "main"), ...fields2.filter((f) => f.group === "timing")];
|
|
5927
6581
|
}
|
|
5928
6582
|
function parseMotionFieldInput(field, raw) {
|
|
5929
6583
|
if (field.kind === "boolean") return raw === true ? { set: true } : { remove: true };
|
|
@@ -5942,12 +6596,12 @@ function parseMotionFieldInput(field, raw) {
|
|
|
5942
6596
|
const text = String(raw).trim();
|
|
5943
6597
|
if (text === "") return field.required ? { error: `${field.label} is required.` } : { remove: true };
|
|
5944
6598
|
if (field.kind === "number" || field.kind === "integer" || field.kind === "scale") {
|
|
5945
|
-
const
|
|
5946
|
-
if (!Number.isFinite(
|
|
5947
|
-
if (field.kind === "integer" && !Number.isInteger(
|
|
5948
|
-
if (field.min !== void 0 &&
|
|
5949
|
-
if (field.max !== void 0 &&
|
|
5950
|
-
return { set:
|
|
6599
|
+
const n2 = Number(text);
|
|
6600
|
+
if (!Number.isFinite(n2)) return { error: `${field.label} must be a number${field.unit ? ` (${field.unit})` : ""}.` };
|
|
6601
|
+
if (field.kind === "integer" && !Number.isInteger(n2)) return { error: `${field.label} must be a whole number.` };
|
|
6602
|
+
if (field.min !== void 0 && n2 < field.min) return { error: `${field.label} must be at least ${field.min}.` };
|
|
6603
|
+
if (field.max !== void 0 && n2 > field.max) return { error: `${field.label} must be at most ${field.max}.` };
|
|
6604
|
+
return { set: n2 };
|
|
5951
6605
|
}
|
|
5952
6606
|
if (field.kind === "enum" && field.options && !field.options.includes(text)) return { error: `${field.label} must be one of ${field.options.join(", ")}.` };
|
|
5953
6607
|
return { set: text };
|
|
@@ -6037,18 +6691,18 @@ function motionEaseKey(ease) {
|
|
|
6037
6691
|
function motionEasingCurve(ease, samples = 48, durationMs = 1e3) {
|
|
6038
6692
|
const resolved = resolveEasing(ease);
|
|
6039
6693
|
const ms2 = resolved !== "hold" && resolved.type === "spring" ? springSettleMs(resolved) : durationMs;
|
|
6040
|
-
const
|
|
6694
|
+
const n2 = Math.max(2, Math.floor(samples));
|
|
6041
6695
|
const points = [];
|
|
6042
|
-
for (let i = 0; i <=
|
|
6043
|
-
const u = i /
|
|
6044
|
-
points.push({ u, v: resolved === "hold" ? i ===
|
|
6696
|
+
for (let i = 0; i <= n2; i++) {
|
|
6697
|
+
const u = i / n2;
|
|
6698
|
+
points.push({ u, v: resolved === "hold" ? i === n2 ? 1 : 0 : evaluateEasing(ease, u, ms2) });
|
|
6045
6699
|
}
|
|
6046
6700
|
const values = points.map((p) => p.v);
|
|
6047
6701
|
return { points, min: Math.min(0, ...values), max: Math.max(1, ...values), durationMs: ms2 };
|
|
6048
6702
|
}
|
|
6049
6703
|
function motionEasingPath(curve, width, height, pad = 4) {
|
|
6050
6704
|
const span = curve.max - curve.min || 1;
|
|
6051
|
-
const f = (
|
|
6705
|
+
const f = (n2) => String(Math.round(n2 * 100) / 100);
|
|
6052
6706
|
return curve.points.map((p, i) => `${i ? "L" : "M"}${f(pad + p.u * (width - 2 * pad))} ${f(pad + (1 - (p.v - curve.min) / span) * (height - 2 * pad))}`).join(" ");
|
|
6053
6707
|
}
|
|
6054
6708
|
function motionEasingGuides(curve, height, pad = 4) {
|
|
@@ -6158,7 +6812,7 @@ function parseMotionDrivers(text) {
|
|
|
6158
6812
|
if (factor !== void 0) d.factor = Number(factor);
|
|
6159
6813
|
if (offset !== void 0) d.offset = (sign === "-" ? -1 : 1) * Number(offset);
|
|
6160
6814
|
if (min !== void 0) d.clamp = [Number(min), Number(max)];
|
|
6161
|
-
if ([d.factor, d.offset, ...d.clamp ?? []].some((
|
|
6815
|
+
if ([d.factor, d.offset, ...d.clamp ?? []].some((n2) => n2 !== void 0 && !Number.isFinite(n2))) return { error: `Line ${i + 1} has a number that cannot be read.` };
|
|
6162
6816
|
drivers.push(d);
|
|
6163
6817
|
}
|
|
6164
6818
|
return { drivers };
|
|
@@ -6229,6 +6883,163 @@ function animationCameraMoveTools(controller, ctx) {
|
|
|
6229
6883
|
];
|
|
6230
6884
|
}
|
|
6231
6885
|
|
|
6886
|
+
// src/animation-editor/layer-tools.ts
|
|
6887
|
+
var LAYER_USES = ["generator", "filter", "overlay", "title"];
|
|
6888
|
+
var SHOT2 = { type: "string", minLength: 1, description: "A shot id from get_animation." };
|
|
6889
|
+
var LAYER = { type: "string", minLength: 1, description: 'A layer id from list_shot_layers (e.g. "layer1").' };
|
|
6890
|
+
var SUBJECT = {
|
|
6891
|
+
anyOf: [
|
|
6892
|
+
{ type: "object", properties: { image: { type: "string", minLength: 1, description: "A picture URL or data: URL." } }, required: ["image"], additionalProperties: false, description: '{ "image": url }' },
|
|
6893
|
+
{ type: "object", properties: { svg: { type: "string", minLength: 1, description: "SVG markup or an SVG URL." } }, required: ["svg"], additionalProperties: false, description: '{ "svg": markup }' },
|
|
6894
|
+
{
|
|
6895
|
+
type: "object",
|
|
6896
|
+
properties: { text: { type: "string", minLength: 1, description: "The words." }, font: { type: "string", description: "A CSS font family (default system-ui)." }, weight: { anyOf: [{ type: "number" }, { type: "string" }], description: "A CSS font weight (default 700)." } },
|
|
6897
|
+
required: ["text"],
|
|
6898
|
+
additionalProperties: false,
|
|
6899
|
+
description: '{ "text": words }'
|
|
6900
|
+
},
|
|
6901
|
+
{ type: "object", properties: { slot: { type: "string", minLength: 1, description: "A slot name the host fills later." } }, required: ["slot"], additionalProperties: false, description: '{ "slot": name }: draws nothing until filled.' }
|
|
6902
|
+
],
|
|
6903
|
+
description: "What the layer shows, or feeds its effect (a filter changes it): a picture, SVG, words, or a slot to fill later. Leave out for a generator, overlay or title that draws its own picture."
|
|
6904
|
+
};
|
|
6905
|
+
var RECT = {
|
|
6906
|
+
type: "object",
|
|
6907
|
+
properties: {
|
|
6908
|
+
x: { type: "number", description: "Left edge, artwork units." },
|
|
6909
|
+
y: { type: "number", description: "Top edge, artwork units." },
|
|
6910
|
+
width: { type: "number", exclusiveMinimum: 0, description: "Width, artwork units." },
|
|
6911
|
+
height: { type: "number", exclusiveMinimum: 0, description: "Height, artwork units." }
|
|
6912
|
+
},
|
|
6913
|
+
required: ["x", "y", "width", "height"],
|
|
6914
|
+
additionalProperties: false,
|
|
6915
|
+
description: "Where the layer sits on the artwork (it moves with the camera). Leave out to fill the screen."
|
|
6916
|
+
};
|
|
6917
|
+
var TIME = (what, fallback) => ({
|
|
6918
|
+
anyOf: [
|
|
6919
|
+
{ type: "number", minimum: 0, description: "Ms after the shot starts." },
|
|
6920
|
+
{ type: "object", properties: { shots: { type: "integer", minimum: 1, description: "1: when the next shot starts; 2: the one after\u2026" } }, required: ["shots"], additionalProperties: false, description: '{ "shots": n }' }
|
|
6921
|
+
],
|
|
6922
|
+
description: `When the layer ${what}: ms after its shot starts, or { "shots": n } for when the n-th shot after its own starts (default ${fallback}).`
|
|
6923
|
+
});
|
|
6924
|
+
var fields = (ctx) => ({
|
|
6925
|
+
effect: { ...ctx.effectIdSchema(LAYER_USES), description: "A generator, filter, overlay or title effect id from search_effects. Leave out for a bare subject." },
|
|
6926
|
+
preset: { type: "string", minLength: 1, description: "A preset of the effect by name, from describe_effect; its values are written out on the layer." },
|
|
6927
|
+
values: ctx.valuesSchema("Control values over the effect's defaults (and the preset's), from describe_effect"),
|
|
6928
|
+
subject: SUBJECT,
|
|
6929
|
+
role: { type: "string", enum: [...EFFECT_LAYER_ROLES], description: "background: under the artwork; subject: over the artwork; overlay: over everything. Default subject with a rect, else background." },
|
|
6930
|
+
rect: RECT,
|
|
6931
|
+
from: TIME("starts", "0: as its shot starts"),
|
|
6932
|
+
until: TIME("stops", '{ "shots": 1 }: as the next shot starts'),
|
|
6933
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0\u20131 (default 1)." },
|
|
6934
|
+
fade: { type: "number", minimum: 0, maximum: 6e4, description: "Ms it fades in and out over (default 0: it cuts)." }
|
|
6935
|
+
});
|
|
6936
|
+
var result = (r) => {
|
|
6937
|
+
if (!r.ok) throw new Error(r.error);
|
|
6938
|
+
const { ok: _ok, notes, ...rest } = r;
|
|
6939
|
+
return { ...rest, ...notes.length ? { notes } : {} };
|
|
6940
|
+
};
|
|
6941
|
+
function animationShotLayerTools(controller, ctx) {
|
|
6942
|
+
const f = fields(ctx);
|
|
6943
|
+
return [
|
|
6944
|
+
{
|
|
6945
|
+
name: "list_shot_layers",
|
|
6946
|
+
title: "List shot layers",
|
|
6947
|
+
description: "Lists the layers and transition of every shot, or of one: [{ shot, layers: [{ id, effect, name, subject, role, rect, from, until, opacity, fade, values, index (0 = bottom), start, end (ms when it shows) }], transition: { effect, ms, values, start, end } or null }]. Use the layer ids with update_shot_layer, move_shot_layer and remove_shot_layer. Works without a provider.",
|
|
6948
|
+
inputSchema: { type: "object", properties: { shot: { ...SHOT2, description: "Only this shot (default every shot)." } }, additionalProperties: false },
|
|
6949
|
+
annotations: { title: "List shot layers", ...ctx.annotations.read },
|
|
6950
|
+
handler: async (args) => ({ shots: controller.shotLayers(args.shot) })
|
|
6951
|
+
},
|
|
6952
|
+
{
|
|
6953
|
+
name: "add_shot_layer",
|
|
6954
|
+
title: "Add a shot layer",
|
|
6955
|
+
description: "Adds a layer to a shot: a provider effect (a generator such as a gradient background, a filter applied to a subject, an overlay or title) or a bare subject (a picture, SVG or words). It shows from its shot's start until the next shot starts unless from / until say otherwise, fills the screen unless given a rect, and goes on top unless given an index. Checked first: an effect that cannot be a layer (a transition, a camera move, a motion effect) is refused with the tool to use instead, as are a subject the effect cannot take, a filter without a subject, an unknown preset and values outside its controls. One undo step. Returns { shot, layer (with its id, start and end), layers (the shot's, bottom to top), transition, notes }. Check it with render_animation_frame.",
|
|
6956
|
+
inputSchema: {
|
|
6957
|
+
type: "object",
|
|
6958
|
+
properties: { shot: SHOT2, ...f, id: { type: "string", pattern: "^[A-Za-z_][\\w.-]*$", description: "The layer's id (default layer1, layer2\u2026); unique in the scene." }, index: { type: "integer", minimum: 0, description: "Its place in the shot, 0 = bottom (default the top)." } },
|
|
6959
|
+
required: ["shot"],
|
|
6960
|
+
additionalProperties: false
|
|
6961
|
+
},
|
|
6962
|
+
annotations: { title: "Add a shot layer", ...ctx.annotations.write },
|
|
6963
|
+
handler: async ({ shot, index, ...layer }) => {
|
|
6964
|
+
if (layer.effect !== void 0) ctx.requireProvider();
|
|
6965
|
+
if (layer.effect === void 0 && layer.subject === void 0) throw new Error('Give an "effect" (search_effects with use generator, filter, overlay or title), a "subject" (a picture, SVG or words), or both.');
|
|
6966
|
+
return result(await controller.addShotLayer(shot, layer, { index }));
|
|
6967
|
+
}
|
|
6968
|
+
},
|
|
6969
|
+
{
|
|
6970
|
+
name: "update_shot_layer",
|
|
6971
|
+
title: "Change a shot layer",
|
|
6972
|
+
description: `Changes a layer's fields: give the ones to change, and list in "clear" the optional ones to remove (back to their defaults). A new effect starts from its own defaults unless values are given. Checked against the provider like add_shot_layer. One undo step. Returns { shot, layer, layers, transition }.`,
|
|
6973
|
+
inputSchema: {
|
|
6974
|
+
type: "object",
|
|
6975
|
+
properties: {
|
|
6976
|
+
layer: LAYER,
|
|
6977
|
+
...f,
|
|
6978
|
+
clear: { type: "array", items: { type: "string", enum: ["effect", "preset", "values", "subject", "role", "rect", "from", "until", "opacity", "fade"] }, uniqueItems: true, description: "Fields to remove." }
|
|
6979
|
+
},
|
|
6980
|
+
required: ["layer"],
|
|
6981
|
+
additionalProperties: false
|
|
6982
|
+
},
|
|
6983
|
+
annotations: { title: "Change a shot layer", ...ctx.annotations.writeIdempotent },
|
|
6984
|
+
handler: async ({ layer, clear, ...set }) => {
|
|
6985
|
+
const changes = { ...set };
|
|
6986
|
+
for (const k of clear ?? []) {
|
|
6987
|
+
if (k in set) throw new Error(`"${k}" is both given and cleared; do one or the other.`);
|
|
6988
|
+
changes[k] = null;
|
|
6989
|
+
}
|
|
6990
|
+
if (!Object.keys(changes).length) throw new Error('Give at least one field to change, or a field in "clear".');
|
|
6991
|
+
return result(await controller.updateShotLayer(layer, changes));
|
|
6992
|
+
}
|
|
6993
|
+
},
|
|
6994
|
+
{
|
|
6995
|
+
name: "move_shot_layer",
|
|
6996
|
+
title: "Restack a shot layer",
|
|
6997
|
+
description: "Moves a layer to another place in its shot's stack (0 = bottom; within a role, later layers draw over earlier ones). One undo step. Returns { shot, layer, layers }.",
|
|
6998
|
+
inputSchema: { type: "object", properties: { layer: LAYER, index: { type: "integer", minimum: 0, description: "Its new place in the shot, 0 = bottom." } }, required: ["layer", "index"], additionalProperties: false },
|
|
6999
|
+
annotations: { title: "Restack a shot layer", ...ctx.annotations.writeIdempotent },
|
|
7000
|
+
handler: async ({ layer, index }) => result(await controller.moveShotLayer(layer, index))
|
|
7001
|
+
},
|
|
7002
|
+
{
|
|
7003
|
+
name: "remove_shot_layer",
|
|
7004
|
+
title: "Remove a shot layer",
|
|
7005
|
+
description: "Removes a layer from its shot. Works without a provider. One undo step. Returns the shot's remaining layers.",
|
|
7006
|
+
inputSchema: { type: "object", properties: { layer: LAYER }, required: ["layer"], additionalProperties: false },
|
|
7007
|
+
annotations: { title: "Remove a shot layer", ...ctx.annotations.destructive },
|
|
7008
|
+
handler: async ({ layer }) => result(await controller.removeShotLayer(layer))
|
|
7009
|
+
},
|
|
7010
|
+
{
|
|
7011
|
+
name: "set_shot_transition",
|
|
7012
|
+
title: "Set a shot's transition",
|
|
7013
|
+
description: `Sets the transition into a shot: a provider effect with the use transition (search_effects with use "transition") blends the last frame of the shot before into this shot over ms. Not on the first shot (there is no shot before it). With no camera move in (set_shot_camera_move, role transition), the camera cuts and the transition does the move; with one, both play together. Checked first: an effect that is not a transition, an unknown preset or values outside its controls are refused. One undo step. Returns { shot, layers, transition: { effect, ms, values, start, end }, notes }.`,
|
|
7014
|
+
inputSchema: {
|
|
7015
|
+
type: "object",
|
|
7016
|
+
properties: {
|
|
7017
|
+
shot: { ...SHOT2, description: "A shot id from get_animation (not the first shot)." },
|
|
7018
|
+
effect: { ...ctx.effectIdSchema(["transition"]), description: "A transition effect id from search_effects (use transition)." },
|
|
7019
|
+
ms: { type: "number", exclusiveMinimum: 0, maximum: 6e4, description: `How long it takes, ms (default ${EFFECT_TRANSITION_MS}).` },
|
|
7020
|
+
preset: { type: "string", minLength: 1, description: "A preset of the transition by name, from describe_effect." },
|
|
7021
|
+
values: ctx.valuesSchema("Control values over the transition's defaults (and the preset's)")
|
|
7022
|
+
},
|
|
7023
|
+
required: ["shot", "effect"],
|
|
7024
|
+
additionalProperties: false
|
|
7025
|
+
},
|
|
7026
|
+
annotations: { title: "Set a shot's transition", ...ctx.annotations.writeIdempotent },
|
|
7027
|
+
handler: async ({ shot, effect, ms: ms2, preset: preset2, values }) => {
|
|
7028
|
+
ctx.requireProvider();
|
|
7029
|
+
return result(await controller.setShotTransition(shot, { effect, ...ms2 !== void 0 ? { ms: ms2 } : {}, ...preset2 !== void 0 ? { preset: preset2 } : {}, ...values !== void 0 ? { values } : {} }));
|
|
7030
|
+
}
|
|
7031
|
+
},
|
|
7032
|
+
{
|
|
7033
|
+
name: "clear_shot_transition",
|
|
7034
|
+
title: "Clear a shot's transition",
|
|
7035
|
+
description: "Removes the transition into a shot, so it cuts (or the camera eases) from the shot before. Works without a provider. One undo step. Returns { shot, layers, transition: null }.",
|
|
7036
|
+
inputSchema: { type: "object", properties: { shot: SHOT2 }, required: ["shot"], additionalProperties: false },
|
|
7037
|
+
annotations: { title: "Clear a shot's transition", ...ctx.annotations.destructive },
|
|
7038
|
+
handler: async ({ shot }) => result(controller.clearShotTransition(shot))
|
|
7039
|
+
}
|
|
7040
|
+
];
|
|
7041
|
+
}
|
|
7042
|
+
|
|
6232
7043
|
// src/animation-editor/effect-tools.ts
|
|
6233
7044
|
var info = (e) => ({ id: e.instance, effect: e.effect, ...e.name ? { name: e.name } : {}, ...e.provider ? { provider: e.provider } : {}, role: e.kind === "vector" ? "vector picture" : "motion", enabled: true, values: e.values, targets: e.targets, shots: e.shots });
|
|
6234
7045
|
function createAnimationEffectsToolProvider(controller, options = {}) {
|
|
@@ -6275,6 +7086,7 @@ function createAnimationEffectsToolProvider(controller, options = {}) {
|
|
|
6275
7086
|
subscribe: (notify) => controller.subscribe(() => notify()),
|
|
6276
7087
|
extraTools: (ctx) => [
|
|
6277
7088
|
...animationCameraMoveTools(controller, ctx),
|
|
7089
|
+
...animationShotLayerTools(controller, ctx),
|
|
6278
7090
|
{
|
|
6279
7091
|
name: "add_vector_effect",
|
|
6280
7092
|
title: "Add a vector effect",
|
|
@@ -6318,8 +7130,8 @@ function createAnimationEditorDocument(controller) {
|
|
|
6318
7130
|
kind: "animation",
|
|
6319
7131
|
get: () => controller.getScene(),
|
|
6320
7132
|
load: (document, { history }) => {
|
|
6321
|
-
const
|
|
6322
|
-
if (!
|
|
7133
|
+
const result2 = controller.load(document, { clearHistory: history === "reset" });
|
|
7134
|
+
if (!result2.ok) throw new Error(result2.error);
|
|
6323
7135
|
},
|
|
6324
7136
|
subscribe: (listener) => {
|
|
6325
7137
|
let scene = controller.getScene();
|
|
@@ -6375,14 +7187,18 @@ export {
|
|
|
6375
7187
|
MOTION_TEXT_STAGGER,
|
|
6376
7188
|
MOTION_TEXT_UNITS,
|
|
6377
7189
|
MOTION_TRIGGERS,
|
|
7190
|
+
MotionLayersEngine,
|
|
6378
7191
|
MotionSceneFormatError,
|
|
6379
7192
|
activeSegment,
|
|
6380
7193
|
animationCameraMoveTools,
|
|
7194
|
+
animationShotLayerTools,
|
|
6381
7195
|
applyMotionControlValues,
|
|
6382
7196
|
applyMotionDrivers,
|
|
6383
7197
|
applyMotionEdits,
|
|
7198
|
+
browserMotionLayerHost,
|
|
6384
7199
|
checkMotionCameraMoves,
|
|
6385
7200
|
checkMotionScene,
|
|
7201
|
+
checkMotionShotLayers,
|
|
6386
7202
|
clipsShapeProblems,
|
|
6387
7203
|
commandShapeProblems,
|
|
6388
7204
|
compileMotionScene,
|
|
@@ -6396,6 +7212,7 @@ export {
|
|
|
6396
7212
|
driversShapeProblems,
|
|
6397
7213
|
duplicateMotionCommandEdit,
|
|
6398
7214
|
effectsPlayCamera,
|
|
7215
|
+
findMotionShotLayer,
|
|
6399
7216
|
fitMotionRect,
|
|
6400
7217
|
fitMotionScene,
|
|
6401
7218
|
fitMotionSceneEdits,
|
|
@@ -6404,6 +7221,7 @@ export {
|
|
|
6404
7221
|
flairEaseFor,
|
|
6405
7222
|
formatMotionDrivers,
|
|
6406
7223
|
formatMotionNarration,
|
|
7224
|
+
freshMotionLayerId,
|
|
6407
7225
|
freshShotId,
|
|
6408
7226
|
fromFlairSvgMotion,
|
|
6409
7227
|
fromFlairVectorEffect,
|
|
@@ -6459,6 +7277,10 @@ export {
|
|
|
6459
7277
|
motionIssue,
|
|
6460
7278
|
motionIssueKey,
|
|
6461
7279
|
motionIssueLine,
|
|
7280
|
+
motionLayerIds,
|
|
7281
|
+
motionLayerSlot,
|
|
7282
|
+
motionLayerSpans,
|
|
7283
|
+
motionLayersAt,
|
|
6462
7284
|
motionMovingIds,
|
|
6463
7285
|
motionNodeAtElement,
|
|
6464
7286
|
motionNodeClips,
|
|
@@ -6474,11 +7296,13 @@ export {
|
|
|
6474
7296
|
motionRevealPadding,
|
|
6475
7297
|
motionRulerTicks,
|
|
6476
7298
|
motionSceneEffects,
|
|
7299
|
+
motionSceneHasLayers,
|
|
6477
7300
|
motionSceneShapeProblems,
|
|
6478
7301
|
motionSceneToJson,
|
|
6479
7302
|
motionShotAt,
|
|
6480
7303
|
motionShotCameraMove,
|
|
6481
7304
|
motionShotCueTime,
|
|
7305
|
+
motionShotLayersInfo,
|
|
6482
7306
|
motionShotTimeScale,
|
|
6483
7307
|
motionStageSvg,
|
|
6484
7308
|
motionTakeProblems,
|
|
@@ -6486,6 +7310,8 @@ export {
|
|
|
6486
7310
|
motionTextLayout,
|
|
6487
7311
|
motionTextUnits,
|
|
6488
7312
|
motionTrackKey,
|
|
7313
|
+
motionTransitionAt,
|
|
7314
|
+
motionTransitionSvg,
|
|
6489
7315
|
namedWithSuggestion,
|
|
6490
7316
|
nextMotionEffectInstance,
|
|
6491
7317
|
parseMotionDrivers,
|
|
@@ -6495,13 +7321,17 @@ export {
|
|
|
6495
7321
|
planAnimationToolPlayback,
|
|
6496
7322
|
resolveMotionCamera,
|
|
6497
7323
|
resolveMotionEditRefs,
|
|
7324
|
+
resolveMotionShotLayer,
|
|
7325
|
+
resolveMotionShotTransition,
|
|
6498
7326
|
segmentProgress,
|
|
6499
7327
|
segmentValue,
|
|
7328
|
+
shotLayersShapeProblems,
|
|
6500
7329
|
shotShapeProblems,
|
|
6501
7330
|
simplifyTimedSamples,
|
|
6502
7331
|
smoothMotionLegs,
|
|
6503
7332
|
smoothMotionTimes,
|
|
6504
7333
|
suggestName,
|
|
7334
|
+
svgDataUrl,
|
|
6505
7335
|
toFlairEffectDocument,
|
|
6506
7336
|
toFlairSvgMotion,
|
|
6507
7337
|
toFlairVectorEffect,
|
|
@@ -6510,6 +7340,7 @@ export {
|
|
|
6510
7340
|
withAnimationPlayback,
|
|
6511
7341
|
withCameraViewEffects,
|
|
6512
7342
|
withMotionLayers,
|
|
7343
|
+
withMotionShotLayers,
|
|
6513
7344
|
withViewBox,
|
|
6514
7345
|
withoutMotionEffect
|
|
6515
7346
|
};
|