@pixerate/editor 0.124.1 → 0.126.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.
Files changed (97) hide show
  1. package/dist/agent/index.cjs +3 -3
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.js +3 -3
  4. package/dist/animation-editor/index.cjs +422 -85
  5. package/dist/animation-editor/index.cjs.map +1 -1
  6. package/dist/animation-editor/index.d.cts +174 -7
  7. package/dist/animation-editor/index.d.ts +174 -7
  8. package/dist/animation-editor/index.js +346 -9
  9. package/dist/animation-editor/index.js.map +1 -1
  10. package/dist/{chunk-ORHMVUR6.js → chunk-FICSDDDF.js} +5 -3
  11. package/dist/{chunk-ORHMVUR6.js.map → chunk-FICSDDDF.js.map} +1 -1
  12. package/dist/chunk-FJ3324TZ.js +59 -0
  13. package/dist/chunk-FJ3324TZ.js.map +1 -0
  14. package/dist/{chunk-5EO2IXQE.js → chunk-FPRIUR4T.js} +1 -1
  15. package/dist/chunk-FPRIUR4T.js.map +1 -0
  16. package/dist/{chunk-FHYDQOPC.cjs → chunk-JODTP4DP.cjs} +178 -7
  17. package/dist/chunk-JODTP4DP.cjs.map +1 -0
  18. package/dist/{chunk-NGYD3AMN.js → chunk-MPQAO33O.js} +2 -2
  19. package/dist/{chunk-PEHYWUVV.cjs → chunk-O5RVCDEC.cjs} +82 -82
  20. package/dist/{chunk-PEHYWUVV.cjs.map → chunk-O5RVCDEC.cjs.map} +1 -1
  21. package/dist/{chunk-JRXJTPES.js → chunk-QFSTQHZY.js} +176 -5
  22. package/dist/chunk-QFSTQHZY.js.map +1 -0
  23. package/dist/{chunk-5XXMLBH4.cjs → chunk-QQKTOA4U.cjs} +1 -1
  24. package/dist/chunk-QQKTOA4U.cjs.map +1 -0
  25. package/dist/{chunk-4QQZV5Y5.cjs → chunk-SFCPGMCT.cjs} +11 -9
  26. package/dist/chunk-SFCPGMCT.cjs.map +1 -0
  27. package/dist/{chunk-WE25VLCO.cjs → chunk-V5YGPRCZ.cjs} +36 -36
  28. package/dist/{chunk-WE25VLCO.cjs.map → chunk-V5YGPRCZ.cjs.map} +1 -1
  29. package/dist/chunk-W6QTLHZ3.cjs +59 -0
  30. package/dist/chunk-W6QTLHZ3.cjs.map +1 -0
  31. package/dist/{chunk-WATAQ2Z2.js → chunk-WUDPFWAN.js} +2 -2
  32. package/dist/effects/index.cjs +59 -33
  33. package/dist/effects/index.cjs.map +1 -1
  34. package/dist/effects/index.d.cts +4 -5
  35. package/dist/effects/index.d.ts +4 -5
  36. package/dist/effects/index.js +33 -7
  37. package/dist/effects/index.js.map +1 -1
  38. package/dist/{export-WmYoWmLM.d.cts → export-Bp7ji2r4.d.cts} +1 -2
  39. package/dist/{export-BdfY_lH0.d.ts → export-C1s4eolZ.d.ts} +1 -1
  40. package/dist/{export-BGtXgMJ3.d.ts → export-DAyPY94f.d.ts} +1 -2
  41. package/dist/{export-CbpjwxaI.d.cts → export-DVl4fWy5.d.cts} +1 -1
  42. package/dist/generation/index.cjs +3 -3
  43. package/dist/generation/index.cjs.map +1 -1
  44. package/dist/generation/index.js +3 -3
  45. package/dist/helpers-BBLlekuM.d.ts +2009 -0
  46. package/dist/helpers-i95Ob6Vi.d.cts +2009 -0
  47. package/dist/image-editor/index.cjs +627 -627
  48. package/dist/image-editor/index.cjs.map +1 -1
  49. package/dist/image-editor/index.d.cts +5 -7
  50. package/dist/image-editor/index.d.ts +5 -7
  51. package/dist/image-editor/index.js +13 -13
  52. package/dist/paint-editor/index.cjs +736 -115
  53. package/dist/paint-editor/index.cjs.map +1 -1
  54. package/dist/paint-editor/index.d.cts +248 -13
  55. package/dist/paint-editor/index.d.ts +248 -13
  56. package/dist/paint-editor/index.js +634 -13
  57. package/dist/paint-editor/index.js.map +1 -1
  58. package/dist/{svg-BDrjRnL1.d.cts → svg-BkRL-fZJ.d.cts} +1 -1
  59. package/dist/{svg-C-vZ3q50.d.ts → svg-DJKriJ4A.d.ts} +1 -1
  60. package/dist/tools/index.cjs +2 -2
  61. package/dist/tools/index.cjs.map +1 -1
  62. package/dist/tools/index.js +3 -3
  63. package/dist/{tools-C7JtLYzo.d.ts → tools-CM8Oe8F3.d.ts} +1 -1
  64. package/dist/{tools-DHKGHiSK.d.cts → tools-DZCv2kX1.d.cts} +1 -1
  65. package/dist/vector/index.cjs +1030 -883
  66. package/dist/vector/index.cjs.map +1 -1
  67. package/dist/vector/index.d.cts +61 -6
  68. package/dist/vector/index.d.ts +61 -6
  69. package/dist/vector/index.js +153 -6
  70. package/dist/vector/index.js.map +1 -1
  71. package/dist/video-editor/index.cjs +272 -273
  72. package/dist/video-editor/index.cjs.map +1 -1
  73. package/dist/video-editor/index.d.cts +4 -6
  74. package/dist/video-editor/index.d.ts +4 -6
  75. package/dist/video-editor/index.js +6 -7
  76. package/dist/video-editor/index.js.map +1 -1
  77. package/package.json +1 -1
  78. package/skills/pixerate-animation-editor/SKILL.md +24 -1
  79. package/skills/pixerate-paint-editor/SKILL.md +40 -1
  80. package/skills/pixerate-vector/SKILL.md +15 -0
  81. package/dist/chunk-4QQZV5Y5.cjs.map +0 -1
  82. package/dist/chunk-5EO2IXQE.js.map +0 -1
  83. package/dist/chunk-5XXMLBH4.cjs.map +0 -1
  84. package/dist/chunk-CN4PJHUG.cjs +0 -234
  85. package/dist/chunk-CN4PJHUG.cjs.map +0 -1
  86. package/dist/chunk-FHYDQOPC.cjs.map +0 -1
  87. package/dist/chunk-JRXJTPES.js.map +0 -1
  88. package/dist/chunk-QEG3GRN2.js +0 -234
  89. package/dist/chunk-QEG3GRN2.js.map +0 -1
  90. package/dist/flair-CxB3fOcU.d.ts +0 -739
  91. package/dist/flair-D7BxyBwL.d.cts +0 -739
  92. package/dist/helpers-777mC6dc.d.cts +0 -253
  93. package/dist/helpers-DX2_7L5t.d.ts +0 -253
  94. package/dist/types-BcX7sCsn.d.ts +0 -993
  95. package/dist/types-CznabG7m.d.cts +0 -993
  96. /package/dist/{chunk-NGYD3AMN.js.map → chunk-MPQAO33O.js.map} +0 -0
  97. /package/dist/{chunk-WATAQ2Z2.js.map → chunk-WUDPFWAN.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createVectorMorph
3
- } from "../chunk-NGYD3AMN.js";
3
+ } from "../chunk-MPQAO33O.js";
4
4
  import {
5
5
  MOTION_ISSUE_CODES,
6
6
  motionIssue,
@@ -31,7 +31,14 @@ import {
31
31
  vectorNodeBounds,
32
32
  vectorPathWorldBezier,
33
33
  vectorTextHasOutlines
34
- } from "../chunk-5EO2IXQE.js";
34
+ } from "../chunk-FPRIUR4T.js";
35
+ import {
36
+ EffectUnsupportedError,
37
+ EffectsSlot,
38
+ createEffectToolProvider,
39
+ isEffectError,
40
+ resolveEffectValues
41
+ } from "../chunk-QFSTQHZY.js";
35
42
  import {
36
43
  bezierPathLength,
37
44
  bezierPathPointAtLength,
@@ -40,6 +47,7 @@ import {
40
47
  transformBezierPath
41
48
  } from "../chunk-FG274FYT.js";
42
49
  import "../chunk-GLWZXQ4Q.js";
50
+ import "../chunk-4FX5ACVG.js";
43
51
  import {
44
52
  readZip
45
53
  } from "../chunk-33AHNSEB.js";
@@ -166,7 +174,7 @@ ${problems.map((p) => `- ${p}`).join("\n")}`);
166
174
  var SCENE_KEYS = ["version", "name", "description", "artwork", "shots", "clips", "drivers", "motionBlur", "loop", "trigger", "fps", "steps", "beat", "narration", "target", "controls", "presets"];
167
175
  var SHOT_KEYS = ["id", "label", "camera", "at", "run", "hold", "speed"];
168
176
  var CAMERA_KEYS = ["x", "y", "width", "height", "frame", "padding", "ease", "ms", "geometric"];
169
- var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade"];
177
+ var BASE_KEYS = ["type", "targets", "duration", "delay", "stagger", "with", "ease", "note", "repeat", "repeatMode", "steps", "split", "cascade", "source"];
170
178
  var DRAW_ORDER_KEYS = ["order", "pace", "align", "sortBy", "origin"];
171
179
  var COMMAND_KEYS = {
172
180
  enter: [...BASE_KEYS, ...DRAW_ORDER_KEYS, "style", "direction", "distance", "scale", "shape", "orientation"],
@@ -224,6 +232,11 @@ function commandShapeProblems(command3, where) {
224
232
  if (command3.origin !== void 0 && !isPoint(command3.origin)) p(`"origin" must be { "x": number, "y": number } (document units).`);
225
233
  }
226
234
  if (command3.note !== void 0 && typeof command3.note !== "string") p(`"note" must be a string.`);
235
+ if (command3.source !== void 0) {
236
+ const src = command3.source;
237
+ if (!isObject(src) || typeof src.instance !== "string" || !src.instance || typeof src.effect !== "string" || !src.effect) p(`"source" must be { instance, effect, provider?, name?, values? } (set by applyMotionEffect).`);
238
+ else unknownKeys(src, ["instance", "effect", "provider", "name", "values"], `${where} (${type}) source`, problems);
239
+ }
227
240
  if (type === "wait") {
228
241
  if (!isNum(command3.duration) || command3.duration < 0) p(`"duration" must be a number of milliseconds \u2265 0.`);
229
242
  return problems;
@@ -3484,11 +3497,103 @@ ${problems.join("\n")}`);
3484
3497
  return { scene, notes: [...new Set(ctx.notes)], issues: checkMotionScene(scene), slides: slidePaths.length };
3485
3498
  }
3486
3499
 
3500
+ // src/animation-editor/effects.ts
3501
+ var sourceOf = (c) => c.source;
3502
+ function motionSceneEffects(scene) {
3503
+ const out = /* @__PURE__ */ new Map();
3504
+ const groups = new Set(scene.artwork.nodes.filter((n) => n.type === "group").map((n) => n.id));
3505
+ for (const shot of scene.shots) {
3506
+ for (const c of shot.run) {
3507
+ const s = sourceOf(c);
3508
+ if (!s) continue;
3509
+ let e = out.get(s.instance);
3510
+ if (!e) {
3511
+ const vector = groups.has(s.instance);
3512
+ e = { instance: s.instance, effect: s.effect, ...s.provider ? { provider: s.provider } : {}, ...s.name ? { name: s.name } : {}, values: { ...s.values ?? {} }, kind: vector ? "vector" : "motion", targets: vector ? [s.instance] : [], shots: [], ...vector ? { group: s.instance } : {} };
3513
+ out.set(s.instance, e);
3514
+ }
3515
+ if (!e.shots.includes(shot.id)) e.shots.push(shot.id);
3516
+ if (e.kind === "motion" && "targets" in c) {
3517
+ for (const t of c.targets) if (!e.targets.includes(t)) e.targets.push(t);
3518
+ }
3519
+ }
3520
+ }
3521
+ return [...out.values()];
3522
+ }
3523
+ function nextMotionEffectInstance(scene) {
3524
+ const used = /* @__PURE__ */ new Set([...motionSceneEffects(scene).map((e) => e.instance), ...scene.shots.map((s) => s.id)]);
3525
+ const walk = (nodes) => nodes.forEach((n) => (used.add(n.id), n.type === "group" && walk(n.children)));
3526
+ walk(scene.artwork.nodes);
3527
+ for (let i = 1; ; i++) if (!used.has(`fx${i}`)) return `fx${i}`;
3528
+ }
3529
+ var motionArtworkSvg = (artwork) => vectorDocumentToSvg(artwork);
3530
+ var tag = (commands, source) => commands.map((c) => c.type === "wait" ? c : { ...c, source });
3531
+ function motionCommandsFromEffect(artwork, motion, source) {
3532
+ const imported = fromFlairSvgMotion({ svg: motionArtworkSvg(artwork), animate: [...motion.animate ?? []] });
3533
+ const commands = [];
3534
+ imported.scene.shots.forEach((shot, i) => {
3535
+ shot.run.forEach((c, j) => commands.push(i > 0 && j === 0 ? { ...c, with: "after" } : c));
3536
+ });
3537
+ return { commands: tag(commands, source), notes: imported.notes };
3538
+ }
3539
+ function prefixed(nodes, shots, prefix) {
3540
+ const map = /* @__PURE__ */ new Map();
3541
+ const rename = (list) => list.map((n) => {
3542
+ map.set(n.id, prefix + n.id);
3543
+ const next = { ...n, id: prefix + n.id };
3544
+ if (next.type === "group") next.children = rename(n.children);
3545
+ return next;
3546
+ });
3547
+ const out = rename(nodes);
3548
+ const id = (v) => typeof v === "string" && map.has(v) ? map.get(v) : v;
3549
+ const remapped = shots.map((s) => ({
3550
+ ...s,
3551
+ run: s.run.map((c) => {
3552
+ const next = { ...c };
3553
+ if (Array.isArray(next.targets)) next.targets = next.targets.map((t) => id(t));
3554
+ for (const k of ["path", "into"]) if (k in next) next[k] = id(next[k]);
3555
+ return next;
3556
+ })
3557
+ }));
3558
+ return { nodes: out, shots: remapped };
3559
+ }
3560
+ function motionPartsFromVectorEffect(artwork, motion, source, box = { x: 0, y: 0, width: artwork.width, height: artwork.height }) {
3561
+ const imported = fromFlairSvgMotion({ svg: motion.svg, animate: [...motion.animate ?? []] }, { hold: 0 });
3562
+ const art = imported.scene.artwork;
3563
+ const prefix = `${source.instance}-`;
3564
+ const { nodes, shots } = prefixed(art.nodes, imported.scene.shots, prefix);
3565
+ const k = Math.min(box.width / art.width, box.height / art.height);
3566
+ const dx = box.x + (box.width - art.width * k) / 2;
3567
+ const dy = box.y + (box.height - art.height * k) / 2;
3568
+ const group = { type: "group", id: source.instance, name: source.name ?? source.effect, children: nodes, transform: [k, 0, 0, k, dx, dy] };
3569
+ const tagged = shots.map((s, i) => ({ ...s, id: i === 0 ? source.instance + "-shot" : `${source.instance}-shot-${i + 1}`, run: tag(s.run, source) }));
3570
+ return { group, shots: tagged.map((s) => s.hold === 0 ? (({ hold: _h, ...rest }) => rest)(s) : s), notes: imported.notes };
3571
+ }
3572
+ function withoutMotionEffect(scene, instance) {
3573
+ let place = null;
3574
+ const removedShots = [];
3575
+ const shots = [];
3576
+ for (const shot of scene.shots) {
3577
+ const run = shot.run.filter((c, i) => {
3578
+ const mine = sourceOf(c)?.instance === instance;
3579
+ if (mine && !place) place = { shot: shot.id, index: i };
3580
+ return !mine;
3581
+ });
3582
+ if (run.length === shot.run.length) shots.push(shot);
3583
+ else if (run.length === 0) removedShots.push(shot.id);
3584
+ else shots.push({ ...shot, run });
3585
+ }
3586
+ const artwork = { ...scene.artwork, nodes: scene.artwork.nodes.filter((n) => n.id !== instance) };
3587
+ return { scene: { ...scene, shots, artwork }, place, removedShots };
3588
+ }
3589
+
3487
3590
  // src/animation-editor/controller.ts
3488
3591
  var BLANK = { width: 800, height: 450, background: "#ffffff", nodes: [] };
3489
3592
  var AnimationEditorController = class {
3490
3593
  history;
3491
3594
  player;
3595
+ /** The effects provider slot (see `setEffectsProvider`). */
3596
+ effects = new EffectsSlot();
3492
3597
  scene;
3493
3598
  issues;
3494
3599
  selection = null;
@@ -3506,6 +3611,7 @@ var AnimationEditorController = class {
3506
3611
  this.issues = checkMotionScene(this.scene);
3507
3612
  this.readOnly = options.readOnly ?? false;
3508
3613
  this.onChange = options.onChange;
3614
+ if (options.effects) this.effects.set(options.effects);
3509
3615
  this.player = new AnimationPlayer({ clock: options.clock, duration: () => compileMotionScene(this.scene).duration, loop: this.scene.loop ?? "none" });
3510
3616
  this.state = this.computeState();
3511
3617
  this.connect();
@@ -3516,7 +3622,8 @@ var AnimationEditorController = class {
3516
3622
  this.player.subscribe(() => this.notify()),
3517
3623
  this.history.subscribe(() => {
3518
3624
  if (!this.applying) this.notify();
3519
- })
3625
+ }),
3626
+ this.effects.subscribe(() => this.notify())
3520
3627
  );
3521
3628
  }
3522
3629
  /**
@@ -3559,7 +3666,8 @@ var AnimationEditorController = class {
3559
3666
  canRedo: h.canRedo,
3560
3667
  undoLabel: h.undoName,
3561
3668
  redoLabel: h.redoName,
3562
- revision: this.revision
3669
+ revision: this.revision,
3670
+ effectsProvider: this.effects.provider
3563
3671
  };
3564
3672
  }
3565
3673
  notify() {
@@ -3592,7 +3700,7 @@ var AnimationEditorController = class {
3592
3700
  this.notify();
3593
3701
  this.onChange?.(scene);
3594
3702
  }
3595
- commit(label, next) {
3703
+ commit(label, next, merge) {
3596
3704
  const before = this.scene;
3597
3705
  this.error = null;
3598
3706
  if (next === before) {
@@ -3606,7 +3714,8 @@ var AnimationEditorController = class {
3606
3714
  undo: () => this.setScene(before),
3607
3715
  redo: () => this.setScene(next)
3608
3716
  };
3609
- this.history.execute(command3);
3717
+ if (merge) this.history.executeMerged(command3, { key: merge });
3718
+ else this.history.execute(command3);
3610
3719
  }
3611
3720
  refuse(message) {
3612
3721
  this.error = message;
@@ -3626,13 +3735,155 @@ var AnimationEditorController = class {
3626
3735
  if (reason) return this.refuse(reason);
3627
3736
  const result = applyMotionEdits(this.scene, edits);
3628
3737
  if (!result.ok) return options.dryRun ? result : this.refuse(result.error);
3629
- if (!options.dryRun) this.commit(label, result.scene);
3738
+ if (!options.dryRun) this.commit(label, result.scene, options.merge);
3630
3739
  return result;
3631
3740
  }
3632
3741
  /** Replaces the artwork (e.g. from a vector editor's `onChange`), as one undo step. */
3633
3742
  setArtwork(artwork, label = "Change artwork") {
3634
3743
  return this.apply({ op: "set_artwork", artwork }, label);
3635
3744
  }
3745
+ // ---------------------------------------------------------------------------------------
3746
+ // Effects
3747
+ // ---------------------------------------------------------------------------------------
3748
+ /** The registered effects provider, or `null`. */
3749
+ get effectsProvider() {
3750
+ return this.effects.provider;
3751
+ }
3752
+ /** Registers (or with `null` removes) the effects provider. Not an edit: no undo step. */
3753
+ setEffectsProvider(provider) {
3754
+ this.effects.set(provider);
3755
+ }
3756
+ /** Registers `provider` and returns a function that removes it again (if it is still the one set). */
3757
+ useEffects(provider) {
3758
+ return this.effects.use(provider);
3759
+ }
3760
+ /** The provider effects applied in the scene. */
3761
+ appliedEffects() {
3762
+ return motionSceneEffects(this.scene);
3763
+ }
3764
+ async effectEdit(work, merge) {
3765
+ const reason = this.readOnlyReason();
3766
+ if (reason) return this.refuse(reason);
3767
+ let plan;
3768
+ try {
3769
+ plan = await work();
3770
+ } catch (error) {
3771
+ if (isEffectError(error) || error instanceof Error) return this.refuse(error.message);
3772
+ throw error;
3773
+ }
3774
+ const r = this.apply({ op: "replace_scene", scene: plan.scene }, plan.label, { merge });
3775
+ if (!r.ok) return r;
3776
+ const effect = motionSceneEffects(this.scene).find((e) => e.instance === plan.instance);
3777
+ if (!effect) return this.refuse("The effect made no changes the scene can play.");
3778
+ return { ok: true, effect, notes: plan.notes };
3779
+ }
3780
+ async describeFor(effect, kinds) {
3781
+ const provider = this.effects.require();
3782
+ if (!provider.applyVectorEffect) throw new EffectUnsupportedError(`${provider.name} has no vector or motion effects.`);
3783
+ const d = await provider.describe(effect);
3784
+ if (!kinds.includes(d.kind)) {
3785
+ const hint = d.kind === "motion" ? "apply it to nodes with apply_effect" : d.kind === "vector" ? "add it with add_vector_effect" : "it draws pixels, which scenes cannot hold";
3786
+ throw new EffectUnsupportedError(`"${d.id}" (${d.name}) is a ${d.kind} effect: ${hint}.`, d.id);
3787
+ }
3788
+ return d;
3789
+ }
3790
+ knownNodes(ids) {
3791
+ const all = /* @__PURE__ */ new Set();
3792
+ const walk = (nodes) => nodes.forEach((n) => (all.add(n.id), n.type === "group" && walk(n.children)));
3793
+ walk(this.scene.artwork.nodes);
3794
+ const missing = ids.filter((id) => !all.has(id));
3795
+ if (missing.length) throw new Error(`The artwork has no node ${missing.map((m) => `"${m}"`).join(", ")}; get_animation lists the node ids.`);
3796
+ if (!ids.length) throw new Error("Give at least one node to animate.");
3797
+ }
3798
+ /**
3799
+ * Applies a provider motion effect to artwork nodes: the provider animates them and the changes
3800
+ * become commands, in a new shot (default) or added to the end of `shot`. One undo step.
3801
+ * Returns the applied effect (its `instance` id changes or removes it).
3802
+ */
3803
+ applyMotionEffect(effect, targets, options = {}) {
3804
+ return this.effectEdit(async () => {
3805
+ this.knownNodes(targets);
3806
+ const d = await this.describeFor(effect, ["motion"]);
3807
+ const values = resolveEffectValues(d, options.values);
3808
+ const instance = options.instance ?? nextMotionEffectInstance(this.scene);
3809
+ const motion = await this.effects.require().applyVectorEffect(d.id, { svg: motionArtworkSvg(this.scene.artwork), nodeIds: [...targets] }, values);
3810
+ const source = { instance, effect: d.id, provider: this.effects.require().id, name: d.name, values };
3811
+ const { commands, notes } = motionCommandsFromEffect(this.scene.artwork, motion, source);
3812
+ if (!commands.length) throw new Error(`"${d.name}" made no changes the scene can play${notes.length ? `: ${notes.join(" ")}` : ""}.`);
3813
+ let shots = this.scene.shots;
3814
+ if (options.shot) {
3815
+ if (!shots.some((s) => s.id === options.shot)) throw new Error(`There is no shot "${options.shot}"; the shots are ${shots.map((s) => `"${s.id}"`).join(", ")}.`);
3816
+ shots = shots.map((s) => s.id === options.shot ? { ...s, run: [...s.run, ...commands] } : s);
3817
+ } else shots = [...shots, { id: instance, label: d.name, run: commands }];
3818
+ return { scene: { ...this.scene, shots }, label: `Apply ${d.name}`, instance, notes };
3819
+ });
3820
+ }
3821
+ /**
3822
+ * Adds a provider vector effect (one with its own picture: a lower third, a loader): its SVG as
3823
+ * an artwork group (id: the instance) fitted into `box` (default the whole artwork), and its
3824
+ * motion as shots after the last one. One undo step.
3825
+ */
3826
+ addVectorEffect(effect, options = {}) {
3827
+ return this.effectEdit(async () => {
3828
+ const d = await this.describeFor(effect, ["vector"]);
3829
+ const values = resolveEffectValues(d, options.values);
3830
+ const instance = options.instance ?? nextMotionEffectInstance(this.scene);
3831
+ const motion = await this.effects.require().applyVectorEffect(d.id, motionArtworkSvg(this.scene.artwork), values);
3832
+ const source = { instance, effect: d.id, provider: this.effects.require().id, name: d.name, values };
3833
+ const { group, shots, notes } = motionPartsFromVectorEffect(this.scene.artwork, motion, source, options.box);
3834
+ return { scene: { ...this.scene, artwork: { ...this.scene.artwork, nodes: [...this.scene.artwork.nodes, group] }, shots: [...this.scene.shots, ...shots] }, label: `Add ${d.name}`, instance, notes };
3835
+ });
3836
+ }
3837
+ /**
3838
+ * Makes an applied effect again with new `values` (merged over its own; `null` resets a
3839
+ * control), in the same place. One undo step.
3840
+ */
3841
+ updateMotionEffect(instance, values, options = {}) {
3842
+ return this.effectEdit(async () => {
3843
+ const was = motionSceneEffects(this.scene).find((e) => e.instance === instance);
3844
+ if (!was) throw new Error(`There is no applied effect "${instance}"; list_applied_effects lists them.`);
3845
+ const d = await this.describeFor(was.effect, [was.kind]);
3846
+ const base = { ...was.values };
3847
+ const given = {};
3848
+ for (const [k, v] of Object.entries(values)) {
3849
+ if (v !== null) given[k] = v;
3850
+ else if (k in d.controls.properties) base[k] = d.defaults[k];
3851
+ else throw new Error(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
3852
+ }
3853
+ const merged = { ...base, ...resolveEffectValues({ ...d, defaults: base }, given) };
3854
+ const { scene: bare, place } = withoutMotionEffect(this.scene, instance);
3855
+ const provider = this.effects.require();
3856
+ const source = { instance, effect: d.id, provider: provider.id, name: d.name, values: merged };
3857
+ if (was.kind === "vector") {
3858
+ const old = this.scene.artwork.nodes.find((n) => n.id === instance);
3859
+ const motion2 = await provider.applyVectorEffect(d.id, motionArtworkSvg(bare.artwork), merged);
3860
+ const { group, shots: shots2, notes: notes2 } = motionPartsFromVectorEffect(bare.artwork, motion2, source);
3861
+ const index = this.scene.artwork.nodes.findIndex((n) => n.id === instance);
3862
+ const nodes = [...bare.artwork.nodes];
3863
+ nodes.splice(Math.max(0, index), 0, old?.transform ? { ...group, transform: old.transform } : group);
3864
+ const at = this.scene.shots.findIndex((s) => was.shots.includes(s.id));
3865
+ const kept = bare.shots;
3866
+ const nextShots = [...kept.slice(0, Math.max(0, at)), ...shots2, ...kept.slice(Math.max(0, at))];
3867
+ return { scene: { ...bare, artwork: { ...bare.artwork, nodes }, shots: nextShots }, label: `Change ${d.name}`, instance, notes: notes2 };
3868
+ }
3869
+ const motion = await provider.applyVectorEffect(d.id, { svg: motionArtworkSvg(bare.artwork), nodeIds: was.targets }, merged);
3870
+ const { commands, notes } = motionCommandsFromEffect(bare.artwork, motion, source);
3871
+ let shots = bare.shots;
3872
+ if (place && shots.some((s) => s.id === place.shot)) shots = shots.map((s) => s.id === place.shot ? { ...s, run: [...s.run.slice(0, place.index), ...commands, ...s.run.slice(place.index)] } : s);
3873
+ else {
3874
+ const at = this.scene.shots.findIndex((s) => s.id === place?.shot);
3875
+ shots = [...shots.slice(0, Math.max(0, at)), { id: instance, label: d.name, run: commands }, ...shots.slice(Math.max(0, at))];
3876
+ }
3877
+ return { scene: { ...bare, shots }, label: `Change ${d.name}`, instance, notes };
3878
+ }, options.merge);
3879
+ }
3880
+ /** Removes an applied effect: its commands, shots it alone filled and a vector effect's group. One undo step. Works without a provider. */
3881
+ removeMotionEffect(instance) {
3882
+ const was = motionSceneEffects(this.scene).find((e) => e.instance === instance);
3883
+ if (!was) return this.refuse(`There is no applied effect "${instance}"; list_applied_effects lists them.`);
3884
+ const r = this.apply({ op: "replace_scene", scene: withoutMotionEffect(this.scene, instance).scene }, `Remove ${was.name ?? was.effect}`);
3885
+ return r.ok ? { ok: true, effect: was, notes: [] } : r;
3886
+ }
3636
3887
  /** Opens a scene (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
3637
3888
  load(scene, options = {}) {
3638
3889
  let parsed;
@@ -4970,6 +5221,85 @@ function parseMotionDrivers(text) {
4970
5221
  }
4971
5222
  return { drivers };
4972
5223
  }
5224
+
5225
+ // src/animation-editor/effect-tools.ts
5226
+ 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 });
5227
+ function createAnimationEffectsToolProvider(controller, options = {}) {
5228
+ const readOnlyReason = () => {
5229
+ const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
5230
+ if (ro) return typeof ro === "string" ? ro : "The animation is read-only.";
5231
+ return controller.readOnlyReason();
5232
+ };
5233
+ const done = (r, target) => {
5234
+ if (!r.ok) throw new Error(r.error);
5235
+ return { target, effect: info(r.effect), effects: controller.appliedEffects().filter((e) => e.targets.includes(target)).map(info), ...r.notes.length ? { notes: r.notes } : {} };
5236
+ };
5237
+ return createEffectToolProvider({
5238
+ id: options.id ?? "animation-effects",
5239
+ namespace: options.namespace,
5240
+ target: { noun: "node", id: "A node id of the artwork from get_animation (a group animates as one)." },
5241
+ applyUses: ["motion"],
5242
+ applyDetails: "The provider animates the node and its changes become ordinary commands (enter, fade, move, pulse\u2026), in a new shot after the last one, tagged with the effect so it can be changed or removed as a whole. They preview, play and export to Flair like any command. Vector effects with their own picture go through add_vector_effect. index, enabled and opacity do not apply here.",
5243
+ provider: () => controller.effectsProvider,
5244
+ readOnlyReason,
5245
+ list: (target) => {
5246
+ const all = controller.appliedEffects();
5247
+ if (target) return [{ target, effects: all.filter((e) => e.targets.includes(target)).map(info) }];
5248
+ const byTarget = /* @__PURE__ */ new Map();
5249
+ for (const e of all) {
5250
+ const key = e.targets[0] ?? e.instance;
5251
+ byTarget.set(key, [...byTarget.get(key) ?? [], info(e)]);
5252
+ }
5253
+ return [...byTarget].map(([t, effects]) => ({ target: t, effects }));
5254
+ },
5255
+ apply: async (target, effect, args) => {
5256
+ if (args.index !== void 0 || args.opacity !== void 0 || args.enabled === false) throw new Error("index, opacity and enabled do not apply to motion effects; the effect becomes commands in a new shot.");
5257
+ return done(await controller.applyMotionEffect(effect, [target], { values: args.values }), target);
5258
+ },
5259
+ update: async (target, instance, changes) => {
5260
+ const extra = ["enabled", "opacity", "index"].filter((k) => changes[k] !== void 0);
5261
+ if (extra.length) throw new Error(`A motion effect only takes values; ${extra.join(", ")} do not apply (move or delete its commands with edit_animation).`);
5262
+ return done(await controller.updateMotionEffect(instance, changes.values ?? {}), target);
5263
+ },
5264
+ remove: async (target, instance) => done(controller.removeMotionEffect(instance), target),
5265
+ subscribe: (notify) => controller.subscribe(() => notify()),
5266
+ extraTools: (ctx) => [
5267
+ {
5268
+ name: "add_vector_effect",
5269
+ title: "Add a vector effect",
5270
+ description: "Adds a provider vector effect that brings its own picture (search_effects with kind vector: lower thirds, loaders, stickers): its SVG becomes an artwork group, fitted into box (default the whole artwork), and its motion becomes shots after the last one. Change it with update_effect (target and effect: the returned instance) or remove it with remove_effect. One undo step. Returns the instance, the group id and any notes on what could not be carried over.",
5271
+ inputSchema: {
5272
+ type: "object",
5273
+ properties: {
5274
+ effect: { type: "string", minLength: 1, description: "A vector effect id from search_effects (kind vector)." },
5275
+ values: ctx.valuesSchema("Control values over the effect's defaults"),
5276
+ box: {
5277
+ type: "object",
5278
+ properties: {
5279
+ x: { type: "number", description: "Left edge, artwork units." },
5280
+ y: { type: "number", description: "Top edge, artwork units." },
5281
+ width: { type: "number", exclusiveMinimum: 0, description: "Width, artwork units." },
5282
+ height: { type: "number", exclusiveMinimum: 0, description: "Height, artwork units." }
5283
+ },
5284
+ required: ["x", "y", "width", "height"],
5285
+ additionalProperties: false,
5286
+ description: "Where the picture goes in the artwork, fitted keeping its proportions (default the whole artwork)."
5287
+ }
5288
+ },
5289
+ required: ["effect"],
5290
+ additionalProperties: false
5291
+ },
5292
+ annotations: { title: "Add a vector effect", ...ctx.annotations.write },
5293
+ handler: async (args) => {
5294
+ ctx.requireProvider();
5295
+ const r = await controller.addVectorEffect(args.effect, { values: args.values, box: args.box });
5296
+ if (!r.ok) throw new Error(r.error);
5297
+ return { instance: r.effect.instance, group: r.effect.group, effect: info(r.effect), ...r.notes.length ? { notes: r.notes } : {} };
5298
+ }
5299
+ }
5300
+ ]
5301
+ });
5302
+ }
4973
5303
  export {
4974
5304
  ANIMATION_EDITOR_AGENT_INSTRUCTIONS,
4975
5305
  AnimationEditorController,
@@ -5014,6 +5344,7 @@ export {
5014
5344
  commandShapeProblems,
5015
5345
  compileMotionScene,
5016
5346
  createAnimationEditorToolProvider,
5347
+ createAnimationEffectsToolProvider,
5017
5348
  createMotionScene,
5018
5349
  defaultMotionCommand,
5019
5350
  describeMotionElements,
@@ -5039,6 +5370,7 @@ export {
5039
5370
  isMotionShot,
5040
5371
  matchMotionNodes,
5041
5372
  motionAddTrackKey,
5373
+ motionArtworkSvg,
5042
5374
  motionBeatAt,
5043
5375
  motionBeatTime,
5044
5376
  motionBlurTimes,
@@ -5051,6 +5383,7 @@ export {
5051
5383
  motionCommandRows,
5052
5384
  motionCommandSlots,
5053
5385
  motionCommandSummary,
5386
+ motionCommandsFromEffect,
5054
5387
  motionContactSheetSvg,
5055
5388
  motionCueChoices,
5056
5389
  motionCueFromKey,
@@ -5077,6 +5410,7 @@ export {
5077
5410
  motionNodeAtElement,
5078
5411
  motionNodeClips,
5079
5412
  motionNodeOffset,
5413
+ motionPartsFromVectorEffect,
5080
5414
  motionPath,
5081
5415
  motionPathLength,
5082
5416
  motionPathsSvg,
@@ -5085,6 +5419,7 @@ export {
5085
5419
  motionRevealClip,
5086
5420
  motionRevealPadding,
5087
5421
  motionRulerTicks,
5422
+ motionSceneEffects,
5088
5423
  motionSceneShapeProblems,
5089
5424
  motionSceneToJson,
5090
5425
  motionShotAt,
@@ -5095,6 +5430,7 @@ export {
5095
5430
  motionTextUnits,
5096
5431
  motionTrackKey,
5097
5432
  namedWithSuggestion,
5433
+ nextMotionEffectInstance,
5098
5434
  parseMotionDrivers,
5099
5435
  parseMotionFieldInput,
5100
5436
  parseMotionNarration,
@@ -5113,6 +5449,7 @@ export {
5113
5449
  toLottie,
5114
5450
  trackValue,
5115
5451
  withMotionLayers,
5116
- withViewBox
5452
+ withViewBox,
5453
+ withoutMotionEffect
5117
5454
  };
5118
5455
  //# sourceMappingURL=index.js.map