@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
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  toVideoFfmpeg
3
- } from "../chunk-WDDUPWVG.js";
3
+ } from "../chunk-L3JFOTBQ.js";
4
4
  import {
5
5
  deserializeLayers,
6
6
  exportPsd,
7
7
  resolveExternalAssets,
8
8
  serializeLayers,
9
9
  serializeLayersExternal
10
- } from "../chunk-3E36VDMJ.js";
10
+ } from "../chunk-NE6OOKWJ.js";
11
11
  import "../chunk-3CI7FRMK.js";
12
12
  import "../chunk-ZNA3IQZF.js";
13
13
  import {
@@ -55,7 +55,7 @@ import {
55
55
  realPresenceClock,
56
56
  tweenPresenceCursor
57
57
  } from "../chunk-IVSWRVB4.js";
58
- import "../chunk-7YGBSFC6.js";
58
+ import "../chunk-ILM6GYAX.js";
59
59
  import {
60
60
  BRUSH_ID,
61
61
  BRUSH_ROTATIONS,
@@ -72,6 +72,8 @@ import {
72
72
  DEFAULT_STICKER,
73
73
  DEFAULT_WET_BRUSH,
74
74
  DryStroke,
75
+ EFFECT_MAP_REPEATS,
76
+ EFFECT_STRIP_HEIGHT,
75
77
  IDENTITY_COPY,
76
78
  IMPASTO_TOOLS,
77
79
  MULTI_COLOR_MODES,
@@ -92,7 +94,9 @@ import {
92
94
  brushToJSON,
93
95
  clonePaper,
94
96
  drawSticker,
97
+ effectDabField,
95
98
  effectPaintKey,
99
+ effectPaintSize,
96
100
  hexToRgb8,
97
101
  isPaintEffectPaint,
98
102
  isWetBrush,
@@ -114,6 +118,7 @@ import {
114
118
  paperPreset,
115
119
  parseBrushLibrary,
116
120
  penOrientation,
121
+ readEffectStrokeMap,
117
122
  readPaintEffectPaint,
118
123
  renderPaper,
119
124
  renderStickers,
@@ -126,7 +131,7 @@ import {
126
131
  tiltToOrientation,
127
132
  toStrokePoint,
128
133
  usesPaintExchange
129
- } from "../chunk-PJYG6FCF.js";
134
+ } from "../chunk-XBZK47LV.js";
130
135
  import "../chunk-M6DNZNVY.js";
131
136
  import "../chunk-WEIK3UYB.js";
132
137
  import "../chunk-PWBZH3FK.js";
@@ -138,7 +143,7 @@ import {
138
143
  EffectsSlot,
139
144
  createEffectToolProvider,
140
145
  resolveEffectValues
141
- } from "../chunk-SIK3LT6M.js";
146
+ } from "../chunk-OFH6Q67U.js";
142
147
  import "../chunk-V2ZCJRDM.js";
143
148
  import {
144
149
  CoverageMap,
@@ -458,7 +463,7 @@ var PaintDabStroke = class {
458
463
  let flow = 1;
459
464
  if (this.options.effect) {
460
465
  const rgb = [0, 0, 0];
461
- flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb);
466
+ flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb, d);
462
467
  ex.setReservoirColor(rgb[0], rgb[1], rgb[2]);
463
468
  } else if (this.sampler && !this.gradient) {
464
469
  const g = this.sampler.dab(d, this.sample);
@@ -711,6 +716,7 @@ var ThickStrokeDriver = class {
711
716
  height = /* @__PURE__ */ new Map();
712
717
  material = /* @__PURE__ */ new Map();
713
718
  paint = [0, 0, 0];
719
+ centre = [0, 0, 0];
714
720
  tip = new Float32Array(0);
715
721
  lastRaw = null;
716
722
  lastFed = null;
@@ -837,7 +843,11 @@ var ThickStrokeDriver = class {
837
843
  }
838
844
  const ex = st.exchange;
839
845
  let flow = 1;
840
- if (this.sampler) {
846
+ const effect = this.init.effect;
847
+ if (effect) {
848
+ sampleEffectAt(effect, d.x, d.y, this.centre, d);
849
+ ex.setReservoirColor(this.centre[0], this.centre[1], this.centre[2]);
850
+ } else if (this.sampler) {
841
851
  const g = this.sampler.dab(d, this.sample);
842
852
  ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
843
853
  flow = g.alpha;
@@ -849,7 +859,12 @@ var ThickStrokeDriver = class {
849
859
  if (dep > 0) {
850
860
  const cov = new Float32Array(n);
851
861
  for (let p = 0; p < n; p++) cov[p] = tip[p] > 0 ? Math.min(1, tip[p] * (im.tool === "roller" || im.tool === "gloop" || im.tool === "tube" ? 1.6 : 1.1)) : 0;
852
- this.medium.deposit(rect, cov, dep, this.paint, ex.paintPigment);
862
+ let field = null;
863
+ if (effect) {
864
+ field = new Float32Array(n * 3);
865
+ effectDabField(effect, rect, cov, this.centre, this.paint, field, 0, d);
866
+ }
867
+ this.medium.deposit(rect, cov, dep, this.paint, ex.paintPigment, field);
853
868
  }
854
869
  const seed = this.init.seed;
855
870
  for (let yy = 0, p = 0; yy < rect.height; yy++) {
@@ -996,7 +1011,6 @@ var PaintStrokeSession = class {
996
1011
  const brush = init.brush;
997
1012
  if (init.wet) this.wetDriver = init.wet.driver;
998
1013
  const effect = init.effect?.pixels ?? null;
999
- if (effect && (init.wet || init.thick || init.sticker)) throw new Error(`The ${brush.name} brush (${brush.engine} engine) cannot paint with effect paint.`);
1000
1014
  this.apply = {
1001
1015
  mode: brush.engine === "eraser" ? "erase" : "paint",
1002
1016
  color: init.color,
@@ -1009,10 +1023,10 @@ var PaintStrokeSession = class {
1009
1023
  };
1010
1024
  if (this.wetDriver) {
1011
1025
  } else if (init.sticker) {
1012
- this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null });
1026
+ this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect });
1013
1027
  } else if (init.thick) {
1014
1028
  const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
1015
- this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null });
1029
+ this.thickDriver = new ThickStrokeDriver({ brush, target: this.tileTarget(), channels: init.thick.channels, color: rgb(init.color), load: init.mixerLoad, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect });
1016
1030
  } else if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush, Boolean(init.paint))) {
1017
1031
  const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
1018
1032
  this.wet = new PaintDabStroke(brush, {
@@ -1530,6 +1544,7 @@ function varyStroke(stroke, m, v) {
1530
1544
  const brush = s !== 1 ? normalizePaintBrush({ ...stroke.brush, tip: { ...stroke.brush.tip, size: stroke.brush.tip.size * s } }) : stroke.brush;
1531
1545
  const out = { ...stroke, brush, points };
1532
1546
  if (stroke.paint?.type === "gradient" && s !== 1) out.paint = scalePaintGradient(stroke.paint, s);
1547
+ if (stroke.paint?.type === "effect" && stroke.paint.map && s !== 1) out.paint = { ...stroke.paint, map: { ...stroke.paint.map, length: Math.min(4096, Math.max(16, Math.round(stroke.paint.map.length * s))) } };
1533
1548
  if (stroke.copies?.length) {
1534
1549
  const inv = invertAffine(m);
1535
1550
  out.copies = stroke.copies.map((c) => composeAffine(m, composeAffine(c, inv)));
@@ -1941,6 +1956,7 @@ var WetStrokeDriver = class {
1941
1956
  tip = new Float32Array(0);
1942
1957
  cov = new Float32Array(0);
1943
1958
  paint = [0, 0, 0];
1959
+ centre = [0, 0, 0];
1944
1960
  depth;
1945
1961
  sampler;
1946
1962
  sample = { rgb: [0, 0, 0], alpha: 1 };
@@ -2041,14 +2057,23 @@ var WetStrokeDriver = class {
2041
2057
  case "paint": {
2042
2058
  const ex = st.exchange;
2043
2059
  let flow = 1;
2044
- if (this.sampler) {
2060
+ const effect = this.init.effect;
2061
+ if (effect) {
2062
+ sampleEffectAt(effect, d.x, d.y, this.centre, d);
2063
+ ex.setReservoirColor(this.centre[0], this.centre[1], this.centre[2]);
2064
+ } else if (this.sampler) {
2045
2065
  const g = this.sampler.dab(d, this.sample);
2046
2066
  ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
2047
2067
  flow = g.alpha;
2048
2068
  }
2049
2069
  ex.pickUp(this.medium.sample(rect, tip));
2050
2070
  const total = ex.paint(this.paint);
2051
- if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);
2071
+ if (total > 0 && flow > 0 && effect) {
2072
+ const ec = cov.slice();
2073
+ const field = new Float32Array(ec.length * 3);
2074
+ effectDabField(effect, rect, ec, this.centre, this.paint, field, COVERAGE_STEPS, d);
2075
+ this.medium.deposit(rect, ec, Math.min(1, total) * k, this.paint, null, field);
2076
+ } else if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);
2052
2077
  else world.paint(laneId, rect, cov, this.medium.options.water * k, null);
2053
2078
  ex.drain();
2054
2079
  if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);
@@ -3510,6 +3535,15 @@ var PaintEditorController = class _PaintEditorController {
3510
3535
  dirty = "all";
3511
3536
  seedCounter = 1;
3512
3537
  strokeCounter = 1;
3538
+ /** Live effect paint: playing and the frame rate cap (editor state), and the time the view shows. */
3539
+ effectPlaybackState = { playing: true, fps: 30 };
3540
+ effectTime = 0;
3541
+ /** While a frame is drawn: the time live effect strokes are drawn at (else their own time). */
3542
+ liveEffectAt = null;
3543
+ /** The view's stroke layers at `effectTime`, each with the document surface it was drawn from (stale once that changes). */
3544
+ animatedLayers = /* @__PURE__ */ new Map();
3545
+ /** `renderAnimationFrame` / `withEffectTime`: layer surfaces at another time, for one render. */
3546
+ frameOverride = null;
3513
3547
  /** The area each stroke painted (document pixels; `null`: nothing), learnt as it is painted or drawn, so a stroke layer edit redraws only where it changes (see `drawLiveStrokes`). */
3514
3548
  paintedBounds = /* @__PURE__ */ new WeakMap();
3515
3549
  guides;
@@ -3598,6 +3632,7 @@ var PaintEditorController = class _PaintEditorController {
3598
3632
  mixerLoad: this.mixerLoad,
3599
3633
  view: this.view,
3600
3634
  hand: this.hand,
3635
+ effectPlayback: { ...this.effectPlaybackState, time: this.effectTime },
3601
3636
  selectedStroke: this.selectedStroke,
3602
3637
  selectedSticker: this.selectedSticker,
3603
3638
  canUndo: this.undoStack.length > 0 && !this.session,
@@ -4401,16 +4436,25 @@ var PaintEditorController = class _PaintEditorController {
4401
4436
  * size, and brushes then take each pixel's colour (and alpha) from it, fixed to the canvas, as if
4402
4437
  * painting a hidden picture into view. `setColor` and `setPaint` go back to colour. Editor state,
4403
4438
  * not an undo step. Strokes that would paint with it throw while it renders. Returns the paint.
4439
+ * With `map: { by: 'stroke' }` the effect lies along each stroke instead of the canvas; with
4440
+ * `live`, strokes on a stroke layer keep playing it (see `setEffectPlayback`).
4404
4441
  */
4405
4442
  async setEffectPaint(source) {
4406
4443
  if (!source || typeof source.effect !== "string" || !source.effect) throw new PaintEditorError("effect is missing; pass an effect id from search_effects (use generator, overlay or title).");
4407
4444
  this.checkTime(source.time);
4445
+ let map;
4446
+ try {
4447
+ map = readEffectStrokeMap(source.map);
4448
+ } catch (e) {
4449
+ throw new PaintEditorError(e.message);
4450
+ }
4451
+ if (source.live !== void 0 && typeof source.live !== "boolean") throw new PaintEditorError("live must be true or false.");
4408
4452
  const epoch = ++this.paintEpoch;
4409
4453
  const changed = (name) => new PaintEditorError(`The paint changed while "${name}" rendered (set_color, set_paint or another set_effect_paint); call set_effect_paint again to paint with it.`);
4410
4454
  const d = await this.describeEffect(source.effect, "paint");
4411
4455
  const values = this.effectValues(d, { ...d.defaults }, source.values);
4412
4456
  if (epoch !== this.paintEpoch) throw changed(d.name);
4413
- const paint = { type: "effect", effect: d.id, provider: this.effects.require().id, values, time: source.time ?? 0 };
4457
+ const paint = { type: "effect", effect: d.id, provider: this.effects.require().id, values, time: source.time ?? 0, ...map ? { map } : {}, ...source.live ? { live: true } : {} };
4414
4458
  this.effectPaintPending = paint;
4415
4459
  this.emit([]);
4416
4460
  try {
@@ -4433,7 +4477,8 @@ var PaintEditorController = class _PaintEditorController {
4433
4477
  effectPaintImage(p) {
4434
4478
  const key = effectPaintKey(p);
4435
4479
  const img = this.effectPaintPixels.get(key);
4436
- if (!img || img.width !== this.doc.canvas.width || img.height !== this.doc.canvas.height) return null;
4480
+ const size = effectPaintSize(p, this.doc.canvas);
4481
+ if (!img || img.width !== size.width || img.height !== size.height) return null;
4437
4482
  this.effectPaintPixels.delete(key);
4438
4483
  this.effectPaintPixels.set(key, img);
4439
4484
  return img;
@@ -4446,14 +4491,14 @@ var PaintEditorController = class _PaintEditorController {
4446
4491
  const what = `the effect "${p.effect}"${p.provider ? ` from provider "${p.provider}"` : ""}`;
4447
4492
  if (!provider) throw new PaintEditorError(`Effect paint needs ${what}, but no effects provider is registered; register it (setEffectsProvider) and try again.`);
4448
4493
  if (p.provider && provider.id !== p.provider) throw new PaintEditorError(`Effect paint needs ${what}, but the registered provider is "${provider.id}"; register "${p.provider}" (setEffectsProvider) and try again.`);
4449
- const { width, height } = this.doc.canvas;
4494
+ const { width, height } = effectPaintSize(p, this.doc.canvas);
4450
4495
  let pixels;
4451
4496
  try {
4452
4497
  pixels = await this.effectsRenderer.render(provider, p.effect, { width, height }, { time: p.time, values: p.values });
4453
4498
  } catch (e) {
4454
4499
  throw new PaintEditorError(`Effect paint could not render ${what}: ${e instanceof Error ? e.message : String(e)}`);
4455
4500
  }
4456
- const img = { width, height, data: new Uint8ClampedArray(pixels.data) };
4501
+ const img = { width, height, data: new Uint8ClampedArray(pixels.data), ...p.map ? { map: p.map } : {} };
4457
4502
  this.effectPaintPixels.set(effectPaintKey(p), img);
4458
4503
  while (this.effectPaintPixels.size > _PaintEditorController.EFFECT_PAINT_CACHE) this.effectPaintPixels.delete(this.effectPaintPixels.keys().next().value);
4459
4504
  return img;
@@ -4514,11 +4559,6 @@ var PaintEditorController = class _PaintEditorController {
4514
4559
  source = this.effectPaint;
4515
4560
  }
4516
4561
  if (!source) return null;
4517
- if (brush.engine === "wet" || brush.engine === "impasto" || brush.engine === "sticker") {
4518
- throw new PaintEditorError(
4519
- `The ${brush.name} brush (${brush.engine} engine) cannot paint with effect paint: it carries one colour as ${brush.engine === "sticker" ? "stickers" : brush.engine === "wet" ? "wet pigment" : "thick paint"}. Paint with a dry brush (a dab, particle, bristle or mixer brush), or paint the shape first and fill it with fill_layer_with_effect {lockTransparency: true}.`
4520
- );
4521
- }
4522
4562
  const pixels = this.effectPaintImage(source);
4523
4563
  if (!pixels) throw new PaintEditorError(`The effect paint "${source.effect}"${source.provider ? ` from provider "${source.provider}"` : ""} is not rendered at this canvas size; call set_effect_paint (or prepareEffectPaints) first.`);
4524
4564
  return { source, pixels };
@@ -4815,6 +4855,13 @@ var PaintEditorController = class _PaintEditorController {
4815
4855
  const why = liveStrokeRefusal(brush);
4816
4856
  if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
4817
4857
  }
4858
+ if (doc.liveStrokes?.[layer.id] && effect?.source.live) {
4859
+ const key = effectPaintKey(effect.source);
4860
+ const { sources } = this.liveEffects(doc);
4861
+ if (!sources.some((p) => effectPaintKey(p) === key) && sources.length >= MAX_LIVE_EFFECT_SOURCES) {
4862
+ throw new PaintEditorError(`The painting already plays ${MAX_LIVE_EFFECT_SOURCES} live effects, the most it can; paint with one of them, or set_effect_paint with live false to bake this one.`);
4863
+ }
4864
+ }
4818
4865
  const stickerLayer = Boolean(doc.stickers?.[layer.id]?.length);
4819
4866
  if (brush.engine === "sticker") {
4820
4867
  if (!stickerLayer && contentBounds(layer.surface)) throw new PaintEditorError(`Layer "${layer.name}" already has paint; spray stickers onto an empty layer (add_layer) so they stay editable.`);
@@ -4854,7 +4901,8 @@ var PaintEditorController = class _PaintEditorController {
4854
4901
  seed: seedW,
4855
4902
  copies: [IDENTITY_AFFINE, ...copiesW].map(affineCopy),
4856
4903
  mask: init.maskStencils ? this.stencilSelection(init.maskStencils) : init.stencils !== false ? this.stencilSelection() : null,
4857
- paint: wetGradient
4904
+ paint: effect ? null : wetGradient,
4905
+ effect: effect?.pixels ?? null
4858
4906
  });
4859
4907
  init = { ...init, seed: seedW, load };
4860
4908
  wet = { driver, surface: () => world.display(id).surface };
@@ -5418,8 +5466,10 @@ var PaintEditorController = class _PaintEditorController {
5418
5466
  }
5419
5467
  /** The layers with the live stroke's surface in place. */
5420
5468
  currentLayers() {
5469
+ const override = this.frameOverride;
5470
+ if (override) return Array.from(override).reduce((ls, [id, surface]) => mapLayer(ls, id, (l) => ({ ...l, surface })), this.doc.layers);
5421
5471
  const live = this.live;
5422
- if (!live) return this.doc.layers;
5472
+ if (!live) return this.animatedView();
5423
5473
  if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;
5424
5474
  const layers = mapLayer(this.doc.layers, live.layerId, (l) => ({ ...l, surface: live.surface }));
5425
5475
  this.liveLayers = { key: live.surface, layers };
@@ -6357,11 +6407,13 @@ var PaintEditorController = class _PaintEditorController {
6357
6407
  if (!cancelled && drawn) out = drawn;
6358
6408
  }
6359
6409
  };
6360
- const init = { layerId: plain.id, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...st.paint ? { paint: st.paint } : {}, snapToPerspective: false, stencils: false, maskStencils: st.stencils ?? [] };
6410
+ const paint = st.paint && this.liveEffectAt !== null && isLiveEffect(st) ? framePaint(st.paint, this.liveEffectAt) : st.paint;
6411
+ const init = { layerId: plain.id, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...paint ? { paint } : {}, snapToPerspective: false, stencils: false, maskStencils: st.stencils ?? [] };
6361
6412
  const session = this.sessionFor({ ...doc, layers: mapLayer(doc.layers, plain.id, () => ({ ...plain, surface })) }, init, host);
6362
6413
  session.push(st.points);
6363
6414
  const result = session.end();
6364
- this.paintedBounds.set(st, result.bounds);
6415
+ const had = this.paintedBounds.get(st);
6416
+ this.paintedBounds.set(st, had && result.bounds ? unionRect(had, result.bounds) : result.bounds ?? had ?? null);
6365
6417
  return { surface: out, bounds: result.bounds };
6366
6418
  }
6367
6419
  /**
@@ -6398,6 +6450,146 @@ var PaintEditorController = class _PaintEditorController {
6398
6450
  for (const r of rects) surface = writeRegion(surface, r, readRegion(drawn, r));
6399
6451
  return surface;
6400
6452
  }
6453
+ // --- Live effect paint ------------------------------------------------------------
6454
+ /** The view's layers: stroke layers playing live effects at `effectTime`, the rest as they are. */
6455
+ animatedView() {
6456
+ if (!this.animatedLayers.size) return this.doc.layers;
6457
+ let layers = this.doc.layers;
6458
+ for (const [id, a] of this.animatedLayers) {
6459
+ const l = findLayer(layers, id)?.layer;
6460
+ if (isPixelLayer(l) && l.surface === a.base) layers = mapLayer(layers, id, () => ({ ...l, surface: a.surface }));
6461
+ }
6462
+ return layers;
6463
+ }
6464
+ /** Stroke layers with live effect strokes, and the distinct live sources (frame 0). */
6465
+ liveEffects(doc = this.doc) {
6466
+ const layers = [];
6467
+ const sources = /* @__PURE__ */ new Map();
6468
+ for (const [id, strokes] of Object.entries(doc.liveStrokes ?? {})) {
6469
+ const layer = findLayer(doc.layers, id)?.layer;
6470
+ const live = strokes.filter(isLiveEffect);
6471
+ if (!isPixelLayer(layer) || !live.length) continue;
6472
+ layers.push({ layer, strokes });
6473
+ for (const st of live) sources.set(effectPaintKey(st.paint), st.paint);
6474
+ }
6475
+ return { layers, sources: [...sources.values()] };
6476
+ }
6477
+ /** What plays: the live effect strokes and their layers, or `null` when nothing does. */
6478
+ getAnimation() {
6479
+ const { layers, sources } = this.liveEffects();
6480
+ if (!layers.length) return null;
6481
+ return { layerIds: layers.map((x) => x.layer.id), strokes: layers.reduce((n, x) => n + x.strokes.filter(isLiveEffect).length, 0), sources: sources.length };
6482
+ }
6483
+ /** Plays or pauses live effect paint, and caps its frame rate (1..60). Editor state, not an undo step. */
6484
+ setEffectPlayback(update) {
6485
+ if (update.playing !== void 0 && typeof update.playing !== "boolean") throw new PaintEditorError("playing must be true or false.");
6486
+ if (update.fps !== void 0 && !(Number.isInteger(update.fps) && update.fps >= 1 && update.fps <= 60)) throw new PaintEditorError(`fps ${update.fps} is out of range; use a whole number 1..60.`);
6487
+ this.effectPlaybackState = { ...this.effectPlaybackState, ...update };
6488
+ this.emit([]);
6489
+ return { ...this.effectPlaybackState, time: this.effectTime };
6490
+ }
6491
+ /** The surfaces of every stroke layer with live effects at `time` ms, drawn in full (frames rendered first). */
6492
+ async liveSurfacesAt(time) {
6493
+ const { layers, sources } = this.liveEffects();
6494
+ await this.prepareEffectPaints(sources.map((p) => framePaint(p, time)));
6495
+ const out = /* @__PURE__ */ new Map();
6496
+ this.liveEffectAt = time;
6497
+ try {
6498
+ for (const { layer, strokes } of layers) out.set(layer.id, this.drawLiveStrokes({ ...layer, offset: { x: 0, y: 0 } }, strokes));
6499
+ } finally {
6500
+ this.liveEffectAt = null;
6501
+ }
6502
+ return out;
6503
+ }
6504
+ /**
6505
+ * Shows the live effects at `time` ms (each live stroke's effect at its own time plus `time`):
6506
+ * renders their frames, then redraws only where live effect strokes paint. Nothing in the
6507
+ * document changes. Resolves `false` when the document changed while frames rendered (the next
6508
+ * frame catches up).
6509
+ */
6510
+ async showEffectTime(time) {
6511
+ this.checkTime(time);
6512
+ const { layers, sources } = this.liveEffects();
6513
+ const revision = this.docRevision;
6514
+ await this.prepareEffectPaints(sources.map((p) => framePaint(p, time)));
6515
+ if (revision !== this.docRevision || this.session) return false;
6516
+ const next = /* @__PURE__ */ new Map();
6517
+ const dirty = [];
6518
+ this.liveEffectAt = time;
6519
+ try {
6520
+ for (const { layer, strokes } of layers) {
6521
+ const shown = this.animatedLayers.get(layer.id);
6522
+ const from = shown && shown.base === layer.surface ? shown.surface : layer.surface;
6523
+ const areas = strokes.filter(isLiveEffect).flatMap((st) => [liveStrokeBounds(st), ...this.paintedBounds.get(st) ? [this.paintedBounds.get(st)] : []]);
6524
+ const surface = this.drawLiveStrokes({ ...layer, surface: from, offset: { x: 0, y: 0 } }, strokes, layer.offset.x || layer.offset.y ? null : areas);
6525
+ next.set(layer.id, { base: layer.surface, surface });
6526
+ dirty.push(...areas);
6527
+ }
6528
+ } finally {
6529
+ this.liveEffectAt = null;
6530
+ }
6531
+ this.animatedLayers = next;
6532
+ this.effectTime = time;
6533
+ this.emit(dirty);
6534
+ return true;
6535
+ }
6536
+ /**
6537
+ * Plays live effect paint in the view until `stop`: a frame per tick of `clock` (default
6538
+ * animation frames) at most `fps` a second, one render in flight. Idle while paused, while a
6539
+ * stroke is painted, or when nothing is live. The bindings start it while the editor is shown.
6540
+ */
6541
+ playEffects(clock = defaultEffectClock) {
6542
+ let stopped = false;
6543
+ let cancel = () => {
6544
+ };
6545
+ let last = -Infinity;
6546
+ let started = clock.now() - this.effectTime;
6547
+ const tick = () => {
6548
+ if (stopped) return;
6549
+ const now = clock.now();
6550
+ const { playing, fps } = this.effectPlaybackState;
6551
+ if (!playing) started = now - this.effectTime;
6552
+ if (!playing || this.session || now - last < 1e3 / fps - 1 || !this.liveEffects().layers.length) {
6553
+ cancel = clock.frame(tick);
6554
+ return;
6555
+ }
6556
+ last = now;
6557
+ this.showEffectTime(Math.max(0, now - started)).then(
6558
+ () => {
6559
+ if (!stopped) cancel = clock.frame(tick);
6560
+ },
6561
+ () => {
6562
+ if (!stopped) cancel = clock.frame(tick);
6563
+ }
6564
+ );
6565
+ };
6566
+ cancel = clock.frame(tick);
6567
+ return {
6568
+ stop() {
6569
+ stopped = true;
6570
+ cancel();
6571
+ }
6572
+ };
6573
+ }
6574
+ /**
6575
+ * Runs `fn` with the painting as it is `time` ms into its live effects (see `getAnimation`): for
6576
+ * exports at a moment. Nothing in the document changes.
6577
+ */
6578
+ async withEffectTime(time, fn) {
6579
+ this.checkTime(time);
6580
+ const surfaces = await this.liveSurfacesAt(time);
6581
+ const was = this.frameOverride;
6582
+ this.frameOverride = surfaces;
6583
+ try {
6584
+ return await fn();
6585
+ } finally {
6586
+ this.frameOverride = was;
6587
+ }
6588
+ }
6589
+ /** The painting composited `time` ms into its live effects, like `compositePixels`. Nothing in the document changes. */
6590
+ async renderAnimationFrame(time, options = {}) {
6591
+ return this.withEffectTime(time, () => this.compositePixels(options));
6592
+ }
6401
6593
  // --- Stencils --------------------------------------------------------------------
6402
6594
  stencilCache = null;
6403
6595
  nextStencilId = 1;
@@ -6615,6 +6807,24 @@ var PaintEditorController = class _PaintEditorController {
6615
6807
  var MAX_IMAGE_SIDE = 8192;
6616
6808
  var movePoint = (p, dx, dy) => ({ x: p.x + dx, y: p.y + dy });
6617
6809
  var moveRect = (r, dx, dy) => ({ ...r, x: r.x + dx, y: r.y + dy });
6810
+ function isLiveEffect(s) {
6811
+ return s.paint?.type === "effect" && s.paint.live === true;
6812
+ }
6813
+ function framePaint(p, t) {
6814
+ return { ...p, time: p.time + t };
6815
+ }
6816
+ var MAX_LIVE_EFFECT_SOURCES = 8;
6817
+ var defaultEffectClock = {
6818
+ now: () => typeof performance !== "undefined" ? performance.now() : Date.now(),
6819
+ frame: (cb) => {
6820
+ if (typeof requestAnimationFrame === "undefined") {
6821
+ const t = setTimeout(cb, 16);
6822
+ return () => clearTimeout(t);
6823
+ }
6824
+ const id = requestAnimationFrame(cb);
6825
+ return () => cancelAnimationFrame(id);
6826
+ }
6827
+ };
6618
6828
  function clipRect(r, width, height) {
6619
6829
  const x0 = Math.max(0, Math.floor(r.x));
6620
6830
  const y0 = Math.max(0, Math.floor(r.y));
@@ -8068,6 +8278,7 @@ var BRUSH_FIELDS = {
8068
8278
  sticker: STICKER_BRUSH,
8069
8279
  paint: BRUSH_PAINT
8070
8280
  };
8281
+ var TIME = { type: "number", minimum: 0, description: "Draw live effect paint (strokes on stroke layers painted with live effect paint) this many ms into its animation (default: as painted, its own time)." };
8071
8282
  var POINTS = {
8072
8283
  type: "array",
8073
8284
  minItems: 1,
@@ -8904,20 +9115,22 @@ function createPaintEditorToolProvider(controller, options = {}) {
8904
9115
  add({
8905
9116
  name: "export_image",
8906
9117
  title: "Export the painting",
8907
- description: "Exports the painting as PNG, JPEG or WebP (base64). JPEG always includes the paper; JPEG and WebP need a browser canvas. Changes nothing. Returns the data, its MIME type and size.",
9118
+ description: "Exports the painting as PNG, JPEG or WebP (base64). JPEG always includes the paper; JPEG and WebP need a browser canvas. With time, live effect paint is drawn at that moment (get_effect_playback / set_effect_playback). Changes nothing. Returns the data, its MIME type and size.",
8908
9119
  inputSchema: {
8909
9120
  type: "object",
8910
9121
  properties: {
8911
9122
  format: { type: "string", enum: ["image/png", "image/jpeg", "image/webp"], description: "File type (default image/png)." },
8912
9123
  quality: { type: "number", minimum: 0, maximum: 1, description: "JPEG / WebP quality 0..1 (default 0.92)." },
8913
9124
  scale: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Shrink by this factor (default 1)." },
8914
- paper: { type: "boolean", description: "Include the paper (default true)." }
9125
+ paper: { type: "boolean", description: "Include the paper (default true)." },
9126
+ time: TIME
8915
9127
  },
8916
9128
  additionalProperties: false
8917
9129
  },
8918
9130
  annotations: { title: "Export the painting", ...READ6 },
8919
- handler: async ({ format, quality, scale, paper }) => {
8920
- const bytes = await c.exportImage({ format, quality, scale, paper });
9131
+ handler: async ({ format, quality, scale, paper, time }) => {
9132
+ const run = () => c.exportImage({ format, quality, scale, paper });
9133
+ const bytes = time === void 0 ? await run() : await c.withEffectTime(time, run);
8921
9134
  return { mimeType: format ?? "image/png", bytes: bytes.length, data: bytesToBase64(bytes) };
8922
9135
  }
8923
9136
  });
@@ -8925,10 +9138,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
8925
9138
  name: "export_psd",
8926
9139
  title: "Export as PSD",
8927
9140
  description: 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked "Paper" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',
8928
- inputSchema: EMPTY3,
9141
+ inputSchema: { type: "object", properties: { time: TIME }, additionalProperties: false },
8929
9142
  annotations: { title: "Export as PSD", ...READ6 },
8930
- handler: () => {
8931
- const r = c.exportPsd();
9143
+ handler: async ({ time }) => {
9144
+ const r = time === void 0 ? c.exportPsd() : await c.withEffectTime(time, () => c.exportPsd());
8932
9145
  return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
8933
9146
  }
8934
9147
  });
@@ -9660,7 +9873,7 @@ function createPaintEffectsToolProvider(controller, options = {}) {
9660
9873
  };
9661
9874
  const paintInfo = () => {
9662
9875
  const p = controller.getEffectPaint();
9663
- return p ? [{ id: PAINT_TARGET, effect: p.effect, provider: p.provider, role: controller.getState().effectPaintRendering ? "effect paint (rendering)" : "effect paint", enabled: true, values: p.values, ...p.time ? { time: p.time } : {} }] : [];
9876
+ return p ? [{ id: PAINT_TARGET, effect: p.effect, provider: p.provider, role: controller.getState().effectPaintRendering ? "effect paint (rendering)" : "effect paint", enabled: true, values: p.values, ...p.time ? { time: p.time } : {}, ...p.map ? { map: p.map } : {}, ...p.live ? { live: true } : {} }] : [];
9664
9877
  };
9665
9878
  const layerInfo = (layerId) => {
9666
9879
  const s = controller.getEffectLayer(layerId);
@@ -9729,7 +9942,7 @@ function createPaintEffectsToolProvider(controller, options = {}) {
9729
9942
  if (target === PAINT_TARGET) {
9730
9943
  if (changes.opacity !== void 0) throw new Error("opacity does not apply to the effect paint: change the brush opacity (set_brush tip.opacity).");
9731
9944
  const p = controller.getEffectPaint();
9732
- await controller.setEffectPaint({ effect: p.effect, values: { ...p.values, ...changes.values ?? {} }, time: p.time });
9945
+ await controller.setEffectPaint({ effect: p.effect, values: { ...p.values, ...changes.values ?? {} }, time: p.time, map: p.map, live: p.live });
9733
9946
  const effects2 = paintInfo();
9734
9947
  return { target, effect: effects2[0], effects: effects2 };
9735
9948
  }
@@ -9788,13 +10001,25 @@ function createPaintEffectsToolProvider(controller, options = {}) {
9788
10001
  {
9789
10002
  name: "set_effect_paint",
9790
10003
  title: "Paint with an effect",
9791
- description: `Makes brushes paint with a provider generator, overlay or title effect (search_effects with use generator, overlay or title): it is rendered once at the canvas size, and each stroke then takes every pixel's colour from it, fixed to the canvas, as if painting a hidden picture into view (the effect's transparency paints nothing). Dab, particle and bristle brushes take the colour per pixel; mixer brushes and dab brushes that pick up paint take it per dab, at its centre. Wet, impasto and sticker brushes refuse it (paint the shape and use fill_layer_with_effect with lockTransparency instead); erasers, blenders and smudges ignore it. It replaces the paint colour or gradient until set_color or set_paint; a brush with its own gradient paints that instead. Strokes record it and replay exactly. Editor state, not an undo step. Returns the effect paint: { effect, provider, values (every control), time } (list_applied_effects target "paint" shows it; update_effect / remove_effect with target and effect "paint" change or clear it).`,
10004
+ description: `Makes brushes paint with a provider generator, overlay or title effect (search_effects with use generator, overlay or title): it is rendered once at the canvas size, and each stroke then takes every pixel's colour from it, fixed to the canvas, as if painting a hidden picture into view (the effect's transparency paints nothing). Dab, particle and bristle brushes take the colour per pixel; mixer brushes and dab brushes that pick up paint take it per dab, at its centre. Watercolour, gouache, oil and impasto brushes take it per pixel too, shifted by paint they pick up (in wet paint the pattern then flows with the water; impasto keeps its relief), and sprayed stickers take the colour at their centre. Erasers, blenders and smudges ignore it. It replaces the paint colour or gradient until set_color or set_paint; a brush with its own gradient paints that instead. Strokes record it and replay exactly. Editor state, not an undo step. Returns the effect paint: { effect, provider, values (every control), time } (list_applied_effects target "paint" shows it; update_effect / remove_effect with target and effect "paint" change or clear it).`,
9792
10005
  inputSchema: {
9793
10006
  type: "object",
9794
10007
  properties: {
9795
10008
  effect: ctx.effectIdSchema(["generator", "overlay", "title"]),
9796
10009
  values: ctx.valuesSchema("Control values over the effect's defaults"),
9797
- time: { type: "number", minimum: 0, description: "Which moment of an animated effect to paint with, in ms (default 0)." }
10010
+ time: { type: "number", minimum: 0, description: "Which moment of an animated effect to paint with, in ms (default 0)." },
10011
+ map: {
10012
+ type: "object",
10013
+ description: "How the effect lies on the paint: by canvas (default: fixed to the canvas, painting reveals a hidden picture) or by stroke (along each stroke: the effect is rendered as a strip length px long, and each pixel takes the colour at its distance along the stroke and its place across the tip).",
10014
+ properties: {
10015
+ by: { type: "string", enum: ["canvas", "stroke"], description: "canvas (default) or stroke." },
10016
+ length: { type: "number", minimum: 16, maximum: 4096, description: "by stroke: px of stroke per repeat of the effect (default 300)." },
10017
+ repeat: { type: "string", enum: ["repeat", "mirror", "clamp"], description: "by stroke: past the strip end start again (default), run back, or hold the last colour." }
10018
+ },
10019
+ required: ["by"],
10020
+ additionalProperties: false
10021
+ },
10022
+ live: { type: "boolean", description: "Live effect paint (default false): strokes on a stroke layer (add_layer type strokes) keep playing the effect, so the paint moves (set_effect_playback); elsewhere strokes bake the frame at time." }
9798
10023
  },
9799
10024
  required: ["effect"],
9800
10025
  additionalProperties: false
@@ -9802,9 +10027,24 @@ function createPaintEffectsToolProvider(controller, options = {}) {
9802
10027
  annotations: { title: "Paint with an effect", ...ctx.annotations.writeIdempotent },
9803
10028
  handler: async (args) => {
9804
10029
  ctx.requireProvider();
9805
- return { effectPaint: await controller.setEffectPaint({ effect: args.effect, values: values(args.values), time: args.time }) };
10030
+ return { effectPaint: await controller.setEffectPaint({ effect: args.effect, values: values(args.values), time: args.time, map: args.map, live: args.live }) };
9806
10031
  }
9807
10032
  },
10033
+ {
10034
+ name: "set_effect_playback",
10035
+ title: "Play or pause live effect paint",
10036
+ description: "Plays or pauses live effect paint (strokes painted on stroke layers with set_effect_paint live true) in the editor view, and caps its frame rate. Exports draw it at a chosen time with export_image / export_psd time. Editor state, not an undo step. Returns playing, fps, the time the view shows (ms) and what plays (animation: layer ids, live strokes, sources; null when nothing does).",
10037
+ inputSchema: {
10038
+ type: "object",
10039
+ properties: {
10040
+ playing: { type: "boolean", description: "Play (true, the default state) or pause at the current frame." },
10041
+ fps: { type: "integer", minimum: 1, maximum: 60, description: "Frame rate cap, frames a second (default 30); lower it on large canvases." }
10042
+ },
10043
+ additionalProperties: false
10044
+ },
10045
+ annotations: { title: "Play or pause live effect paint", ...ctx.annotations.writeIdempotent },
10046
+ handler: async (args) => ({ ...controller.setEffectPlayback({ playing: args.playing, fps: args.fps }), animation: controller.getAnimation() })
10047
+ },
9808
10048
  {
9809
10049
  name: "add_effect_layer",
9810
10050
  title: "Add an effect layer",
@@ -9914,6 +10154,8 @@ export {
9914
10154
  DEFAULT_WET_BRUSH,
9915
10155
  DEFAULT_WET_PARAMS,
9916
10156
  DryStroke,
10157
+ EFFECT_MAP_REPEATS,
10158
+ EFFECT_STRIP_HEIGHT,
9917
10159
  GPU_LEAD,
9918
10160
  GRAIN_PATTERNS,
9919
10161
  GRAIN_TILE,
@@ -9929,6 +10171,7 @@ export {
9929
10171
  MAX_CANVAS_SIDE,
9930
10172
  MAX_COORDINATE,
9931
10173
  MAX_IMAGE_SIDE,
10174
+ MAX_LIVE_EFFECT_SOURCES,
9932
10175
  MAX_ZOOM,
9933
10176
  MIN_CANVAS_SIDE,
9934
10177
  MIN_ZOOM,
@@ -10048,8 +10291,10 @@ export {
10048
10291
  downsampleRgba,
10049
10292
  drawSticker,
10050
10293
  drawWaterOverlay,
10294
+ effectDabField,
10051
10295
  effectLayersToJSON,
10052
10296
  effectPaintKey,
10297
+ effectPaintSize,
10053
10298
  emptiedLayers,
10054
10299
  emptyThickChannels,
10055
10300
  fitZoom,
@@ -10135,6 +10380,7 @@ export {
10135
10380
  quickStencil,
10136
10381
  rand01,
10137
10382
  randSigned,
10383
+ readEffectStrokeMap,
10138
10384
  readPaintEffectPaint,
10139
10385
  readPaintEffectSource,
10140
10386
  readPaintSource,
@@ -14,6 +14,7 @@ import { TileMedium, type PaintTileTarget } from '../paint/exchange.cjs';
14
14
  import { type PaintGradientSource } from '../paint/gradient-paint.cjs';
15
15
  import type { PaintBrush } from './brushes.cjs';
16
16
  import { type PenPoint, type StrokeCopy } from './engine.cjs';
17
+ import { type EffectPaintPixels } from './effect-paint.cjs';
17
18
  export type StampKind = 'dot' | 'star' | 'heart' | 'leaf' | 'flower' | 'confetti' | 'splat' | 'sparkle' | 'ring';
18
19
  export declare const STAMP_KINDS: readonly StampKind[];
19
20
  /** One sticker (document pixels). */
@@ -73,6 +74,11 @@ export interface StickerSprayInit {
73
74
  nextId: number;
74
75
  /** Gradient paint (resolved): each sticker takes its dab's colour (across the tip: by its offset). */
75
76
  paint?: PaintGradientSource | null;
77
+ /**
78
+ * Effect paint: each sticker takes the effect's colour at its centre (it keeps one colour, so it
79
+ * stays recolourable); none lands where the effect is transparent.
80
+ */
81
+ effect?: EffectPaintPixels | null;
76
82
  }
77
83
  /** A sticker-spray stroke: drops stickers along the path and draws them as it goes. */
78
84
  export declare class StickerSpray {