@pixerate/editor 0.252.0 → 0.254.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 (73) hide show
  1. package/dist/animation-editor/index.cjs +53 -53
  2. package/dist/animation-editor/index.js +2 -2
  3. package/dist/brushes/index.cjs +229 -2014
  4. package/dist/brushes/index.d.cts +5 -1
  5. package/dist/brushes/index.d.ts +5 -1
  6. package/dist/brushes/index.js +236 -2021
  7. package/dist/brushes/patch.d.cts +23 -0
  8. package/dist/brushes/patch.d.ts +23 -0
  9. package/dist/brushes/ui.d.cts +105 -0
  10. package/dist/brushes/ui.d.ts +105 -0
  11. package/dist/canvas/index.cjs +7 -7
  12. package/dist/canvas/index.js +1 -1
  13. package/dist/{chunk-QKXO6JPP.cjs → chunk-2OQGPEHH.cjs} +14 -14
  14. package/dist/chunk-34KLWUH4.cjs +2284 -0
  15. package/dist/{chunk-Z3BRH4JK.cjs → chunk-557BDWKO.cjs} +6 -6
  16. package/dist/{chunk-ZBCT5MEA.cjs → chunk-CBVNCWJM.cjs} +7 -7
  17. package/dist/{chunk-226QDRO3.js → chunk-DHQPRKXF.js} +1 -1
  18. package/dist/{chunk-ABL7IWFZ.js → chunk-GBEF2D4P.js} +1 -1
  19. package/dist/{chunk-ISIO63Q5.cjs → chunk-L2JTLW6B.cjs} +150 -25
  20. package/dist/{chunk-JA57JIT5.js → chunk-MV7CYFMJ.js} +133 -8
  21. package/dist/{chunk-5E2Q7ZHI.js → chunk-MWUZCEFT.js} +1 -1
  22. package/dist/chunk-QPHNVZHO.js +2284 -0
  23. package/dist/effects/conformance.d.cts +1 -0
  24. package/dist/effects/conformance.d.ts +1 -0
  25. package/dist/effects/fake.d.cts +7 -1
  26. package/dist/effects/fake.d.ts +7 -1
  27. package/dist/effects/helpers.d.cts +36 -4
  28. package/dist/effects/helpers.d.ts +36 -4
  29. package/dist/effects/index.cjs +331 -102
  30. package/dist/effects/index.js +238 -9
  31. package/dist/effects/types.d.cts +84 -5
  32. package/dist/effects/types.d.ts +84 -5
  33. package/dist/image-editor/index.cjs +15 -15
  34. package/dist/image-editor/index.js +1 -1
  35. package/dist/paint-editor/brush-controls.d.cts +57 -0
  36. package/dist/paint-editor/brush-controls.d.ts +57 -0
  37. package/dist/paint-editor/brush-library-tools.d.cts +15 -0
  38. package/dist/paint-editor/brush-library-tools.d.ts +15 -0
  39. package/dist/paint-editor/brush-library.d.cts +29 -0
  40. package/dist/paint-editor/brush-library.d.ts +29 -0
  41. package/dist/paint-editor/controller.d.cts +76 -17
  42. package/dist/paint-editor/controller.d.ts +76 -17
  43. package/dist/paint-editor/index.cjs +543 -858
  44. package/dist/paint-editor/index.d.cts +1 -0
  45. package/dist/paint-editor/index.d.ts +1 -0
  46. package/dist/paint-editor/index.js +329 -644
  47. package/dist/paint-editor/serialize.d.cts +7 -4
  48. package/dist/paint-editor/serialize.d.ts +7 -4
  49. package/dist/paint-editor/sticker-tools.d.cts +1 -1
  50. package/dist/paint-editor/sticker-tools.d.ts +1 -1
  51. package/dist/paint-editor/strokes.d.cts +10 -1
  52. package/dist/paint-editor/strokes.d.ts +10 -1
  53. package/dist/paint-editor/tools.d.cts +1 -1
  54. package/dist/paint-editor/tools.d.ts +1 -1
  55. package/dist/paint-editor/ui.d.cts +3 -51
  56. package/dist/paint-editor/ui.d.ts +3 -51
  57. package/dist/paint-editor/wet-tools.d.cts +1 -1
  58. package/dist/paint-editor/wet-tools.d.ts +1 -1
  59. package/dist/vector/index.cjs +6 -6
  60. package/dist/vector/index.js +1 -1
  61. package/dist/video-editor/index.cjs +344 -344
  62. package/dist/video-editor/index.js +4 -4
  63. package/package.json +1 -1
  64. package/skills/pixerate-brushes/SKILL.md +5 -3
  65. package/skills/pixerate-paint-editor/SKILL.md +26 -24
  66. package/skills/pixerate-paint-editor/references/brush-facets.md +30 -0
  67. package/skills/pixerate-paint-editor/references/gradient-paint.md +1 -1
  68. package/skills/pixerate-paint-editor/references/light-paint.md +2 -2
  69. package/skills/pixerate-paint-editor/references/oils.md +1 -1
  70. package/skills/pixerate-paint-editor/references/particle-brushes.md +8 -5
  71. package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -1
  72. package/skills/pixerate-stickers/SKILL.md +1 -1
  73. package/skills/pixerate-video-editor/SKILL.md +1 -1
@@ -4,7 +4,7 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkISIO63Q5cjs = require('./chunk-ISIO63Q5.cjs');
7
+ var _chunkL2JTLW6Bcjs = require('./chunk-L2JTLW6B.cjs');
8
8
 
9
9
  // src/effects/templates.ts
10
10
  var SLOT_NAME = /^[A-Za-z][\w-]*$/;
@@ -133,7 +133,7 @@ function searchTemplateSummaries(catalog, options = {}) {
133
133
  const words = (_nullishCoalesce(options.query, () => ( ""))).toLowerCase().split(/\s+/).filter(Boolean);
134
134
  const scored = [];
135
135
  for (const t of catalog) {
136
- if (!_chunkISIO63Q5cjs.hasEffectTags.call(void 0, t, options.tags)) continue;
136
+ if (!_chunkL2JTLW6Bcjs.hasEffectTags.call(void 0, t, options.tags)) continue;
137
137
  let score = 0;
138
138
  const name = t.name.toLowerCase();
139
139
  const rest = [t.id, ..._nullishCoalesce(t.tags, () => ( [])), t.description].join(" ").toLowerCase();
@@ -200,7 +200,7 @@ function checkEffectSequence(sequence, catalog) {
200
200
  const p = effectSubjectProblem(layer.subject);
201
201
  if (p) problems.push(`${where}: ${p}.`);
202
202
  }
203
- if (layer.role !== void 0 && !_chunkISIO63Q5cjs.EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: role is one of ${_chunkISIO63Q5cjs.EFFECT_LAYER_ROLES.join(", ")}.`);
203
+ if (layer.role !== void 0 && !_chunkL2JTLW6Bcjs.EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: role is one of ${_chunkL2JTLW6Bcjs.EFFECT_LAYER_ROLES.join(", ")}.`);
204
204
  if (layer.rect !== void 0 && !isRect(layer.rect)) problems.push(`${where}: rect must be { x, y, width, height } with width and height > 0.`);
205
205
  for (const k of ["from", "until"]) if (layer[k] !== void 0 && !isLayerTime(layer[k])) problems.push(`${where}: ${k} is ms \u2265 0 or { shots: n } with n \u2265 1.`);
206
206
  if (layer.opacity !== void 0 && !(typeof layer.opacity === "number" && layer.opacity >= 0 && layer.opacity <= 1)) problems.push(`${where}: opacity is 0\u20131.`);
@@ -235,8 +235,8 @@ function effectSequenceSlots(sequence) {
235
235
  return [...out];
236
236
  }
237
237
  async function fillEffectTemplate(provider, id, values = {}, options = {}) {
238
- if (!provider) throw new (0, _chunkISIO63Q5cjs.NoEffectsProviderError)();
239
- if (!provider.capabilities.supportsTemplates || !provider.describeTemplate || !provider.instantiateTemplate) throw new (0, _chunkISIO63Q5cjs.EffectUnsupportedError)(`${provider.name} has no sequence templates (capabilities.supportsTemplates is not set).`);
238
+ if (!provider) throw new (0, _chunkL2JTLW6Bcjs.NoEffectsProviderError)();
239
+ if (!provider.capabilities.supportsTemplates || !provider.describeTemplate || !provider.instantiateTemplate) throw new (0, _chunkL2JTLW6Bcjs.EffectUnsupportedError)(`${provider.name} has no sequence templates (capabilities.supportsTemplates is not set).`);
240
240
  const template = await provider.describeTemplate(id);
241
241
  const errors = checkTemplateValues(template.slots, values);
242
242
  if (errors.length) throw new Error(`${errors.join(" ")} describe_template lists what each slot takes.`);
@@ -256,7 +256,7 @@ function effectSequenceTiming(sequence) {
256
256
  const leadIns = [];
257
257
  let t = 0;
258
258
  sequence.shots.forEach((shot, i) => {
259
- const transition = i > 0 && shot.transition ? _nullishCoalesce(shot.transition.ms, () => ( _chunkISIO63Q5cjs.EFFECT_TRANSITION_MS)) : 0;
259
+ const transition = i > 0 && shot.transition ? _nullishCoalesce(shot.transition.ms, () => ( _chunkL2JTLW6Bcjs.EFFECT_TRANSITION_MS)) : 0;
260
260
  const move = typeof _optionalChain([shot, 'access', _4 => _4.camera, 'optionalAccess', _5 => _5.in]) === "object" && i > 0 ? _nullishCoalesce(shot.camera.in.ms, () => ( 0)) : 0;
261
261
  const lead = Math.max(transition, move);
262
262
  starts.push(t);
@@ -7,7 +7,7 @@ var _chunkPY4GWSE2cjs = require('./chunk-PY4GWSE2.cjs');
7
7
 
8
8
 
9
9
 
10
- var _chunkISIO63Q5cjs = require('./chunk-ISIO63Q5.cjs');
10
+ var _chunkL2JTLW6Bcjs = require('./chunk-L2JTLW6B.cjs');
11
11
 
12
12
 
13
13
  var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
@@ -1316,12 +1316,12 @@ function videoEffectLayerKey(layer, width, height) {
1316
1316
  }
1317
1317
  function mixEffectPixels(a, b, k) {
1318
1318
  if (k >= 1) return b;
1319
- const out = _chunkISIO63Q5cjs.createEffectPixels.call(void 0, a.width, a.height);
1319
+ const out = _chunkL2JTLW6Bcjs.createEffectPixels.call(void 0, a.width, a.height);
1320
1320
  for (let i = 0; i < out.data.length; i++) out.data[i] = a.data[i] + (b.data[i] - a.data[i]) * k;
1321
1321
  return out;
1322
1322
  }
1323
1323
  function overEffectPixels(base, top, opacity = 1) {
1324
- const out = _chunkISIO63Q5cjs.createEffectPixels.call(void 0, base.width, base.height);
1324
+ const out = _chunkL2JTLW6Bcjs.createEffectPixels.call(void 0, base.width, base.height);
1325
1325
  const o = out.data;
1326
1326
  const b = base.data;
1327
1327
  const t = top.data;
@@ -1366,7 +1366,7 @@ var VideoEffectsEngine = (_class2 = class {
1366
1366
  __init10() {this.generation = 0}
1367
1367
 
1368
1368
  constructor(options = {}) {;_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);
1369
- this.slot = _nullishCoalesce(options.slot, () => ( new (0, _chunkISIO63Q5cjs.EffectsSlot)()));
1369
+ this.slot = _nullishCoalesce(options.slot, () => ( new (0, _chunkL2JTLW6Bcjs.EffectsSlot)()));
1370
1370
  this.maxEntries = _nullishCoalesce(options.maxEntries, () => ( 120));
1371
1371
  this.toImage = _nullishCoalesce(options.toImage, () => ( defaultToImage));
1372
1372
  this.pool = new (0, _chunkPY4GWSE2cjs.EffectRendererPool)({ maxRenderers: options.maxRenderers });
@@ -1508,7 +1508,7 @@ function checkVideoEffectPlace(d, place) {
1508
1508
  if (d.kind === "motion" || !d.uses.some((u) => allowed.includes(u))) {
1509
1509
  const where = place === "clip" ? "on a clip" : place === "transition" ? "as a transition" : "as an effect clip";
1510
1510
  const instead = d.uses.includes("transition") ? "set_video_effect_transition" : d.uses.some((u) => VIDEO_EFFECT_PLACES.generator.includes(u)) ? "add_video_effect_clip" : d.uses.some((u) => u === "filter") ? "apply_effect" : null;
1511
- throw new (0, _chunkISIO63Q5cjs.EffectUnsupportedError)(
1511
+ throw new (0, _chunkL2JTLW6Bcjs.EffectUnsupportedError)(
1512
1512
  `"${d.id}" (${d.name}) is a ${d.uses.join(" / ")} effect and cannot be used ${where}, which takes ${allowed.join(" or ")} effects${instead ? `; use ${instead} instead` : ""}. search_effects with use "${allowed[0]}" lists ones that fit.`,
1513
1513
  d.id
1514
1514
  );
@@ -1529,12 +1529,12 @@ function checkVideoEffectValues(d, values, options = {}) {
1529
1529
  else if (type !== "number" && type !== "integer") errors.push({ path: `/${k}`, message: "only number controls can be keyframed." });
1530
1530
  else for (const [i, key] of v.keys.entries()) for (const e of _chunkWOFXZ3XUcjs.validateJsonSchema.call(void 0, key.v, schema)) errors.push({ path: `/${k}/keys/${i}/v`, message: e.message });
1531
1531
  }
1532
- for (const e of _chunkISIO63Q5cjs.effectValueErrors.call(void 0, d, plain)) {
1532
+ for (const e of _chunkL2JTLW6Bcjs.effectValueErrors.call(void 0, d, plain)) {
1533
1533
  const name = e.path.split("/")[1];
1534
1534
  const about = name && _optionalChain([d, 'access', _46 => _46.controls, 'access', _47 => _47.properties, 'access', _48 => _48[name], 'optionalAccess', _49 => _49.description]);
1535
1535
  errors.push(about ? { path: e.path, message: `${e.message} (${about})` } : e);
1536
1536
  }
1537
- if (errors.length) throw new (0, _chunkISIO63Q5cjs.EffectControlError)(d.id, errors);
1537
+ if (errors.length) throw new (0, _chunkL2JTLW6Bcjs.EffectControlError)(d.id, errors);
1538
1538
  return { ..._nullishCoalesce(options.base, () => ( d.defaults)), ..._nullishCoalesce(values, () => ( {})) };
1539
1539
  }
1540
1540
 
@@ -4,7 +4,7 @@ import {
4
4
  EffectUnsupportedError,
5
5
  NoEffectsProviderError,
6
6
  hasEffectTags
7
- } from "./chunk-JA57JIT5.js";
7
+ } from "./chunk-MV7CYFMJ.js";
8
8
 
9
9
  // src/effects/templates.ts
10
10
  var SLOT_NAME = /^[A-Za-z][\w-]*$/;
@@ -3,7 +3,7 @@ import {
3
3
  EffectDisposedError,
4
4
  EffectUnsupportedError,
5
5
  effectValueErrors
6
- } from "./chunk-JA57JIT5.js";
6
+ } from "./chunk-MV7CYFMJ.js";
7
7
  import {
8
8
  HistoryManager
9
9
  } from "./chunk-NS62CQ2B.js";
@@ -3,11 +3,15 @@
3
3
  var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
4
4
 
5
5
  // src/effects/types.ts
6
- var EFFECT_KINDS = ["shader", "vector", "motion", "camera"];
7
- var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera"];
6
+ var EFFECT_KINDS = ["shader", "vector", "motion", "camera", "timing"];
7
+ var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera", "timing"];
8
8
  var CAMERA_MOVE_ROLES = ["transition", "hold"];
9
9
  var EFFECT_ACCEPTS = ["path"];
10
10
  var EFFECT_PATH_FORMAT = "svg-path";
11
+ var TIMING_DEFAULT_FPS = 30;
12
+ var TIMING_MAX_FRAMES = 128;
13
+ var TIMING_MAX_FPS = 240;
14
+ var TIMING_BLENDS = ["average", "lighten"];
11
15
  var EFFECT_SUBJECT_KINDS = ["image", "video", "svg", "text", "slot"];
12
16
  var EFFECT_LAYER_ROLES = ["background", "subject", "overlay"];
13
17
  var EFFECT_TRANSITION_MS = 800;
@@ -87,17 +91,18 @@ function resolveEffectValues(description, values) {
87
91
  if (errors.length) throw new EffectControlError(description.id, errors);
88
92
  return { ...description.defaults, ..._nullishCoalesce(values, () => ( {})) };
89
93
  }
94
+ var PIXELLESS_KINDS = ["motion", "camera", "timing"];
90
95
  function effectOffered(e, uses) {
91
- if (!uses) return e.kind !== "motion" && e.kind !== "camera";
92
- if (e.kind === "motion" && !uses.includes("motion")) return false;
93
- if (e.kind === "camera" && !uses.includes("camera")) return false;
96
+ const pixelless = PIXELLESS_KINDS.includes(e.kind);
97
+ if (!uses) return !pixelless;
98
+ if (pixelless && !uses.includes(e.kind)) return false;
94
99
  return e.uses.some((u) => uses.includes(u));
95
100
  }
96
101
  function normaliseEffectTag(tag) {
97
102
  return tag.trim().toLowerCase().replace(/[\s_-]+/g, "-");
98
103
  }
99
104
  function hasEffectTags(e, tags) {
100
- if (!_optionalChain([tags, 'optionalAccess', _ => _.length])) return true;
105
+ if (!_optionalChain([tags, 'optionalAccess', _2 => _2.length])) return true;
101
106
  const own = new Set((_nullishCoalesce(e.tags, () => ( []))).map(normaliseEffectTag));
102
107
  return tags.every((t) => own.has(normaliseEffectTag(t)));
103
108
  }
@@ -106,10 +111,10 @@ function countEffectTags(effects) {
106
111
  for (const e of effects) for (const tag of new Set((_nullishCoalesce(e.tags, () => ( []))).map(normaliseEffectTag))) if (tag) counts.set(tag, (_nullishCoalesce(counts.get(tag), () => ( 0))) + 1);
107
112
  return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
108
113
  }
109
- var matchesFilters = (e, o) => (!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use)) && hasEffectTags(e, o.tags) && (!_optionalChain([o, 'access', _2 => _2.accepts, 'optionalAccess', _3 => _3.length]) || o.accepts.every((a) => _optionalChain([e, 'access', _4 => _4.accepts, 'optionalAccess', _5 => _5.includes, 'call', _6 => _6(a)])));
114
+ var matchesFilters = (e, o) => (!o.kind || e.kind === o.kind) && (!o.category || e.category === o.category) && (!o.use || e.uses.includes(o.use)) && hasEffectTags(e, o.tags) && (!_optionalChain([o, 'access', _3 => _3.accepts, 'optionalAccess', _4 => _4.length]) || o.accepts.every((a) => _optionalChain([e, 'access', _5 => _5.accepts, 'optionalAccess', _6 => _6.includes, 'call', _7 => _7(a)])));
110
115
  function pathControlsOf(d) {
111
116
  const isPath = (s) => !!s && (s.format === EFFECT_PATH_FORMAT || [..._nullishCoalesce(s.anyOf, () => ( [])), ..._nullishCoalesce(s.oneOf, () => ( []))].some((x) => isPath(x)));
112
- return Object.entries(_nullishCoalesce(_optionalChain([d, 'access', _7 => _7.controls, 'optionalAccess', _8 => _8.properties]), () => ( {}))).filter(([, s]) => isPath(s)).map(([name]) => name);
117
+ return Object.entries(_nullishCoalesce(_optionalChain([d, 'access', _8 => _8.controls, 'optionalAccess', _9 => _9.properties]), () => ( {}))).filter(([, s]) => isPath(s)).map(([name]) => name);
113
118
  }
114
119
  function searchEffectSummaries(catalog, query = "", options = {}) {
115
120
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -170,7 +175,7 @@ function applyEffectMask(result, mask, source) {
170
175
  }
171
176
  function layerImageInput(d) {
172
177
  const images = d.inputs.filter((i) => i.kind === "image");
173
- return _nullishCoalesce(_optionalChain([(_nullishCoalesce(_nullishCoalesce(images.find((i) => i.name === "source"), () => ( images.find((i) => i.required))), () => ( images[0]))), 'optionalAccess', _9 => _9.name]), () => ( null));
178
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(_nullishCoalesce(images.find((i) => i.name === "source"), () => ( images.find((i) => i.required))), () => ( images[0]))), 'optionalAccess', _10 => _10.name]), () => ( null));
174
179
  }
175
180
  var EffectsSlot = (_class = class {constructor() { _class.prototype.__init.call(this);_class.prototype.__init2.call(this); }
176
181
  __init() {this.current = null}
@@ -238,7 +243,7 @@ async function sampleEffectCameraMove(provider, id, request) {
238
243
  }
239
244
  var cameraMoveField = (role) => role === "transition" ? "in" : "during";
240
245
  function cameraMovesFor(catalog, role) {
241
- return catalog.filter((e) => e.kind === "camera" && _optionalChain([e, 'access', _10 => _10.camera, 'optionalAccess', _11 => _11.role]) === role);
246
+ return catalog.filter((e) => e.kind === "camera" && _optionalChain([e, 'access', _11 => _11.camera, 'optionalAccess', _12 => _12.role]) === role);
242
247
  }
243
248
  function cameraMoveIdList(catalog, role) {
244
249
  const ids = cameraMovesFor(catalog, role).map((e) => JSON.stringify(e.id));
@@ -299,6 +304,117 @@ function cameraBoxOfView(view, stage) {
299
304
  const height = stage.height / view.zoom;
300
305
  return { x: -view.scrollX, y: -view.scrollY, width, height };
301
306
  }
307
+ function checkTimingFootage(footageMs, id) {
308
+ if (typeof footageMs !== "number" || !Number.isFinite(footageMs) || footageMs <= 0) {
309
+ throw new EffectUnsupportedError(`The footage's length is a number of ms above 0; it was ${String(footageMs)}.`, id);
310
+ }
311
+ }
312
+ function checkTimingRequest(request, id) {
313
+ if (!request || typeof request !== "object") throw new EffectUnsupportedError("A timing request is { time, footage, fps? }: ms into the retimed clip, the footage's length in ms, and its frame rate.", id);
314
+ checkTimingFootage(request.footage, id);
315
+ const fps = _nullishCoalesce(request.fps, () => ( TIMING_DEFAULT_FPS));
316
+ if (typeof fps !== "number" || !Number.isFinite(fps) || fps <= 0 || fps > TIMING_MAX_FPS) {
317
+ throw new EffectUnsupportedError(`The footage's frame rate is above 0 and at most ${TIMING_MAX_FPS} frames per second; it was ${String(fps)}.`, id);
318
+ }
319
+ if (typeof request.time !== "number" || !Number.isFinite(request.time)) throw new EffectUnsupportedError(`The time is a number of ms into the retimed clip; it was ${String(request.time)}.`, id);
320
+ return fps;
321
+ }
322
+ var lastFrameOf = (footageMs, fps) => Math.max(0, Math.ceil(footageMs * fps / 1e3 - 1e-9) - 1);
323
+ function normaliseTimingSample(raw, request, blend = "average") {
324
+ const fps = _nullishCoalesce(request.fps, () => ( TIMING_DEFAULT_FPS));
325
+ const frameMs = 1e3 / fps;
326
+ const last = lastFrameOf(request.footage, fps);
327
+ const frameOf = (t) => Math.min(last, Math.max(0, Math.floor(t / frameMs + 1e-6)));
328
+ const weights = /* @__PURE__ */ new Map();
329
+ for (const f of raw) {
330
+ if (!Number.isFinite(f.time) || !Number.isFinite(f.weight) || f.weight <= 0) continue;
331
+ const i = frameOf(f.time);
332
+ weights.set(i, (_nullishCoalesce(weights.get(i), () => ( 0))) + f.weight);
333
+ }
334
+ if (!weights.size) weights.set(Number.isFinite(request.time) ? frameOf(request.time) : 0, 1);
335
+ let frames = [...weights.entries()].sort((a, b) => a[0] - b[0]);
336
+ if (frames.length > TIMING_MAX_FRAMES) {
337
+ const stride = frames.length / TIMING_MAX_FRAMES;
338
+ frames = Array.from({ length: TIMING_MAX_FRAMES }, (_, k) => frames[Math.floor(k * stride)]);
339
+ }
340
+ const total = frames.reduce((sum, [, w]) => sum + w, 0);
341
+ return { frames: frames.map(([i, w]) => ({ time: i * 1e3 / fps, weight: w / total })), blend };
342
+ }
343
+ function timingSampleProblems(sample, request) {
344
+ if (!sample || typeof sample !== "object") return ["the sample is not an object { frames, blend }"];
345
+ const { frames, blend } = sample;
346
+ const problems = [];
347
+ if (!TIMING_BLENDS.includes(blend)) problems.push(`blend "${String(blend)}" is not one of ${TIMING_BLENDS.join(", ")}`);
348
+ if (!Array.isArray(frames) || !frames.length) return [...problems, "frames must be a list of at least one { time, weight }"];
349
+ if (frames.length > TIMING_MAX_FRAMES) problems.push(`it has ${frames.length} frames; at most ${TIMING_MAX_FRAMES} are allowed`);
350
+ const fps = _nullishCoalesce(request.fps, () => ( TIMING_DEFAULT_FPS));
351
+ const last = lastFrameOf(request.footage, fps);
352
+ let total = 0;
353
+ let previous = -Infinity;
354
+ for (const f of frames) {
355
+ if (!f || !Number.isFinite(f.time) || !Number.isFinite(f.weight)) {
356
+ problems.push(`frame ${JSON.stringify(f)} needs a finite time and weight`);
357
+ continue;
358
+ }
359
+ const index = f.time * fps / 1e3;
360
+ if (Math.abs(index - Math.round(index)) > 1e-6) problems.push(`frame time ${f.time} is not the start of a frame at ${fps} fps (index \xD7 ${+(1e3 / fps).toFixed(6)} ms)`);
361
+ else if (Math.round(index) < 0 || Math.round(index) > last) problems.push(`frame time ${f.time} is outside the footage (frames 0 to ${last}, the last at ${+(last * 1e3 / fps).toFixed(6)} ms)`);
362
+ if (f.time <= previous) problems.push(`frame times must be distinct and earliest first; ${f.time} follows ${previous}`);
363
+ previous = f.time;
364
+ if (!(f.weight > 0)) problems.push(`frame at ${f.time} has weight ${f.weight}; weights are > 0`);
365
+ total += f.weight;
366
+ }
367
+ if (Math.abs(total - 1) > 1e-6) problems.push(`weights add up to ${total}, not 1`);
368
+ return problems;
369
+ }
370
+ function blendTimingFrames(frames, blend, out) {
371
+ if (!frames.length) throw new EffectUnsupportedError("There are no frames to blend; a timing sample has at least one.");
372
+ const { width, height } = frames[0].pixels;
373
+ for (const f of frames) {
374
+ if (f.pixels.width !== width || f.pixels.height !== height) throw new EffectUnsupportedError(`A ${f.pixels.width}\xD7${f.pixels.height} frame cannot be blended with ${width}\xD7${height} frames.`);
375
+ }
376
+ const target = _nullishCoalesce(out, () => ( createEffectPixels(width, height)));
377
+ if (target.width !== width || target.height !== height) throw new EffectUnsupportedError(`The output is ${target.width}\xD7${target.height}; the frames are ${width}\xD7${height}.`);
378
+ const o = target.data;
379
+ if (frames.length === 1) {
380
+ o.set(frames[0].pixels.data);
381
+ return target;
382
+ }
383
+ const s = new Float64Array(o.length);
384
+ if (blend === "lighten") {
385
+ const heaviest = Math.max(...frames.map((f) => f.weight));
386
+ for (const f of frames) {
387
+ const k = heaviest > 0 ? f.weight / heaviest : 1;
388
+ const p = f.pixels.data;
389
+ for (let i = 0; i < s.length; i += 4) {
390
+ const a = p[i + 3] / 255 * k;
391
+ for (let c = 0; c < 3; c++) if (p[i + c] * a > s[i + c]) s[i + c] = p[i + c] * a;
392
+ if (a > s[i + 3]) s[i + 3] = a;
393
+ }
394
+ }
395
+ for (let i = 0; i < s.length; i += 4) {
396
+ for (let c = 0; c < 3; c++) o[i + c] = Math.round(s[i + c]);
397
+ o[i + 3] = Math.round(s[i + 3] * 255);
398
+ }
399
+ return target;
400
+ }
401
+ let total = 0;
402
+ for (const f of frames) {
403
+ total += f.weight;
404
+ const p = f.pixels.data;
405
+ for (let i = 0; i < s.length; i += 4) {
406
+ const a = p[i + 3] * f.weight;
407
+ for (let c = 0; c < 3; c++) s[i + c] += p[i + c] * a;
408
+ s[i + 3] += a;
409
+ }
410
+ }
411
+ for (let i = 0; i < s.length; i += 4) {
412
+ const a = s[i + 3];
413
+ for (let c = 0; c < 3; c++) o[i + c] = a > 0 ? Math.round(s[i + c] / a) : 0;
414
+ o[i + 3] = total > 0 ? Math.round(a / total) : 0;
415
+ }
416
+ return target;
417
+ }
302
418
 
303
419
  // src/effects/tools.ts
304
420
  var ANNOTATIONS = {
@@ -328,12 +444,12 @@ var summaryOut = (e) => ({
328
444
  category: e.category,
329
445
  uses: e.uses,
330
446
  ...e.description ? { description: e.description } : {},
331
- ..._optionalChain([e, 'access', _12 => _12.tags, 'optionalAccess', _13 => _13.length]) ? { tags: e.tags } : {},
447
+ ..._optionalChain([e, 'access', _13 => _13.tags, 'optionalAccess', _14 => _14.length]) ? { tags: e.tags } : {},
332
448
  transparent: e.transparent,
333
449
  loop: e.loop,
334
450
  ...e.duration !== void 0 ? { duration: e.duration } : {},
335
451
  ...e.camera ? { camera: e.camera } : {},
336
- ..._optionalChain([e, 'access', _14 => _14.accepts, 'optionalAccess', _15 => _15.length]) ? { accepts: e.accepts } : {}
452
+ ..._optionalChain([e, 'access', _15 => _15.accepts, 'optionalAccess', _16 => _16.length]) ? { accepts: e.accepts } : {}
337
453
  });
338
454
  function createEffectToolProvider(host) {
339
455
  let catalog = null;
@@ -341,7 +457,7 @@ function createEffectToolProvider(host) {
341
457
  const notify = () => listeners.forEach((l) => l());
342
458
  const catalogFor = (provider) => {
343
459
  if (!provider) return null;
344
- if (_optionalChain([catalog, 'optionalAccess', _16 => _16.provider]) !== provider) {
460
+ if (_optionalChain([catalog, 'optionalAccess', _17 => _17.provider]) !== provider) {
345
461
  const entry = { provider, effects: null };
346
462
  catalog = entry;
347
463
  Promise.resolve().then(() => provider.list()).then((effects) => {
@@ -378,7 +494,7 @@ function createEffectToolProvider(host) {
378
494
  });
379
495
  const effectIdSchema = (uses) => {
380
496
  const open = host.applyImageInputs && uses === host.applyUses;
381
- const effects = open ? void 0 : _optionalChain([catalogFor, 'call', _17 => _17(host.provider()), 'optionalAccess', _18 => _18.filter, 'call', _19 => _19((e) => effectOffered(e, uses))]);
497
+ const effects = open ? void 0 : _optionalChain([catalogFor, 'call', _18 => _18(host.provider()), 'optionalAccess', _19 => _19.filter, 'call', _20 => _20((e) => effectOffered(e, uses))]);
382
498
  const base = `An effect id from search_effects (use ${uses.map((u) => `"${u}"`).join(" or ")}${open ? `, or any effect whose describe_effect inputs take an image: the ${host.target.noun} is its picture` : ""}).`;
383
499
  return effects && effects.length && effects.length <= ENUM_LIMIT ? { type: "string", enum: effects.map((e) => e.id), description: `${base} One of: ${effects.map((e) => `${e.id} (${e.name})`).join(", ")}.` } : { type: "string", minLength: 1, description: base };
384
500
  };
@@ -392,12 +508,12 @@ function createEffectToolProvider(host) {
392
508
  {
393
509
  name: "search_effects",
394
510
  title: "Search effects",
395
- description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }], total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
511
+ description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera, timing), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }] (timing effects, kind timing, retime video footage and have no duration: their length depends on the footage), total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
396
512
  inputSchema: object({
397
513
  query: { type: "string", description: "Words to match (default: every effect)." },
398
- kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art) or camera (moves a scene's camera; no pixels)." },
514
+ kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art), camera (moves a scene's camera; no pixels) or timing (retimes video footage: slow motion, reverse, freeze, long exposure; no pixels of its own)." },
399
515
  category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
400
- use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera." },
516
+ use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera, timing retimes video footage." },
401
517
  tags: { type: "array", items: { type: "string", minLength: 1 }, description: "Only effects carrying every one of these tags, as search_effects lists them (any case; spaces count as hyphens)." },
402
518
  limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." },
403
519
  accepts: { type: "array", items: { type: "string", enum: [...EFFECT_ACCEPTS], description: "path: the effect has a control that draws along a path given at run time." }, description: "Only effects that take every one of these (path: effects that draw along a path you give; describe_effect names the controls in pathControls)." }
@@ -408,8 +524,8 @@ function createEffectToolProvider(host) {
408
524
  if (!p) return { available: false, reason: NO_PROVIDER, effects: [], total: 0 };
409
525
  const tags = args.tags;
410
526
  const accepts = args.accepts;
411
- let found = (await explain(async () => p.search(_nullishCoalesce(args.query, () => ( "")), { kind: args.kind, category: args.category, use: args.use, tags, ..._optionalChain([accepts, 'optionalAccess', _20 => _20.length]) ? { accepts } : {} }))).filter((e) => hasEffectTags(e, tags));
412
- if (_optionalChain([accepts, 'optionalAccess', _21 => _21.includes, 'call', _22 => _22("path")])) {
527
+ let found = (await explain(async () => p.search(_nullishCoalesce(args.query, () => ( "")), { kind: args.kind, category: args.category, use: args.use, tags, ..._optionalChain([accepts, 'optionalAccess', _21 => _21.length]) ? { accepts } : {} }))).filter((e) => hasEffectTags(e, tags));
528
+ if (_optionalChain([accepts, 'optionalAccess', _22 => _22.includes, 'call', _23 => _23("path")])) {
413
529
  const kept = [];
414
530
  for (const e of found) {
415
531
  if (e.accepts) {
@@ -420,7 +536,7 @@ function createEffectToolProvider(host) {
420
536
  }
421
537
  const limit = _nullishCoalesce(args.limit, () => ( 20));
422
538
  const out = { available: true, provider: { id: p.id, name: p.name, capabilities: p.capabilities }, effects: found.slice(0, limit).map(summaryOut), total: found.length, tags: countEffectTags(found).slice(0, TAG_LIMIT) };
423
- if (_optionalChain([tags, 'optionalAccess', _23 => _23.length]) && !found.length) {
539
+ if (_optionalChain([tags, 'optionalAccess', _24 => _24.length]) && !found.length) {
424
540
  const known = countEffectTags(await explain(async () => p.list()));
425
541
  const have = new Set(known.map((k) => k.tag));
426
542
  const unknown = tags.filter((t) => !have.has(normaliseEffectTag(t)));
@@ -536,16 +652,16 @@ function createEffectToolProvider(host) {
536
652
  ...host.description ? { description: host.description } : {},
537
653
  ...host.skill ? { skill: host.skill } : {},
538
654
  tools: () => {
539
- const extra = _nullishCoalesce(_optionalChain([host, 'access', _24 => _24.extraTools, 'optionalCall', _25 => _25(context)]), () => ( []));
540
- if (host.readOnlyReason()) return [...readTools(), ...extra.filter((t) => _optionalChain([t, 'access', _26 => _26.annotations, 'optionalAccess', _27 => _27.readOnlyHint]))];
655
+ const extra = _nullishCoalesce(_optionalChain([host, 'access', _25 => _25.extraTools, 'optionalCall', _26 => _26(context)]), () => ( []));
656
+ if (host.readOnlyReason()) return [...readTools(), ...extra.filter((t) => _optionalChain([t, 'access', _27 => _27.annotations, 'optionalAccess', _28 => _28.readOnlyHint]))];
541
657
  return [...readTools(), ...writeTools(), ...extra];
542
658
  },
543
659
  subscribe: (l) => {
544
660
  listeners.add(l);
545
- const off = _optionalChain([host, 'access', _28 => _28.subscribe, 'optionalCall', _29 => _29(l)]);
661
+ const off = _optionalChain([host, 'access', _29 => _29.subscribe, 'optionalCall', _30 => _30(l)]);
546
662
  return () => {
547
663
  listeners.delete(l);
548
- _optionalChain([off, 'optionalCall', _30 => _30()]);
664
+ _optionalChain([off, 'optionalCall', _31 => _31()]);
549
665
  };
550
666
  }
551
667
  };
@@ -598,4 +714,13 @@ function createEffectToolProvider(host) {
598
714
 
599
715
 
600
716
 
601
- exports.EFFECT_KINDS = EFFECT_KINDS; exports.EFFECT_USES = EFFECT_USES; exports.CAMERA_MOVE_ROLES = CAMERA_MOVE_ROLES; exports.EFFECT_ACCEPTS = EFFECT_ACCEPTS; exports.EFFECT_PATH_FORMAT = EFFECT_PATH_FORMAT; exports.EFFECT_SUBJECT_KINDS = EFFECT_SUBJECT_KINDS; exports.EFFECT_LAYER_ROLES = EFFECT_LAYER_ROLES; exports.EFFECT_TRANSITION_MS = EFFECT_TRANSITION_MS; exports.EFFECT_TEMPLATE_SLOT_TYPES = EFFECT_TEMPLATE_SLOT_TYPES; exports.EFFECT_ERROR_CODES = EFFECT_ERROR_CODES; exports.EffectError = EffectError; exports.UnknownEffectError = UnknownEffectError; exports.UnknownTemplateError = UnknownTemplateError; exports.EffectControlError = EffectControlError; exports.EffectUnsupportedError = EffectUnsupportedError; exports.EffectDisposedError = EffectDisposedError; exports.NoEffectsProviderError = NoEffectsProviderError; exports.isEffectError = isEffectError; exports.effectValueErrors = effectValueErrors; exports.resolveEffectValues = resolveEffectValues; exports.effectOffered = effectOffered; exports.normaliseEffectTag = normaliseEffectTag; exports.hasEffectTags = hasEffectTags; exports.countEffectTags = countEffectTags; exports.pathControlsOf = pathControlsOf; exports.searchEffectSummaries = searchEffectSummaries; exports.createEffectPixels = createEffectPixels; exports.isEffectPixels = isEffectPixels; exports.srgbToLinear = srgbToLinear; exports.linearToSrgb = linearToSrgb; exports.encodeEffectFloat = encodeEffectFloat; exports.applyEffectMask = applyEffectMask; exports.layerImageInput = layerImageInput; exports.EffectsSlot = EffectsSlot; exports.isCameraView = isCameraView; exports.sampleCameraViews = sampleCameraViews; exports.sampleEffectCameraMove = sampleEffectCameraMove; exports.cameraMoveField = cameraMoveField; exports.cameraMovesFor = cameraMovesFor; exports.cameraMoveIdList = cameraMoveIdList; exports.cameraMoveParts = cameraMoveParts; exports.CAMERA_MOVE_MAX_MS = CAMERA_MOVE_MAX_MS; exports.resolveCameraMoveSpec = resolveCameraMoveSpec; exports.cameraViewOfBox = cameraViewOfBox; exports.cameraBoxOfView = cameraBoxOfView; exports.createEffectToolProvider = createEffectToolProvider;
717
+
718
+
719
+
720
+
721
+
722
+
723
+
724
+
725
+
726
+ exports.EFFECT_KINDS = EFFECT_KINDS; exports.EFFECT_USES = EFFECT_USES; exports.CAMERA_MOVE_ROLES = CAMERA_MOVE_ROLES; exports.EFFECT_ACCEPTS = EFFECT_ACCEPTS; exports.EFFECT_PATH_FORMAT = EFFECT_PATH_FORMAT; exports.TIMING_DEFAULT_FPS = TIMING_DEFAULT_FPS; exports.TIMING_MAX_FRAMES = TIMING_MAX_FRAMES; exports.TIMING_MAX_FPS = TIMING_MAX_FPS; exports.TIMING_BLENDS = TIMING_BLENDS; exports.EFFECT_SUBJECT_KINDS = EFFECT_SUBJECT_KINDS; exports.EFFECT_LAYER_ROLES = EFFECT_LAYER_ROLES; exports.EFFECT_TRANSITION_MS = EFFECT_TRANSITION_MS; exports.EFFECT_TEMPLATE_SLOT_TYPES = EFFECT_TEMPLATE_SLOT_TYPES; exports.EFFECT_ERROR_CODES = EFFECT_ERROR_CODES; exports.EffectError = EffectError; exports.UnknownEffectError = UnknownEffectError; exports.UnknownTemplateError = UnknownTemplateError; exports.EffectControlError = EffectControlError; exports.EffectUnsupportedError = EffectUnsupportedError; exports.EffectDisposedError = EffectDisposedError; exports.NoEffectsProviderError = NoEffectsProviderError; exports.isEffectError = isEffectError; exports.effectValueErrors = effectValueErrors; exports.resolveEffectValues = resolveEffectValues; exports.effectOffered = effectOffered; exports.normaliseEffectTag = normaliseEffectTag; exports.hasEffectTags = hasEffectTags; exports.countEffectTags = countEffectTags; exports.pathControlsOf = pathControlsOf; exports.searchEffectSummaries = searchEffectSummaries; exports.createEffectPixels = createEffectPixels; exports.isEffectPixels = isEffectPixels; exports.srgbToLinear = srgbToLinear; exports.linearToSrgb = linearToSrgb; exports.encodeEffectFloat = encodeEffectFloat; exports.applyEffectMask = applyEffectMask; exports.layerImageInput = layerImageInput; exports.EffectsSlot = EffectsSlot; exports.isCameraView = isCameraView; exports.sampleCameraViews = sampleCameraViews; exports.sampleEffectCameraMove = sampleEffectCameraMove; exports.cameraMoveField = cameraMoveField; exports.cameraMovesFor = cameraMovesFor; exports.cameraMoveIdList = cameraMoveIdList; exports.cameraMoveParts = cameraMoveParts; exports.CAMERA_MOVE_MAX_MS = CAMERA_MOVE_MAX_MS; exports.resolveCameraMoveSpec = resolveCameraMoveSpec; exports.cameraViewOfBox = cameraViewOfBox; exports.cameraBoxOfView = cameraBoxOfView; exports.checkTimingFootage = checkTimingFootage; exports.checkTimingRequest = checkTimingRequest; exports.normaliseTimingSample = normaliseTimingSample; exports.timingSampleProblems = timingSampleProblems; exports.blendTimingFrames = blendTimingFrames; exports.createEffectToolProvider = createEffectToolProvider;
@@ -3,11 +3,15 @@ import {
3
3
  } from "./chunk-V2ZCJRDM.js";
4
4
 
5
5
  // src/effects/types.ts
6
- var EFFECT_KINDS = ["shader", "vector", "motion", "camera"];
7
- var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera"];
6
+ var EFFECT_KINDS = ["shader", "vector", "motion", "camera", "timing"];
7
+ var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera", "timing"];
8
8
  var CAMERA_MOVE_ROLES = ["transition", "hold"];
9
9
  var EFFECT_ACCEPTS = ["path"];
10
10
  var EFFECT_PATH_FORMAT = "svg-path";
11
+ var TIMING_DEFAULT_FPS = 30;
12
+ var TIMING_MAX_FRAMES = 128;
13
+ var TIMING_MAX_FPS = 240;
14
+ var TIMING_BLENDS = ["average", "lighten"];
11
15
  var EFFECT_SUBJECT_KINDS = ["image", "video", "svg", "text", "slot"];
12
16
  var EFFECT_LAYER_ROLES = ["background", "subject", "overlay"];
13
17
  var EFFECT_TRANSITION_MS = 800;
@@ -87,10 +91,11 @@ function resolveEffectValues(description, values) {
87
91
  if (errors.length) throw new EffectControlError(description.id, errors);
88
92
  return { ...description.defaults, ...values ?? {} };
89
93
  }
94
+ var PIXELLESS_KINDS = ["motion", "camera", "timing"];
90
95
  function effectOffered(e, uses) {
91
- if (!uses) return e.kind !== "motion" && e.kind !== "camera";
92
- if (e.kind === "motion" && !uses.includes("motion")) return false;
93
- if (e.kind === "camera" && !uses.includes("camera")) return false;
96
+ const pixelless = PIXELLESS_KINDS.includes(e.kind);
97
+ if (!uses) return !pixelless;
98
+ if (pixelless && !uses.includes(e.kind)) return false;
94
99
  return e.uses.some((u) => uses.includes(u));
95
100
  }
96
101
  function normaliseEffectTag(tag) {
@@ -299,6 +304,117 @@ function cameraBoxOfView(view, stage) {
299
304
  const height = stage.height / view.zoom;
300
305
  return { x: -view.scrollX, y: -view.scrollY, width, height };
301
306
  }
307
+ function checkTimingFootage(footageMs, id) {
308
+ if (typeof footageMs !== "number" || !Number.isFinite(footageMs) || footageMs <= 0) {
309
+ throw new EffectUnsupportedError(`The footage's length is a number of ms above 0; it was ${String(footageMs)}.`, id);
310
+ }
311
+ }
312
+ function checkTimingRequest(request, id) {
313
+ if (!request || typeof request !== "object") throw new EffectUnsupportedError("A timing request is { time, footage, fps? }: ms into the retimed clip, the footage's length in ms, and its frame rate.", id);
314
+ checkTimingFootage(request.footage, id);
315
+ const fps = request.fps ?? TIMING_DEFAULT_FPS;
316
+ if (typeof fps !== "number" || !Number.isFinite(fps) || fps <= 0 || fps > TIMING_MAX_FPS) {
317
+ throw new EffectUnsupportedError(`The footage's frame rate is above 0 and at most ${TIMING_MAX_FPS} frames per second; it was ${String(fps)}.`, id);
318
+ }
319
+ if (typeof request.time !== "number" || !Number.isFinite(request.time)) throw new EffectUnsupportedError(`The time is a number of ms into the retimed clip; it was ${String(request.time)}.`, id);
320
+ return fps;
321
+ }
322
+ var lastFrameOf = (footageMs, fps) => Math.max(0, Math.ceil(footageMs * fps / 1e3 - 1e-9) - 1);
323
+ function normaliseTimingSample(raw, request, blend = "average") {
324
+ const fps = request.fps ?? TIMING_DEFAULT_FPS;
325
+ const frameMs = 1e3 / fps;
326
+ const last = lastFrameOf(request.footage, fps);
327
+ const frameOf = (t) => Math.min(last, Math.max(0, Math.floor(t / frameMs + 1e-6)));
328
+ const weights = /* @__PURE__ */ new Map();
329
+ for (const f of raw) {
330
+ if (!Number.isFinite(f.time) || !Number.isFinite(f.weight) || f.weight <= 0) continue;
331
+ const i = frameOf(f.time);
332
+ weights.set(i, (weights.get(i) ?? 0) + f.weight);
333
+ }
334
+ if (!weights.size) weights.set(Number.isFinite(request.time) ? frameOf(request.time) : 0, 1);
335
+ let frames = [...weights.entries()].sort((a, b) => a[0] - b[0]);
336
+ if (frames.length > TIMING_MAX_FRAMES) {
337
+ const stride = frames.length / TIMING_MAX_FRAMES;
338
+ frames = Array.from({ length: TIMING_MAX_FRAMES }, (_, k) => frames[Math.floor(k * stride)]);
339
+ }
340
+ const total = frames.reduce((sum, [, w]) => sum + w, 0);
341
+ return { frames: frames.map(([i, w]) => ({ time: i * 1e3 / fps, weight: w / total })), blend };
342
+ }
343
+ function timingSampleProblems(sample, request) {
344
+ if (!sample || typeof sample !== "object") return ["the sample is not an object { frames, blend }"];
345
+ const { frames, blend } = sample;
346
+ const problems = [];
347
+ if (!TIMING_BLENDS.includes(blend)) problems.push(`blend "${String(blend)}" is not one of ${TIMING_BLENDS.join(", ")}`);
348
+ if (!Array.isArray(frames) || !frames.length) return [...problems, "frames must be a list of at least one { time, weight }"];
349
+ if (frames.length > TIMING_MAX_FRAMES) problems.push(`it has ${frames.length} frames; at most ${TIMING_MAX_FRAMES} are allowed`);
350
+ const fps = request.fps ?? TIMING_DEFAULT_FPS;
351
+ const last = lastFrameOf(request.footage, fps);
352
+ let total = 0;
353
+ let previous = -Infinity;
354
+ for (const f of frames) {
355
+ if (!f || !Number.isFinite(f.time) || !Number.isFinite(f.weight)) {
356
+ problems.push(`frame ${JSON.stringify(f)} needs a finite time and weight`);
357
+ continue;
358
+ }
359
+ const index = f.time * fps / 1e3;
360
+ if (Math.abs(index - Math.round(index)) > 1e-6) problems.push(`frame time ${f.time} is not the start of a frame at ${fps} fps (index \xD7 ${+(1e3 / fps).toFixed(6)} ms)`);
361
+ else if (Math.round(index) < 0 || Math.round(index) > last) problems.push(`frame time ${f.time} is outside the footage (frames 0 to ${last}, the last at ${+(last * 1e3 / fps).toFixed(6)} ms)`);
362
+ if (f.time <= previous) problems.push(`frame times must be distinct and earliest first; ${f.time} follows ${previous}`);
363
+ previous = f.time;
364
+ if (!(f.weight > 0)) problems.push(`frame at ${f.time} has weight ${f.weight}; weights are > 0`);
365
+ total += f.weight;
366
+ }
367
+ if (Math.abs(total - 1) > 1e-6) problems.push(`weights add up to ${total}, not 1`);
368
+ return problems;
369
+ }
370
+ function blendTimingFrames(frames, blend, out) {
371
+ if (!frames.length) throw new EffectUnsupportedError("There are no frames to blend; a timing sample has at least one.");
372
+ const { width, height } = frames[0].pixels;
373
+ for (const f of frames) {
374
+ if (f.pixels.width !== width || f.pixels.height !== height) throw new EffectUnsupportedError(`A ${f.pixels.width}\xD7${f.pixels.height} frame cannot be blended with ${width}\xD7${height} frames.`);
375
+ }
376
+ const target = out ?? createEffectPixels(width, height);
377
+ if (target.width !== width || target.height !== height) throw new EffectUnsupportedError(`The output is ${target.width}\xD7${target.height}; the frames are ${width}\xD7${height}.`);
378
+ const o = target.data;
379
+ if (frames.length === 1) {
380
+ o.set(frames[0].pixels.data);
381
+ return target;
382
+ }
383
+ const s = new Float64Array(o.length);
384
+ if (blend === "lighten") {
385
+ const heaviest = Math.max(...frames.map((f) => f.weight));
386
+ for (const f of frames) {
387
+ const k = heaviest > 0 ? f.weight / heaviest : 1;
388
+ const p = f.pixels.data;
389
+ for (let i = 0; i < s.length; i += 4) {
390
+ const a = p[i + 3] / 255 * k;
391
+ for (let c = 0; c < 3; c++) if (p[i + c] * a > s[i + c]) s[i + c] = p[i + c] * a;
392
+ if (a > s[i + 3]) s[i + 3] = a;
393
+ }
394
+ }
395
+ for (let i = 0; i < s.length; i += 4) {
396
+ for (let c = 0; c < 3; c++) o[i + c] = Math.round(s[i + c]);
397
+ o[i + 3] = Math.round(s[i + 3] * 255);
398
+ }
399
+ return target;
400
+ }
401
+ let total = 0;
402
+ for (const f of frames) {
403
+ total += f.weight;
404
+ const p = f.pixels.data;
405
+ for (let i = 0; i < s.length; i += 4) {
406
+ const a = p[i + 3] * f.weight;
407
+ for (let c = 0; c < 3; c++) s[i + c] += p[i + c] * a;
408
+ s[i + 3] += a;
409
+ }
410
+ }
411
+ for (let i = 0; i < s.length; i += 4) {
412
+ const a = s[i + 3];
413
+ for (let c = 0; c < 3; c++) o[i + c] = a > 0 ? Math.round(s[i + c] / a) : 0;
414
+ o[i + 3] = total > 0 ? Math.round(a / total) : 0;
415
+ }
416
+ return target;
417
+ }
302
418
 
303
419
  // src/effects/tools.ts
304
420
  var ANNOTATIONS = {
@@ -392,12 +508,12 @@ function createEffectToolProvider(host) {
392
508
  {
393
509
  name: "search_effects",
394
510
  title: "Search effects",
395
- description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }], total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
511
+ description: `Searches the effects provider's catalog. Every word of query must appear in an effect's id, name, category, tags or description; filter by kind (shader, vector, motion, camera, timing), category, use (${EFFECT_USES.join(", ")}) or tags (effects carrying every one), and accepts ["path"] for effects that draw along a path you give. For this editor, apply_effect takes uses ${host.applyUses.join(" or ")}. Returns { provider, effects: [{ id, name, kind, category, uses, description, tags, transparent, loop, duration (ms), camera: { role, durationMs, cut } for camera moves, accepts }] (timing effects, kind timing, retime video footage and have no duration: their length depends on the footage), total, tags: [{ tag, count }] }: tags lists the tags of every match, the most used first, so a broad search (or none) shows the tags to narrow by. A tag the catalog does not have comes back in unknownTags with the nearest ones. With no provider registered it returns available: false and the reason. Call describe_effect next for an effect's controls.`,
396
512
  inputSchema: object({
397
513
  query: { type: "string", description: "Words to match (default: every effect)." },
398
- kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art) or camera (moves a scene's camera; no pixels)." },
514
+ kind: { type: "string", enum: [...EFFECT_KINDS], description: "shader (pixels), vector (animated SVG), motion (animates vector art), camera (moves a scene's camera; no pixels) or timing (retimes video footage: slow motion, reverse, freeze, long exposure; no pixels of its own)." },
399
515
  category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
400
- use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera." },
516
+ use: { type: "string", enum: [...EFFECT_USES], description: "What the effect is for: filter changes a picture, transition blends two, generator draws a background, overlay draws over content, title draws a title, motion animates vector art, interactive reacts to input, camera moves a scene's camera, timing retimes video footage." },
401
517
  tags: { type: "array", items: { type: "string", minLength: 1 }, description: "Only effects carrying every one of these tags, as search_effects lists them (any case; spaces count as hyphens)." },
402
518
  limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." },
403
519
  accepts: { type: "array", items: { type: "string", enum: [...EFFECT_ACCEPTS], description: "path: the effect has a control that draws along a path given at run time." }, description: "Only effects that take every one of these (path: effects that draw along a path you give; describe_effect names the controls in pathControls)." }
@@ -557,6 +673,10 @@ export {
557
673
  CAMERA_MOVE_ROLES,
558
674
  EFFECT_ACCEPTS,
559
675
  EFFECT_PATH_FORMAT,
676
+ TIMING_DEFAULT_FPS,
677
+ TIMING_MAX_FRAMES,
678
+ TIMING_MAX_FPS,
679
+ TIMING_BLENDS,
560
680
  EFFECT_SUBJECT_KINDS,
561
681
  EFFECT_LAYER_ROLES,
562
682
  EFFECT_TRANSITION_MS,
@@ -597,5 +717,10 @@ export {
597
717
  resolveCameraMoveSpec,
598
718
  cameraViewOfBox,
599
719
  cameraBoxOfView,
720
+ checkTimingFootage,
721
+ checkTimingRequest,
722
+ normaliseTimingSample,
723
+ timingSampleProblems,
724
+ blendTimingFrames,
600
725
  createEffectToolProvider
601
726
  };
@@ -7,7 +7,7 @@ import {
7
7
  EffectsSlot,
8
8
  createEffectPixels,
9
9
  effectValueErrors
10
- } from "./chunk-JA57JIT5.js";
10
+ } from "./chunk-MV7CYFMJ.js";
11
11
  import {
12
12
  validateJsonSchema
13
13
  } from "./chunk-V2ZCJRDM.js";