@pixerate/editor 0.240.0 → 0.242.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 (67) 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-WB67GMUP.js → brushstrokeRender-MOYGEXOC.js} +2 -2
  8. package/dist/{brushstrokeRender-MPOM7BFW.cjs → brushstrokeRender-UKADOV5D.cjs} +5 -5
  9. package/dist/canvas/index.cjs +15 -15
  10. package/dist/canvas/index.js +2 -2
  11. package/dist/{chunk-NKTNLJUJ.cjs → chunk-5BR7LC24.cjs} +99 -11
  12. package/dist/{chunk-ZXNO3ZOX.cjs → chunk-7F4G74UQ.cjs} +35 -35
  13. package/dist/{chunk-A7PQNIBX.cjs → chunk-7V7QMFTI.cjs} +14 -14
  14. package/dist/{chunk-AD7GA3TL.cjs → chunk-AFLMXITL.cjs} +23 -23
  15. package/dist/{chunk-JAOSL334.cjs → chunk-GHCSC2WP.cjs} +38 -16
  16. package/dist/{chunk-PP6BXVRL.cjs → chunk-HGGJ36HR.cjs} +1 -1
  17. package/dist/{chunk-BBUE3UJL.cjs → chunk-HW72ABGR.cjs} +86 -86
  18. package/dist/{chunk-7YGBSFC6.js → chunk-ILM6GYAX.js} +1 -1
  19. package/dist/{chunk-JWCKJNDR.cjs → chunk-K5DS2JTA.cjs} +6 -6
  20. package/dist/{chunk-THXFU3JB.cjs → chunk-KCMV2534.cjs} +2 -2
  21. package/dist/{chunk-WDDUPWVG.js → chunk-L3JFOTBQ.js} +1 -1
  22. package/dist/{chunk-3E36VDMJ.js → chunk-NE6OOKWJ.js} +1 -1
  23. package/dist/{chunk-SIK3LT6M.js → chunk-OFH6Q67U.js} +28 -6
  24. package/dist/{chunk-RV5DKZHU.js → chunk-QP2B3MID.js} +1 -1
  25. package/dist/{chunk-G3RUKJ5C.js → chunk-RXZL4NG5.js} +1 -1
  26. package/dist/{chunk-UUBO6RB5.js → chunk-UDBDYZET.js} +1 -1
  27. package/dist/{chunk-TEFQISBY.js → chunk-X3ZINLIP.js} +1 -1
  28. package/dist/{chunk-PJYG6FCF.js → chunk-XBZK47LV.js} +98 -10
  29. package/dist/{chunk-ZQIWBJGH.cjs → chunk-XNZFVYC2.cjs} +7 -7
  30. package/dist/{chunk-XK4SST43.js → chunk-ZDPLZDTT.js} +1 -1
  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/controller.d.cts +67 -0
  42. package/dist/paint-editor/controller.d.ts +67 -0
  43. package/dist/paint-editor/effect-paint.d.cts +60 -3
  44. package/dist/paint-editor/effect-paint.d.ts +60 -3
  45. package/dist/paint-editor/index.cjs +682 -436
  46. package/dist/paint-editor/index.js +284 -38
  47. package/dist/paint-editor/stickers.d.cts +6 -0
  48. package/dist/paint-editor/stickers.d.ts +6 -0
  49. package/dist/paint-editor/thick.d.cts +4 -0
  50. package/dist/paint-editor/thick.d.ts +4 -0
  51. package/dist/paint-editor/wet.d.cts +8 -0
  52. package/dist/paint-editor/wet.d.ts +8 -0
  53. package/dist/vector/index.cjs +968 -968
  54. package/dist/vector/index.js +5 -5
  55. package/dist/video-editor/controller.d.cts +49 -1
  56. package/dist/video-editor/controller.d.ts +49 -1
  57. package/dist/video-editor/effect-path.d.cts +66 -0
  58. package/dist/video-editor/effect-path.d.ts +66 -0
  59. package/dist/video-editor/index.cjs +616 -427
  60. package/dist/video-editor/index.d.cts +1 -0
  61. package/dist/video-editor/index.d.ts +1 -0
  62. package/dist/video-editor/index.js +193 -4
  63. package/package.json +1 -1
  64. package/skills/pixerate-animation-editor/SKILL.md +1 -1
  65. package/skills/pixerate-paint-editor/SKILL.md +17 -21
  66. package/skills/pixerate-paint-editor/references/effect-paint.md +25 -0
  67. package/skills/pixerate-video-editor/SKILL.md +9 -2
@@ -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);
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2;
2
2
 
3
- var _chunkNKTNLJUJcjs = require('./chunk-NKTNLJUJ.cjs');
3
+ var _chunk5BR7LC24cjs = require('./chunk-5BR7LC24.cjs');
4
4
 
5
5
 
6
6
  var _chunkCVFAAZXEcjs = require('./chunk-CVFAAZXE.cjs');
@@ -2527,7 +2527,7 @@ function vectorExtrudeFaces(path, params) {
2527
2527
  // src/vector/brushstroke.ts
2528
2528
  var VECTOR_BRUSHSTROKE_SCALE = 2;
2529
2529
  function vectorBrushstrokeBrushes() {
2530
- return _chunkNKTNLJUJcjs.BUILTIN_BRUSHES.filter((b) => _chunkPQ3H2AANcjs.isBrushstrokeBrush.call(void 0, b));
2530
+ return _chunk5BR7LC24cjs.BUILTIN_BRUSHES.filter((b) => _chunkPQ3H2AANcjs.isBrushstrokeBrush.call(void 0, b));
2531
2531
  }
2532
2532
  function vectorBrushstrokeBrush(id) {
2533
2533
  return _nullishCoalesce(vectorBrushstrokeBrushes().find((b) => b.id === id), () => ( null));
@@ -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";
@@ -104,7 +104,7 @@ import {
104
104
  transformTextRect,
105
105
  translateMatrix,
106
106
  warpPresetMesh
107
- } from "./chunk-7YGBSFC6.js";
107
+ } from "./chunk-ILM6GYAX.js";
108
108
  import {
109
109
  bezierPathBounds,
110
110
  outlineStroke,
@@ -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,
@@ -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-WB67GMUP.js").catch((error) => {
77
+ loading ??= import("./brushstrokeRender-MOYGEXOC.js").catch((error) => {
78
78
  loading = null;
79
79
  throw error;
80
80
  });
@@ -4,7 +4,7 @@ import {
4
4
  EffectUnsupportedError,
5
5
  NoEffectsProviderError,
6
6
  hasEffectTags
7
- } from "./chunk-SIK3LT6M.js";
7
+ } from "./chunk-OFH6Q67U.js";
8
8
 
9
9
  // src/effects/templates.ts
10
10
  var SLOT_NAME = /^[A-Za-z][\w-]*$/;
@@ -5,7 +5,7 @@ import {
5
5
  moveAnchorHandle,
6
6
  subpathSegment,
7
7
  subpathSegmentCount
8
- } from "./chunk-7YGBSFC6.js";
8
+ } from "./chunk-ILM6GYAX.js";
9
9
  import {
10
10
  segmentPoint
11
11
  } from "./chunk-WEIK3UYB.js";
@@ -3,7 +3,7 @@ import {
3
3
  EffectDisposedError,
4
4
  EffectUnsupportedError,
5
5
  effectValueErrors
6
- } from "./chunk-SIK3LT6M.js";
6
+ } from "./chunk-OFH6Q67U.js";
7
7
  import {
8
8
  HistoryManager
9
9
  } from "./chunk-RNSANXPT.js";
@@ -36,12 +36,50 @@ import {
36
36
  } from "./chunk-F67XMVDI.js";
37
37
 
38
38
  // src/paint-editor/effect-paint.ts
39
+ var EFFECT_STRIP_HEIGHT = 256;
40
+ var EFFECT_MAP_REPEATS = ["repeat", "mirror", "clamp"];
41
+ function effectPaintSize(p, canvas) {
42
+ return p.map ? { width: p.map.length, height: EFFECT_STRIP_HEIGHT } : { width: canvas.width, height: canvas.height };
43
+ }
44
+ function readEffectStrokeMap(raw) {
45
+ if (raw === void 0 || raw === null || raw.by === "canvas") return void 0;
46
+ const r = raw;
47
+ if (r.by !== "stroke") throw new Error(`map.by ${JSON.stringify(r.by)} is not a mapping; use "canvas" (fixed to the canvas) or "stroke" (along each stroke).`);
48
+ const length = r.length === void 0 ? 300 : Number(r.length);
49
+ if (!(Number.isFinite(length) && length >= 16 && length <= 4096)) throw new Error(`map.length ${JSON.stringify(r.length)} is out of range; use 16..4096 px of stroke per repeat.`);
50
+ const repeat = r.repeat === void 0 ? "repeat" : r.repeat;
51
+ if (!EFFECT_MAP_REPEATS.includes(repeat)) throw new Error(`map.repeat ${JSON.stringify(r.repeat)} is not a repeat mode; use ${EFFECT_MAP_REPEATS.join(", ")}.`);
52
+ return { by: "stroke", length: Math.round(length), repeat };
53
+ }
54
+ function stripIndex(img, map, dab, x, y) {
55
+ const ca = Math.cos(dab.direction);
56
+ const sa = Math.sin(dab.direction);
57
+ const dx = x - dab.x;
58
+ const dy = y - dab.y;
59
+ let u = (dab.travel + dx * ca - dy * sa) / map.length;
60
+ if (map.repeat === "clamp") u = Math.min(1, Math.max(0, u));
61
+ else if (map.repeat === "mirror") {
62
+ const m = (u % 2 + 2) % 2;
63
+ u = m > 1 ? 2 - m : m;
64
+ } else u = (u % 1 + 1) % 1;
65
+ const v = Math.min(1, Math.max(0, ((dx * sa + dy * ca) / Math.max(0.5, dab.radius) + 1) / 2));
66
+ const px = Math.min(img.width - 1, Math.floor(u * img.width));
67
+ const py = Math.min(img.height - 1, Math.floor(v * img.height));
68
+ return (py * img.width + px) * 4;
69
+ }
39
70
  function effectPaintKey(p) {
40
- return JSON.stringify({ type: "effect", effect: p.effect, provider: p.provider, values: p.values, time: p.time });
71
+ return JSON.stringify({ type: "effect", effect: p.effect, provider: p.provider, values: p.values, time: p.time, ...p.map ? { map: p.map } : {} });
41
72
  }
42
73
  function isPaintEffectPaint(raw) {
43
74
  return Boolean(raw) && typeof raw === "object" && raw.type === "effect";
44
75
  }
76
+ function readMapLeniently(raw) {
77
+ try {
78
+ return readEffectStrokeMap(raw);
79
+ } catch {
80
+ return void 0;
81
+ }
82
+ }
45
83
  function readPaintEffectPaint(raw) {
46
84
  if (!isPaintEffectPaint(raw)) return null;
47
85
  const r = raw;
@@ -51,11 +89,27 @@ function readPaintEffectPaint(raw) {
51
89
  effect: r.effect,
52
90
  provider: typeof r.provider === "string" ? r.provider : "",
53
91
  values: JSON.parse(JSON.stringify(r.values)),
54
- time: typeof r.time === "number" && Number.isFinite(r.time) && r.time > 0 ? r.time : 0
92
+ time: typeof r.time === "number" && Number.isFinite(r.time) && r.time > 0 ? r.time : 0,
93
+ ...readMapLeniently(r.map) ? { map: readMapLeniently(r.map) } : {},
94
+ ...r.live === true ? { live: true } : {}
55
95
  };
56
96
  }
57
- function sampleEffectField(img, rect, vals, field) {
97
+ function sampleEffectField(img, rect, vals, field, dab) {
58
98
  const { width, height, data } = img;
99
+ if (img.map && dab) {
100
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
101
+ for (let xx = 0; xx < rect.width; xx++, k++) {
102
+ const i = stripIndex(img, img.map, dab, rect.x + xx + 0.5, rect.y + yy + 0.5);
103
+ const f = k * 3;
104
+ const a = data[i + 3];
105
+ if (a < 255) vals[k] *= a / 255;
106
+ field[f] = data[i] / 255;
107
+ field[f + 1] = data[i + 1] / 255;
108
+ field[f + 2] = data[i + 2] / 255;
109
+ }
110
+ }
111
+ return;
112
+ }
59
113
  for (let yy = 0, k = 0; yy < rect.height; yy++) {
60
114
  const y = rect.y + yy;
61
115
  const inY = y >= 0 && y < height;
@@ -76,7 +130,14 @@ function sampleEffectField(img, rect, vals, field) {
76
130
  }
77
131
  }
78
132
  }
79
- function sampleEffectAt(img, x, y, out) {
133
+ function sampleEffectAt(img, x, y, out, dab) {
134
+ if (img.map && dab) {
135
+ const i2 = stripIndex(img, img.map, dab, x, y);
136
+ out[0] = img.data[i2] / 255;
137
+ out[1] = img.data[i2 + 1] / 255;
138
+ out[2] = img.data[i2 + 2] / 255;
139
+ return img.data[i2 + 3] / 255;
140
+ }
80
141
  const px = Math.floor(x);
81
142
  const py = Math.floor(y);
82
143
  if (px < 0 || py < 0 || px >= img.width || py >= img.height) return 0;
@@ -86,6 +147,20 @@ function sampleEffectAt(img, x, y, out) {
86
147
  out[2] = img.data[i + 2] / 255;
87
148
  return img.data[i + 3] / 255;
88
149
  }
150
+ function effectDabField(img, rect, cov, centre, mixed, field, steps = 0, dab = null) {
151
+ sampleEffectField(img, rect, cov, field, dab);
152
+ const d0 = mixed[0] - centre[0];
153
+ const d1 = mixed[1] - centre[1];
154
+ const d2 = mixed[2] - centre[2];
155
+ const n = rect.width * rect.height;
156
+ for (let k = 0; k < n; k++) {
157
+ if (steps) cov[k] = Math.round(cov[k] * steps) / steps;
158
+ const f = k * 3;
159
+ field[f] = Math.min(1, Math.max(0, field[f] + d0));
160
+ field[f + 1] = Math.min(1, Math.max(0, field[f + 1] + d1));
161
+ field[f + 2] = Math.min(1, Math.max(0, field[f + 2] + d2));
162
+ }
163
+ }
89
164
 
90
165
  // src/paint-editor/paper.ts
91
166
  var PAPER_FAMILIES = ["watercolor", "drawing", "pastel", "canvas", "panel", "smooth"];
@@ -490,7 +565,7 @@ var DryStroke = class {
490
565
  if (this.options.effect) {
491
566
  if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
492
567
  field = this.field.subarray(0, n * 3);
493
- sampleEffectField(this.options.effect, rect, vals, field);
568
+ sampleEffectField(this.options.effect, rect, vals, field, d);
494
569
  }
495
570
  this.coverage.accumulate(rect, vals, d.opacity, null, field);
496
571
  rects.push(rect);
@@ -692,6 +767,10 @@ var StickerSpray = class {
692
767
  const a = rand01(seed, i, 53) * Math.PI * 2;
693
768
  const dist = rand01(seed, i, 55) * off;
694
769
  const base = this.colorAt(d, Math.cos(a) * dist, Math.sin(a) * dist);
770
+ if (!(base.alpha > 0)) {
771
+ this.next++;
772
+ return;
773
+ }
695
774
  const sticker = {
696
775
  id: `st${this.next++}`,
697
776
  stamp: st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "dot",
@@ -710,6 +789,12 @@ var StickerSpray = class {
710
789
  sampler;
711
790
  /** A sticker's colour: the paint colour, or gradient paint at its dab (`dx`, `dy` from the dab's centre). */
712
791
  colorAt(d, dx, dy) {
792
+ const effect = this.init.effect;
793
+ if (effect) {
794
+ const rgb = [0, 0, 0];
795
+ const alpha = sampleEffectAt(effect, d.x + dx, d.y + dy, rgb, d);
796
+ return { color: "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join(""), alpha };
797
+ }
713
798
  const g = this.init.paint;
714
799
  if (!g) return { color: this.init.color, alpha: 1 };
715
800
  this.sampler ??= new PaintGradientSampler(g, this.init.seed);
@@ -841,10 +926,8 @@ var MarkStroke = class {
841
926
  return rects;
842
927
  }
843
928
  step(copy, d, rects) {
844
- if (this.sampler) {
845
- this.pen = d;
846
- if (!this.tipTable) this.sampler.dab(d, this.penColor);
847
- }
929
+ this.pen = d;
930
+ if (this.sampler && !this.tipTable) this.sampler.dab(d, this.penColor);
848
931
  this.advance(copy, d, rects);
849
932
  }
850
933
  /** The colour of a mark at (x, y): its pen dab's, or across the tip by its offset from the pen. */
@@ -887,7 +970,7 @@ var MarkStroke = class {
887
970
  if (this.options.effect) {
888
971
  if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
889
972
  const field = this.field.subarray(0, n * 3);
890
- sampleEffectField(this.options.effect, rect, vals, field);
973
+ sampleEffectField(this.options.effect, rect, vals, field, this.pen);
891
974
  this.coverage.accumulate(rect, vals, dab.opacity, null, field);
892
975
  } else {
893
976
  const color = this.markColor(x, y);
@@ -1888,11 +1971,16 @@ function parseBrushLibrary(raw, problems = []) {
1888
1971
  }
1889
1972
 
1890
1973
  export {
1974
+ EFFECT_STRIP_HEIGHT,
1975
+ EFFECT_MAP_REPEATS,
1976
+ effectPaintSize,
1977
+ readEffectStrokeMap,
1891
1978
  effectPaintKey,
1892
1979
  isPaintEffectPaint,
1893
1980
  readPaintEffectPaint,
1894
1981
  sampleEffectField,
1895
1982
  sampleEffectAt,
1983
+ effectDabField,
1896
1984
  PAPER_FAMILIES,
1897
1985
  PAPER_LIBRARY,
1898
1986
  DEFAULT_PAPER,
@@ -7,7 +7,7 @@ var _chunkPY4GWSE2cjs = require('./chunk-PY4GWSE2.cjs');
7
7
 
8
8
 
9
9
 
10
- var _chunkJAOSL334cjs = require('./chunk-JAOSL334.cjs');
10
+ var _chunkGHCSC2WPcjs = require('./chunk-GHCSC2WP.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 = _chunkJAOSL334cjs.createEffectPixels.call(void 0, a.width, a.height);
1319
+ const out = _chunkGHCSC2WPcjs.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 = _chunkJAOSL334cjs.createEffectPixels.call(void 0, base.width, base.height);
1324
+ const out = _chunkGHCSC2WPcjs.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, _chunkJAOSL334cjs.EffectsSlot)()));
1369
+ this.slot = _nullishCoalesce(options.slot, () => ( new (0, _chunkGHCSC2WPcjs.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, _chunkJAOSL334cjs.EffectUnsupportedError)(
1511
+ throw new (0, _chunkGHCSC2WPcjs.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 _chunkJAOSL334cjs.effectValueErrors.call(void 0, d, plain)) {
1532
+ for (const e of _chunkGHCSC2WPcjs.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, _chunkJAOSL334cjs.EffectControlError)(d.id, errors);
1537
+ if (errors.length) throw new (0, _chunkGHCSC2WPcjs.EffectControlError)(d.id, errors);
1538
1538
  return { ..._nullishCoalesce(options.base, () => ( d.defaults)), ..._nullishCoalesce(values, () => ( {})) };
1539
1539
  }
1540
1540
 
@@ -18,7 +18,7 @@ import {
18
18
  unionBounds,
19
19
  vectorGradientPoints,
20
20
  widthProfileAt
21
- } from "./chunk-7YGBSFC6.js";
21
+ } from "./chunk-ILM6GYAX.js";
22
22
  import {
23
23
  segmentLength,
24
24
  segmentParamAtLength,
@@ -9,6 +9,10 @@
9
9
  * - `crossfade` (transition)
10
10
  * - `fade-in` (motion, vector only)
11
11
  * - `badge` (a vector effect with its own picture: a disc that fades in; vector only)
12
+ * - `trail` (an overlay that draws a line along its `path` control, transparent)
13
+ * - `route` (a vector effect that draws its `path` control as a line; vector only)
14
+ *
15
+ * It writes words as path data (`pathFromText`): a zig-zag per letter, font `zig`.
12
16
  *
13
17
  * With `camera: true` it also has camera moves (no pixels): `glide` and `whip` (transitions; the
14
18
  * whip cuts halfway) and `drift` (a hold that zooms in slowly).
@@ -9,6 +9,10 @@
9
9
  * - `crossfade` (transition)
10
10
  * - `fade-in` (motion, vector only)
11
11
  * - `badge` (a vector effect with its own picture: a disc that fades in; vector only)
12
+ * - `trail` (an overlay that draws a line along its `path` control, transparent)
13
+ * - `route` (a vector effect that draws its `path` control as a line; vector only)
14
+ *
15
+ * It writes words as path data (`pathFromText`): a zig-zag per letter, font `zig`.
12
16
  *
13
17
  * With `camera: true` it also has camera moves (no pixels): `glide` and `whip` (transitions; the
14
18
  * whip cuts halfway) and `drift` (a hold that zooms in slowly).
@@ -27,6 +27,12 @@ export declare function countEffectTags(effects: readonly Pick<EffectSummary, 't
27
27
  tag: string;
28
28
  count: number;
29
29
  }[];
30
+ /**
31
+ * The controls of an effect that draw along a path given at run time: those whose schema (or one
32
+ * of its `anyOf` / `oneOf` choices) has `format: 'svg-path'`. See `EFFECT_PATH_FORMAT` for the
33
+ * values they take.
34
+ */
35
+ export declare function pathControlsOf(d: Pick<EffectDescription, 'controls'>): string[];
30
36
  /**
31
37
  * Filters a catalog (kind, category, use and every tag) and ranks it against `query` (every word
32
38
  * must appear in the id, name, category, tags or description; name matches rank first). An empty query keeps catalog order.
@@ -27,6 +27,12 @@ export declare function countEffectTags(effects: readonly Pick<EffectSummary, 't
27
27
  tag: string;
28
28
  count: number;
29
29
  }[];
30
+ /**
31
+ * The controls of an effect that draw along a path given at run time: those whose schema (or one
32
+ * of its `anyOf` / `oneOf` choices) has `format: 'svg-path'`. See `EFFECT_PATH_FORMAT` for the
33
+ * values they take.
34
+ */
35
+ export declare function pathControlsOf(d: Pick<EffectDescription, 'controls'>): string[];
30
36
  /**
31
37
  * Filters a catalog (kind, category, use and every tag) and ranks it against `query` (every word
32
38
  * must appear in the id, name, category, tags or description; name matches rank first). An empty query keeps catalog order.