@pixerate/flair 0.1.1 → 0.3.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 (44) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +65 -14
  3. package/dist/catalog/index.d.ts +5 -4
  4. package/dist/catalog/index.js +12 -4
  5. package/dist/{chunk-JWZCPMFG.js → chunk-D7NLHC7K.js} +117 -674
  6. package/dist/chunk-D7NLHC7K.js.map +1 -0
  7. package/dist/{chunk-3FAUZOGL.js → chunk-FIJOMGPG.js} +2 -2
  8. package/dist/{chunk-UQQNZSYL.js → chunk-GTIAOYOK.js} +211 -7
  9. package/dist/chunk-GTIAOYOK.js.map +1 -0
  10. package/dist/{chunk-MWVTQFV5.js → chunk-IWKM5FJK.js} +410 -59
  11. package/dist/chunk-IWKM5FJK.js.map +1 -0
  12. package/dist/{chunk-HKXOHRCD.js → chunk-VA74HBPK.js} +2 -2
  13. package/dist/chunk-WAMA45G3.js +998 -0
  14. package/dist/chunk-WAMA45G3.js.map +1 -0
  15. package/dist/{chunk-J55UBNAT.js → chunk-WC3VADZC.js} +42 -36
  16. package/dist/chunk-WC3VADZC.js.map +1 -0
  17. package/dist/cli.js +12 -11
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{controls-DdXMbovi.d.ts → controls-Syr6drzO.d.ts} +4 -3
  20. package/dist/editor/index.d.ts +101 -49
  21. package/dist/editor/index.js +198 -99
  22. package/dist/editor/index.js.map +1 -1
  23. package/dist/{export-FEBPEEO7.js → export-DH2HMACO.js} +4 -3
  24. package/dist/index.d.ts +193 -17
  25. package/dist/index.js +69 -33
  26. package/dist/index.js.map +1 -1
  27. package/dist/node.d.ts +1 -1
  28. package/dist/node.js +2 -2
  29. package/dist/recorder-KEX3U35D.js +10 -0
  30. package/dist/{renderer-Oyynz-fs.d.ts → renderer-mzvhKzKa.d.ts} +1 -1
  31. package/dist/types-CsFUZZIv.d.ts +102 -0
  32. package/package.json +3 -3
  33. package/dist/chunk-36DTNSHU.js +0 -160
  34. package/dist/chunk-36DTNSHU.js.map +0 -1
  35. package/dist/chunk-J55UBNAT.js.map +0 -1
  36. package/dist/chunk-JWZCPMFG.js.map +0 -1
  37. package/dist/chunk-MWVTQFV5.js.map +0 -1
  38. package/dist/chunk-UQQNZSYL.js.map +0 -1
  39. package/dist/recorder-WJTWMEQN.js +0 -10
  40. package/dist/types-CkKkavvB.d.ts +0 -71
  41. /package/dist/{chunk-3FAUZOGL.js.map → chunk-FIJOMGPG.js.map} +0 -0
  42. /package/dist/{chunk-HKXOHRCD.js.map → chunk-VA74HBPK.js.map} +0 -0
  43. /package/dist/{export-FEBPEEO7.js.map → export-DH2HMACO.js.map} +0 -0
  44. /package/dist/{recorder-WJTWMEQN.js.map → recorder-KEX3U35D.js.map} +0 -0
@@ -3,8 +3,9 @@ import {
3
3
  findEffects,
4
4
  getEffect,
5
5
  renderVectorEffect,
6
- vectorControlDef
7
- } from "../chunk-MWVTQFV5.js";
6
+ vectorControlIssues,
7
+ vectorEffectControls
8
+ } from "../chunk-IWKM5FJK.js";
8
9
  import {
9
10
  svgMotionIssues,
10
11
  svgView
@@ -12,7 +13,7 @@ import {
12
13
  import {
13
14
  BUILTIN_UNIFORMS,
14
15
  interpolateOklab
15
- } from "../chunk-HKXOHRCD.js";
16
+ } from "../chunk-VA74HBPK.js";
16
17
  import "../chunk-EHDJ3CPJ.js";
17
18
  import {
18
19
  realClock
@@ -21,88 +22,22 @@ import {
21
22
  checkControlValue,
22
23
  controlValue,
23
24
  parseControls
24
- } from "../chunk-J55UBNAT.js";
25
+ } from "../chunk-WC3VADZC.js";
25
26
 
26
27
  // src/editor/timeline.ts
27
- var EASING_PRESETS = [
28
- "linear",
29
- "ease",
30
- "ease-in",
31
- "ease-out",
32
- "ease-in-out",
33
- "ease-in-back",
34
- "ease-out-back",
35
- "spring",
36
- "hold"
37
- ];
38
- var BEZIERS = {
39
- ease: [0.25, 0.1, 0.25, 1],
40
- "ease-in": [0.42, 0, 1, 1],
41
- "ease-out": [0, 0, 0.58, 1],
42
- "ease-in-out": [0.42, 0, 0.58, 1],
43
- "ease-in-back": [0.36, 0, 0.66, -0.56],
44
- "ease-out-back": [0.34, 1.56, 0.64, 1]
45
- };
28
+ import {
29
+ ANIMATION_EASING_PRESETS,
30
+ cubicBezierAt as editorCubicBezierAt,
31
+ evaluateEasing as evaluateAnimationEasing,
32
+ isAnimationEasing
33
+ } from "@pixerate/editor/animation";
34
+ var EASING_PRESETS = ANIMATION_EASING_PRESETS;
35
+ var evaluateEasing = evaluateAnimationEasing;
36
+ var cubicBezierAt = editorCubicBezierAt;
46
37
  function isEasing(value) {
47
- if (typeof value === "string") return EASING_PRESETS.includes(value);
48
- if (!value || typeof value !== "object") return false;
49
- const e = value;
50
- if (e.type === "cubic-bezier") return ["x1", "y1", "x2", "y2"].every((k) => Number.isFinite(e[k]));
51
- if (e.type === "spring")
52
- return ["stiffness", "damping", "mass"].every((k) => e[k] === void 0 || typeof e[k] === "number" && e[k] > 0);
53
- return false;
54
- }
55
- function cubicBezierAt(x1, y1, x2, y2, x) {
56
- if (x <= 0) return 0;
57
- if (x >= 1) return 1;
58
- x1 = Math.min(1, Math.max(0, x1));
59
- x2 = Math.min(1, Math.max(0, x2));
60
- const bx = (t2) => 3 * x1 * t2 * (1 - t2) ** 2 + 3 * x2 * t2 * t2 * (1 - t2) + t2 ** 3;
61
- const by = (t2) => 3 * y1 * t2 * (1 - t2) ** 2 + 3 * y2 * t2 * t2 * (1 - t2) + t2 ** 3;
62
- const dx = (t2) => 3 * x1 * (1 - t2) ** 2 + 6 * (x2 - x1) * t2 * (1 - t2) + 3 * (1 - x2) * t2 * t2;
63
- let t = x;
64
- for (let i = 0; i < 8; i++) {
65
- const err = bx(t) - x;
66
- if (Math.abs(err) < 1e-6) return by(t);
67
- const d = dx(t);
68
- if (Math.abs(d) < 1e-6) break;
69
- t -= err / d;
70
- }
71
- let lo = 0;
72
- let hi = 1;
73
- t = x;
74
- for (let i = 0; i < 40; i++) {
75
- if (bx(t) < x) lo = t;
76
- else hi = t;
77
- t = (lo + hi) / 2;
78
- }
79
- return by(t);
80
- }
81
- function springAt(easing, u, durationMs) {
82
- const k = easing.stiffness ?? 170;
83
- const c = easing.damping ?? 26;
84
- const m = easing.mass ?? 1;
85
- const w0 = Math.sqrt(k / m);
86
- const zeta = c / (2 * Math.sqrt(k * m));
87
- const t = u * Math.max(durationMs / 1e3, 0.6);
88
- if (zeta < 1) {
89
- const wd = w0 * Math.sqrt(1 - zeta * zeta);
90
- return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + zeta * w0 / wd * Math.sin(wd * t));
91
- }
92
- return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
93
- }
94
- function evaluateEasing(easing, u, durationMs = 1e3) {
95
- if (u <= 0) return 0;
96
- if (u >= 1) return 1;
97
- if (!easing || easing === "linear") return u;
98
- if (easing === "hold") return 0;
99
- if (easing === "spring") return springAt({ type: "spring" }, u, durationMs);
100
- if (typeof easing === "string") {
101
- const b = BEZIERS[easing];
102
- return b ? cubicBezierAt(b[0], b[1], b[2], b[3], u) : u;
103
- }
104
- if (easing.type === "spring") return springAt(easing, u, durationMs);
105
- return cubicBezierAt(easing.x1, easing.y1, easing.x2, easing.y2, u);
38
+ if (!isAnimationEasing(value)) return false;
39
+ if (typeof value !== "object" || value.type !== "spring") return true;
40
+ return [value.stiffness, value.damping, value.mass].every((v) => v === void 0 || v > 0);
106
41
  }
107
42
  function interpolateControlValue(control, a, b, t) {
108
43
  if (typeof a === "number" && typeof b === "number") {
@@ -164,7 +99,12 @@ function createEffectDocument(from) {
164
99
  version: 1,
165
100
  name: effect.name,
166
101
  basedOn: effect.id,
167
- effect: effect.kind === "shader" ? { kind: "shader", source: effect.source } : { kind: "vector", motion: structuredClone(effect.motion), controls: effect.controls.map((c) => ({ ...c })), hold: effect.hold },
102
+ effect: effect.kind === "shader" ? { kind: "shader", source: effect.source } : {
103
+ kind: "vector",
104
+ motion: structuredClone(effect.motion),
105
+ ...effect.controls && { controls: effect.controls.map((c) => ({ ...c })) },
106
+ hold: effect.hold
107
+ },
168
108
  values: {},
169
109
  tracks: {},
170
110
  duration: effect.kind === "shader" && effect.loop ? Math.round(effect.loop * 1e3) : DEFAULT_DURATION,
@@ -177,7 +117,7 @@ function blankDocument(name, source) {
177
117
  }
178
118
  function documentControls(doc) {
179
119
  if (doc.effect.kind === "shader") return parseControls(doc.effect.source);
180
- return doc.effect.controls.map((c, i) => vectorControlDef(c, i + 1));
120
+ return vectorEffectControls(doc.effect);
181
121
  }
182
122
  function valuesAt(doc, time, controls = documentControls(doc)) {
183
123
  const out = {};
@@ -217,7 +157,7 @@ function documentIssues(doc) {
217
157
  for (const preset of doc.presets) for (const [name, value] of Object.entries(preset.values)) check(`preset "${preset.name}"`, name, value);
218
158
  if (doc.effect.kind === "vector") {
219
159
  const motion = motionAt(doc, 0);
220
- issues.push(...svgMotionIssues(motion));
160
+ issues.push(...vectorControlIssues(doc.effect.motion), ...svgMotionIssues(motion));
221
161
  if (/\{\{[^}]+\}\}/.test(JSON.stringify(motion))) issues.push("the picture has placeholders no control fills");
222
162
  }
223
163
  return issues;
@@ -241,7 +181,7 @@ function parseEffectDocument(input) {
241
181
  else if (effect.kind === "vector") {
242
182
  const v = effect;
243
183
  if (!v.motion || typeof v.motion.svg !== "string") errors.push("`effect.motion.svg` must be a string");
244
- if (!Array.isArray(v.controls)) errors.push("`effect.controls` must be a list");
184
+ if (v.controls !== void 0 && !Array.isArray(v.controls)) errors.push("`effect.controls` must be a list");
245
185
  }
246
186
  const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
247
187
  if (o.values !== void 0 && !isRecord(o.values)) errors.push("`values` must be an object");
@@ -282,7 +222,7 @@ var easingSchema = {
282
222
  description: `An easing preset (${EASING_PRESETS.join(", ")}) or {"type":"cubic-bezier","x1","y1","x2","y2"} or {"type":"spring","stiffness","damping","mass"}.`
283
223
  };
284
224
  var valueSchema = {
285
- description: 'A control value: a number (float, int), true/false (bool), an array of numbers (vec2\u2013vec4; colours 0\u20131), a "#RRGGBB" colour, or an image URL (sampler2D).'
225
+ description: 'A control value: a number (float, int), true/false (bool), an array of numbers (vec2\u2013vec4; colours 0\u20131), a "#RRGGBB" colour, an image URL (sampler2D), or a string (text).'
286
226
  };
287
227
  function describeEditor(editor) {
288
228
  const state = editor.getState();
@@ -291,13 +231,13 @@ function describeEditor(editor) {
291
231
  name: doc.name,
292
232
  basedOn: doc.basedOn,
293
233
  kind: doc.effect.kind,
294
- ...doc.effect.kind === "shader" ? { source: doc.effect.source } : { motion: doc.effect.motion, vectorControls: doc.effect.controls },
234
+ ...doc.effect.kind === "shader" ? { source: doc.effect.source } : { motion: doc.effect.motion, ...doc.effect.controls && { vectorControls: doc.effect.controls } },
295
235
  controls: state.controls.map((c) => ({
296
236
  name: c.name,
297
237
  type: c.type,
298
238
  label: c.label,
299
239
  ...c.color && { color: true },
300
- ...c.type !== "bool" && c.type !== "sampler2D" && { min: c.min, max: c.max, step: c.step },
240
+ ...c.type !== "bool" && c.type !== "sampler2D" && c.type !== "text" && { min: c.min, max: c.max, step: c.step },
301
241
  default: c.default
302
242
  })),
303
243
  values: state.values,
@@ -456,6 +396,29 @@ function effectCommands() {
456
396
  },
457
397
  run: (editor, p) => done(editor.setKeyframeEasing(p.name, p.time, p.easing))
458
398
  },
399
+ {
400
+ id: "keyframe.setTrack",
401
+ label: "Set keyframes",
402
+ group: "Keyframes",
403
+ description: "Replaces all of a control's keyframes at once (an empty list removes them). Times are ms; each easing shapes the segment leaving its keyframe.",
404
+ params: {
405
+ type: "object",
406
+ properties: {
407
+ name: { type: "string" },
408
+ keyframes: {
409
+ type: "array",
410
+ items: {
411
+ type: "object",
412
+ properties: { time: { type: "number", minimum: 0 }, value: valueSchema, easing: easingSchema },
413
+ required: ["time", "value"]
414
+ }
415
+ }
416
+ },
417
+ required: ["name", "keyframes"],
418
+ additionalProperties: false
419
+ },
420
+ run: (editor, p) => done(editor.setTrack(p.name, { keyframes: p.keyframes }))
421
+ },
459
422
  {
460
423
  id: "keyframe.clear",
461
424
  label: "Clear keyframes",
@@ -618,7 +581,7 @@ var EffectEditor = class {
618
581
  this.commit("Edit source", { ...this.doc, effect: { kind: "shader", source } }, "source");
619
582
  return ok;
620
583
  }
621
- /** Replaces a vector effect's picture (and optionally its controls). */
584
+ /** Replaces a vector effect's picture, whose `{{name: type …}}` placeholders declare its controls (and optionally a `controls` list). */
622
585
  setMotion(motion, controls) {
623
586
  if (this.doc.effect.kind !== "vector") return fail("This is a shader effect; use setSource.");
624
587
  this.commit("Edit picture", { ...this.doc, effect: { ...this.doc.effect, motion, controls: controls ?? this.doc.effect.controls } });
@@ -669,17 +632,17 @@ var EffectEditor = class {
669
632
  this.commit("Reset values", { ...this.doc, values });
670
633
  return ok;
671
634
  }
672
- /** Random values in each control's range (colours anywhere), for exploring. */
635
+ /** Random values in each control's range (colours anywhere), for exploring. Images and text keep theirs. */
673
636
  randomize(names, random = Math.random) {
674
637
  const values = {};
675
638
  for (const control of this.controls) {
676
- if (names ? !names.includes(control.name) : control.type === "sampler2D") continue;
639
+ if (names ? !names.includes(control.name) : control.type === "sampler2D" || control.type === "text") continue;
677
640
  const pick = () => {
678
641
  const v = control.min + random() * (control.max - control.min);
679
642
  return control.type === "int" ? Math.round(v) : +v.toFixed(3);
680
643
  };
681
644
  if (control.type === "bool") values[control.name] = random() < 0.5;
682
- else if (control.type === "sampler2D") continue;
645
+ else if (control.type === "sampler2D" || control.type === "text") continue;
683
646
  else if (control.color) values[control.name] = Array.from({ length: control.type === "vec4" ? 4 : 3 }, () => +random().toFixed(3));
684
647
  else if (control.type === "float" || control.type === "int") values[control.name] = pick();
685
648
  else values[control.name] = Array.from({ length: Number(control.type.slice(3)) }, pick);
@@ -757,6 +720,33 @@ var EffectEditor = class {
757
720
  this.commit("Clear keyframes", { ...this.doc, tracks, values });
758
721
  return ok;
759
722
  }
723
+ /**
724
+ * Replaces a control's whole track (or removes it with `undefined`), one
725
+ * undo step. Every keyframe is checked like `setKeyframe`; values may be
726
+ * any form the control accepts (colours as "#RRGGBB" too).
727
+ */
728
+ setTrack(name, track) {
729
+ const control = this.control(name);
730
+ if (!control) return this.unknownControl(name);
731
+ const tracks = { ...this.doc.tracks };
732
+ if (!track || track.keyframes.length === 0) {
733
+ if (!tracks[name]) return ok;
734
+ delete tracks[name];
735
+ this.commit("Clear keyframes", { ...this.doc, tracks });
736
+ return ok;
737
+ }
738
+ const keyframes = [];
739
+ for (const k of track.keyframes) {
740
+ if (!(k.time >= 0)) return fail("A keyframe's time is 0 ms or more.");
741
+ if (k.easing !== void 0 && !isEasing(k.easing)) return fail(`Unknown easing ${JSON.stringify(k.easing)}.`);
742
+ const check = checkControlValue(control, k.value);
743
+ if (!check.ok) return fail(`At ${k.time} ms: ${check.message}`);
744
+ keyframes.push({ time: k.time, value: check.value, ...k.easing !== void 0 && { easing: k.easing } });
745
+ }
746
+ tracks[name] = { keyframes: normalizeKeyframes(keyframes), ...track.enabled === false && { enabled: false } };
747
+ this.commit(`Keyframes ${control.label}`, { ...this.doc, tracks });
748
+ return ok;
749
+ }
760
750
  setTiming(timing) {
761
751
  if (timing.duration !== void 0 && !(timing.duration > 0)) return fail("The duration must be above 0 ms.");
762
752
  this.commit("Set timing", { ...this.doc, duration: timing.duration ?? this.doc.duration, loop: timing.loop ?? this.doc.loop });
@@ -872,6 +862,10 @@ function createEffectEditor(options = {}) {
872
862
 
873
863
  // src/editor/agent.ts
874
864
  import { createCommandToolProvider } from "@pixerate/editor/commands";
865
+ import {
866
+ createToolRegistry,
867
+ toToolResult
868
+ } from "@pixerate/editor/tools";
875
869
  function createEffectToolProvider(editor, options = {}) {
876
870
  const base = createCommandToolProvider(editor.commands, {
877
871
  id: options.id ?? "flair-effect",
@@ -898,6 +892,11 @@ function createEffectToolProvider(editor, options = {}) {
898
892
  const baseTools = () => typeof base.tools === "function" ? base.tools() : base.tools;
899
893
  return { id: base.id, namespace: base.namespace, subscribe: base.subscribe, tools: () => [...baseTools(), ...extra] };
900
894
  }
895
+ function createEffectToolRegistry(editor, options = {}) {
896
+ const registry = createToolRegistry({ validateArguments: true, ...options.registry });
897
+ registry.register(createEffectToolProvider(editor, options));
898
+ return registry;
899
+ }
901
900
  function toolDefinitions(provider) {
902
901
  const tools = typeof provider.tools === "function" ? provider.tools() : provider.tools;
903
902
  return tools.map(({ handler: _handler, ...definition }) => definition);
@@ -907,11 +906,7 @@ async function callTool(provider, name, args = {}) {
907
906
  const tool = tools.find((t) => t.name === name);
908
907
  if (!tool) return { content: [{ type: "text", text: `There is no tool "${name}".` }], isError: true };
909
908
  try {
910
- const result = await tool.handler(args, { name, providerId: provider.id });
911
- if (result && typeof result === "object" && Array.isArray(result.content)) return result;
912
- if (result === void 0) return { content: [] };
913
- if (typeof result === "string") return { content: [{ type: "text", text: result }] };
914
- return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
909
+ return toToolResult(await tool.handler(args, { name, providerId: provider.id }));
915
910
  } catch (e) {
916
911
  return { content: [{ type: "text", text: e.message }], isError: true };
917
912
  }
@@ -932,7 +927,9 @@ Shader effects are GLSL ES 3.00 fragment shaders written Shadertoy-style:
932
927
  - Write float literals with a decimal point (1.0, not 1); GLSL does not convert int to float implicitly. Use \`texture()\`, not \`texture2D()\`.
933
928
  - After \`effect.setSource\`, call \`effect.describe\` and read \`compile\`: fix any errors (their line numbers match your source) before going on.
934
929
 
935
- Vector effects are animated SVG pictures: change their controls' values; their picture is edited with care, keeping element ids that animations refer to.
930
+ Vector effects are animated SVG pictures that declare their controls in themselves, like shader uniforms: the first \`{{name: type \u2026}}\` declares a control (types: float, int, bool, color, text; attributes as for \`@control\`, values with spaces in single quotes), and every \`{{name}}\` is replaced by its value, e.g.
931
+ <!-- {{title: text default='Hello' label='Title'}} {{accent: color default=#6366F1}} --><text fill="{{accent}}">{{title}}</text>
932
+ Change their controls' values freely (a text control takes any words); edit the picture with care, keeping element ids that animations refer to.
936
933
 
937
934
  Make effects that loop smoothly and read well behind content: keep motion calm unless asked otherwise, keep contrast where text may sit, and expose the few controls a designer would actually want (colours, speed, scale, intensity) with sensible ranges. Use keyframes (\`keyframe.set\`, in ms) for changes over the timeline. When you can, look at the result with \`view_effect\` and refine it.`;
938
935
 
@@ -980,16 +977,114 @@ function bindSvgView(editor, container) {
980
977
  view?.element.remove();
981
978
  };
982
979
  }
980
+
981
+ // src/editor/parameters.ts
982
+ import {
983
+ ParametersController,
984
+ rgba01ToHex
985
+ } from "@pixerate/editor/parameters";
986
+ function parameterFieldFor(control, options = {}) {
987
+ const base = { id: control.name, label: control.label };
988
+ switch (control.type) {
989
+ case "bool":
990
+ return { ...base, type: "boolean", default: control.default === true };
991
+ case "int":
992
+ return { ...base, type: "int", min: control.min, max: control.max, step: Math.max(1, Math.round(control.step)), default: Number(control.default) };
993
+ case "float":
994
+ return { ...base, type: "number", min: control.min, max: control.max, step: control.step, default: Number(control.default) };
995
+ case "sampler2D":
996
+ return { ...base, type: "image", default: String(control.default), ...options.images && { options: options.images } };
997
+ case "text":
998
+ return { ...base, type: "text", default: String(control.default) };
999
+ default: {
1000
+ const value = control.default;
1001
+ if (control.color) {
1002
+ const alpha = control.type === "vec4";
1003
+ return { ...base, type: "color", alpha, default: rgba01ToHex(value, alpha) };
1004
+ }
1005
+ return { ...base, type: "vector", size: value.length, min: control.min, max: control.max, step: control.step, default: [...value] };
1006
+ }
1007
+ }
1008
+ }
1009
+ function parameterSchemaFor(controls, options = {}) {
1010
+ return { ...options.title && { title: options.title }, fields: controls.map((c) => parameterFieldFor(c, options)) };
1011
+ }
1012
+ function toParameterValue(control, value) {
1013
+ if (control.color && Array.isArray(value)) return rgba01ToHex(value, control.type === "vec4");
1014
+ return Array.isArray(value) ? [...value] : value;
1015
+ }
1016
+ function toParameterValues(controls, values) {
1017
+ const out = {};
1018
+ for (const control of controls) if (values[control.name] !== void 0) out[control.name] = toParameterValue(control, values[control.name]);
1019
+ return out;
1020
+ }
1021
+ function toParameterTrack(control, track) {
1022
+ if (!track) return void 0;
1023
+ return {
1024
+ ...track.enabled === false && { enabled: false },
1025
+ keyframes: track.keyframes.map((k) => ({ ...k, value: control ? toParameterValue(control, k.value) : k.value }))
1026
+ };
1027
+ }
1028
+ function bindEffectParameters(editor, options = {}) {
1029
+ let state = editor.getState();
1030
+ let controls = state.controls;
1031
+ let signature = JSON.stringify(controls);
1032
+ let syncing = false;
1033
+ const byName = () => new Map(controls.map((c) => [c.name, c]));
1034
+ const keyframes = {
1035
+ getTrack: (id) => toParameterTrack(byName().get(id), editor.getState().document.tracks[id]),
1036
+ setTrack: (id, track) => {
1037
+ editor.setTrack(id, track);
1038
+ },
1039
+ subscribe: (listener) => editor.subscribe(() => listener())
1040
+ };
1041
+ const controller = new ParametersController({
1042
+ schema: parameterSchemaFor(controls, options),
1043
+ values: toParameterValues(controls, state.values),
1044
+ time: state.time,
1045
+ keyframes,
1046
+ onChange: (values, changed) => {
1047
+ if (syncing) return;
1048
+ const result = changed.length === 1 ? editor.setValue(changed[0], values[changed[0]]) : editor.setValues(Object.fromEntries(changed.map((id) => [id, values[id]])));
1049
+ if (!result.ok) sync(editor.getState());
1050
+ }
1051
+ });
1052
+ const sync = (next) => {
1053
+ state = next;
1054
+ const nextSignature = JSON.stringify(next.controls);
1055
+ syncing = true;
1056
+ try {
1057
+ if (nextSignature !== signature) {
1058
+ signature = nextSignature;
1059
+ controls = next.controls;
1060
+ controller.setSchema(parameterSchemaFor(controls, options));
1061
+ }
1062
+ controller.setTime(next.time, toParameterValues(controls, next.values));
1063
+ } finally {
1064
+ syncing = false;
1065
+ }
1066
+ };
1067
+ const unsubscribe = editor.subscribe(sync);
1068
+ return {
1069
+ controller,
1070
+ dispose: () => {
1071
+ unsubscribe();
1072
+ controller.destroy();
1073
+ }
1074
+ };
1075
+ }
983
1076
  export {
984
1077
  EASING_PRESETS,
985
1078
  EFFECT_AGENT_INSTRUCTIONS,
986
1079
  EffectEditor,
1080
+ bindEffectParameters,
987
1081
  bindShaderRenderer,
988
1082
  bindSvgView,
989
1083
  callTool,
990
1084
  createEffectDocument,
991
1085
  createEffectEditor,
992
1086
  createEffectToolProvider,
1087
+ createEffectToolRegistry,
993
1088
  cubicBezierAt,
994
1089
  describeEditor,
995
1090
  documentControls,
@@ -1002,7 +1097,11 @@ export {
1002
1097
  keyframeTimes,
1003
1098
  motionAt,
1004
1099
  normalizeKeyframes,
1100
+ parameterFieldFor,
1101
+ parameterSchemaFor,
1005
1102
  parseEffectDocument,
1103
+ toParameterValue,
1104
+ toParameterValues,
1006
1105
  toolDefinitions,
1007
1106
  valuesAt
1008
1107
  };