@pixerate/editor 0.241.0 → 0.243.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 (75) hide show
  1. package/dist/animation-editor/controller.d.cts +10 -0
  2. package/dist/animation-editor/controller.d.ts +10 -0
  3. package/dist/animation-editor/effects.d.cts +1 -1
  4. package/dist/animation-editor/effects.d.ts +1 -1
  5. package/dist/animation-editor/index.cjs +195 -146
  6. package/dist/animation-editor/index.js +63 -14
  7. package/dist/{brushstrokeRender-MOYGEXOC.js → brushstrokeRender-274HGNZR.js} +3 -3
  8. package/dist/{brushstrokeRender-UKADOV5D.cjs → brushstrokeRender-C7RJZPMC.cjs} +5 -5
  9. package/dist/canvas/index.cjs +15 -15
  10. package/dist/canvas/index.js +2 -2
  11. package/dist/{chunk-KCMV2534.cjs → chunk-3WOZEBXZ.cjs} +2 -2
  12. package/dist/{chunk-ZDPLZDTT.js → chunk-4ZHXLFXS.js} +1 -1
  13. package/dist/{chunk-5BR7LC24.cjs → chunk-5FZS7J7C.cjs} +50 -4
  14. package/dist/{chunk-ILM6GYAX.js → chunk-6L3WWIDW.js} +1 -1
  15. package/dist/{chunk-A7PQNIBX.cjs → chunk-7V7QMFTI.cjs} +14 -14
  16. package/dist/{chunk-JAOSL334.cjs → chunk-GHCSC2WP.cjs} +38 -16
  17. package/dist/{chunk-UDBDYZET.js → chunk-H4Z72Z4Z.js} +1 -1
  18. package/dist/{chunk-XBZK47LV.js → chunk-HQZHOPNE.js} +49 -3
  19. package/dist/{chunk-QP2B3MID.js → chunk-IXI5KATU.js} +1 -1
  20. package/dist/{chunk-JWCKJNDR.cjs → chunk-K5DS2JTA.cjs} +6 -6
  21. package/dist/{chunk-HGGJ36HR.cjs → chunk-KK2FWMA2.cjs} +1 -1
  22. package/dist/{chunk-WDDUPWVG.js → chunk-L3JFOTBQ.js} +1 -1
  23. package/dist/{chunk-SIK3LT6M.js → chunk-OFH6Q67U.js} +28 -6
  24. package/dist/{chunk-AFLMXITL.cjs → chunk-OONFIFEU.cjs} +23 -23
  25. package/dist/{chunk-HW72ABGR.cjs → chunk-R2FVAKFH.cjs} +86 -86
  26. package/dist/{chunk-G3RUKJ5C.js → chunk-RXZL4NG5.js} +1 -1
  27. package/dist/{chunk-NE6OOKWJ.js → chunk-SRXWDOUI.js} +1 -1
  28. package/dist/{chunk-7F4G74UQ.cjs → chunk-UV7OOZJQ.cjs} +35 -35
  29. package/dist/{chunk-TEFQISBY.js → chunk-X3ZINLIP.js} +1 -1
  30. package/dist/{chunk-ZQIWBJGH.cjs → chunk-XNZFVYC2.cjs} +7 -7
  31. package/dist/effects/fake.d.cts +4 -0
  32. package/dist/effects/fake.d.ts +4 -0
  33. package/dist/effects/helpers.d.cts +6 -0
  34. package/dist/effects/helpers.d.ts +6 -0
  35. package/dist/effects/index.cjs +222 -94
  36. package/dist/effects/index.js +131 -3
  37. package/dist/effects/types.d.cts +31 -0
  38. package/dist/effects/types.d.ts +31 -0
  39. package/dist/image-editor/index.cjs +788 -788
  40. package/dist/image-editor/index.js +5 -5
  41. package/dist/paint-editor/brushes.d.cts +18 -2
  42. package/dist/paint-editor/brushes.d.ts +18 -2
  43. package/dist/paint-editor/controller.d.cts +28 -0
  44. package/dist/paint-editor/controller.d.ts +28 -0
  45. package/dist/paint-editor/document.d.cts +3 -0
  46. package/dist/paint-editor/document.d.ts +3 -0
  47. package/dist/paint-editor/index.cjs +1017 -499
  48. package/dist/paint-editor/index.js +562 -44
  49. package/dist/paint-editor/light.d.cts +74 -0
  50. package/dist/paint-editor/light.d.ts +74 -0
  51. package/dist/paint-editor/stroke.d.cts +14 -0
  52. package/dist/paint-editor/stroke.d.ts +14 -0
  53. package/dist/paint-editor/thick-tools.d.cts +2 -2
  54. package/dist/paint-editor/thick-tools.d.ts +2 -2
  55. package/dist/paint-editor/thick.d.cts +5 -1
  56. package/dist/paint-editor/thick.d.ts +5 -1
  57. package/dist/paint-editor/ui.d.cts +2 -0
  58. package/dist/paint-editor/ui.d.ts +2 -0
  59. package/dist/vector/index.cjs +968 -968
  60. package/dist/vector/index.js +5 -5
  61. package/dist/video-editor/controller.d.cts +49 -1
  62. package/dist/video-editor/controller.d.ts +49 -1
  63. package/dist/video-editor/effect-path.d.cts +66 -0
  64. package/dist/video-editor/effect-path.d.ts +66 -0
  65. package/dist/video-editor/index.cjs +616 -427
  66. package/dist/video-editor/index.d.cts +1 -0
  67. package/dist/video-editor/index.d.ts +1 -0
  68. package/dist/video-editor/index.js +193 -4
  69. package/package.json +1 -1
  70. package/skills/pixerate-animation-editor/SKILL.md +1 -1
  71. package/skills/pixerate-paint-editor/SKILL.md +9 -9
  72. package/skills/pixerate-paint-editor/references/canvas-hosted.md +8 -0
  73. package/skills/pixerate-paint-editor/references/light-paint.md +31 -0
  74. package/skills/pixerate-paint-oil-impasto/SKILL.md +1 -0
  75. package/skills/pixerate-video-editor/SKILL.md +9 -2
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- var _chunkJAOSL334cjs = require('./chunk-JAOSL334.cjs');
6
+ var _chunkGHCSC2WPcjs = require('./chunk-GHCSC2WP.cjs');
7
7
 
8
8
 
9
9
  var _chunkFJ2MSI43cjs = require('./chunk-FJ2MSI43.cjs');
@@ -26,7 +26,7 @@ function documentErrors(doc, description) {
26
26
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
27
27
  if (doc.values !== void 0) {
28
28
  if (!isRecord(doc.values)) errors.push({ path: "/values", message: "must be an object of control values" });
29
- else errors.push(..._chunkJAOSL334cjs.effectValueErrors.call(void 0, description, doc.values).map((e) => ({ ...e, path: `/values${e.path}` })));
29
+ else errors.push(..._chunkGHCSC2WPcjs.effectValueErrors.call(void 0, description, doc.values).map((e) => ({ ...e, path: `/values${e.path}` })));
30
30
  }
31
31
  if (doc.tracks !== void 0) {
32
32
  if (!isRecord(doc.tracks)) errors.push({ path: "/tracks", message: "must be an object of keyframe lists" });
@@ -39,7 +39,7 @@ function documentErrors(doc, description) {
39
39
  kfs.forEach((kf, i) => {
40
40
  if (!isRecord(kf)) return errors.push({ path: `${at}/${i}`, message: "must be a keyframe object" });
41
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(..._chunkJAOSL334cjs.effectValueErrors.call(void 0, description, { [control]: kf.value }).map((e) => ({ ...e, path: `${at}/${i}/value` })));
42
+ errors.push(..._chunkGHCSC2WPcjs.effectValueErrors.call(void 0, description, { [control]: kf.value }).map((e) => ({ ...e, path: `${at}/${i}/value` })));
43
43
  if (kf.easing !== void 0 && !_chunkDWJJI6RTcjs.isAnimationEasing.call(void 0, kf.easing)) errors.push({ path: `${at}/${i}/easing`, message: "is not a valid easing" });
44
44
  });
45
45
  }
@@ -49,7 +49,7 @@ function documentErrors(doc, description) {
49
49
  else
50
50
  doc.presets.forEach((p, i) => {
51
51
  if (!isRecord(p) || typeof p.name !== "string" || !p.name) return errors.push({ path: `/presets/${i}`, message: "must have a name" });
52
- errors.push(..._chunkJAOSL334cjs.effectValueErrors.call(void 0, description, p.values).map((e) => ({ ...e, path: `/presets/${i}/values${e.path}` })));
52
+ errors.push(..._chunkGHCSC2WPcjs.effectValueErrors.call(void 0, description, p.values).map((e) => ({ ...e, path: `/presets/${i}/values${e.path}` })));
53
53
  });
54
54
  }
55
55
  if (doc.duration !== void 0 && (typeof doc.duration !== "number" || !Number.isFinite(doc.duration) || !(doc.duration > 0))) {
@@ -170,7 +170,7 @@ async function createEffectAuthoringSession(source, options) {
170
170
  if (isDocumentSource(source)) {
171
171
  const doc = source.document;
172
172
  if (doc.version !== 1 || !doc.effectId) {
173
- throw new (0, _chunkJAOSL334cjs.EffectUnsupportedError)("Invalid effect authoring document: version 1 required");
173
+ throw new (0, _chunkGHCSC2WPcjs.EffectUnsupportedError)("Invalid effect authoring document: version 1 required");
174
174
  }
175
175
  effectId = doc.effectId;
176
176
  initialDoc = doc;
@@ -178,15 +178,15 @@ async function createEffectAuthoringSession(source, options) {
178
178
  effectId = source.effectId;
179
179
  requestedPreset = source.preset;
180
180
  } else {
181
- throw new (0, _chunkJAOSL334cjs.EffectUnsupportedError)("Invalid effect authoring source");
181
+ throw new (0, _chunkGHCSC2WPcjs.EffectUnsupportedError)("Invalid effect authoring source");
182
182
  }
183
183
  const description = await provider.describe(effectId);
184
184
  if (initialDoc) {
185
185
  const problems = documentErrors(initialDoc, description);
186
- if (problems.length) throw new (0, _chunkJAOSL334cjs.EffectControlError)(effectId, problems);
186
+ if (problems.length) throw new (0, _chunkGHCSC2WPcjs.EffectControlError)(effectId, problems);
187
187
  }
188
188
  if (description.kind === "motion") {
189
- throw new (0, _chunkJAOSL334cjs.EffectUnsupportedError)("Motion effects cannot be authored as pixel effects", effectId);
189
+ throw new (0, _chunkGHCSC2WPcjs.EffectUnsupportedError)("Motion effects cannot be authored as pixel effects", effectId);
190
190
  }
191
191
  let disposed = false;
192
192
  let playing = false;
@@ -207,7 +207,7 @@ async function createEffectAuthoringSession(source, options) {
207
207
  const found = description.presets.find((p) => p.name === requestedPreset);
208
208
  if (!found) {
209
209
  const validNames = description.presets.map((p) => `"${p.name}"`).join(", ");
210
- throw new (0, _chunkJAOSL334cjs.EffectUnsupportedError)(
210
+ throw new (0, _chunkGHCSC2WPcjs.EffectUnsupportedError)(
211
211
  `Unknown preset "${requestedPreset}" for effect "${effectId}". Valid presets are: ${validNames || "none"}`,
212
212
  effectId
213
213
  );
@@ -321,7 +321,7 @@ async function createEffectAuthoringSession(source, options) {
321
321
  if (!validNames.includes(name)) {
322
322
  return { ok: false, message: `there is no control "${name}"; the controls are ${validNames.join(", ")}` };
323
323
  }
324
- const errors = _chunkJAOSL334cjs.effectValueErrors.call(void 0, description, { [name]: value });
324
+ const errors = _chunkGHCSC2WPcjs.effectValueErrors.call(void 0, description, { [name]: value });
325
325
  if (errors.length > 0) {
326
326
  return { ok: false, message: valueErrorMessage(errors) };
327
327
  }
@@ -391,7 +391,7 @@ async function createEffectAuthoringSession(source, options) {
391
391
  if (!validNames.includes(k)) {
392
392
  return { ok: false, message: `there is no control "${k}"; the controls are ${validNames.join(", ")}` };
393
393
  }
394
- const errors = _chunkJAOSL334cjs.effectValueErrors.call(void 0, description, { [k]: v });
394
+ const errors = _chunkGHCSC2WPcjs.effectValueErrors.call(void 0, description, { [k]: v });
395
395
  if (errors.length > 0) {
396
396
  return { ok: false, message: valueErrorMessage(errors) };
397
397
  }
@@ -528,7 +528,7 @@ async function createEffectAuthoringSession(source, options) {
528
528
  return { ok: false, message: `invalid easing: ${String(easing)}` };
529
529
  }
530
530
  const targetValue = value !== void 0 ? value : valuesAtTime(time, baseValues, tracks)[name];
531
- const errors = _chunkJAOSL334cjs.effectValueErrors.call(void 0, description, { [name]: targetValue });
531
+ const errors = _chunkGHCSC2WPcjs.effectValueErrors.call(void 0, description, { [name]: targetValue });
532
532
  if (errors.length > 0) {
533
533
  return { ok: false, message: valueErrorMessage(errors) };
534
534
  }
@@ -705,7 +705,7 @@ async function createEffectAuthoringSession(source, options) {
705
705
  },
706
706
  async createPreview(opts) {
707
707
  if (disposed) {
708
- throw new (0, _chunkJAOSL334cjs.EffectDisposedError)(effectId);
708
+ throw new (0, _chunkGHCSC2WPcjs.EffectDisposedError)(effectId);
709
709
  }
710
710
  let renderer;
711
711
  try {
@@ -730,7 +730,7 @@ async function createEffectAuthoringSession(source, options) {
730
730
  height: renderer.height,
731
731
  depth: renderer.depth,
732
732
  render: async (req) => {
733
- if (disposed) throw new (0, _chunkJAOSL334cjs.EffectDisposedError)(effectId);
733
+ if (disposed) throw new (0, _chunkGHCSC2WPcjs.EffectDisposedError)(effectId);
734
734
  const effectiveValues = _nullishCoalesce(req.values, () => ( valuesAtTime(req.time, baseValues, tracks)));
735
735
  const pixels = await renderer.render({
736
736
  ...req,
@@ -6,6 +6,8 @@ var _chunkWOFXZ3XUcjs = require('./chunk-WOFXZ3XU.cjs');
6
6
  var EFFECT_KINDS = ["shader", "vector", "motion", "camera"];
7
7
  var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera"];
8
8
  var CAMERA_MOVE_ROLES = ["transition", "hold"];
9
+ var EFFECT_ACCEPTS = ["path"];
10
+ var EFFECT_PATH_FORMAT = "svg-path";
9
11
  var EFFECT_SUBJECT_KINDS = ["image", "video", "svg", "text", "slot"];
10
12
  var EFFECT_LAYER_ROLES = ["background", "subject", "overlay"];
11
13
  var EFFECT_TRANSITION_MS = 800;
@@ -104,7 +106,11 @@ function countEffectTags(effects) {
104
106
  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);
105
107
  return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
106
108
  }
107
- 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);
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)])));
110
+ function pathControlsOf(d) {
111
+ 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);
113
+ }
108
114
  function searchEffectSummaries(catalog, query = "", options = {}) {
109
115
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
110
116
  const scored = [];
@@ -228,7 +234,7 @@ async function sampleEffectCameraMove(provider, id, request) {
228
234
  }
229
235
  var cameraMoveField = (role) => role === "transition" ? "in" : "during";
230
236
  function cameraMovesFor(catalog, role) {
231
- return catalog.filter((e) => e.kind === "camera" && _optionalChain([e, 'access', _2 => _2.camera, 'optionalAccess', _3 => _3.role]) === role);
237
+ return catalog.filter((e) => e.kind === "camera" && _optionalChain([e, 'access', _9 => _9.camera, 'optionalAccess', _10 => _10.role]) === role);
232
238
  }
233
239
  function cameraMoveIdList(catalog, role) {
234
240
  const ids = cameraMovesFor(catalog, role).map((e) => JSON.stringify(e.id));
@@ -318,11 +324,12 @@ var summaryOut = (e) => ({
318
324
  category: e.category,
319
325
  uses: e.uses,
320
326
  ...e.description ? { description: e.description } : {},
321
- ..._optionalChain([e, 'access', _4 => _4.tags, 'optionalAccess', _5 => _5.length]) ? { tags: e.tags } : {},
327
+ ..._optionalChain([e, 'access', _11 => _11.tags, 'optionalAccess', _12 => _12.length]) ? { tags: e.tags } : {},
322
328
  transparent: e.transparent,
323
329
  loop: e.loop,
324
330
  ...e.duration !== void 0 ? { duration: e.duration } : {},
325
- ...e.camera ? { camera: e.camera } : {}
331
+ ...e.camera ? { camera: e.camera } : {},
332
+ ..._optionalChain([e, 'access', _13 => _13.accepts, 'optionalAccess', _14 => _14.length]) ? { accepts: e.accepts } : {}
326
333
  });
327
334
  function createEffectToolProvider(host) {
328
335
  let catalog = null;
@@ -330,7 +337,7 @@ function createEffectToolProvider(host) {
330
337
  const notify = () => listeners.forEach((l) => l());
331
338
  const catalogFor = (provider) => {
332
339
  if (!provider) return null;
333
- if (_optionalChain([catalog, 'optionalAccess', _6 => _6.provider]) !== provider) {
340
+ if (_optionalChain([catalog, 'optionalAccess', _15 => _15.provider]) !== provider) {
334
341
  const entry = { provider, effects: null };
335
342
  catalog = entry;
336
343
  Promise.resolve().then(() => provider.list()).then((effects) => {
@@ -366,7 +373,7 @@ function createEffectToolProvider(host) {
366
373
  description: `${what}: control name \u2192 value, as listed by describe_effect (its "controls" schema: types, ranges, units and defaults). Only the given controls change; colours are "#rrggbb".${keyframes ? " A number control may instead be keyframes { keys: [{ t: ms from the start, v, ease }] }." : ""} Unknown controls and out-of-range values are refused with the valid ones.`
367
374
  });
368
375
  const effectIdSchema = (uses) => {
369
- const effects = _optionalChain([catalogFor, 'call', _7 => _7(host.provider()), 'optionalAccess', _8 => _8.filter, 'call', _9 => _9((e) => effectOffered(e, uses))]);
376
+ const effects = _optionalChain([catalogFor, 'call', _16 => _16(host.provider()), 'optionalAccess', _17 => _17.filter, 'call', _18 => _18((e) => effectOffered(e, uses))]);
370
377
  const base = `An effect id from search_effects (use ${uses.map((u) => `"${u}"`).join(" or ")}).`;
371
378
  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 };
372
379
  };
@@ -380,24 +387,35 @@ function createEffectToolProvider(host) {
380
387
  {
381
388
  name: "search_effects",
382
389
  title: "Search effects",
383
- 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). 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 }], 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.`,
390
+ 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.`,
384
391
  inputSchema: object({
385
392
  query: { type: "string", description: "Words to match (default: every effect)." },
386
393
  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)." },
387
394
  category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
388
395
  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." },
389
396
  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)." },
390
- limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." }
397
+ limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." },
398
+ 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)." }
391
399
  }),
392
400
  annotations: { title: "Search effects", ...ANNOTATIONS.read },
393
401
  handler: async (args) => {
394
402
  const p = host.provider();
395
403
  if (!p) return { available: false, reason: NO_PROVIDER, effects: [], total: 0 };
396
404
  const tags = args.tags;
397
- const found = (await explain(async () => p.search(_nullishCoalesce(args.query, () => ( "")), { kind: args.kind, category: args.category, use: args.use, tags }))).filter((e) => hasEffectTags(e, tags));
405
+ const accepts = args.accepts;
406
+ let found = (await explain(async () => p.search(_nullishCoalesce(args.query, () => ( "")), { kind: args.kind, category: args.category, use: args.use, tags, ..._optionalChain([accepts, 'optionalAccess', _19 => _19.length]) ? { accepts } : {} }))).filter((e) => hasEffectTags(e, tags));
407
+ if (_optionalChain([accepts, 'optionalAccess', _20 => _20.includes, 'call', _21 => _21("path")])) {
408
+ const kept = [];
409
+ for (const e of found) {
410
+ if (e.accepts) {
411
+ if (accepts.every((a) => e.accepts.includes(a))) kept.push(e);
412
+ } else if (kept.length < 200 && pathControlsOf(await explain(async () => p.describe(e.id))).length) kept.push(e);
413
+ }
414
+ found = kept;
415
+ }
398
416
  const limit = _nullishCoalesce(args.limit, () => ( 20));
399
417
  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) };
400
- if (_optionalChain([tags, 'optionalAccess', _10 => _10.length]) && !found.length) {
418
+ if (_optionalChain([tags, 'optionalAccess', _22 => _22.length]) && !found.length) {
401
419
  const known = countEffectTags(await explain(async () => p.list()));
402
420
  const have = new Set(known.map((k) => k.tag));
403
421
  const unknown = tags.filter((t) => !have.has(normaliseEffectTag(t)));
@@ -416,7 +434,8 @@ function createEffectToolProvider(host) {
416
434
  const p = host.provider();
417
435
  if (!p) return { available: false, reason: NO_PROVIDER };
418
436
  const d = await explain(async () => p.describe(args.effect));
419
- return { available: true, ...summaryOut(d), controls: d.controls, defaults: d.defaults, presets: d.presets, inputs: d.inputs, appliesHere: effectOffered(d, host.applyUses) };
437
+ const pathControls = pathControlsOf(d);
438
+ return { available: true, ...summaryOut(d), controls: d.controls, defaults: d.defaults, presets: d.presets, inputs: d.inputs, appliesHere: effectOffered(d, host.applyUses), ...pathControls.length ? { pathControls } : {} };
420
439
  }
421
440
  },
422
441
  {
@@ -512,16 +531,16 @@ function createEffectToolProvider(host) {
512
531
  ...host.description ? { description: host.description } : {},
513
532
  ...host.skill ? { skill: host.skill } : {},
514
533
  tools: () => {
515
- const extra = _nullishCoalesce(_optionalChain([host, 'access', _11 => _11.extraTools, 'optionalCall', _12 => _12(context)]), () => ( []));
516
- if (host.readOnlyReason()) return [...readTools(), ...extra.filter((t) => _optionalChain([t, 'access', _13 => _13.annotations, 'optionalAccess', _14 => _14.readOnlyHint]))];
534
+ const extra = _nullishCoalesce(_optionalChain([host, 'access', _23 => _23.extraTools, 'optionalCall', _24 => _24(context)]), () => ( []));
535
+ if (host.readOnlyReason()) return [...readTools(), ...extra.filter((t) => _optionalChain([t, 'access', _25 => _25.annotations, 'optionalAccess', _26 => _26.readOnlyHint]))];
517
536
  return [...readTools(), ...writeTools(), ...extra];
518
537
  },
519
538
  subscribe: (l) => {
520
539
  listeners.add(l);
521
- const off = _optionalChain([host, 'access', _15 => _15.subscribe, 'optionalCall', _16 => _16(l)]);
540
+ const off = _optionalChain([host, 'access', _27 => _27.subscribe, 'optionalCall', _28 => _28(l)]);
522
541
  return () => {
523
542
  listeners.delete(l);
524
- _optionalChain([off, 'optionalCall', _17 => _17()]);
543
+ _optionalChain([off, 'optionalCall', _29 => _29()]);
525
544
  };
526
545
  }
527
546
  };
@@ -570,4 +589,7 @@ function createEffectToolProvider(host) {
570
589
 
571
590
 
572
591
 
573
- exports.EFFECT_KINDS = EFFECT_KINDS; exports.EFFECT_USES = EFFECT_USES; exports.CAMERA_MOVE_ROLES = CAMERA_MOVE_ROLES; 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.searchEffectSummaries = searchEffectSummaries; exports.createEffectPixels = createEffectPixels; exports.isEffectPixels = isEffectPixels; exports.srgbToLinear = srgbToLinear; exports.linearToSrgb = linearToSrgb; exports.encodeEffectFloat = encodeEffectFloat; exports.applyEffectMask = applyEffectMask; 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;
592
+
593
+
594
+
595
+ 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.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;
@@ -5,7 +5,7 @@ import {
5
5
  moveAnchorHandle,
6
6
  subpathSegment,
7
7
  subpathSegmentCount
8
- } from "./chunk-ILM6GYAX.js";
8
+ } from "./chunk-6L3WWIDW.js";
9
9
  import {
10
10
  segmentPoint
11
11
  } from "./chunk-WEIK3UYB.js";
@@ -1218,7 +1218,7 @@ var BristleStroke = class extends MarkStroke {
1218
1218
 
1219
1219
  // src/paint-editor/brushes.ts
1220
1220
  var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer", "blender", "smudge", "wet", "impasto", "particle", "bristle", "sticker"];
1221
- var PAINT_BRUSH_CATEGORIES = ["pencil", "pastel", "charcoal", "airbrush", "marker", "ink", "paint", "eraser", "blender", "smudge", "mixer", "watercolor", "wet-tool", "oil", "impasto", "procedural", "bristle", "sticker", "custom"];
1221
+ var PAINT_BRUSH_CATEGORIES = ["pencil", "pastel", "charcoal", "airbrush", "marker", "ink", "paint", "eraser", "blender", "smudge", "mixer", "watercolor", "wet-tool", "oil", "impasto", "procedural", "bristle", "sticker", "light", "custom"];
1222
1222
  var WET_TOOLS = ["paint", "water", "dry", "blow", "blend", "smudge", "blot"];
1223
1223
  var WET_MEDIA = ["watercolor", "gouache", "oil"];
1224
1224
  var DEFAULT_WET_BRUSH = { tool: "paint", water: 0.5, pigment: 0.5, body: 0, strength: 0.5, rewet: 0, medium: "watercolor", load: 1, thinner: 0 };
@@ -1236,7 +1236,11 @@ function normalizeWetBrush(raw = {}, base = DEFAULT_WET_BRUSH) {
1236
1236
  };
1237
1237
  }
1238
1238
  var IMPASTO_TOOLS = ["brush", "tube", "knife", "roller", "gloop"];
1239
- var DEFAULT_IMPASTO = { tool: "brush", thickness: 0.6, gloss: 0.4, metallic: 0, load: 0.9, thinner: 0, drag: 0.4 };
1239
+ var DEFAULT_IMPASTO = { tool: "brush", thickness: 0.6, gloss: 0.4, metallic: 0, load: 0.9, thinner: 0, drag: 0.4, rim: 0, clarity: 0 };
1240
+ var DEFAULT_LIGHT = { amount: 0, pressure: false };
1241
+ function normalizeLight(raw = {}, base = DEFAULT_LIGHT) {
1242
+ return { amount: unit4(raw.amount, base.amount), pressure: typeof raw.pressure === "boolean" ? raw.pressure : base.pressure };
1243
+ }
1240
1244
  function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
1241
1245
  return {
1242
1246
  tool: IMPASTO_TOOLS.includes(raw.tool) ? raw.tool : base.tool,
@@ -1245,7 +1249,9 @@ function normalizeImpasto(raw = {}, base = DEFAULT_IMPASTO) {
1245
1249
  metallic: unit4(raw.metallic, base.metallic),
1246
1250
  load: unit4(raw.load, base.load),
1247
1251
  thinner: unit4(raw.thinner, base.thinner),
1248
- drag: unit4(raw.drag, base.drag)
1252
+ drag: unit4(raw.drag, base.drag),
1253
+ rim: unit4(raw.rim, base.rim ?? 0),
1254
+ clarity: unit4(raw.clarity, base.clarity ?? 0)
1249
1255
  };
1250
1256
  }
1251
1257
  var MULTI_COLOR_MODES = ["off", "tip", "dab"];
@@ -1403,6 +1409,7 @@ function normalizePaintBrush(raw = {}, base) {
1403
1409
  particle: normalizeParticle(raw.particle ?? {}, b.particle ?? DEFAULT_PARTICLE),
1404
1410
  bristle: normalizeBristle(raw.bristle ?? {}, b.bristle ?? DEFAULT_BRISTLE),
1405
1411
  sticker: normalizeStickerSettings(raw.sticker ?? {}, b.sticker ?? DEFAULT_STICKER),
1412
+ light: normalizeLight(raw.light ?? {}, b.light ?? DEFAULT_LIGHT),
1406
1413
  paint,
1407
1414
  ...raw.builtin ? { builtin: true } : {}
1408
1415
  };
@@ -1427,6 +1434,7 @@ var BASE_BRUSH = {
1427
1434
  particle: { ...DEFAULT_PARTICLE },
1428
1435
  bristle: { ...DEFAULT_BRISTLE },
1429
1436
  sticker: { ...DEFAULT_STICKER, stamps: [...DEFAULT_STICKER.stamps] },
1437
+ light: { ...DEFAULT_LIGHT },
1430
1438
  paint: null
1431
1439
  };
1432
1440
  var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
@@ -1848,6 +1856,42 @@ var BUILTIN_BRUSHES = [
1848
1856
  grain: 0.6,
1849
1857
  bristle: { count: 48, stiffness: 0.35, splay: 0.7, thickness: 0.05, load: 0.15, jitter: 0.8 }
1850
1858
  }),
1859
+ // Light paint: brushes that lay light with their colour (glow and spill; see light.ts).
1860
+ builtin({
1861
+ id: "glow-gel",
1862
+ name: "Glow gel",
1863
+ category: "light",
1864
+ engine: "impasto",
1865
+ tip: { size: 16, hardness: 0.9, spacing: 0.05, opacity: 1, flow: 1, smoothing: 0.5, pressureSize: true, pressureOpacity: false },
1866
+ dynamics: { minSize: 0.6 },
1867
+ impasto: { tool: "gloop", thickness: 1, gloss: 0.95, load: 1, drag: 0, rim: 0.8, clarity: 0.5 },
1868
+ light: { amount: 0.85 }
1869
+ }),
1870
+ builtin({
1871
+ id: "neon-marker",
1872
+ name: "Neon marker",
1873
+ category: "light",
1874
+ tip: { size: 10, hardness: 0.9, spacing: 0.04, opacity: 1, flow: 1, smoothing: 0.45, pressureSize: false, pressureOpacity: false },
1875
+ light: { amount: 1 }
1876
+ }),
1877
+ builtin({
1878
+ id: "light-airbrush",
1879
+ name: "Light airbrush",
1880
+ category: "light",
1881
+ tip: { size: 90, hardness: 0, spacing: 0.04, opacity: 1, flow: 0.06, smoothing: 0.3, pressureSize: false, pressureOpacity: false },
1882
+ pressureSmoothing: 0.3,
1883
+ dynamics: { pressureFlow: 1 },
1884
+ light: { amount: 0.7, pressure: true }
1885
+ }),
1886
+ builtin({
1887
+ id: "spark-trail",
1888
+ name: "Spark trail",
1889
+ category: "light",
1890
+ engine: "particle",
1891
+ tip: { size: 50, hardness: 0.7, opacity: 1, flow: 1, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
1892
+ particle: { kind: "flow", render: "dot", count: 8, size: 0.06, spread: 0.7, speed: 0.9, life: 0.3 },
1893
+ light: { amount: 1 }
1894
+ }),
1851
1895
  builtin({
1852
1896
  id: "sticker-stars",
1853
1897
  name: "Star stickers",
@@ -2024,6 +2068,8 @@ export {
2024
2068
  normalizeWetBrush,
2025
2069
  IMPASTO_TOOLS,
2026
2070
  DEFAULT_IMPASTO,
2071
+ DEFAULT_LIGHT,
2072
+ normalizeLight,
2027
2073
  normalizeImpasto,
2028
2074
  MULTI_COLOR_MODES,
2029
2075
  DEFAULT_MULTI_COLOR,
@@ -74,7 +74,7 @@ function profileAlongPolyline(points, values, count = 32) {
74
74
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
75
75
  var loading = null;
76
76
  function loadCanvasBrushstrokeRenderer() {
77
- loading ??= import("./brushstrokeRender-MOYGEXOC.js").catch((error) => {
77
+ loading ??= import("./brushstrokeRender-274HGNZR.js").catch((error) => {
78
78
  loading = null;
79
79
  throw error;
80
80
  });
@@ -4,7 +4,7 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkJAOSL334cjs = require('./chunk-JAOSL334.cjs');
7
+ var _chunkGHCSC2WPcjs = require('./chunk-GHCSC2WP.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 (!_chunkJAOSL334cjs.hasEffectTags.call(void 0, t, options.tags)) continue;
136
+ if (!_chunkGHCSC2WPcjs.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 && !_chunkJAOSL334cjs.EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: role is one of ${_chunkJAOSL334cjs.EFFECT_LAYER_ROLES.join(", ")}.`);
203
+ if (layer.role !== void 0 && !_chunkGHCSC2WPcjs.EFFECT_LAYER_ROLES.includes(layer.role)) problems.push(`${where}: role is one of ${_chunkGHCSC2WPcjs.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, _chunkJAOSL334cjs.NoEffectsProviderError)();
239
- if (!provider.capabilities.supportsTemplates || !provider.describeTemplate || !provider.instantiateTemplate) throw new (0, _chunkJAOSL334cjs.EffectUnsupportedError)(`${provider.name} has no sequence templates (capabilities.supportsTemplates is not set).`);
238
+ if (!provider) throw new (0, _chunkGHCSC2WPcjs.NoEffectsProviderError)();
239
+ if (!provider.capabilities.supportsTemplates || !provider.describeTemplate || !provider.instantiateTemplate) throw new (0, _chunkGHCSC2WPcjs.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, () => ( _chunkJAOSL334cjs.EFFECT_TRANSITION_MS)) : 0;
259
+ const transition = i > 0 && shot.transition ? _nullishCoalesce(shot.transition.ms, () => ( _chunkGHCSC2WPcjs.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);
@@ -74,7 +74,7 @@ function profileAlongPolyline(points, values, count = 32) {
74
74
  var round3 = (v) => Math.round(v * 1e3) / 1e3;
75
75
  var loading = null;
76
76
  function loadCanvasBrushstrokeRenderer() {
77
- loading ??= Promise.resolve().then(() => _interopRequireWildcard(require("./brushstrokeRender-UKADOV5D.cjs"))).catch((error) => {
77
+ loading ??= Promise.resolve().then(() => _interopRequireWildcard(require("./brushstrokeRender-C7RJZPMC.cjs"))).catch((error) => {
78
78
  loading = null;
79
79
  throw error;
80
80
  });
@@ -7,7 +7,7 @@ import {
7
7
  EffectsSlot,
8
8
  createEffectPixels,
9
9
  effectValueErrors
10
- } from "./chunk-SIK3LT6M.js";
10
+ } from "./chunk-OFH6Q67U.js";
11
11
  import {
12
12
  validateJsonSchema
13
13
  } from "./chunk-V2ZCJRDM.js";
@@ -6,6 +6,8 @@ import {
6
6
  var EFFECT_KINDS = ["shader", "vector", "motion", "camera"];
7
7
  var EFFECT_USES = ["filter", "transition", "generator", "overlay", "title", "motion", "interactive", "camera"];
8
8
  var CAMERA_MOVE_ROLES = ["transition", "hold"];
9
+ var EFFECT_ACCEPTS = ["path"];
10
+ var EFFECT_PATH_FORMAT = "svg-path";
9
11
  var EFFECT_SUBJECT_KINDS = ["image", "video", "svg", "text", "slot"];
10
12
  var EFFECT_LAYER_ROLES = ["background", "subject", "overlay"];
11
13
  var EFFECT_TRANSITION_MS = 800;
@@ -104,7 +106,11 @@ function countEffectTags(effects) {
104
106
  for (const e of effects) for (const tag of new Set((e.tags ?? []).map(normaliseEffectTag))) if (tag) counts.set(tag, (counts.get(tag) ?? 0) + 1);
105
107
  return [...counts].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
106
108
  }
107
- 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);
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) && (!o.accepts?.length || o.accepts.every((a) => e.accepts?.includes(a)));
110
+ function pathControlsOf(d) {
111
+ const isPath = (s) => !!s && (s.format === EFFECT_PATH_FORMAT || [...s.anyOf ?? [], ...s.oneOf ?? []].some((x) => isPath(x)));
112
+ return Object.entries(d.controls?.properties ?? {}).filter(([, s]) => isPath(s)).map(([name]) => name);
113
+ }
108
114
  function searchEffectSummaries(catalog, query = "", options = {}) {
109
115
  const words = query.toLowerCase().split(/\s+/).filter(Boolean);
110
116
  const scored = [];
@@ -322,7 +328,8 @@ var summaryOut = (e) => ({
322
328
  transparent: e.transparent,
323
329
  loop: e.loop,
324
330
  ...e.duration !== void 0 ? { duration: e.duration } : {},
325
- ...e.camera ? { camera: e.camera } : {}
331
+ ...e.camera ? { camera: e.camera } : {},
332
+ ...e.accepts?.length ? { accepts: e.accepts } : {}
326
333
  });
327
334
  function createEffectToolProvider(host) {
328
335
  let catalog = null;
@@ -380,21 +387,32 @@ function createEffectToolProvider(host) {
380
387
  {
381
388
  name: "search_effects",
382
389
  title: "Search effects",
383
- 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). 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 }], 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.`,
390
+ 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.`,
384
391
  inputSchema: object({
385
392
  query: { type: "string", description: "Words to match (default: every effect)." },
386
393
  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)." },
387
394
  category: { type: "string", minLength: 1, description: `The provider's category, e.g. "gradient", "light", "transition".` },
388
395
  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." },
389
396
  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)." },
390
- limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." }
397
+ limit: { type: "integer", minimum: 1, maximum: 200, description: "Most effects to return (default 20)." },
398
+ 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)." }
391
399
  }),
392
400
  annotations: { title: "Search effects", ...ANNOTATIONS.read },
393
401
  handler: async (args) => {
394
402
  const p = host.provider();
395
403
  if (!p) return { available: false, reason: NO_PROVIDER, effects: [], total: 0 };
396
404
  const tags = args.tags;
397
- const found = (await explain(async () => p.search(args.query ?? "", { kind: args.kind, category: args.category, use: args.use, tags }))).filter((e) => hasEffectTags(e, tags));
405
+ const accepts = args.accepts;
406
+ let found = (await explain(async () => p.search(args.query ?? "", { kind: args.kind, category: args.category, use: args.use, tags, ...accepts?.length ? { accepts } : {} }))).filter((e) => hasEffectTags(e, tags));
407
+ if (accepts?.includes("path")) {
408
+ const kept = [];
409
+ for (const e of found) {
410
+ if (e.accepts) {
411
+ if (accepts.every((a) => e.accepts.includes(a))) kept.push(e);
412
+ } else if (kept.length < 200 && pathControlsOf(await explain(async () => p.describe(e.id))).length) kept.push(e);
413
+ }
414
+ found = kept;
415
+ }
398
416
  const limit = args.limit ?? 20;
399
417
  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) };
400
418
  if (tags?.length && !found.length) {
@@ -416,7 +434,8 @@ function createEffectToolProvider(host) {
416
434
  const p = host.provider();
417
435
  if (!p) return { available: false, reason: NO_PROVIDER };
418
436
  const d = await explain(async () => p.describe(args.effect));
419
- return { available: true, ...summaryOut(d), controls: d.controls, defaults: d.defaults, presets: d.presets, inputs: d.inputs, appliesHere: effectOffered(d, host.applyUses) };
437
+ const pathControls = pathControlsOf(d);
438
+ return { available: true, ...summaryOut(d), controls: d.controls, defaults: d.defaults, presets: d.presets, inputs: d.inputs, appliesHere: effectOffered(d, host.applyUses), ...pathControls.length ? { pathControls } : {} };
420
439
  }
421
440
  },
422
441
  {
@@ -531,6 +550,8 @@ export {
531
550
  EFFECT_KINDS,
532
551
  EFFECT_USES,
533
552
  CAMERA_MOVE_ROLES,
553
+ EFFECT_ACCEPTS,
554
+ EFFECT_PATH_FORMAT,
534
555
  EFFECT_SUBJECT_KINDS,
535
556
  EFFECT_LAYER_ROLES,
536
557
  EFFECT_TRANSITION_MS,
@@ -550,6 +571,7 @@ export {
550
571
  normaliseEffectTag,
551
572
  hasEffectTags,
552
573
  countEffectTags,
574
+ pathControlsOf,
553
575
  searchEffectSummaries,
554
576
  createEffectPixels,
555
577
  isEffectPixels,