@pixerate/editor 0.160.0 → 0.162.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 (146) hide show
  1. package/dist/agent/index.cjs +36 -35
  2. package/dist/agent/index.js +7 -6
  3. package/dist/animation/index.cjs +5 -4
  4. package/dist/animation/index.js +4 -3
  5. package/dist/animation-editor/index.cjs +62 -61
  6. package/dist/animation-editor/index.js +6 -5
  7. package/dist/canvas/index.cjs +33 -32
  8. package/dist/canvas/index.js +6 -5
  9. package/dist/chart/index.cjs +88 -87
  10. package/dist/chart/index.js +7 -6
  11. package/dist/{chunk-QMI5HZWA.cjs → chunk-236TKNUR.cjs} +3 -3
  12. package/dist/{chunk-6UPGM3JB.js → chunk-2TKHLX6H.js} +1 -1
  13. package/dist/{chunk-3YDQLQH2.cjs → chunk-32MMQYCQ.cjs} +81 -81
  14. package/dist/{chunk-MHEXLG3M.cjs → chunk-3GJH2MGH.cjs} +37 -37
  15. package/dist/{chunk-AKDWAWSY.cjs → chunk-3IHNXRUV.cjs} +45 -45
  16. package/dist/{chunk-VKF3PWKR.cjs → chunk-3OSXHLIT.cjs} +259 -28
  17. package/dist/{chunk-TVO6W2Q6.cjs → chunk-4NGVHHJB.cjs} +17 -17
  18. package/dist/chunk-4VU4CP7R.js +769 -0
  19. package/dist/chunk-6DM3PUQD.cjs +767 -0
  20. package/dist/{chunk-DBIVICAO.js → chunk-7CKPUUN4.js} +1 -1
  21. package/dist/{chunk-57JLBGJ5.cjs → chunk-7YBDRJXT.cjs} +8 -8
  22. package/dist/{chunk-KY3FYKP3.js → chunk-BQWGN5WH.js} +1 -1
  23. package/dist/{chunk-LEUZ4FRC.cjs → chunk-CIPIESX4.cjs} +3 -3
  24. package/dist/chunk-CJE5MOIS.js +347 -0
  25. package/dist/{chunk-5QBRAIVK.js → chunk-CX6JLDFC.js} +3 -3
  26. package/dist/{chunk-R3QY7PPL.js → chunk-DTSTNRPH.js} +1 -1
  27. package/dist/{chunk-6LUYFHHN.cjs → chunk-EQ46F75O.cjs} +51 -51
  28. package/dist/{chunk-UKCJ43YL.cjs → chunk-ES3UEVUT.cjs} +130 -898
  29. package/dist/{chunk-UM3RPYC5.js → chunk-EVPZ5IKN.js} +1 -1
  30. package/dist/{chunk-XNQ5EFB4.cjs → chunk-EY22AY6R.cjs} +16 -16
  31. package/dist/chunk-FKUKJ3XA.cjs +347 -0
  32. package/dist/{chunk-YABNLNY7.js → chunk-GLMP22FT.js} +3 -3
  33. package/dist/{chunk-SUF6WRXR.cjs → chunk-GTELFJRE.cjs} +2 -2
  34. package/dist/{chunk-72BGL3I2.js → chunk-GWV4T5NZ.js} +1 -1
  35. package/dist/{chunk-AY65VQSU.js → chunk-H3OEM3Q2.js} +1 -1
  36. package/dist/{chunk-4YWAEV4W.cjs → chunk-HDGBM7EA.cjs} +26 -26
  37. package/dist/{chunk-H5XX4KHB.js → chunk-HLOKB7WW.js} +5 -3
  38. package/dist/{chunk-WNAAWLZ5.js → chunk-KGTF55R3.js} +2 -2
  39. package/dist/{chunk-WPPJH6X4.cjs → chunk-LL3H5XUH.cjs} +6 -4
  40. package/dist/{chunk-4T44CJ5K.cjs → chunk-NA4UVVJ4.cjs} +15 -15
  41. package/dist/{chunk-Q6PA362P.js → chunk-NO5QQDHZ.js} +2 -2
  42. package/dist/{chunk-AA34BA7G.js → chunk-QYZGBVNC.js} +30 -798
  43. package/dist/chunk-S6X2QRVU.cjs +769 -0
  44. package/dist/{chunk-SSCL7QQ4.js → chunk-XDCFJEU7.js} +2 -2
  45. package/dist/{chunk-IQ6KKXOU.js → chunk-YN4AYYBJ.js} +243 -12
  46. package/dist/chunk-ZGOHKS3X.js +767 -0
  47. package/dist/color/index.cjs +6 -2
  48. package/dist/color/index.d.cts +1 -0
  49. package/dist/color/index.d.ts +1 -0
  50. package/dist/color/index.js +28 -24
  51. package/dist/color/suggest.d.cts +12 -0
  52. package/dist/color/suggest.d.ts +12 -0
  53. package/dist/effects/index.cjs +44 -771
  54. package/dist/effects/index.js +34 -761
  55. package/dist/effects/ui.d.cts +20 -3
  56. package/dist/effects/ui.d.ts +20 -3
  57. package/dist/export/index.cjs +6 -5
  58. package/dist/export/index.js +5 -4
  59. package/dist/extensions/index.cjs +9 -8
  60. package/dist/extensions/index.js +8 -7
  61. package/dist/gradient/index.cjs +28 -26
  62. package/dist/gradient/index.js +7 -5
  63. package/dist/image-editor/index.cjs +638 -637
  64. package/dist/image-editor/index.js +5 -4
  65. package/dist/index.cjs +15 -14
  66. package/dist/index.js +9 -8
  67. package/dist/paint-editor/index.cjs +17 -16
  68. package/dist/paint-editor/index.js +6 -5
  69. package/dist/typography/accessibility.d.cts +44 -0
  70. package/dist/typography/accessibility.d.ts +44 -0
  71. package/dist/typography/catalog.d.cts +98 -0
  72. package/dist/typography/catalog.d.ts +98 -0
  73. package/dist/typography/index.cjs +1099 -0
  74. package/dist/typography/index.d.cts +16 -0
  75. package/dist/typography/index.d.ts +16 -0
  76. package/dist/typography/index.js +1099 -0
  77. package/dist/typography/metrics.d.cts +31 -0
  78. package/dist/typography/metrics.d.ts +31 -0
  79. package/dist/typography/picker.d.cts +132 -0
  80. package/dist/typography/picker.d.ts +132 -0
  81. package/dist/typography/registry.d.cts +66 -0
  82. package/dist/typography/registry.d.ts +66 -0
  83. package/dist/typography/scale.d.cts +62 -0
  84. package/dist/typography/scale.d.ts +62 -0
  85. package/dist/typography/segment.d.cts +26 -0
  86. package/dist/typography/segment.d.ts +26 -0
  87. package/dist/typography/stack.d.cts +26 -0
  88. package/dist/typography/stack.d.ts +26 -0
  89. package/dist/typography/tools.d.cts +34 -0
  90. package/dist/typography/tools.d.ts +34 -0
  91. package/dist/typography/types.d.cts +96 -0
  92. package/dist/typography/types.d.ts +96 -0
  93. package/dist/typography/weight.d.cts +13 -0
  94. package/dist/typography/weight.d.ts +13 -0
  95. package/dist/vector/index.cjs +950 -949
  96. package/dist/vector/index.js +5 -4
  97. package/dist/video-editor/audio.d.cts +9 -0
  98. package/dist/video-editor/audio.d.ts +9 -0
  99. package/dist/video-editor/check.d.cts +1 -1
  100. package/dist/video-editor/check.d.ts +1 -1
  101. package/dist/video-editor/controller.d.cts +47 -0
  102. package/dist/video-editor/controller.d.ts +47 -0
  103. package/dist/video-editor/dialogue-tools.d.cts +19 -0
  104. package/dist/video-editor/dialogue-tools.d.ts +19 -0
  105. package/dist/video-editor/effect-authoring.d.cts +54 -0
  106. package/dist/video-editor/effect-authoring.d.ts +54 -0
  107. package/dist/video-editor/evaluate.d.cts +11 -0
  108. package/dist/video-editor/evaluate.d.ts +11 -0
  109. package/dist/video-editor/export.d.cts +7 -1
  110. package/dist/video-editor/export.d.ts +7 -1
  111. package/dist/video-editor/index.cjs +1857 -531
  112. package/dist/video-editor/index.d.cts +7 -0
  113. package/dist/video-editor/index.d.ts +7 -0
  114. package/dist/video-editor/index.js +1516 -190
  115. package/dist/video-editor/lut.d.cts +12 -2
  116. package/dist/video-editor/lut.d.ts +12 -2
  117. package/dist/video-editor/mask.d.cts +48 -0
  118. package/dist/video-editor/mask.d.ts +48 -0
  119. package/dist/video-editor/mix.d.cts +107 -0
  120. package/dist/video-editor/mix.d.ts +107 -0
  121. package/dist/video-editor/music-tools.d.cts +19 -0
  122. package/dist/video-editor/music-tools.d.ts +19 -0
  123. package/dist/video-editor/parse.d.cts +3 -1
  124. package/dist/video-editor/parse.d.ts +3 -1
  125. package/dist/video-editor/plan.d.cts +75 -0
  126. package/dist/video-editor/plan.d.ts +75 -0
  127. package/dist/video-editor/render.d.cts +1 -1
  128. package/dist/video-editor/render.d.ts +1 -1
  129. package/dist/video-editor/timeline.d.cts +2 -0
  130. package/dist/video-editor/timeline.d.ts +2 -0
  131. package/dist/video-editor/tools.d.cts +90 -0
  132. package/dist/video-editor/tools.d.ts +90 -0
  133. package/dist/video-editor/transcript-controller.d.cts +106 -0
  134. package/dist/video-editor/transcript-controller.d.ts +106 -0
  135. package/dist/video-editor/transcript.d.cts +40 -0
  136. package/dist/video-editor/transcript.d.ts +40 -0
  137. package/dist/video-editor/types.d.cts +62 -2
  138. package/dist/video-editor/types.d.ts +62 -2
  139. package/dist/video-editor/ui.d.cts +21 -1
  140. package/dist/video-editor/ui.d.ts +21 -1
  141. package/package.json +11 -1
  142. package/skills/README.md +3 -0
  143. package/skills/pixerate-bgm-alignment/SKILL.md +82 -0
  144. package/skills/pixerate-talking-head-edit/SKILL.md +80 -0
  145. package/skills/pixerate-typography/SKILL.md +91 -0
  146. package/skills/pixerate-video-editor/SKILL.md +40 -2
@@ -0,0 +1,767 @@
1
+ import {
2
+ EffectControlError,
3
+ EffectDisposedError,
4
+ EffectUnsupportedError,
5
+ effectValueErrors
6
+ } from "./chunk-7KWJEYIK.js";
7
+ import {
8
+ HistoryManager
9
+ } from "./chunk-RNSANXPT.js";
10
+ import {
11
+ evaluateEasing,
12
+ isAnimationEasing
13
+ } from "./chunk-5UJHB3BW.js";
14
+
15
+ // src/effects/authoring-session.ts
16
+ var HEX_RE = /^#([0-9a-f]{6}|[0-9a-f]{8})$/i;
17
+ function valueErrorMessage(errors) {
18
+ return errors.map((e) => {
19
+ const [control, ...rest] = e.path.replace(/^\//, "").split("/");
20
+ return `control "${control}"${rest.length ? ` (item ${rest.join("/")})` : ""}: ${e.message}`;
21
+ }).join("; ");
22
+ }
23
+ function documentErrors(doc, description) {
24
+ const errors = [];
25
+ const controls = Object.keys(description.controls.properties);
26
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
27
+ if (doc.values !== void 0) {
28
+ if (!isRecord(doc.values)) errors.push({ path: "/values", message: "must be an object of control values" });
29
+ else errors.push(...effectValueErrors(description, doc.values).map((e) => ({ ...e, path: `/values${e.path}` })));
30
+ }
31
+ if (doc.tracks !== void 0) {
32
+ if (!isRecord(doc.tracks)) errors.push({ path: "/tracks", message: "must be an object of keyframe lists" });
33
+ else
34
+ for (const [control, kfs] of Object.entries(doc.tracks)) {
35
+ const at = `/tracks/${control}`;
36
+ if (!controls.includes(control)) errors.push({ path: at, message: `there is no control "${control}"; the controls are ${controls.join(", ")}` });
37
+ else if (!Array.isArray(kfs)) errors.push({ path: at, message: "must be a list of keyframes" });
38
+ else
39
+ kfs.forEach((kf, i) => {
40
+ if (!isRecord(kf)) return errors.push({ path: `${at}/${i}`, message: "must be a keyframe object" });
41
+ if (typeof kf.time !== "number" || !Number.isFinite(kf.time) || kf.time < 0) errors.push({ path: `${at}/${i}/time`, message: "must be a finite number of ms >= 0" });
42
+ errors.push(...effectValueErrors(description, { [control]: kf.value }).map((e) => ({ ...e, path: `${at}/${i}/value` })));
43
+ if (kf.easing !== void 0 && !isAnimationEasing(kf.easing)) errors.push({ path: `${at}/${i}/easing`, message: "is not a valid easing" });
44
+ });
45
+ }
46
+ }
47
+ if (doc.presets !== void 0) {
48
+ if (!Array.isArray(doc.presets)) errors.push({ path: "/presets", message: "must be a list of presets" });
49
+ else
50
+ doc.presets.forEach((p, i) => {
51
+ if (!isRecord(p) || typeof p.name !== "string" || !p.name) return errors.push({ path: `/presets/${i}`, message: "must have a name" });
52
+ errors.push(...effectValueErrors(description, p.values).map((e) => ({ ...e, path: `/presets/${i}/values${e.path}` })));
53
+ });
54
+ }
55
+ if (doc.duration !== void 0 && (typeof doc.duration !== "number" || !Number.isFinite(doc.duration) || !(doc.duration > 0))) {
56
+ errors.push({ path: "/duration", message: "must be a finite number of ms > 0" });
57
+ }
58
+ if (doc.loop !== void 0 && typeof doc.loop !== "boolean") errors.push({ path: "/loop", message: "must be a boolean" });
59
+ return errors;
60
+ }
61
+ function parseHex(hex) {
62
+ if (!HEX_RE.test(hex)) return null;
63
+ const body = hex.slice(1);
64
+ const r = parseInt(body.slice(0, 2), 16);
65
+ const g = parseInt(body.slice(2, 4), 16);
66
+ const b = parseInt(body.slice(4, 6), 16);
67
+ const hasAlpha = body.length === 8;
68
+ const a = hasAlpha ? parseInt(body.slice(6, 8), 16) : 255;
69
+ return { r, g, b, a, hasAlpha };
70
+ }
71
+ function toHex2(n) {
72
+ const clamped = Math.round(Math.max(0, Math.min(255, n)));
73
+ return clamped.toString(16).padStart(2, "0");
74
+ }
75
+ function lerpHex(c1, c2, t) {
76
+ const p1 = parseHex(c1);
77
+ const p2 = parseHex(c2);
78
+ if (!p1 || !p2) return t >= 1 ? c2 : c1;
79
+ const r = p1.r + (p2.r - p1.r) * t;
80
+ const g = p1.g + (p2.g - p1.g) * t;
81
+ const b = p1.b + (p2.b - p1.b) * t;
82
+ const a = p1.a + (p2.a - p1.a) * t;
83
+ const withAlpha = p1.hasAlpha || p2.hasAlpha;
84
+ return `#${toHex2(r)}${toHex2(g)}${toHex2(b)}${withAlpha ? toHex2(a) : ""}`;
85
+ }
86
+ function interpolateValue(v1, v2, progress) {
87
+ if (typeof v1 === "number" && typeof v2 === "number") {
88
+ return v1 + (v2 - v1) * progress;
89
+ }
90
+ if (Array.isArray(v1) && Array.isArray(v2) && v1.length === v2.length) {
91
+ return v1.map((num, i) => {
92
+ const other = v2[i];
93
+ return typeof num === "number" && typeof other === "number" ? num + (other - num) * progress : num;
94
+ });
95
+ }
96
+ if (typeof v1 === "string" && typeof v2 === "string" && v1.startsWith("#") && v2.startsWith("#")) {
97
+ return lerpHex(v1, v2, progress);
98
+ }
99
+ return progress >= 1 ? v2 : v1;
100
+ }
101
+ function valuesAtTime(time, baseValues, tracks) {
102
+ const result = { ...baseValues };
103
+ for (const [control, kfs] of Object.entries(tracks)) {
104
+ if (!kfs || kfs.length === 0) continue;
105
+ if (kfs.length === 1) {
106
+ result[control] = kfs[0].value;
107
+ continue;
108
+ }
109
+ if (time <= kfs[0].time) {
110
+ result[control] = kfs[0].value;
111
+ continue;
112
+ }
113
+ const last = kfs[kfs.length - 1];
114
+ if (time >= last.time) {
115
+ result[control] = last.value;
116
+ continue;
117
+ }
118
+ let idx = 0;
119
+ while (idx < kfs.length - 1 && kfs[idx + 1].time <= time) {
120
+ idx++;
121
+ }
122
+ const kf1 = kfs[idx];
123
+ const kf2 = kfs[idx + 1];
124
+ const span = kf2.time - kf1.time;
125
+ const rawProgress = span > 0 ? (time - kf1.time) / span : 0;
126
+ const easing = kf1.easing ?? "linear";
127
+ const eased = evaluateEasing(easing, rawProgress);
128
+ result[control] = interpolateValue(kf1.value, kf2.value, eased);
129
+ }
130
+ return result;
131
+ }
132
+ var defaultSchedule = (frame) => {
133
+ if (typeof requestAnimationFrame === "function") {
134
+ let handle2;
135
+ let running2 = true;
136
+ const tick2 = (now) => {
137
+ if (!running2) return;
138
+ frame(now);
139
+ if (running2) handle2 = requestAnimationFrame(tick2);
140
+ };
141
+ handle2 = requestAnimationFrame(tick2);
142
+ return () => {
143
+ running2 = false;
144
+ cancelAnimationFrame(handle2);
145
+ };
146
+ }
147
+ let handle;
148
+ let running = true;
149
+ const interval = 16;
150
+ const tick = () => {
151
+ if (!running) return;
152
+ frame(Date.now());
153
+ if (running) handle = setTimeout(tick, interval);
154
+ };
155
+ handle = setTimeout(tick, interval);
156
+ return () => {
157
+ running = false;
158
+ clearTimeout(handle);
159
+ };
160
+ };
161
+ function isDocumentSource(source) {
162
+ return "document" in source && typeof source.document === "object" && source.document !== null;
163
+ }
164
+ async function createEffectAuthoringSession(source, options) {
165
+ const { provider, history = new HistoryManager(), mergeWindowMs = 400 } = options;
166
+ const scheduleFn = options.schedule ?? defaultSchedule;
167
+ let effectId;
168
+ let requestedPreset;
169
+ let initialDoc = null;
170
+ if (isDocumentSource(source)) {
171
+ const doc = source.document;
172
+ if (doc.version !== 1 || !doc.effectId) {
173
+ throw new EffectUnsupportedError("Invalid effect authoring document: version 1 required");
174
+ }
175
+ effectId = doc.effectId;
176
+ initialDoc = doc;
177
+ } else if ("effectId" in source && typeof source.effectId === "string") {
178
+ effectId = source.effectId;
179
+ requestedPreset = source.preset;
180
+ } else {
181
+ throw new EffectUnsupportedError("Invalid effect authoring source");
182
+ }
183
+ const description = await provider.describe(effectId);
184
+ if (initialDoc) {
185
+ const problems = documentErrors(initialDoc, description);
186
+ if (problems.length) throw new EffectControlError(effectId, problems);
187
+ }
188
+ if (description.kind === "motion") {
189
+ throw new EffectUnsupportedError("Motion effects cannot be authored as pixel effects", effectId);
190
+ }
191
+ let disposed = false;
192
+ let playing = false;
193
+ let cancelSchedule = null;
194
+ let lastScheduleTime = null;
195
+ let playheadTime = 0;
196
+ let duration = initialDoc?.duration ?? description.duration ?? 1e3;
197
+ if (!(duration > 0)) duration = 1e3;
198
+ let loop = initialDoc?.loop ?? description.loop ?? false;
199
+ let compiled = null;
200
+ let savedPresets = initialDoc ? [...initialDoc.presets ?? []] : [];
201
+ let baseValues = { ...description.defaults };
202
+ if (initialDoc?.values) {
203
+ baseValues = { ...baseValues, ...initialDoc.values };
204
+ } else if (requestedPreset) {
205
+ const found = description.presets.find((p) => p.name === requestedPreset);
206
+ if (!found) {
207
+ const validNames = description.presets.map((p) => `"${p.name}"`).join(", ");
208
+ throw new EffectUnsupportedError(
209
+ `Unknown preset "${requestedPreset}" for effect "${effectId}". Valid presets are: ${validNames || "none"}`,
210
+ effectId
211
+ );
212
+ }
213
+ baseValues = { ...baseValues, ...found.values };
214
+ }
215
+ let tracks = {};
216
+ if (initialDoc?.tracks) {
217
+ for (const [k, kfs] of Object.entries(initialDoc.tracks)) {
218
+ tracks[k] = [...kfs].sort((a, b) => a.time - b.time);
219
+ }
220
+ }
221
+ const livePreviews = /* @__PURE__ */ new Set();
222
+ const listeners = /* @__PURE__ */ new Set();
223
+ let cachedState = null;
224
+ function mergePresets() {
225
+ const result = [];
226
+ const seen = /* @__PURE__ */ new Set();
227
+ for (const p of savedPresets) {
228
+ result.push(p);
229
+ seen.add(p.name);
230
+ }
231
+ for (const p of description.presets) {
232
+ if (!seen.has(p.name)) {
233
+ result.push(p);
234
+ seen.add(p.name);
235
+ }
236
+ }
237
+ return result;
238
+ }
239
+ function currentMergedDescription() {
240
+ return {
241
+ ...description,
242
+ presets: mergePresets()
243
+ };
244
+ }
245
+ function buildState() {
246
+ const desc = currentMergedDescription();
247
+ const currentValues = valuesAtTime(playheadTime, baseValues, tracks);
248
+ const historyState = history.getState();
249
+ return {
250
+ providerId: provider.id,
251
+ effectId,
252
+ name: initialDoc?.name ?? description.name,
253
+ kind: description.kind,
254
+ description: desc,
255
+ source: null,
256
+ sourceLanguage: null,
257
+ values: currentValues,
258
+ tracks: { ...tracks },
259
+ issues: [],
260
+ compiled,
261
+ time: playheadTime,
262
+ duration,
263
+ loop,
264
+ playing,
265
+ canUndo: historyState.canUndo,
266
+ canRedo: historyState.canRedo
267
+ };
268
+ }
269
+ function getState() {
270
+ if (!cachedState) {
271
+ cachedState = buildState();
272
+ }
273
+ return cachedState;
274
+ }
275
+ function invalidateState() {
276
+ cachedState = null;
277
+ const next = getState();
278
+ listeners.forEach((l) => l(next));
279
+ }
280
+ function validControlNames() {
281
+ return Object.keys(description.controls.properties);
282
+ }
283
+ function checkDisposed() {
284
+ if (disposed) {
285
+ return { ok: false, message: "the session is closed" };
286
+ }
287
+ return null;
288
+ }
289
+ function pauseInternal() {
290
+ if (!playing) return;
291
+ playing = false;
292
+ if (cancelSchedule) {
293
+ cancelSchedule();
294
+ cancelSchedule = null;
295
+ }
296
+ lastScheduleTime = null;
297
+ invalidateState();
298
+ }
299
+ function seekInternal(t) {
300
+ const clamped = Math.max(0, Math.min(duration, t));
301
+ if (clamped !== playheadTime) {
302
+ playheadTime = clamped;
303
+ invalidateState();
304
+ }
305
+ }
306
+ const session = {
307
+ getState,
308
+ subscribe(listener) {
309
+ listeners.add(listener);
310
+ return () => {
311
+ listeners.delete(listener);
312
+ };
313
+ },
314
+ setValue(name, value, opts) {
315
+ const disp = checkDisposed();
316
+ if (disp) return disp;
317
+ const validNames = validControlNames();
318
+ if (!validNames.includes(name)) {
319
+ return { ok: false, message: `there is no control "${name}"; the controls are ${validNames.join(", ")}` };
320
+ }
321
+ const errors = effectValueErrors(description, { [name]: value });
322
+ if (errors.length > 0) {
323
+ return { ok: false, message: valueErrorMessage(errors) };
324
+ }
325
+ const isAnimated = (tracks[name]?.length ?? 0) > 0;
326
+ if (isAnimated) {
327
+ const oldTracks = { ...tracks };
328
+ const t = playheadTime;
329
+ const currentTrack = [...tracks[name] ?? []];
330
+ const existingIdx = currentTrack.findIndex((kf) => kf.time === t);
331
+ const prevKf = existingIdx >= 0 ? currentTrack[existingIdx] : void 0;
332
+ const execute2 = () => {
333
+ const track = [...tracks[name] ?? []];
334
+ const idx = track.findIndex((kf) => kf.time === t);
335
+ if (idx >= 0) {
336
+ track[idx] = { ...track[idx], value };
337
+ } else {
338
+ track.push({ time: t, value });
339
+ track.sort((a, b) => a.time - b.time);
340
+ }
341
+ tracks[name] = track;
342
+ invalidateState();
343
+ };
344
+ const undo2 = () => {
345
+ tracks = { ...oldTracks };
346
+ invalidateState();
347
+ };
348
+ const cmd2 = {
349
+ name: `Set ${name}`,
350
+ tag: name,
351
+ execute: execute2,
352
+ undo: undo2
353
+ };
354
+ if (opts?.gesture) {
355
+ history.executeMerged(cmd2, { key: opts.gesture });
356
+ } else {
357
+ history.executeMerged(cmd2, { windowMs: mergeWindowMs });
358
+ }
359
+ invalidateState();
360
+ return { ok: true };
361
+ }
362
+ const prevVal = baseValues[name];
363
+ const execute = () => {
364
+ baseValues[name] = value;
365
+ };
366
+ const undo = () => {
367
+ baseValues[name] = prevVal;
368
+ };
369
+ const cmd = {
370
+ name: `Set ${name}`,
371
+ tag: name,
372
+ execute,
373
+ undo
374
+ };
375
+ if (opts?.gesture) {
376
+ history.executeMerged(cmd, { key: opts.gesture });
377
+ } else {
378
+ history.executeMerged(cmd, { windowMs: mergeWindowMs });
379
+ }
380
+ invalidateState();
381
+ return { ok: true };
382
+ },
383
+ setValues(values, opts) {
384
+ const disp = checkDisposed();
385
+ if (disp) return disp;
386
+ const validNames = validControlNames();
387
+ for (const [k, v] of Object.entries(values)) {
388
+ if (!validNames.includes(k)) {
389
+ return { ok: false, message: `there is no control "${k}"; the controls are ${validNames.join(", ")}` };
390
+ }
391
+ const errors = effectValueErrors(description, { [k]: v });
392
+ if (errors.length > 0) {
393
+ return { ok: false, message: valueErrorMessage(errors) };
394
+ }
395
+ }
396
+ const prevBase = { ...baseValues };
397
+ const prevTracks = { ...tracks };
398
+ const t = playheadTime;
399
+ const execute = () => {
400
+ for (const [k, v] of Object.entries(values)) {
401
+ const isAnimated = (tracks[k]?.length ?? 0) > 0;
402
+ if (isAnimated) {
403
+ const track = [...tracks[k] ?? []];
404
+ const idx = track.findIndex((kf) => kf.time === t);
405
+ if (idx >= 0) {
406
+ track[idx] = { ...track[idx], value: v };
407
+ } else {
408
+ track.push({ time: t, value: v });
409
+ track.sort((a, b) => a.time - b.time);
410
+ }
411
+ tracks[k] = track;
412
+ } else {
413
+ baseValues[k] = v;
414
+ }
415
+ }
416
+ };
417
+ const undo = () => {
418
+ baseValues = { ...prevBase };
419
+ tracks = { ...prevTracks };
420
+ };
421
+ const cmd = {
422
+ name: "Set values",
423
+ execute,
424
+ undo
425
+ };
426
+ if (opts?.gesture) {
427
+ history.executeMerged(cmd, { key: opts.gesture });
428
+ } else {
429
+ history.execute(cmd);
430
+ }
431
+ invalidateState();
432
+ return { ok: true };
433
+ },
434
+ resetValues(names) {
435
+ const disp = checkDisposed();
436
+ if (disp) return disp;
437
+ const targets = names ? [...names] : validControlNames();
438
+ const prevBase = { ...baseValues };
439
+ const prevTracks = { ...tracks };
440
+ const execute = () => {
441
+ for (const name of targets) {
442
+ if (name in description.defaults) {
443
+ baseValues[name] = description.defaults[name];
444
+ }
445
+ delete tracks[name];
446
+ }
447
+ };
448
+ const undo = () => {
449
+ baseValues = { ...prevBase };
450
+ tracks = { ...prevTracks };
451
+ };
452
+ history.execute({
453
+ name: "Reset values",
454
+ execute,
455
+ undo
456
+ });
457
+ invalidateState();
458
+ return { ok: true };
459
+ },
460
+ applyPreset(name) {
461
+ const disp = checkDisposed();
462
+ if (disp) return disp;
463
+ const allPresets = mergePresets();
464
+ const preset = allPresets.find((p) => p.name === name);
465
+ if (!preset) {
466
+ const available = allPresets.map((p) => `"${p.name}"`).join(", ");
467
+ return { ok: false, message: `unknown preset "${name}"; available presets are: ${available || "none"}` };
468
+ }
469
+ const prevBase = { ...baseValues };
470
+ const prevTracks = { ...tracks };
471
+ const execute = () => {
472
+ for (const [k, v] of Object.entries(preset.values)) {
473
+ baseValues[k] = v;
474
+ delete tracks[k];
475
+ }
476
+ };
477
+ const undo = () => {
478
+ baseValues = { ...prevBase };
479
+ tracks = { ...prevTracks };
480
+ };
481
+ history.execute({
482
+ name: `Apply preset "${name}"`,
483
+ execute,
484
+ undo
485
+ });
486
+ invalidateState();
487
+ return { ok: true };
488
+ },
489
+ savePreset(name) {
490
+ const disp = checkDisposed();
491
+ if (disp) return disp;
492
+ if (!name || !name.trim()) {
493
+ return { ok: false, message: "preset name cannot be empty" };
494
+ }
495
+ const trimmed = name.trim();
496
+ const currentValues = valuesAtTime(playheadTime, baseValues, tracks);
497
+ const prevSaved = [...savedPresets];
498
+ const execute = () => {
499
+ const next = savedPresets.filter((p) => p.name !== trimmed);
500
+ next.push({ name: trimmed, values: { ...currentValues } });
501
+ savedPresets = next;
502
+ };
503
+ const undo = () => {
504
+ savedPresets = [...prevSaved];
505
+ };
506
+ history.execute({
507
+ name: `Save preset "${trimmed}"`,
508
+ execute,
509
+ undo
510
+ });
511
+ invalidateState();
512
+ return { ok: true };
513
+ },
514
+ setKeyframe(name, time, value, easing) {
515
+ const disp = checkDisposed();
516
+ if (disp) return disp;
517
+ const validNames = validControlNames();
518
+ if (!validNames.includes(name)) {
519
+ return { ok: false, message: `there is no control "${name}"; the controls are ${validNames.join(", ")}` };
520
+ }
521
+ if (typeof time !== "number" || !Number.isFinite(time) || time < 0) {
522
+ return { ok: false, message: "keyframe time must be a finite number >= 0" };
523
+ }
524
+ if (easing !== void 0 && !isAnimationEasing(easing)) {
525
+ return { ok: false, message: `invalid easing: ${String(easing)}` };
526
+ }
527
+ const targetValue = value !== void 0 ? value : valuesAtTime(time, baseValues, tracks)[name];
528
+ const errors = effectValueErrors(description, { [name]: targetValue });
529
+ if (errors.length > 0) {
530
+ return { ok: false, message: valueErrorMessage(errors) };
531
+ }
532
+ const prevTrack = tracks[name] ? [...tracks[name]] : void 0;
533
+ const execute = () => {
534
+ const track = prevTrack ? [...prevTrack] : [];
535
+ const idx = track.findIndex((kf2) => kf2.time === time);
536
+ const kf = {
537
+ time,
538
+ value: targetValue,
539
+ ...easing !== void 0 ? { easing } : {}
540
+ };
541
+ if (idx >= 0) {
542
+ track[idx] = kf;
543
+ } else {
544
+ track.push(kf);
545
+ track.sort((a, b) => a.time - b.time);
546
+ }
547
+ tracks[name] = track;
548
+ };
549
+ const undo = () => {
550
+ if (prevTrack === void 0) {
551
+ delete tracks[name];
552
+ } else {
553
+ tracks[name] = [...prevTrack];
554
+ }
555
+ };
556
+ history.execute({
557
+ name: `Set keyframe on ${name}`,
558
+ execute,
559
+ undo
560
+ });
561
+ invalidateState();
562
+ return { ok: true };
563
+ },
564
+ removeKeyframe(name, time) {
565
+ const disp = checkDisposed();
566
+ if (disp) return disp;
567
+ const validNames = validControlNames();
568
+ if (!validNames.includes(name)) {
569
+ return { ok: false, message: `there is no control "${name}"; the controls are ${validNames.join(", ")}` };
570
+ }
571
+ const track = tracks[name];
572
+ if (!track || !track.some((kf) => kf.time === time)) {
573
+ const times = track?.map((kf) => `${kf.time}`).join(", ");
574
+ return {
575
+ ok: false,
576
+ message: times ? `no keyframe at ${time} ms on control "${name}"; its keyframes are at ${times} ms` : `control "${name}" has no keyframes`
577
+ };
578
+ }
579
+ const prevTrack = [...track];
580
+ const execute = () => {
581
+ const next = tracks[name].filter((kf) => kf.time !== time);
582
+ if (next.length === 0) {
583
+ delete tracks[name];
584
+ } else {
585
+ tracks[name] = next;
586
+ }
587
+ };
588
+ const undo = () => {
589
+ tracks[name] = [...prevTrack];
590
+ };
591
+ history.execute({
592
+ name: `Remove keyframe on ${name}`,
593
+ execute,
594
+ undo
595
+ });
596
+ invalidateState();
597
+ return { ok: true };
598
+ },
599
+ clearTrack(name) {
600
+ const disp = checkDisposed();
601
+ if (disp) return disp;
602
+ const validNames = validControlNames();
603
+ if (!validNames.includes(name)) {
604
+ return { ok: false, message: `there is no control "${name}"; the controls are ${validNames.join(", ")}` };
605
+ }
606
+ const prevTrack = tracks[name] ? [...tracks[name]] : void 0;
607
+ if (!prevTrack || prevTrack.length === 0) {
608
+ const animated = Object.keys(tracks).filter((k) => tracks[k]?.length);
609
+ return {
610
+ ok: false,
611
+ message: `control "${name}" is not animated, so there is nothing to clear; ${animated.length ? `the animated controls are ${animated.join(", ")}` : "no control is animated"}`
612
+ };
613
+ }
614
+ const currentValue = valuesAtTime(playheadTime, baseValues, tracks)[name];
615
+ const prevBase = baseValues[name];
616
+ const execute = () => {
617
+ delete tracks[name];
618
+ baseValues[name] = currentValue;
619
+ };
620
+ const undo = () => {
621
+ tracks[name] = [...prevTrack];
622
+ baseValues[name] = prevBase;
623
+ };
624
+ history.execute({
625
+ name: `Clear animation on ${name}`,
626
+ execute,
627
+ undo
628
+ });
629
+ invalidateState();
630
+ return { ok: true };
631
+ },
632
+ setTiming(timing) {
633
+ const disp = checkDisposed();
634
+ if (disp) return disp;
635
+ if (timing.duration !== void 0 && (!(timing.duration > 0) || !Number.isFinite(timing.duration))) {
636
+ return { ok: false, message: "duration must be a finite number > 0" };
637
+ }
638
+ const prevDuration = duration;
639
+ const prevLoop = loop;
640
+ const prevTime = playheadTime;
641
+ const execute = () => {
642
+ if (timing.duration !== void 0) {
643
+ duration = timing.duration;
644
+ if (playheadTime > duration) playheadTime = duration;
645
+ }
646
+ if (timing.loop !== void 0) {
647
+ loop = timing.loop;
648
+ }
649
+ };
650
+ const undo = () => {
651
+ duration = prevDuration;
652
+ loop = prevLoop;
653
+ playheadTime = prevTime;
654
+ };
655
+ history.execute({
656
+ name: "Set timing",
657
+ execute,
658
+ undo
659
+ });
660
+ invalidateState();
661
+ return { ok: true };
662
+ },
663
+ play() {
664
+ if (disposed || playing) return;
665
+ playing = true;
666
+ lastScheduleTime = null;
667
+ cancelSchedule = scheduleFn((now) => {
668
+ if (!playing) return;
669
+ const delta = lastScheduleTime === null ? 0 : Math.max(0, now - lastScheduleTime);
670
+ lastScheduleTime = now;
671
+ let nextTime = playheadTime + delta;
672
+ if (nextTime >= duration) {
673
+ if (loop) {
674
+ nextTime = duration > 0 ? nextTime % duration : 0;
675
+ } else {
676
+ nextTime = duration;
677
+ pauseInternal();
678
+ }
679
+ }
680
+ seekInternal(nextTime);
681
+ });
682
+ invalidateState();
683
+ },
684
+ pause() {
685
+ pauseInternal();
686
+ },
687
+ seek(time) {
688
+ if (disposed) return;
689
+ seekInternal(time);
690
+ },
691
+ undo() {
692
+ if (disposed) return false;
693
+ const changed = history.undo();
694
+ if (changed) invalidateState();
695
+ return changed;
696
+ },
697
+ redo() {
698
+ if (disposed) return false;
699
+ const changed = history.redo();
700
+ if (changed) invalidateState();
701
+ return changed;
702
+ },
703
+ async createPreview(opts) {
704
+ if (disposed) {
705
+ throw new EffectDisposedError(effectId);
706
+ }
707
+ const renderer = await provider.createRenderer(effectId, opts);
708
+ livePreviews.add(renderer);
709
+ const preview = {
710
+ effectId: renderer.effectId,
711
+ width: renderer.width,
712
+ height: renderer.height,
713
+ depth: renderer.depth,
714
+ render: async (req) => {
715
+ if (disposed) throw new EffectDisposedError(effectId);
716
+ const effectiveValues = req.values ?? valuesAtTime(req.time, baseValues, tracks);
717
+ const pixels = await renderer.render({
718
+ ...req,
719
+ values: effectiveValues
720
+ });
721
+ if (compiled !== true) {
722
+ compiled = true;
723
+ invalidateState();
724
+ }
725
+ return pixels;
726
+ },
727
+ dispose: () => {
728
+ livePreviews.delete(renderer);
729
+ renderer.dispose();
730
+ }
731
+ };
732
+ return preview;
733
+ },
734
+ exportDocument() {
735
+ const currentValues = valuesAtTime(playheadTime, baseValues, tracks);
736
+ return {
737
+ version: 1,
738
+ providerId: provider.id,
739
+ effectId,
740
+ name: initialDoc?.name ?? description.name,
741
+ values: { ...currentValues },
742
+ tracks: JSON.parse(JSON.stringify(tracks)),
743
+ presets: JSON.parse(JSON.stringify(savedPresets)),
744
+ duration,
745
+ loop
746
+ };
747
+ },
748
+ dispose() {
749
+ if (disposed) return;
750
+ disposed = true;
751
+ pauseInternal();
752
+ listeners.clear();
753
+ for (const p of livePreviews) {
754
+ try {
755
+ p.dispose();
756
+ } catch {
757
+ }
758
+ }
759
+ livePreviews.clear();
760
+ }
761
+ };
762
+ return session;
763
+ }
764
+
765
+ export {
766
+ createEffectAuthoringSession
767
+ };