@pixerate/editor 0.256.0 → 0.258.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 (124) hide show
  1. package/dist/agent/index.cjs +40 -38
  2. package/dist/agent/index.js +9 -7
  3. package/dist/animation/index.cjs +4 -2
  4. package/dist/animation/index.js +4 -2
  5. package/dist/animation-editor/index.cjs +63 -61
  6. package/dist/animation-editor/index.js +7 -5
  7. package/dist/brushes/colour.d.cts +11 -0
  8. package/dist/brushes/colour.d.ts +11 -0
  9. package/dist/brushes/index.cjs +51 -30
  10. package/dist/brushes/index.d.cts +1 -0
  11. package/dist/brushes/index.d.ts +1 -0
  12. package/dist/brushes/index.js +25 -4
  13. package/dist/brushes/resolve.d.cts +2 -0
  14. package/dist/brushes/resolve.d.ts +2 -0
  15. package/dist/brushes/schema.d.cts +1 -0
  16. package/dist/brushes/schema.d.ts +1 -0
  17. package/dist/brushes/tools.d.cts +7 -0
  18. package/dist/brushes/tools.d.ts +7 -0
  19. package/dist/brushes/types.d.cts +10 -2
  20. package/dist/brushes/types.d.ts +10 -2
  21. package/dist/brushes/ui.d.cts +14 -3
  22. package/dist/brushes/ui.d.ts +14 -3
  23. package/dist/{brushstrokeRender-HCOQUSWW.js → brushstrokeRender-JI4W6F2G.js} +2 -2
  24. package/dist/{brushstrokeRender-ZZOXI5O5.cjs → brushstrokeRender-MQ6PSDM5.cjs} +11 -11
  25. package/dist/canvas/index.cjs +20 -18
  26. package/dist/canvas/index.js +7 -5
  27. package/dist/chart/index.cjs +159 -157
  28. package/dist/chart/index.js +9 -7
  29. package/dist/{chunk-7272D3K2.js → chunk-2EUW2XZX.js} +27 -276
  30. package/dist/{chunk-L6Y7TSQ4.js → chunk-2VJ6OVJV.js} +1 -1
  31. package/dist/{chunk-6ZZKRH7R.js → chunk-47V3WXDG.js} +2 -2
  32. package/dist/{chunk-H7ET2ISK.cjs → chunk-4IMD2TLY.cjs} +52 -52
  33. package/dist/{chunk-VUKUTT2W.js → chunk-4M4E3N6X.js} +1 -1
  34. package/dist/{chunk-TBAVCLEQ.cjs → chunk-5Q5MLXAD.cjs} +23 -23
  35. package/dist/{chunk-MONXNVZK.js → chunk-6I23ZCGK.js} +5 -3
  36. package/dist/{chunk-GOBOCZZI.js → chunk-6NQPKNUZ.js} +1 -1
  37. package/dist/{chunk-WYGSR3IJ.js → chunk-AORZ5M5C.js} +1 -1
  38. package/dist/chunk-AVTPZO4Z.cjs +142 -0
  39. package/dist/{chunk-WKMUBATU.js → chunk-C2WEIEUB.js} +1 -1
  40. package/dist/{chunk-P3K2JCJA.js → chunk-D5BXSOSU.js} +3 -3
  41. package/dist/chunk-E2V5DA3W.js +36 -0
  42. package/dist/{chunk-WZOGDKCQ.js → chunk-F7CPSRLN.js} +0 -133
  43. package/dist/chunk-FJBQTZS4.js +17 -0
  44. package/dist/{chunk-SOSBOS5T.cjs → chunk-G23BTQA5.cjs} +22 -22
  45. package/dist/{chunk-EUIDTZNQ.cjs → chunk-I3Y2Q3PM.cjs} +59 -308
  46. package/dist/{chunk-YHUMGBZD.js → chunk-ICTPLVSR.js} +22 -4
  47. package/dist/{chunk-LRNUZKWV.cjs → chunk-IPUTIUGQ.cjs} +1 -1
  48. package/dist/{chunk-6GRUVGKU.js → chunk-ITURMC2M.js} +3 -3
  49. package/dist/{chunk-HYWRU5VX.js → chunk-IXQZHCOF.js} +1 -1
  50. package/dist/chunk-J5UQHWVL.js +142 -0
  51. package/dist/{chunk-YBAPT7NL.js → chunk-JC2SHA4A.js} +1 -23
  52. package/dist/chunk-LRTR4ASZ.cjs +17 -0
  53. package/dist/{chunk-HUAISCUX.cjs → chunk-M2HG5DJ2.cjs} +18 -18
  54. package/dist/{chunk-N76AMOKE.cjs → chunk-MDLEGVEN.cjs} +42 -42
  55. package/dist/chunk-MEGSQUQO.cjs +36 -0
  56. package/dist/chunk-MLLTWAXH.cjs +276 -0
  57. package/dist/{chunk-RHPSE2YR.js → chunk-MQ6MGIQM.js} +2 -2
  58. package/dist/{chunk-PGPFWC35.cjs → chunk-MQGQTKVW.cjs} +51 -33
  59. package/dist/{chunk-5YWWQ36H.cjs → chunk-O3ZVZSNJ.cjs} +9 -9
  60. package/dist/{chunk-GCTUOTCW.cjs → chunk-P3PZ63M3.cjs} +35 -35
  61. package/dist/{chunk-WRZ6C4TU.cjs → chunk-P6WYDBTH.cjs} +4 -4
  62. package/dist/{chunk-CJ6TBL2N.cjs → chunk-RHEMMGCN.cjs} +16 -16
  63. package/dist/{chunk-HIMGBZAM.cjs → chunk-RSIKDWOP.cjs} +201 -38
  64. package/dist/{chunk-SABJMH6M.cjs → chunk-RVGP2XLF.cjs} +2 -135
  65. package/dist/{chunk-JVRLGV3C.js → chunk-SMTUQWGY.js} +184 -21
  66. package/dist/{chunk-DMTEQ6SH.js → chunk-SSFMWJDK.js} +4 -4
  67. package/dist/{chunk-TFF2GTGN.cjs → chunk-SXQHWXEL.cjs} +2 -24
  68. package/dist/{chunk-IWEVV63O.cjs → chunk-UEYLANO3.cjs} +18 -16
  69. package/dist/chunk-VR6OGHU5.js +276 -0
  70. package/dist/{chunk-AF23QK5F.js → chunk-YQ4PQTBB.js} +1 -1
  71. package/dist/{chunk-B7V34GIH.cjs → chunk-YSGW7VV4.cjs} +88 -88
  72. package/dist/{chunk-3NS5C7FB.cjs → chunk-YYBW6DF2.cjs} +76 -76
  73. package/dist/{chunk-UFEDHACQ.js → chunk-Z7BX4TWO.js} +2 -2
  74. package/dist/{chunk-RUONPPMC.cjs → chunk-ZSE25IZA.cjs} +8 -8
  75. package/dist/color/index.cjs +7 -3
  76. package/dist/color/index.js +18 -14
  77. package/dist/export/index.cjs +8 -6
  78. package/dist/export/index.js +7 -5
  79. package/dist/extensions/index.cjs +11 -9
  80. package/dist/extensions/index.js +10 -8
  81. package/dist/gradient/index.cjs +33 -31
  82. package/dist/gradient/index.js +5 -3
  83. package/dist/image-editor/brush-presets.d.cts +4 -1
  84. package/dist/image-editor/brush-presets.d.ts +4 -1
  85. package/dist/image-editor/controller.d.cts +10 -5
  86. package/dist/image-editor/controller.d.ts +10 -5
  87. package/dist/image-editor/index.cjs +1090 -1073
  88. package/dist/image-editor/index.js +37 -20
  89. package/dist/index.cjs +17 -15
  90. package/dist/index.js +11 -9
  91. package/dist/paint-editor/brushes.d.cts +18 -1
  92. package/dist/paint-editor/brushes.d.ts +18 -1
  93. package/dist/paint-editor/collaboration.d.cts +38 -0
  94. package/dist/paint-editor/collaboration.d.ts +38 -0
  95. package/dist/paint-editor/controller.d.cts +60 -0
  96. package/dist/paint-editor/controller.d.ts +60 -0
  97. package/dist/paint-editor/engine.d.cts +7 -0
  98. package/dist/paint-editor/engine.d.ts +7 -0
  99. package/dist/paint-editor/index.cjs +1564 -1324
  100. package/dist/paint-editor/index.js +1198 -958
  101. package/dist/paint-editor/mark-settings.d.cts +1 -1
  102. package/dist/paint-editor/mark-settings.d.ts +1 -1
  103. package/dist/paint-editor/procedural.d.cts +4 -2
  104. package/dist/paint-editor/procedural.d.ts +4 -2
  105. package/dist/paint-editor/stamp-tip.d.cts +45 -0
  106. package/dist/paint-editor/stamp-tip.d.ts +45 -0
  107. package/dist/paint-editor/stroke.d.cts +4 -0
  108. package/dist/paint-editor/stroke.d.ts +4 -0
  109. package/dist/stickers/index.cjs +4 -2
  110. package/dist/stickers/index.js +6 -4
  111. package/dist/typography/index.cjs +3 -2
  112. package/dist/typography/index.js +2 -1
  113. package/dist/vector/index.cjs +971 -969
  114. package/dist/vector/index.js +8 -6
  115. package/dist/video-editor/index.cjs +4 -2
  116. package/dist/video-editor/index.js +4 -2
  117. package/package.json +1 -1
  118. package/skills/pixerate-brushes/SKILL.md +21 -11
  119. package/skills/pixerate-brushes-design/SKILL.md +6 -2
  120. package/skills/pixerate-brushes-design/evals/canvas-chalk.json +8 -8
  121. package/skills/pixerate-canvas/SKILL.md +3 -2
  122. package/skills/pixerate-image-editor/SKILL.md +1 -1
  123. package/skills/pixerate-paint-editor/SKILL.md +5 -1
  124. package/skills/pixerate-vector/SKILL.md +1 -1
@@ -1,9 +1,7 @@
1
1
  import {
2
2
  StickerBrowser,
3
- createStickerTools,
4
- describeStickerRef,
5
3
  editorStickers
6
- } from "../chunk-WZOGDKCQ.js";
4
+ } from "../chunk-F7CPSRLN.js";
7
5
  import {
8
6
  BRUSH_PATCH_PROPERTIES,
9
7
  BRUSH_SLIDERS,
@@ -13,7 +11,11 @@ import {
13
11
  mergeBrushPatch,
14
12
  multiColorSliders,
15
13
  patchBrushValue
16
- } from "../chunk-YHUMGBZD.js";
14
+ } from "../chunk-ICTPLVSR.js";
15
+ import {
16
+ createStickerTools,
17
+ describeStickerRef
18
+ } from "../chunk-J5UQHWVL.js";
17
19
  import {
18
20
  toVideoFfmpeg
19
21
  } from "../chunk-MWUZCEFT.js";
@@ -24,9 +26,12 @@ import {
24
26
  resolveExternalAssets,
25
27
  serializeLayers,
26
28
  serializeLayersExternal
27
- } from "../chunk-DMTEQ6SH.js";
28
- import "../chunk-3CI7FRMK.js";
29
+ } from "../chunk-SSFMWJDK.js";
29
30
  import "../chunk-ZNA3IQZF.js";
31
+ import {
32
+ brushColourHex
33
+ } from "../chunk-FJBQTZS4.js";
34
+ import "../chunk-3CI7FRMK.js";
30
35
  import {
31
36
  LAYER_EFFECT_KINDS,
32
37
  LayerEffectError,
@@ -80,7 +85,7 @@ import {
80
85
  realPresenceClock,
81
86
  tweenPresenceCursor
82
87
  } from "../chunk-IVSWRVB4.js";
83
- import "../chunk-AF23QK5F.js";
88
+ import "../chunk-YQ4PQTBB.js";
84
89
  import {
85
90
  BRUSH_ID,
86
91
  BRUSH_PRESETS,
@@ -95,6 +100,7 @@ import {
95
100
  DEFAULT_LIGHT,
96
101
  DEFAULT_MULTI_COLOR,
97
102
  DEFAULT_PARTICLE,
103
+ DEFAULT_SPRAY,
98
104
  DEFAULT_STICKER,
99
105
  DEFAULT_WET_BRUSH,
100
106
  DOT_ROTATIONS,
@@ -134,7 +140,7 @@ import {
134
140
  parseBrushLibrary,
135
141
  resolveBrush,
136
142
  usesPaintExchange
137
- } from "../chunk-JVRLGV3C.js";
143
+ } from "../chunk-SMTUQWGY.js";
138
144
  import {
139
145
  STAMP_KINDS,
140
146
  StickersSlot,
@@ -273,8 +279,10 @@ import {
273
279
  colorHarmony,
274
280
  hueToWheel,
275
281
  wheelToHue
276
- } from "../chunk-7272D3K2.js";
277
- import "../chunk-YBAPT7NL.js";
282
+ } from "../chunk-2EUW2XZX.js";
283
+ import "../chunk-E2V5DA3W.js";
284
+ import "../chunk-VR6OGHU5.js";
285
+ import "../chunk-JC2SHA4A.js";
278
286
  import {
279
287
  linearToSrgb,
280
288
  oklabToOklch,
@@ -577,6 +585,75 @@ function effectDabField(img, rect, cov, centre2, mixed, field, steps = 0, dab =
577
585
  }
578
586
  }
579
587
 
588
+ // src/paint-editor/stamp-tip.ts
589
+ var samplers = /* @__PURE__ */ new WeakMap();
590
+ function stampSampler(ref2, art) {
591
+ let kind = null;
592
+ try {
593
+ kind = editorStampOf(ref2.includes(":") ? ref2 : `editor:${ref2}`);
594
+ } catch {
595
+ return null;
596
+ }
597
+ if (kind) {
598
+ const k = kind;
599
+ return (u, v, seed) => stampCoverage(k, u, v, seed);
600
+ }
601
+ const a = art?.(ref2);
602
+ if (!a || !(a.width > 0 && a.height > 0)) return null;
603
+ const hit = samplers.get(a);
604
+ if (hit) return hit;
605
+ const { width: w, height: h, data } = a;
606
+ const scale = Math.max(w, h) / 2;
607
+ const alpha = (x, y) => x < 0 || y < 0 || x >= w || y >= h ? 0 : data[(y * w + x) * 4 + 3] / 255;
608
+ const sampler = (u, v) => {
609
+ const x = u * scale + w / 2 - 0.5;
610
+ const y = v * scale + h / 2 - 0.5;
611
+ const x0 = Math.floor(x);
612
+ const y0 = Math.floor(y);
613
+ const fx = x - x0;
614
+ const fy = y - y0;
615
+ return (alpha(x0, y0) * (1 - fx) + alpha(x0 + 1, y0) * fx) * (1 - fy) + (alpha(x0, y0 + 1) * (1 - fx) + alpha(x0 + 1, y0 + 1) * fx) * fy;
616
+ };
617
+ samplers.set(a, sampler);
618
+ return sampler;
619
+ }
620
+ function stampDabRect(d) {
621
+ const r = Math.max(0.5, d.radius) * Math.SQRT2 + 1.5;
622
+ const x0 = Math.floor(d.x - r);
623
+ const y0 = Math.floor(d.y - r);
624
+ return { x: x0, y: y0, width: Math.ceil(d.x + r) - x0, height: Math.ceil(d.y + r) - y0 };
625
+ }
626
+ function rasterizeStampDab(d, sample, seed, rect, out) {
627
+ const w = rect.width;
628
+ const h = rect.height;
629
+ out.fill(0, 0, w * h);
630
+ const r = Math.max(0.5, d.radius);
631
+ const ro = Math.min(1, Math.max(0.05, d.roundness));
632
+ const sn = Math.sin(d.angle);
633
+ const cs = Math.cos(d.angle);
634
+ const ss = r < 6 ? 3 : 1;
635
+ for (let yy = 0; yy < h; yy++) {
636
+ for (let xx = 0; xx < w; xx++) {
637
+ let sum = 0;
638
+ for (let sy = 0; sy < ss; sy++) {
639
+ for (let sx = 0; sx < ss; sx++) {
640
+ const dx = rect.x + xx + (sx + 0.5) / ss - d.x;
641
+ const dy = rect.y + yy + (sy + 0.5) / ss - d.y;
642
+ const u = (dx * cs - dy * sn) / r;
643
+ const v = (dx * sn + dy * cs) / (r * ro);
644
+ if (u > -1.05 && u < 1.05 && v > -1.05 && v < 1.05) sum += sample(u, v, seed);
645
+ }
646
+ }
647
+ const val = sum / (ss * ss);
648
+ if (val > 0) out[yy * w + xx] = Math.min(1, val) * d.alpha;
649
+ }
650
+ }
651
+ }
652
+ function brushStickerRefs(brush) {
653
+ const refs = brush.engine === "sticker" ? [...brush.sticker.stamps] : brush.engine === "particle" ? [...brush.particle.dotTextures] : brush.engine === "dab" && brush.stamp ? [brush.stamp] : [];
654
+ return [...new Set(refs.filter((r) => r.includes(":") && !r.startsWith("editor:")))];
655
+ }
656
+
580
657
  // src/paint-editor/engine.ts
581
658
  function penOrientation(p) {
582
659
  if (typeof p.altitude === "number" && Number.isFinite(p.altitude)) {
@@ -795,6 +872,7 @@ var DryStroke = class {
795
872
  constructor(brush, options) {
796
873
  this.brush = brush;
797
874
  this.options = options;
875
+ this.stamp = brush.stamp ? stampSampler(brush.stamp, options.stampArt) : null;
798
876
  this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
799
877
  this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
800
878
  this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear));
@@ -812,6 +890,8 @@ var DryStroke = class {
812
890
  field = new Float32Array(0);
813
891
  lastRaw = null;
814
892
  lastFed = null;
893
+ /** The stamp tip's shape, or `null` for the round computed tip. */
894
+ stamp;
815
895
  /** Feeds raw pen samples; returns the document rectangles the new dabs touched. */
816
896
  push(points) {
817
897
  const dabs = [];
@@ -834,34 +914,52 @@ var DryStroke = class {
834
914
  }
835
915
  paint(dabs) {
836
916
  const rects = [];
837
- const hardness = this.brush.tip.hardness;
838
- const grain = this.options.grain;
917
+ const spray = this.brush.spray;
918
+ const seed = this.options.seed ?? 0;
839
919
  for (const d of dabs) {
840
- const rect = dabRect(d, hardness);
841
- const n = rect.width * rect.height;
842
- if (this.buf.length < n) this.buf = new Float32Array(n);
843
- const vals = this.buf.subarray(0, n);
844
- rasterizeDab(d, hardness, rect, vals);
845
- if (this.depth > 0 && grain) {
846
- for (let yy = 0, k = 0; yy < rect.height; yy++) {
847
- const y = rect.y + yy + 0.5;
848
- for (let xx = 0; xx < rect.width; xx++, k++) {
849
- if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
850
- }
851
- }
920
+ if (!spray) {
921
+ this.mark(d, d, rects);
922
+ continue;
852
923
  }
853
- let field = null;
854
- if (this.options.effect) {
855
- if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
856
- field = this.field.subarray(0, n * 3);
857
- sampleEffectField(this.options.effect, rect, vals, field, d);
924
+ const count = 1 + Math.round(spray.density * 31);
925
+ const key = d.index * 131 + Math.round(d.travel * 8);
926
+ for (let i = 0; i < count; i++) {
927
+ const a = rand01(seed, key * 64 + i, 61) * Math.PI * 2;
928
+ const rr = Math.sqrt(rand01(seed, key * 64 + i, 67)) * d.radius;
929
+ const drop = { ...d, x: d.x + Math.cos(a) * rr, y: d.y + Math.sin(a) * rr, radius: Math.max(0.5, d.radius * spray.size * (0.6 + 0.8 * rand01(seed, key * 64 + i, 71))), roundness: 1, angle: rand01(seed, key * 64 + i, 73) * Math.PI * 2 };
930
+ this.mark(drop, d, rects);
858
931
  }
859
- this.coverage.accumulate(rect, vals, d.opacity, null, field);
860
- rects.push(rect);
861
- this.dabCount++;
862
932
  }
863
933
  return rects;
864
934
  }
935
+ /** Lays one mark (a dab, or a spray droplet of dab `d`): the round tip or the stamp, the paper grain, effect paint. */
936
+ mark(m, d, rects) {
937
+ const hardness = this.brush.tip.hardness;
938
+ const grain = this.options.grain;
939
+ const rect = this.stamp ? stampDabRect(m) : dabRect(m, hardness);
940
+ const n = rect.width * rect.height;
941
+ if (this.buf.length < n) this.buf = new Float32Array(n);
942
+ const vals = this.buf.subarray(0, n);
943
+ if (this.stamp) rasterizeStampDab(m, this.stamp, d.index, rect, vals);
944
+ else rasterizeDab(m, hardness, rect, vals);
945
+ if (this.depth > 0 && grain) {
946
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
947
+ const y = rect.y + yy + 0.5;
948
+ for (let xx = 0; xx < rect.width; xx++, k++) {
949
+ if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
950
+ }
951
+ }
952
+ }
953
+ let field = null;
954
+ if (this.options.effect) {
955
+ if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
956
+ field = this.field.subarray(0, n * 3);
957
+ sampleEffectField(this.options.effect, rect, vals, field, d);
958
+ }
959
+ this.coverage.accumulate(rect, vals, m.opacity, null, field);
960
+ rects.push(rect);
961
+ this.dabCount++;
962
+ }
865
963
  };
866
964
  function mixerTip(brush) {
867
965
  return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };
@@ -1820,984 +1918,986 @@ function lightFromJSON(raw) {
1820
1918
  return surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
1821
1919
  }
1822
1920
 
1823
- // src/paint-editor/sticker-art.ts
1824
- var STICKER_ART_SIZE = 256;
1825
- var defaultStickerRasterizer = async (asset, size) => {
1826
- if (asset.kind === "image") {
1827
- const parsed = parseDataUrl(asset.url);
1828
- if (parsed && parsed.mime === "image/png") {
1829
- const png = decodePng(parsed.bytes);
1830
- return { width: png.width, height: png.height, data: new Uint8ClampedArray(png.data) };
1921
+ // src/paint-editor/procedural.ts
1922
+ var STEP_PX = 1.5;
1923
+ var MarkStroke = class {
1924
+ constructor(brush, options) {
1925
+ this.brush = brush;
1926
+ this.options = options;
1927
+ this.sampler = options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
1928
+ this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;
1929
+ this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
1930
+ this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
1931
+ this.seed = options.seed ?? 0;
1932
+ const fine = { ...brush, tip: { ...brush.tip, spacing: Math.max(0.01, Math.min(0.25, STEP_PX / Math.max(1, brush.tip.size))) } };
1933
+ this.walkers = this.copies.map((c, i) => new PenDabWalker(fine, (options.seed ?? 0) + i * 7919 | 0, c.linear));
1934
+ this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
1935
+ }
1936
+ brush;
1937
+ options;
1938
+ coverage = new CoverageMap();
1939
+ dabCount = 0;
1940
+ smoother;
1941
+ copies;
1942
+ walkers;
1943
+ depth;
1944
+ buf = new Float32Array(0);
1945
+ field = new Float32Array(0);
1946
+ lastRaw = null;
1947
+ lastFed = null;
1948
+ seed;
1949
+ sampler;
1950
+ tipTable;
1951
+ /** The pen dab the marks being made belong to, and its gradient colour. */
1952
+ pen = null;
1953
+ penColor = { rgb: [0, 0, 0], alpha: 1 };
1954
+ push(points) {
1955
+ const rects = [];
1956
+ for (const raw of points) {
1957
+ this.lastRaw = raw;
1958
+ const p = this.smoother.push(raw);
1959
+ this.lastFed = p;
1960
+ this.copies.forEach((c, i) => {
1961
+ const dabs = [];
1962
+ this.walkers[i].push(c.map(p), dabs);
1963
+ for (const d of dabs) this.step(i, d, rects);
1964
+ });
1831
1965
  }
1966
+ return rects;
1832
1967
  }
1833
- const g = globalThis;
1834
- if (!g.createImageBitmap || !g.OffscreenCanvas) {
1835
- throw new StickerError("unsupported_format", `This environment can't draw ${asset.kind === "svg" ? "SVG" : "non-PNG"} sticker art; pass rasterizeSticker in the paint editor's options (or use a provider that offers png).`);
1968
+ step(copy, d, rects) {
1969
+ this.pen = d;
1970
+ if (this.sampler && !this.tipTable) this.sampler.dab(d, this.penColor);
1971
+ this.advance(copy, d, rects);
1836
1972
  }
1837
- const blob = asset.kind === "svg" ? new Blob([asset.svg], { type: "image/svg+xml" }) : await (await g.fetch(asset.url)).blob();
1838
- const bitmap = await g.createImageBitmap(blob);
1839
- const scale = size / Math.max(bitmap.width, bitmap.height, 1);
1840
- const w = Math.max(1, Math.round(bitmap.width * scale));
1841
- const h = Math.max(1, Math.round(bitmap.height * scale));
1842
- const canvas = new g.OffscreenCanvas(w, h);
1843
- const ctx = canvas.getContext("2d");
1844
- ctx.drawImage(bitmap, 0, 0, w, h);
1845
- const img = ctx.getImageData(0, 0, w, h);
1846
- return { width: w, height: h, data: img.data };
1847
- };
1848
- var StickerArtCache = class {
1849
- entries = /* @__PURE__ */ new Map();
1850
- /** The art for `ref` when it has loaded (editor stamps never need any). */
1851
- get = (ref2) => {
1852
- const e = this.entries.get(ref2);
1853
- return e?.status === "ready" ? e.art : void 0;
1854
- };
1855
- /** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
1856
- status(ref2) {
1857
- if (editorStampOf(ref2)) return { status: "ready" };
1858
- const e = this.entries.get(ref2);
1859
- if (!e) return { status: "none" };
1860
- return e.status === "failed" ? { status: "failed", error: e.error } : { status: e.status };
1973
+ /** The colour of a mark at (x, y): its pen dab's, or across the tip by its offset from the pen. */
1974
+ markColor(x, y) {
1975
+ if (!this.sampler || !this.pen) return null;
1976
+ if (!this.tipTable) return this.penColor;
1977
+ const d = this.pen;
1978
+ const u = ((x - d.x) * Math.sin(d.direction) + (y - d.y) * Math.cos(d.direction)) / Math.max(0.5, d.radius);
1979
+ const last = this.tipTable.length - 1;
1980
+ return this.tipTable[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
1981
+ }
1982
+ finish() {
1983
+ const rects = [];
1984
+ const raw = this.lastRaw;
1985
+ const fed = this.lastFed;
1986
+ this.copies.forEach((c, i) => {
1987
+ const dabs = [];
1988
+ if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
1989
+ this.walkers[i].finish(dabs);
1990
+ for (const d of dabs) this.step(i, d, rects);
1991
+ });
1992
+ return rects;
1993
+ }
1994
+ /** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
1995
+ mark(x, y, r, alpha, opacity, pressure, rects) {
1996
+ if (!(r > 0.05) || !(alpha > 0)) return;
1997
+ const dab = { x, y, radius: Math.max(0.5, r), alpha: Math.min(1, alpha), opacity: Math.min(1, opacity), angle: 0, roundness: 1 };
1998
+ const hardness = this.brush.tip.hardness;
1999
+ const rect = dabRect(dab, hardness);
2000
+ const n = rect.width * rect.height;
2001
+ if (n <= 0) return;
2002
+ const vals = this.scratch(n);
2003
+ rasterizeDab(dab, hardness, rect, vals);
2004
+ this.lay(rect, vals, dab.opacity, pressure, x, y, rects);
1861
2005
  }
1862
2006
  /**
1863
- * Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
1864
- * reject with the closest known ones; failures are kept so they are reported, and retried on
1865
- * the next call.
2007
+ * Stamps a textured mark: `kind` from the sticker stamps over a square of half-size `r`, turned
2008
+ * `rotation` radians clockwise, with flow `alpha` towards `opacity`. Small marks are supersampled.
1866
2009
  */
1867
- ensure(provider, ref2, rasterize) {
1868
- if (editorStampOf(ref2)) return Promise.resolve(null);
1869
- const e = this.entries.get(ref2);
1870
- if (e?.status === "ready") return Promise.resolve(e.art);
1871
- if (e?.status === "loading") return e.promise;
1872
- const promise = (async () => {
1873
- const d = await describeStickerRef(provider, ref2);
1874
- const id = stickerIdFor(provider, ref2);
1875
- const max = provider.capabilities.maxSize ?? STICKER_ART_SIZE;
1876
- const size = Math.min(STICKER_ART_SIZE, max);
1877
- const asset = await provider.load(id, d.formats.includes("png") ? { format: "png", size } : { format: d.formats[0] ?? "svg", size });
1878
- const px = await rasterize(asset, size);
1879
- if (!(px.width > 0 && px.height > 0) || px.data.length !== px.width * px.height * 4) throw new StickerError("load_failed", `The art for "${ref2}" came back empty or the wrong size.`);
1880
- return { ...px, tintable: d.tintable };
1881
- })();
1882
- this.entries.set(ref2, { status: "loading", promise });
1883
- return promise.then(
1884
- (art) => {
1885
- this.entries.set(ref2, { status: "ready", art });
1886
- return art;
1887
- },
1888
- (err) => {
1889
- const message = err instanceof Error ? err.message : String(err);
1890
- this.entries.set(ref2, { status: "failed", error: message });
1891
- throw err;
2010
+ stamp(x, y, r, kind, rotation, seed, alpha, opacity, pressure, rects) {
2011
+ if (!(r > 0.05) || !(alpha > 0)) return;
2012
+ const sample = stampSampler(kind, this.options.stampArt);
2013
+ if (!sample) return this.mark(x, y, r, alpha, opacity, pressure, rects);
2014
+ const reach = Math.max(0.5, r) * Math.SQRT2 + 1;
2015
+ const rect = { x: Math.floor(x - reach), y: Math.floor(y - reach), width: 0, height: 0 };
2016
+ rect.width = Math.ceil(x + reach) - rect.x;
2017
+ rect.height = Math.ceil(y + reach) - rect.y;
2018
+ const n = rect.width * rect.height;
2019
+ const vals = this.scratch(n);
2020
+ const ss = r < 10 ? 3 : 1;
2021
+ const c = Math.cos(rotation);
2022
+ const s = Math.sin(rotation);
2023
+ const inv = 1 / Math.max(0.5, r);
2024
+ const a = Math.min(1, alpha);
2025
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
2026
+ for (let xx = 0; xx < rect.width; xx++, k++) {
2027
+ let sum = 0;
2028
+ for (let sy = 0; sy < ss; sy++) {
2029
+ for (let sx = 0; sx < ss; sx++) {
2030
+ const dx = rect.x + xx + (sx + 0.5) / ss - x;
2031
+ const dy = rect.y + yy + (sy + 0.5) / ss - y;
2032
+ sum += sample((dx * c + dy * s) * inv, (dy * c - dx * s) * inv, seed);
2033
+ }
2034
+ }
2035
+ vals[k] = sum / (ss * ss) * a;
1892
2036
  }
1893
- );
2037
+ }
2038
+ this.lay(rect, vals, Math.min(1, opacity), pressure, x, y, rects);
1894
2039
  }
1895
- /** Forgets every load (a new provider). */
1896
- clear() {
1897
- this.entries.clear();
2040
+ /** A scratch buffer of `n` values (reused; valid until the next mark). */
2041
+ scratch(n) {
2042
+ if (this.buf.length < n) this.buf = new Float32Array(n);
2043
+ const vals = this.buf.subarray(0, n);
2044
+ vals.fill(0);
2045
+ return vals;
2046
+ }
2047
+ /**
2048
+ * Lays coverage `vals` over `rect` towards `opacity`: paper grain, then effect paint or the
2049
+ * colour of a mark at (x, y) (gradient paint), into the stroke's coverage.
2050
+ */
2051
+ lay(rect, vals, opacity, pressure, x, y, rects) {
2052
+ const n = rect.width * rect.height;
2053
+ if (n <= 0) return;
2054
+ const grain = this.options.grain;
2055
+ if (this.depth > 0 && grain) {
2056
+ for (let yy = 0, k = 0; yy < rect.height; yy++)
2057
+ for (let xx = 0; xx < rect.width; xx++, k++) if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, pressure);
2058
+ }
2059
+ if (this.options.effect) {
2060
+ if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
2061
+ const field = this.field.subarray(0, n * 3);
2062
+ sampleEffectField(this.options.effect, rect, vals, field, this.pen);
2063
+ this.coverage.accumulate(rect, vals, opacity, null, field);
2064
+ } else {
2065
+ const color = this.markColor(x, y);
2066
+ if (color && color.alpha < 1) for (let k = 0; k < n; k++) vals[k] *= color.alpha;
2067
+ this.coverage.accumulate(rect, vals, opacity, color ? color.rgb : null);
2068
+ }
2069
+ rects.push(rect);
2070
+ this.dabCount++;
2071
+ }
2072
+ /** Marks along a segment, spaced by a fraction of the radius. */
2073
+ segment(x0, y0, x1, y1, r, alpha, opacity, pressure, rects) {
2074
+ const len = Math.hypot(x1 - x0, y1 - y0);
2075
+ const step = Math.max(0.35, r * 0.5);
2076
+ const n = Math.max(1, Math.ceil(len / step));
2077
+ for (let i = 1; i <= n; i++) this.mark(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, r, alpha, opacity, pressure, rects);
1898
2078
  }
1899
2079
  };
1900
- function sampleArt(art, x, y, out) {
1901
- const { width: w, height: h, data } = art;
1902
- const fx = x - 0.5;
1903
- const fy = y - 0.5;
1904
- const x0 = Math.floor(fx);
1905
- const y0 = Math.floor(fy);
1906
- const tx = fx - x0;
1907
- const ty = fy - y0;
1908
- let r = 0;
1909
- let g = 0;
1910
- let b = 0;
1911
- let a = 0;
1912
- for (let k = 0; k < 4; k++) {
1913
- const xx = x0 + (k & 1);
1914
- const yy = y0 + (k >> 1);
1915
- if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
1916
- const wgt = (k & 1 ? tx : 1 - tx) * (k >> 1 ? ty : 1 - ty);
1917
- const i = (yy * w + xx) * 4;
1918
- const al = data[i + 3] * wgt;
1919
- r += data[i] * al;
1920
- g += data[i + 1] * al;
1921
- b += data[i + 2] * al;
1922
- a += al;
1923
- }
1924
- out[0] = a > 0 ? r / a / 255 : 0;
1925
- out[1] = a > 0 ? g / a / 255 : 0;
1926
- out[2] = a > 0 ? b / a / 255 : 0;
1927
- out[3] = a / 255;
1928
- return out;
1929
- }
1930
-
1931
- // src/paint-editor/stickers.ts
1932
- function hexRgb(hex2) {
1933
- const n = parseInt(hex2.slice(1, 7), 16);
1934
- return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
1935
- }
1936
- function stickerRect(s) {
1937
- const r = s.size / 2 + 1;
1938
- const x0 = Math.floor(s.x - r);
1939
- const y0 = Math.floor(s.y - r);
1940
- return { x: x0, y: y0, width: Math.ceil(s.x + r) - x0, height: Math.ceil(s.y + r) - y0 };
2080
+ var FUZZY_AMPLITUDE = 0.12;
2081
+ var NOMINAL_MS_PER_PX = 2;
2082
+ var RIBBON_SUB = 4;
2083
+ function chainDamp(damp, k) {
2084
+ return Math.min(damp, 1 / (1 + 0.8 * Math.sqrt(Math.max(0, k))) ** 2);
1941
2085
  }
1942
- function stickerStamp(s) {
1943
- if (s.ref) {
1944
- try {
1945
- return editorStampOf(s.ref);
1946
- } catch {
1947
- return null;
2086
+ function addPolygonArea(pts, rect, out, weight = 1) {
2087
+ const n = pts.length >> 1;
2088
+ if (n < 3) return;
2089
+ let y0 = Infinity;
2090
+ let y1 = -Infinity;
2091
+ for (let i = 1; i < pts.length; i += 2) {
2092
+ if (pts[i] < y0) y0 = pts[i];
2093
+ if (pts[i] > y1) y1 = pts[i];
2094
+ }
2095
+ const r0 = Math.max(0, Math.floor(y0) - rect.y);
2096
+ const r1 = Math.min(rect.height - 1, Math.floor(y1) - rect.y);
2097
+ const xs = [];
2098
+ const w = weight / RIBBON_SUB;
2099
+ for (let row = r0; row <= r1; row++) {
2100
+ const base = row * rect.width;
2101
+ for (let sub = 0; sub < RIBBON_SUB; sub++) {
2102
+ const y = rect.y + row + (sub + 0.5) / RIBBON_SUB;
2103
+ xs.length = 0;
2104
+ for (let i = 0, j = n - 1; i < n; j = i++) {
2105
+ const yi = pts[i * 2 + 1];
2106
+ const yj = pts[j * 2 + 1];
2107
+ if (yi <= y !== yj <= y) xs.push(pts[i * 2] + (y - yi) / (yj - yi) * (pts[j * 2] - pts[i * 2]));
2108
+ }
2109
+ if (xs.length < 2) continue;
2110
+ xs.sort((a, b) => a - b);
2111
+ for (let k = 0; k + 1 < xs.length; k += 2) {
2112
+ const a = Math.max(rect.x, xs[k]);
2113
+ const b = Math.min(rect.x + rect.width, xs[k + 1]);
2114
+ if (!(b > a)) continue;
2115
+ for (let px = Math.floor(a); px < b; px++) {
2116
+ const cover = Math.min(b, px + 1) - Math.max(a, px);
2117
+ if (cover > 0) out[base + px - rect.x] += cover * w;
2118
+ }
2119
+ }
1948
2120
  }
1949
2121
  }
1950
- return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
1951
2122
  }
1952
- function drawSticker(medium, s, art) {
1953
- const kind = stickerStamp(s);
1954
- if (!kind) {
1955
- const a2 = art?.(s.ref);
1956
- return a2 ? drawArtSticker(medium, s, a2) : stickerRect(s);
2123
+ function curl(x, y, seed) {
2124
+ const n = (u, v) => {
2125
+ const x0 = Math.floor(u);
2126
+ const y0 = Math.floor(v);
2127
+ const fx = u - x0;
2128
+ const fy = v - y0;
2129
+ const sx = fx * fx * (3 - 2 * fx);
2130
+ const sy = fy * fy * (3 - 2 * fy);
2131
+ const h = (a2, b2) => rand01(seed, a2 * 73856093 + b2 * 19349663, 41);
2132
+ const a = h(x0, y0);
2133
+ const b = h(x0 + 1, y0);
2134
+ const c = h(x0, y0 + 1);
2135
+ const d = h(x0 + 1, y0 + 1);
2136
+ return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy;
2137
+ };
2138
+ const e = 0.01;
2139
+ return [(n(x, y + e) - n(x, y - e)) / (2 * e), -(n(x + e, y) - n(x - e, y)) / (2 * e)];
2140
+ }
2141
+ var ParticleStroke = class extends MarkStroke {
2142
+ sets;
2143
+ /** Ribbon pairs per copy: particles in their order around the pen (non-flow kinds). */
2144
+ orders;
2145
+ s;
2146
+ spawn = 0;
2147
+ /** Textured dots drawn so far, per copy (sequence order, random picks). */
2148
+ dots;
2149
+ /** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
2150
+ filled = /* @__PURE__ */ new Map();
2151
+ pts = [];
2152
+ constructor(brush, options) {
2153
+ super(brush, options);
2154
+ this.s = brush.particle;
2155
+ this.sets = this.copies.map(() => []);
2156
+ this.orders = this.copies.map(() => []);
2157
+ this.dots = this.copies.map(() => 0);
1957
2158
  }
1958
- const rect = stickerRect(s);
1959
- const n = rect.width * rect.height;
1960
- const cov = new Float32Array(n);
1961
- const field = new Float32Array(n * 3);
1962
- const rad2 = s.size / 2;
1963
- const a = s.rotation * Math.PI / 180;
1964
- const ca = Math.cos(a);
1965
- const sa = Math.sin(a);
1966
- const [r, g, b] = hexRgb(s.color);
1967
- for (let yy = 0, p = 0; yy < rect.height; yy++) {
1968
- for (let xx = 0, px = rect.x; xx < rect.width; xx++, p++, px++) {
1969
- const dx = px + 0.5 - s.x;
1970
- const dy = rect.y + yy + 0.5 - s.y;
1971
- const u = (dx * ca + dy * sa) / rad2;
1972
- const v = (-dx * sa + dy * ca) / rad2;
1973
- if (u < -1.05 || u > 1.05 || v < -1.05 || v > 1.05) continue;
1974
- let c = 0;
1975
- for (let k = 0; k < 4; k++) c += stampCoverage(kind, u + ((k & 1) - 0.5) / rad2 / 2, v + ((k >> 1) - 0.5) / rad2 / 2, s.seed);
1976
- c /= 4;
1977
- if (c <= 0) continue;
1978
- cov[p] = c;
1979
- const shade = 1 + 0.12 * (-(u + v) / 2) + 0.06 * (hash2(px, rect.y + yy, s.seed) - 0.5);
1980
- field[p * 3] = Math.min(1, Math.max(0, r * shade));
1981
- field[p * 3 + 1] = Math.min(1, Math.max(0, g * shade));
1982
- field[p * 3 + 2] = Math.min(1, Math.max(0, b * shade));
1983
- }
2159
+ /** Strand kinds: a chain of masses behind each particle. */
2160
+ get strands() {
2161
+ return this.s.kind === "string" || this.s.kind === "elastic" || this.s.kind === "fuzzy";
1984
2162
  }
1985
- medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
1986
- return rect;
1987
- }
1988
- function drawArtSticker(medium, s, art) {
1989
- const rect = stickerRect(s);
1990
- const n = rect.width * rect.height;
1991
- const cov = new Float32Array(n);
1992
- const field = new Float32Array(n * 3);
1993
- const a = s.rotation * Math.PI / 180;
1994
- const ca = Math.cos(a);
1995
- const sa = Math.sin(a);
1996
- const scale = Math.max(art.width, art.height) / s.size;
1997
- const [r, g, b] = hexRgb(s.color);
1998
- const px = [0, 0, 0, 0];
1999
- for (let yy = 0, p = 0; yy < rect.height; yy++) {
2000
- for (let xx = 0; xx < rect.width; xx++, p++) {
2001
- const dx = rect.x + xx + 0.5 - s.x;
2002
- const dy = rect.y + yy + 0.5 - s.y;
2003
- const ax = (dx * ca + dy * sa) * scale + art.width / 2;
2004
- const ay = (-dx * sa + dy * ca) * scale + art.height / 2;
2005
- if (ax < -1 || ay < -1 || ax > art.width + 1 || ay > art.height + 1) continue;
2006
- sampleArt(art, ax, ay, px);
2007
- if (!(px[3] > 0)) continue;
2008
- cov[p] = px[3];
2009
- field[p * 3] = art.tintable ? r : px[0];
2010
- field[p * 3 + 1] = art.tintable ? g : px[1];
2011
- field[p * 3 + 2] = art.tintable ? b : px[2];
2012
- }
2163
+ newParticle(x, y, ox, oy, k, phase, i, copy) {
2164
+ const s = this.s;
2165
+ const fuzzy = s.kind === "fuzzy";
2166
+ const jitter = fuzzy ? 1 : s.wavePhaseJitter;
2167
+ return {
2168
+ x,
2169
+ y,
2170
+ vx: 0,
2171
+ vy: 0,
2172
+ ox,
2173
+ oy,
2174
+ k,
2175
+ phase,
2176
+ age: 0,
2177
+ run: 0,
2178
+ bead: 0,
2179
+ alive: true,
2180
+ wavePhase: jitter > 0 ? jitter * Math.PI * 2 * rand01(this.seed, i, 43 + copy) : 0,
2181
+ waveScale: fuzzy ? 0.6 + 0.8 * rand01(this.seed, i, 47 + copy) : 1,
2182
+ dx: x,
2183
+ dy: y,
2184
+ px: NaN,
2185
+ py: NaN,
2186
+ fade: 1
2187
+ };
2013
2188
  }
2014
- medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
2015
- return rect;
2016
- }
2017
- function jitterColor(hex2, amount, seed, i) {
2018
- const [r, g, b] = hexRgb(hex2);
2019
- const lch = oklabToOklch(rgbToOklab({ r, g, b, a: 1 }));
2020
- const l = Math.min(1, Math.max(0, lch.l + (rand01(seed, i, 61) - 0.5) * amount * 0.3));
2021
- const h = lch.h + (rand01(seed, i, 63) - 0.5) * amount * 80;
2022
- const at = (c) => oklabToRgb(oklchToOklab({ l, c, h, alpha: 1 }));
2023
- const inGamut = (o) => [o.r, o.g, o.b].every((v) => v >= -1e-4 && v <= 1 + 1e-4);
2024
- let out = at(lch.c);
2025
- if (!inGamut(out)) {
2026
- let lo = 0;
2027
- let hi = lch.c;
2028
- for (let k = 0; k < 12; k++) {
2029
- const mid = (lo + hi) / 2;
2030
- if (inGamut(at(mid))) lo = mid;
2031
- else hi = mid;
2189
+ init(copy, d) {
2190
+ const s = this.s;
2191
+ const list = this.sets[copy];
2192
+ const size = Math.max(1, d.radius * 2);
2193
+ for (let i = 0; i < (s.kind === "flow" ? 0 : s.count); i++) {
2194
+ const r = rand01(this.seed, i, 3 + copy) * s.spread * size * 0.5;
2195
+ const a = rand01(this.seed, i, 5 + copy) * Math.PI * 2;
2196
+ const ox = Math.cos(a) * r;
2197
+ const oy = Math.sin(a) * r;
2198
+ const p = this.newParticle(d.x + ox, d.y + oy, ox, oy, 0.6 + 0.8 * rand01(this.seed, i, 7), a, i, copy);
2199
+ if (this.strands) p.strand = Array.from({ length: 6 }, () => ({ x: d.x + ox, y: d.y + oy, vx: 0, vy: 0 }));
2200
+ if (s.kind === "orbit") {
2201
+ p.ox = Math.max(1, r + size * 0.15);
2202
+ p.oy = 0;
2203
+ }
2204
+ list.push(p);
2032
2205
  }
2033
- out = at(lo);
2206
+ this.orders[copy] = list.map((_, i) => i).sort((i, j) => list[i].phase - list[j].phase || i - j);
2034
2207
  }
2035
- return "#" + [out.r, out.g, out.b].map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join("");
2036
- }
2037
- var StickerSpray = class {
2038
- constructor(init) {
2039
- this.init = init;
2040
- const b = init.brush;
2041
- this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);
2042
- this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
2043
- const spacing = Math.max(0.05, 1 / Math.max(0.05, b.sticker.density * 4));
2044
- const walk = { ...b, tip: { ...b.tip, spacing }, dynamics: { ...b.dynamics, scatter: 0, sizeJitter: 0, angleJitter: 0 } };
2045
- this.walkers = this.copies.map((c, i) => new PenDabWalker(walk, init.seed + i * 7919 | 0, c.linear));
2046
- this.medium = new TileMedium(init.target, {});
2047
- this.next = init.nextId;
2048
- }
2049
- init;
2050
- dabCount = 0;
2051
- stickers = [];
2052
- /** Refs the spray picked but could not draw because their art had not loaded. */
2053
- skipped = /* @__PURE__ */ new Set();
2054
- smoother;
2055
- walkers;
2056
- copies;
2057
- medium;
2058
- lastRaw = null;
2059
- lastFed = null;
2060
- next;
2061
- push(points) {
2062
- const rects = [];
2063
- for (const raw of points) {
2064
- this.lastRaw = raw;
2065
- const p = this.smoother.push(raw);
2066
- this.lastFed = p;
2067
- this.copies.forEach((c, i) => {
2068
- const dabs = [];
2069
- this.walkers[i].push(c.map(p), dabs);
2070
- for (const d of dabs) this.drop(d, rects);
2071
- });
2072
- }
2073
- return rects;
2074
- }
2075
- finish() {
2076
- const rects = [];
2077
- const raw = this.lastRaw;
2078
- const fed = this.lastFed;
2079
- this.copies.forEach((c, i) => {
2080
- const dabs = [];
2081
- if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
2082
- this.walkers[i].finish(dabs);
2083
- for (const d of dabs) this.drop(d, rects);
2084
- });
2085
- return rects;
2086
- }
2087
- drop(d, rects) {
2088
- const st = this.init.brush.sticker;
2089
- const i = this.next;
2090
- const seed = this.init.seed;
2091
- const size = d.radius * 2 * (1 - st.sizeJitter * 0.7 * rand01(seed, i, 51));
2092
- const off = st.scatter * d.radius * 2;
2093
- const a = rand01(seed, i, 53) * Math.PI * 2;
2094
- const dist = rand01(seed, i, 55) * off;
2095
- const base = this.colorAt(d, Math.cos(a) * dist, Math.sin(a) * dist);
2096
- if (!(base.alpha > 0)) {
2097
- this.next++;
2098
- return;
2099
- }
2100
- const ref2 = st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "editor:dot";
2101
- const stamp = stickerStamp({ ref: ref2 });
2102
- if (!stamp && !this.init.art?.(ref2)) {
2103
- this.skipped.add(ref2);
2104
- this.next++;
2105
- return;
2106
- }
2107
- const sticker = {
2108
- id: `st${this.next++}`,
2109
- ref: ref2,
2110
- ...stamp ? { stamp } : {},
2111
- x: Math.round((d.x + Math.cos(a) * dist) * 100) / 100,
2112
- y: Math.round((d.y + Math.sin(a) * dist) * 100) / 100,
2113
- size: Math.max(2, Math.round(size * 10) / 10),
2114
- rotation: Math.round((-d.angle * 180 / Math.PI + (rand01(seed, i, 59) * 2 - 1) * 180 * st.rotationJitter) * 10) / 10,
2115
- color: st.colorJitter > 0 ? jitterColor(base.color, st.colorJitter, seed, i) : base.color,
2116
- opacity: Math.round(Math.min(1, d.opacity * this.init.brush.tip.opacity * base.alpha) * 1e3) / 1e3,
2117
- seed: Math.floor(rand01(seed, i, 65) * 1e9)
2118
- };
2119
- this.stickers.push(sticker);
2120
- rects.push(drawSticker(this.medium, sticker, this.init.art));
2121
- this.dabCount++;
2122
- }
2123
- sampler;
2124
- /** A sticker's colour: the paint colour, or gradient paint at its dab (`dx`, `dy` from the dab's centre). */
2125
- colorAt(d, dx, dy) {
2126
- const effect = this.init.effect;
2127
- if (effect) {
2128
- const rgb = [0, 0, 0];
2129
- const alpha = sampleEffectAt(effect, d.x + dx, d.y + dy, rgb, d);
2130
- return { color: "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join(""), alpha };
2131
- }
2132
- const g = this.init.paint;
2133
- if (!g) return { color: this.init.color, alpha: 1 };
2134
- this.sampler ??= new PaintGradientSampler(g, this.init.seed);
2135
- const s = g.map.by === "across-tip" ? this.sampler.sample(((dx * Math.sin(d.direction) + dy * Math.cos(d.direction)) / Math.max(0.5, d.radius) + 1) / 2) : this.sampler.dab(d);
2136
- return { color: "#" + s.rgb.map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join(""), alpha: s.alpha };
2137
- }
2138
- };
2139
- function renderStickers(stickers, target, art) {
2140
- const medium = new TileMedium(target, {});
2141
- for (const s of stickers) drawSticker(medium, s, art);
2142
- }
2143
- function normalizeStickerUpdate(u) {
2144
- const out = {};
2145
- if (u.ref !== void 0 && u.stamp !== void 0) throw new Error("Give ref or stamp, not both (stamp is the older name for an editor stamp).");
2146
- if (u.stamp !== void 0) {
2147
- if (!STAMP_KINDS.includes(u.stamp)) throw new Error(`stamp ${JSON.stringify(u.stamp)} is not a stamp; use ${STAMP_KINDS.join(", ")}, or ref for any sticker (find_stickers).`);
2148
- out.ref = `editor:${u.stamp}`;
2149
- out.stamp = u.stamp;
2150
- }
2151
- if (u.ref !== void 0) {
2152
- out.ref = normalizeStickerRef(u.ref);
2153
- const stamp = stickerStamp({ ref: out.ref });
2154
- if (stamp) out.stamp = stamp;
2155
- else out.stamp = void 0;
2156
- }
2157
- for (const k of ["x", "y"]) if (u[k] !== void 0) {
2158
- if (!Number.isFinite(u[k])) throw new Error(`${k} must be a number of pixels.`);
2159
- out[k] = u[k];
2160
- }
2161
- if (u.size !== void 0) {
2162
- if (!(u.size >= 2 && u.size <= 2048)) throw new Error(`size ${u.size} is out of range; use 2..2048 px.`);
2163
- out.size = u.size;
2164
- }
2165
- if (u.rotation !== void 0) {
2166
- if (!Number.isFinite(u.rotation)) throw new Error("rotation must be degrees.");
2167
- out.rotation = u.rotation;
2168
- }
2169
- if (u.color !== void 0) {
2170
- if (!/^#[0-9a-f]{6}$/i.test(u.color)) throw new Error(`color ${JSON.stringify(u.color)} must be #rrggbb.`);
2171
- out.color = u.color.toLowerCase();
2172
- }
2173
- if (u.opacity !== void 0) {
2174
- if (!(u.opacity >= 0 && u.opacity <= 1)) throw new Error(`opacity ${u.opacity} is out of range; use 0..1.`);
2175
- out.opacity = u.opacity;
2176
- }
2177
- return out;
2178
- }
2179
- function stickerSurface(stickers, base, dirty, art) {
2180
- const only = base && dirty ? new Set(dirty.flatMap((r) => tilesInRect(r).map(({ tx, ty }) => tileKey(tx, ty)))) : null;
2181
- const live = /* @__PURE__ */ new Map();
2182
- let scratch = null;
2183
- const target = {
2184
- offset: { x: 0, y: 0 },
2185
- lockTransparency: false,
2186
- selection: null,
2187
- readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? null,
2188
- writeTile: (tx, ty) => {
2189
- const k = tileKey(tx, ty);
2190
- if (only && !only.has(k)) return (scratch ??= new Uint8ClampedArray(TILE * TILE * 4)).fill(0);
2191
- let t = live.get(k);
2192
- if (!t) live.set(k, t = new Uint8ClampedArray(TILE * TILE * 4));
2193
- return t;
2194
- }
2195
- };
2196
- const reaches = (s) => !only || tilesInRect(stickerRect(s)).some(({ tx, ty }) => only.has(tileKey(tx, ty)));
2197
- renderStickers(stickers.filter(reaches), target, art);
2198
- const tiles = /* @__PURE__ */ new Map();
2199
- if (only && base) {
2200
- for (const [k, t] of base.tiles) if (!only.has(k)) tiles.set(k, t);
2201
- }
2202
- for (const [k, d] of live) {
2203
- const t = finishTile(d, 4, 0);
2204
- if (t) tiles.set(k, t);
2208
+ /** The sideways wave of particle `p` at pen dab `d` (px along the normal to the pen's direction). */
2209
+ wave(p, d, size) {
2210
+ const s = this.s;
2211
+ const amp = s.kind === "fuzzy" ? Math.max(FUZZY_AMPLITUDE, s.waveAmplitude) : s.waveAmplitude;
2212
+ if (!(amp > 0)) return 0;
2213
+ const ms = d.time ?? d.travel * NOMINAL_MS_PER_PX;
2214
+ const phase = 2 * Math.PI * d.travel / (s.waveLength * p.waveScale) - s.waveSpeed * 0.025 * ms + p.wavePhase;
2215
+ return amp * size * Math.sin(phase);
2205
2216
  }
2206
- return new TiledSurface(4, tiles, 0);
2207
- }
2208
- function stickerAt(stickers, x, y, art) {
2209
- for (let i = stickers.length - 1; i >= 0; i--) {
2210
- const s = stickers[i];
2211
- const rad2 = s.size / 2;
2212
- const a = s.rotation * Math.PI / 180;
2213
- const dx = x - s.x;
2214
- const dy = y - s.y;
2215
- const u = (dx * Math.cos(a) + dy * Math.sin(a)) / rad2;
2216
- const v = (-dx * Math.sin(a) + dy * Math.cos(a)) / rad2;
2217
- if (Math.abs(u) > 1.05 || Math.abs(v) > 1.05) continue;
2218
- if (s.size < 16) return s;
2219
- const kind = stickerStamp(s);
2220
- if (kind) {
2221
- if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
2222
- continue;
2217
+ advance(copy, d, rects) {
2218
+ const s = this.s;
2219
+ let list = this.sets[copy];
2220
+ if (d.index === 0 && !list.length) this.init(copy, d);
2221
+ const size = Math.max(1, d.radius * 2);
2222
+ const markR = Math.max(0.5, size * s.size / 2);
2223
+ const k = 0.02 + 0.3 * s.stiffness;
2224
+ const damp = 0.6 + 0.38 * (1 - s.damping);
2225
+ const g = s.gravity * 0.15;
2226
+ const op = d.opacity;
2227
+ const al = Math.max(0.05, d.alpha);
2228
+ const nx = Math.sin(d.direction);
2229
+ const ny = Math.cos(d.direction);
2230
+ if (s.kind === "flow") {
2231
+ this.spawn += s.count / 8;
2232
+ while (this.spawn >= 1) {
2233
+ this.spawn -= 1;
2234
+ const i = this.dabCount + list.length * 31 + Math.floor(d.index);
2235
+ const a = rand01(this.seed, i, 11 + copy) * Math.PI * 2;
2236
+ const r = rand01(this.seed, i, 13 + copy) * s.spread * size * 0.5;
2237
+ list.push(this.newParticle(d.x + Math.cos(a) * r, d.y + Math.sin(a) * r, 0, 0, 1, a, i, copy));
2238
+ }
2223
2239
  }
2224
- const pic = art?.(s.ref);
2225
- if (!pic) return s;
2226
- const scale = Math.max(pic.width, pic.height) / 2;
2227
- if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
2228
- }
2229
- return null;
2230
- }
2231
-
2232
- // src/paint-editor/procedural.ts
2233
- var STEP_PX = 1.5;
2234
- var MarkStroke = class {
2235
- constructor(brush, options) {
2236
- this.brush = brush;
2237
- this.options = options;
2238
- this.sampler = options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
2239
- this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;
2240
- this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
2241
- this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
2242
- this.seed = options.seed ?? 0;
2243
- const fine = { ...brush, tip: { ...brush.tip, spacing: Math.max(0.01, Math.min(0.25, STEP_PX / Math.max(1, brush.tip.size))) } };
2244
- this.walkers = this.copies.map((c, i) => new PenDabWalker(fine, (options.seed ?? 0) + i * 7919 | 0, c.linear));
2245
- this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
2246
- }
2247
- brush;
2248
- options;
2249
- coverage = new CoverageMap();
2250
- dabCount = 0;
2251
- smoother;
2252
- copies;
2253
- walkers;
2254
- depth;
2255
- buf = new Float32Array(0);
2256
- field = new Float32Array(0);
2257
- lastRaw = null;
2258
- lastFed = null;
2259
- seed;
2260
- sampler;
2261
- tipTable;
2262
- /** The pen dab the marks being made belong to, and its gradient colour. */
2263
- pen = null;
2264
- penColor = { rgb: [0, 0, 0], alpha: 1 };
2265
- push(points) {
2266
- const rects = [];
2267
- for (const raw of points) {
2268
- this.lastRaw = raw;
2269
- const p = this.smoother.push(raw);
2270
- this.lastFed = p;
2271
- this.copies.forEach((c, i) => {
2272
- const dabs = [];
2273
- this.walkers[i].push(c.map(p), dabs);
2274
- for (const d of dabs) this.step(i, d, rects);
2275
- });
2240
+ const lifeSteps = 8 + 120 * s.life;
2241
+ for (let i = 0; i < list.length; i++) {
2242
+ const p = list[i];
2243
+ if (!p.alive) continue;
2244
+ switch (s.kind) {
2245
+ case "spring": {
2246
+ p.vx = (p.vx + (d.x + p.ox - p.x) * k * p.k) * damp;
2247
+ p.vy = (p.vy + (d.y + p.oy - p.y) * k * p.k + g) * damp;
2248
+ p.x += p.vx;
2249
+ p.y += p.vy;
2250
+ break;
2251
+ }
2252
+ case "string":
2253
+ case "fuzzy": {
2254
+ const strand = p.strand;
2255
+ const dm = s.kind === "fuzzy" ? chainDamp(damp, k * p.k) : damp;
2256
+ let ax = d.x + p.ox;
2257
+ let ay = d.y + p.oy;
2258
+ for (const m of strand) {
2259
+ m.vx = (m.vx + (ax - m.x) * k * p.k + 0) * dm;
2260
+ m.vy = (m.vy + (ay - m.y) * k * p.k + g) * dm;
2261
+ m.x += m.vx;
2262
+ m.y += m.vy;
2263
+ ax = m.x;
2264
+ ay = m.y;
2265
+ }
2266
+ p.x = strand[strand.length - 1].x;
2267
+ p.y = strand[strand.length - 1].y;
2268
+ break;
2269
+ }
2270
+ case "elastic": {
2271
+ const strand = p.strand;
2272
+ const rest = size * 0.12;
2273
+ const ke = Math.min(0.9, (0.1 + 0.6 * s.stiffness) * p.k);
2274
+ const dm = chainDamp(damp, ke);
2275
+ let ax = d.x + p.ox;
2276
+ let ay = d.y + p.oy;
2277
+ for (const m of strand) {
2278
+ const ex = ax - m.x;
2279
+ const ey = ay - m.y;
2280
+ const len = Math.hypot(ex, ey);
2281
+ const pull = len > rest ? (len - rest) / len : 0;
2282
+ m.vx = (m.vx + ex * pull * ke) * dm;
2283
+ m.vy = (m.vy + ey * pull * ke + g) * dm;
2284
+ m.x += m.vx;
2285
+ m.y += m.vy;
2286
+ ax = m.x;
2287
+ ay = m.y;
2288
+ }
2289
+ p.x = strand[strand.length - 1].x;
2290
+ p.y = strand[strand.length - 1].y;
2291
+ break;
2292
+ }
2293
+ case "orbit": {
2294
+ p.phase += (0.05 + 0.3 * s.speed) * (i % 2 ? -1 : 1) * (0.7 + 0.6 * rand01(this.seed, i, 17));
2295
+ p.x = d.x + Math.cos(p.phase) * p.ox;
2296
+ p.y = d.y + Math.sin(p.phase) * p.ox + g * p.ox;
2297
+ break;
2298
+ }
2299
+ case "flow": {
2300
+ const [fx, fy] = curl(p.x / Math.max(8, size * 1.5), p.y / Math.max(8, size * 1.5), this.seed);
2301
+ const push = size * 0.012 * (0.3 + 1.5 * s.speed);
2302
+ const fl = Math.hypot(fx, fy) || 1;
2303
+ p.vx = p.vx * 0.8 + fx / fl * push;
2304
+ p.vy = p.vy * 0.8 + fy / fl * push + g * 0.2;
2305
+ p.x += p.vx;
2306
+ p.y += p.vy;
2307
+ p.age++;
2308
+ if (p.age > lifeSteps) p.alive = false;
2309
+ break;
2310
+ }
2311
+ }
2312
+ const w = this.wave(p, d, size);
2313
+ const qx = w ? p.x + nx * w : p.x;
2314
+ const qy = w ? p.y + ny * w : p.y;
2315
+ const px = p.dx;
2316
+ const py = p.dy;
2317
+ p.px = p.dx;
2318
+ p.py = p.dy;
2319
+ p.dx = qx;
2320
+ p.dy = qy;
2321
+ const fade = s.kind === "flow" ? 1 - p.age / lifeSteps : 1;
2322
+ p.fade = fade;
2323
+ const moved = Math.hypot(qx - px, qy - py);
2324
+ if (d.index === 0 && s.kind !== "flow") continue;
2325
+ if (s.render === "dot") this.dot(copy, qx, qy, qx - px, qy - py, markR * fade, al * fade, op, d, rects);
2326
+ else if (s.render === "line" || s.render === "ribbon") this.segment(px, py, qx, qy, markR * fade, al * fade, op, d.pressure, rects);
2327
+ else {
2328
+ p.run += moved;
2329
+ const gap = Math.max(markR * 2, size * (0.05 + s.interval));
2330
+ while (p.run >= gap) {
2331
+ p.run -= gap;
2332
+ p.bead++;
2333
+ const t = moved > 0 ? 1 - p.run / Math.max(moved, 1e-6) : 1;
2334
+ const bx = px + (qx - px) * Math.min(1, Math.max(0, t));
2335
+ const by = py + (qy - py) * Math.min(1, Math.max(0, t));
2336
+ this.dot(copy, bx, by, qx - px, qy - py, markR * (0.7 + 0.6 * (p.bead % 3 / 2)) * fade, al * fade, op, d, rects);
2337
+ }
2338
+ }
2339
+ }
2340
+ if (s.render === "ribbon" && !(d.index === 0 && s.kind !== "flow")) this.ribbon(copy, list, op, al, d, rects);
2341
+ if (s.kind === "flow" && list.length > 512) {
2342
+ list = list.filter((p) => p.alive);
2343
+ this.sets[copy] = list;
2276
2344
  }
2277
- return rects;
2278
- }
2279
- step(copy, d, rects) {
2280
- this.pen = d;
2281
- if (this.sampler && !this.tipTable) this.sampler.dab(d, this.penColor);
2282
- this.advance(copy, d, rects);
2283
- }
2284
- /** The colour of a mark at (x, y): its pen dab's, or across the tip by its offset from the pen. */
2285
- markColor(x, y) {
2286
- if (!this.sampler || !this.pen) return null;
2287
- if (!this.tipTable) return this.penColor;
2288
- const d = this.pen;
2289
- const u = ((x - d.x) * Math.sin(d.direction) + (y - d.y) * Math.cos(d.direction)) / Math.max(0.5, d.radius);
2290
- const last = this.tipTable.length - 1;
2291
- return this.tipTable[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
2292
- }
2293
- finish() {
2294
- const rects = [];
2295
- const raw = this.lastRaw;
2296
- const fed = this.lastFed;
2297
- this.copies.forEach((c, i) => {
2298
- const dabs = [];
2299
- if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
2300
- this.walkers[i].finish(dabs);
2301
- for (const d of dabs) this.step(i, d, rects);
2302
- });
2303
- return rects;
2304
2345
  }
2305
- /** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
2306
- mark(x, y, r, alpha, opacity, pressure, rects) {
2307
- if (!(r > 0.05) || !(alpha > 0)) return;
2308
- const dab = { x, y, radius: Math.max(0.5, r), alpha: Math.min(1, alpha), opacity: Math.min(1, opacity), angle: 0, roundness: 1 };
2309
- const hardness = this.brush.tip.hardness;
2310
- const rect = dabRect(dab, hardness);
2311
- const n = rect.width * rect.height;
2312
- if (n <= 0) return;
2313
- const vals = this.scratch(n);
2314
- rasterizeDab(dab, hardness, rect, vals);
2315
- this.lay(rect, vals, dab.opacity, pressure, x, y, rects);
2346
+ /** A dot (or bead): round, or a textured stamp turned by the particle's motion (mx, my), at random or to the tip angle. */
2347
+ dot(copy, x, y, mx, my, r, alpha, opacity, d, rects) {
2348
+ const s = this.s;
2349
+ const textures = s.dotTextures;
2350
+ if (!textures.length) return this.mark(x, y, r, alpha, opacity, d.pressure, rects);
2351
+ const n = this.dots[copy]++;
2352
+ const pick = s.dotTextureOrder === "random" ? Math.min(textures.length - 1, Math.floor(rand01(this.seed, n, 31 + copy) * textures.length)) : n % textures.length;
2353
+ const rotation = s.dotRotation === "random" ? rand01(this.seed, n, 37 + copy) * Math.PI * 2 : s.dotRotation === "follow" ? mx || my ? Math.atan2(my, mx) : -d.direction : -this.brush.tip.angle * Math.PI / 180;
2354
+ this.stamp(x, y, r, textures[pick], rotation, this.seed + n * 2654435761 >>> 0, alpha, opacity, d.pressure, rects);
2316
2355
  }
2317
2356
  /**
2318
- * Stamps a textured mark: `kind` from the sticker stamps over a square of half-size `r`, turned
2319
- * `rotation` radians clockwise, with flow `alpha` towards `opacity`. Small marks are supersampled.
2357
+ * Ribbon fill for one step: the band between each pair of joined particles, from where they
2358
+ * were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
2359
+ * share edges exactly, so the fill has no seams; it is telescoped into the coverage
2360
+ * (`telescope`), so it does not build up at the joins.
2320
2361
  */
2321
- stamp(x, y, r, kind, rotation, seed, alpha, opacity, pressure, rects) {
2322
- if (!(r > 0.05) || !(alpha > 0)) return;
2323
- const reach = Math.max(0.5, r) * Math.SQRT2 + 1;
2324
- const rect = { x: Math.floor(x - reach), y: Math.floor(y - reach), width: 0, height: 0 };
2325
- rect.width = Math.ceil(x + reach) - rect.x;
2326
- rect.height = Math.ceil(y + reach) - rect.y;
2327
- const n = rect.width * rect.height;
2328
- const vals = this.scratch(n);
2329
- const ss = r < 10 ? 3 : 1;
2330
- const c = Math.cos(rotation);
2331
- const s = Math.sin(rotation);
2332
- const inv = 1 / Math.max(0.5, r);
2333
- const a = Math.min(1, alpha);
2362
+ ribbon(copy, list, opacity, alpha, d, rects) {
2363
+ const s = this.s;
2364
+ if (!(s.ribbonFill > 0)) return;
2365
+ const live = (p) => !!p && p.alive && !Number.isNaN(p.px);
2366
+ let order;
2367
+ if (s.kind === "flow") order = list.filter(live);
2368
+ else order = this.orders[copy].map((i) => list[i]).filter(live);
2369
+ if (order.length < 2) return;
2370
+ const pairs = [];
2371
+ if (s.ribbonPairs === "first-last") pairs.push([order[0], order[order.length - 1]]);
2372
+ else for (let i = 0; i + 1 < order.length; i++) pairs.push([order[i], order[i + 1]]);
2373
+ const pts = this.pts;
2374
+ const ceil = Math.min(1, opacity * s.ribbonFill);
2375
+ for (const [a, b] of pairs) {
2376
+ pts.length = 0;
2377
+ pts.push(a.px, a.py, a.dx, a.dy, b.dx, b.dy, b.px, b.py);
2378
+ let x0 = Infinity;
2379
+ let y0 = Infinity;
2380
+ let x1 = -Infinity;
2381
+ let y1 = -Infinity;
2382
+ for (let i = 0; i < 8; i += 2) {
2383
+ x0 = Math.min(x0, pts[i]);
2384
+ x1 = Math.max(x1, pts[i]);
2385
+ y0 = Math.min(y0, pts[i + 1]);
2386
+ y1 = Math.max(y1, pts[i + 1]);
2387
+ }
2388
+ const rect = { x: Math.floor(x0), y: Math.floor(y0), width: Math.floor(x1) - Math.floor(x0) + 1, height: Math.floor(y1) - Math.floor(y0) + 1 };
2389
+ const area = this.scratch(rect.width * rect.height);
2390
+ addPolygonArea(pts, rect, area, Math.min(a.fade, b.fade) * alpha);
2391
+ if (this.telescope(rect, area)) this.lay(rect, area, ceil, d.pressure, (a.dx + b.dx) / 2, (a.dy + b.dy) / 2, rects);
2392
+ }
2393
+ }
2394
+ /**
2395
+ * Turns the area newly filled over `rect` into coverage to lay towards the fill's ceiling: the
2396
+ * share of what was left unfilled, so after any number of bands (in any grouping) a pixel's
2397
+ * coverage is the ceiling times the area filled there. False when nothing was added.
2398
+ */
2399
+ telescope(rect, area) {
2400
+ let any = false;
2334
2401
  for (let yy = 0, k = 0; yy < rect.height; yy++) {
2402
+ const gy = rect.y + yy;
2403
+ const ty = Math.floor(gy / TILE);
2404
+ let t;
2405
+ let tx = NaN;
2335
2406
  for (let xx = 0; xx < rect.width; xx++, k++) {
2336
- let sum = 0;
2337
- for (let sy = 0; sy < ss; sy++) {
2338
- for (let sx = 0; sx < ss; sx++) {
2339
- const dx = rect.x + xx + (sx + 0.5) / ss - x;
2340
- const dy = rect.y + yy + (sy + 0.5) / ss - y;
2341
- sum += stampCoverage(kind, (dx * c + dy * s) * inv, (dy * c - dx * s) * inv, seed);
2407
+ const add = area[k];
2408
+ if (!(add > 0)) continue;
2409
+ const gx = rect.x + xx;
2410
+ const ntx = Math.floor(gx / TILE);
2411
+ if (ntx !== tx || !t) {
2412
+ tx = ntx;
2413
+ const key = tileKey(tx, ty);
2414
+ t = this.filled.get(key);
2415
+ if (!t) {
2416
+ t = new Float32Array(TILE * TILE);
2417
+ this.filled.set(key, t);
2342
2418
  }
2343
2419
  }
2344
- vals[k] = sum / (ss * ss) * a;
2420
+ const ti = (gy - ty * TILE) * TILE + (gx - tx * TILE);
2421
+ const before = t[ti];
2422
+ const after = Math.min(1, before + add);
2423
+ t[ti] = after;
2424
+ area[k] = before < 1 ? (after - before) / (1 - before) : 0;
2425
+ any = true;
2345
2426
  }
2346
2427
  }
2347
- this.lay(rect, vals, Math.min(1, opacity), pressure, x, y, rects);
2428
+ return any;
2348
2429
  }
2349
- /** A scratch buffer of `n` values (reused; valid until the next mark). */
2350
- scratch(n) {
2351
- if (this.buf.length < n) this.buf = new Float32Array(n);
2352
- const vals = this.buf.subarray(0, n);
2353
- vals.fill(0);
2354
- return vals;
2430
+ };
2431
+ var BristleStroke = class extends MarkStroke {
2432
+ tufts;
2433
+ s;
2434
+ constructor(brush, options) {
2435
+ super(brush, options);
2436
+ this.s = brush.bristle;
2437
+ const s = this.s;
2438
+ this.tufts = this.copies.map(
2439
+ (_, c) => Array.from({ length: s.count }, (_2, i) => {
2440
+ const a = rand01(this.seed, i, 21 + c) * Math.PI * 2;
2441
+ const r = Math.sqrt(rand01(this.seed, i, 23 + c));
2442
+ return {
2443
+ u: Math.cos(a) * r,
2444
+ v: Math.sin(a) * r,
2445
+ x: 0,
2446
+ y: 0,
2447
+ paint: 1 - s.jitter * 0.4 * rand01(this.seed, i, 25 + c),
2448
+ thirst: 1 + s.jitter * (rand01(this.seed, i, 27 + c) * 2 - 1),
2449
+ w: 1 + s.jitter * 0.6 * (rand01(this.seed, i, 29 + c) - 0.5),
2450
+ started: false
2451
+ };
2452
+ })
2453
+ );
2355
2454
  }
2356
- /**
2357
- * Lays coverage `vals` over `rect` towards `opacity`: paper grain, then effect paint or the
2358
- * colour of a mark at (x, y) (gradient paint), into the stroke's coverage.
2359
- */
2360
- lay(rect, vals, opacity, pressure, x, y, rects) {
2361
- const n = rect.width * rect.height;
2362
- if (n <= 0) return;
2363
- const grain = this.options.grain;
2364
- if (this.depth > 0 && grain) {
2365
- for (let yy = 0, k = 0; yy < rect.height; yy++)
2366
- for (let xx = 0; xx < rect.width; xx++, k++) if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, pressure);
2455
+ advance(copy, d, rects) {
2456
+ const s = this.s;
2457
+ const tuft = this.tufts[copy];
2458
+ const spread = d.radius * (0.55 + 0.9 * s.splay * d.pressure);
2459
+ const ca = Math.cos(d.angle);
2460
+ const sa = Math.sin(d.angle);
2461
+ const ro = Math.max(0.15, d.roundness);
2462
+ const tx = Math.cos(d.direction);
2463
+ const ty = -Math.sin(d.direction);
2464
+ const follow = 0.15 + 0.85 * s.stiffness;
2465
+ const bend = d.radius * (1 - s.stiffness) * d.pressure * 0.8;
2466
+ const lineR = Math.max(0.5, d.radius * 2 * s.thickness * 0.5);
2467
+ const dryRate = 4e-3 * (1 - s.load);
2468
+ for (const b of tuft) {
2469
+ const lu = b.u * spread;
2470
+ const lv = b.v * spread * ro;
2471
+ const ox = lu * ca - lv * sa;
2472
+ const oy = -(lu * sa + lv * ca);
2473
+ const lag = bend * (1 - 0.5 * b.v);
2474
+ const wx = d.x + ox - tx * lag;
2475
+ const wy = d.y + oy - ty * lag;
2476
+ if (!b.started) {
2477
+ b.x = wx;
2478
+ b.y = wy;
2479
+ b.started = true;
2480
+ continue;
2481
+ }
2482
+ const nx = b.x + (wx - b.x) * follow;
2483
+ const ny = b.y + (wy - b.y) * follow;
2484
+ const paint = Math.max(0, b.paint);
2485
+ if (paint > 0.02) this.segment(b.x, b.y, nx, ny, lineR * b.w * (0.6 + 0.4 * d.pressure), d.alpha * Math.min(1, paint * 1.4), d.opacity, d.pressure, rects);
2486
+ b.paint -= dryRate * b.thirst * Math.hypot(nx - b.x, ny - b.y);
2487
+ b.x = nx;
2488
+ b.y = ny;
2367
2489
  }
2368
- if (this.options.effect) {
2369
- if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
2370
- const field = this.field.subarray(0, n * 3);
2371
- sampleEffectField(this.options.effect, rect, vals, field, this.pen);
2372
- this.coverage.accumulate(rect, vals, opacity, null, field);
2373
- } else {
2374
- const color = this.markColor(x, y);
2375
- if (color && color.alpha < 1) for (let k = 0; k < n; k++) vals[k] *= color.alpha;
2376
- this.coverage.accumulate(rect, vals, opacity, color ? color.rgb : null);
2490
+ }
2491
+ };
2492
+
2493
+ // src/paint-editor/sticker-art.ts
2494
+ var STICKER_ART_SIZE = 256;
2495
+ var defaultStickerRasterizer = async (asset, size) => {
2496
+ if (asset.kind === "image") {
2497
+ const parsed = parseDataUrl(asset.url);
2498
+ if (parsed && parsed.mime === "image/png") {
2499
+ const png = decodePng(parsed.bytes);
2500
+ return { width: png.width, height: png.height, data: new Uint8ClampedArray(png.data) };
2377
2501
  }
2378
- rects.push(rect);
2379
- this.dabCount++;
2380
2502
  }
2381
- /** Marks along a segment, spaced by a fraction of the radius. */
2382
- segment(x0, y0, x1, y1, r, alpha, opacity, pressure, rects) {
2383
- const len = Math.hypot(x1 - x0, y1 - y0);
2384
- const step = Math.max(0.35, r * 0.5);
2385
- const n = Math.max(1, Math.ceil(len / step));
2386
- for (let i = 1; i <= n; i++) this.mark(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, r, alpha, opacity, pressure, rects);
2503
+ const g = globalThis;
2504
+ if (!g.createImageBitmap || !g.OffscreenCanvas) {
2505
+ throw new StickerError("unsupported_format", `This environment can't draw ${asset.kind === "svg" ? "SVG" : "non-PNG"} sticker art; pass rasterizeSticker in the paint editor's options (or use a provider that offers png).`);
2506
+ }
2507
+ const blob = asset.kind === "svg" ? new Blob([asset.svg], { type: "image/svg+xml" }) : await (await g.fetch(asset.url)).blob();
2508
+ const bitmap = await g.createImageBitmap(blob);
2509
+ const scale = size / Math.max(bitmap.width, bitmap.height, 1);
2510
+ const w = Math.max(1, Math.round(bitmap.width * scale));
2511
+ const h = Math.max(1, Math.round(bitmap.height * scale));
2512
+ const canvas = new g.OffscreenCanvas(w, h);
2513
+ const ctx = canvas.getContext("2d");
2514
+ ctx.drawImage(bitmap, 0, 0, w, h);
2515
+ const img = ctx.getImageData(0, 0, w, h);
2516
+ return { width: w, height: h, data: img.data };
2517
+ };
2518
+ var StickerArtCache = class {
2519
+ entries = /* @__PURE__ */ new Map();
2520
+ /** The art for `ref` when it has loaded (editor stamps never need any). */
2521
+ get = (ref2) => {
2522
+ const e = this.entries.get(ref2);
2523
+ return e?.status === "ready" ? e.art : void 0;
2524
+ };
2525
+ /** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
2526
+ status(ref2) {
2527
+ if (editorStampOf(ref2)) return { status: "ready" };
2528
+ const e = this.entries.get(ref2);
2529
+ if (!e) return { status: "none" };
2530
+ return e.status === "failed" ? { status: "failed", error: e.error } : { status: e.status };
2531
+ }
2532
+ /**
2533
+ * Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
2534
+ * reject with the closest known ones; failures are kept so they are reported, and retried on
2535
+ * the next call.
2536
+ */
2537
+ ensure(provider, ref2, rasterize) {
2538
+ if (editorStampOf(ref2)) return Promise.resolve(null);
2539
+ const e = this.entries.get(ref2);
2540
+ if (e?.status === "ready") return Promise.resolve(e.art);
2541
+ if (e?.status === "loading") return e.promise;
2542
+ const promise = (async () => {
2543
+ const d = await describeStickerRef(provider, ref2);
2544
+ const id = stickerIdFor(provider, ref2);
2545
+ const max = provider.capabilities.maxSize ?? STICKER_ART_SIZE;
2546
+ const size = Math.min(STICKER_ART_SIZE, max);
2547
+ const asset = await provider.load(id, d.formats.includes("png") ? { format: "png", size } : { format: d.formats[0] ?? "svg", size });
2548
+ const px = await rasterize(asset, size);
2549
+ if (!(px.width > 0 && px.height > 0) || px.data.length !== px.width * px.height * 4) throw new StickerError("load_failed", `The art for "${ref2}" came back empty or the wrong size.`);
2550
+ return { ...px, tintable: d.tintable };
2551
+ })();
2552
+ this.entries.set(ref2, { status: "loading", promise });
2553
+ return promise.then(
2554
+ (art) => {
2555
+ this.entries.set(ref2, { status: "ready", art });
2556
+ return art;
2557
+ },
2558
+ (err) => {
2559
+ const message = err instanceof Error ? err.message : String(err);
2560
+ this.entries.set(ref2, { status: "failed", error: message });
2561
+ throw err;
2562
+ }
2563
+ );
2564
+ }
2565
+ /** Forgets every load (a new provider). */
2566
+ clear() {
2567
+ this.entries.clear();
2387
2568
  }
2388
2569
  };
2389
- var FUZZY_AMPLITUDE = 0.12;
2390
- var NOMINAL_MS_PER_PX = 2;
2391
- var RIBBON_SUB = 4;
2392
- function chainDamp(damp, k) {
2393
- return Math.min(damp, 1 / (1 + 0.8 * Math.sqrt(Math.max(0, k))) ** 2);
2394
- }
2395
- function addPolygonArea(pts, rect, out, weight = 1) {
2396
- const n = pts.length >> 1;
2397
- if (n < 3) return;
2398
- let y0 = Infinity;
2399
- let y1 = -Infinity;
2400
- for (let i = 1; i < pts.length; i += 2) {
2401
- if (pts[i] < y0) y0 = pts[i];
2402
- if (pts[i] > y1) y1 = pts[i];
2570
+ function sampleArt(art, x, y, out) {
2571
+ const { width: w, height: h, data } = art;
2572
+ const fx = x - 0.5;
2573
+ const fy = y - 0.5;
2574
+ const x0 = Math.floor(fx);
2575
+ const y0 = Math.floor(fy);
2576
+ const tx = fx - x0;
2577
+ const ty = fy - y0;
2578
+ let r = 0;
2579
+ let g = 0;
2580
+ let b = 0;
2581
+ let a = 0;
2582
+ for (let k = 0; k < 4; k++) {
2583
+ const xx = x0 + (k & 1);
2584
+ const yy = y0 + (k >> 1);
2585
+ if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
2586
+ const wgt = (k & 1 ? tx : 1 - tx) * (k >> 1 ? ty : 1 - ty);
2587
+ const i = (yy * w + xx) * 4;
2588
+ const al = data[i + 3] * wgt;
2589
+ r += data[i] * al;
2590
+ g += data[i + 1] * al;
2591
+ b += data[i + 2] * al;
2592
+ a += al;
2403
2593
  }
2404
- const r0 = Math.max(0, Math.floor(y0) - rect.y);
2405
- const r1 = Math.min(rect.height - 1, Math.floor(y1) - rect.y);
2406
- const xs = [];
2407
- const w = weight / RIBBON_SUB;
2408
- for (let row = r0; row <= r1; row++) {
2409
- const base = row * rect.width;
2410
- for (let sub = 0; sub < RIBBON_SUB; sub++) {
2411
- const y = rect.y + row + (sub + 0.5) / RIBBON_SUB;
2412
- xs.length = 0;
2413
- for (let i = 0, j = n - 1; i < n; j = i++) {
2414
- const yi = pts[i * 2 + 1];
2415
- const yj = pts[j * 2 + 1];
2416
- if (yi <= y !== yj <= y) xs.push(pts[i * 2] + (y - yi) / (yj - yi) * (pts[j * 2] - pts[i * 2]));
2417
- }
2418
- if (xs.length < 2) continue;
2419
- xs.sort((a, b) => a - b);
2420
- for (let k = 0; k + 1 < xs.length; k += 2) {
2421
- const a = Math.max(rect.x, xs[k]);
2422
- const b = Math.min(rect.x + rect.width, xs[k + 1]);
2423
- if (!(b > a)) continue;
2424
- for (let px = Math.floor(a); px < b; px++) {
2425
- const cover = Math.min(b, px + 1) - Math.max(a, px);
2426
- if (cover > 0) out[base + px - rect.x] += cover * w;
2427
- }
2428
- }
2594
+ out[0] = a > 0 ? r / a / 255 : 0;
2595
+ out[1] = a > 0 ? g / a / 255 : 0;
2596
+ out[2] = a > 0 ? b / a / 255 : 0;
2597
+ out[3] = a / 255;
2598
+ return out;
2599
+ }
2600
+
2601
+ // src/paint-editor/stickers.ts
2602
+ function hexRgb(hex2) {
2603
+ const n = parseInt(hex2.slice(1, 7), 16);
2604
+ return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
2605
+ }
2606
+ function stickerRect(s) {
2607
+ const r = s.size / 2 + 1;
2608
+ const x0 = Math.floor(s.x - r);
2609
+ const y0 = Math.floor(s.y - r);
2610
+ return { x: x0, y: y0, width: Math.ceil(s.x + r) - x0, height: Math.ceil(s.y + r) - y0 };
2611
+ }
2612
+ function stickerStamp(s) {
2613
+ if (s.ref) {
2614
+ try {
2615
+ return editorStampOf(s.ref);
2616
+ } catch {
2617
+ return null;
2429
2618
  }
2430
2619
  }
2620
+ return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
2431
2621
  }
2432
- function curl(x, y, seed) {
2433
- const n = (u, v) => {
2434
- const x0 = Math.floor(u);
2435
- const y0 = Math.floor(v);
2436
- const fx = u - x0;
2437
- const fy = v - y0;
2438
- const sx = fx * fx * (3 - 2 * fx);
2439
- const sy = fy * fy * (3 - 2 * fy);
2440
- const h = (a2, b2) => rand01(seed, a2 * 73856093 + b2 * 19349663, 41);
2441
- const a = h(x0, y0);
2442
- const b = h(x0 + 1, y0);
2443
- const c = h(x0, y0 + 1);
2444
- const d = h(x0 + 1, y0 + 1);
2445
- return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy;
2446
- };
2447
- const e = 0.01;
2448
- return [(n(x, y + e) - n(x, y - e)) / (2 * e), -(n(x + e, y) - n(x - e, y)) / (2 * e)];
2622
+ function drawSticker(medium, s, art) {
2623
+ const kind = stickerStamp(s);
2624
+ if (!kind) {
2625
+ const a2 = art?.(s.ref);
2626
+ return a2 ? drawArtSticker(medium, s, a2) : stickerRect(s);
2627
+ }
2628
+ const rect = stickerRect(s);
2629
+ const n = rect.width * rect.height;
2630
+ const cov = new Float32Array(n);
2631
+ const field = new Float32Array(n * 3);
2632
+ const rad2 = s.size / 2;
2633
+ const a = s.rotation * Math.PI / 180;
2634
+ const ca = Math.cos(a);
2635
+ const sa = Math.sin(a);
2636
+ const [r, g, b] = hexRgb(s.color);
2637
+ for (let yy = 0, p = 0; yy < rect.height; yy++) {
2638
+ for (let xx = 0, px = rect.x; xx < rect.width; xx++, p++, px++) {
2639
+ const dx = px + 0.5 - s.x;
2640
+ const dy = rect.y + yy + 0.5 - s.y;
2641
+ const u = (dx * ca + dy * sa) / rad2;
2642
+ const v = (-dx * sa + dy * ca) / rad2;
2643
+ if (u < -1.05 || u > 1.05 || v < -1.05 || v > 1.05) continue;
2644
+ let c = 0;
2645
+ for (let k = 0; k < 4; k++) c += stampCoverage(kind, u + ((k & 1) - 0.5) / rad2 / 2, v + ((k >> 1) - 0.5) / rad2 / 2, s.seed);
2646
+ c /= 4;
2647
+ if (c <= 0) continue;
2648
+ cov[p] = c;
2649
+ const shade = 1 + 0.12 * (-(u + v) / 2) + 0.06 * (hash2(px, rect.y + yy, s.seed) - 0.5);
2650
+ field[p * 3] = Math.min(1, Math.max(0, r * shade));
2651
+ field[p * 3 + 1] = Math.min(1, Math.max(0, g * shade));
2652
+ field[p * 3 + 2] = Math.min(1, Math.max(0, b * shade));
2653
+ }
2654
+ }
2655
+ medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
2656
+ return rect;
2449
2657
  }
2450
- var ParticleStroke = class extends MarkStroke {
2451
- sets;
2452
- /** Ribbon pairs per copy: particles in their order around the pen (non-flow kinds). */
2453
- orders;
2454
- s;
2455
- spawn = 0;
2456
- /** Textured dots drawn so far, per copy (sequence order, random picks). */
2457
- dots;
2458
- /** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
2459
- filled = /* @__PURE__ */ new Map();
2460
- pts = [];
2461
- constructor(brush, options) {
2462
- super(brush, options);
2463
- this.s = brush.particle;
2464
- this.sets = this.copies.map(() => []);
2465
- this.orders = this.copies.map(() => []);
2466
- this.dots = this.copies.map(() => 0);
2658
+ function drawArtSticker(medium, s, art) {
2659
+ const rect = stickerRect(s);
2660
+ const n = rect.width * rect.height;
2661
+ const cov = new Float32Array(n);
2662
+ const field = new Float32Array(n * 3);
2663
+ const a = s.rotation * Math.PI / 180;
2664
+ const ca = Math.cos(a);
2665
+ const sa = Math.sin(a);
2666
+ const scale = Math.max(art.width, art.height) / s.size;
2667
+ const [r, g, b] = hexRgb(s.color);
2668
+ const px = [0, 0, 0, 0];
2669
+ for (let yy = 0, p = 0; yy < rect.height; yy++) {
2670
+ for (let xx = 0; xx < rect.width; xx++, p++) {
2671
+ const dx = rect.x + xx + 0.5 - s.x;
2672
+ const dy = rect.y + yy + 0.5 - s.y;
2673
+ const ax = (dx * ca + dy * sa) * scale + art.width / 2;
2674
+ const ay = (-dx * sa + dy * ca) * scale + art.height / 2;
2675
+ if (ax < -1 || ay < -1 || ax > art.width + 1 || ay > art.height + 1) continue;
2676
+ sampleArt(art, ax, ay, px);
2677
+ if (!(px[3] > 0)) continue;
2678
+ cov[p] = px[3];
2679
+ field[p * 3] = art.tintable ? r : px[0];
2680
+ field[p * 3 + 1] = art.tintable ? g : px[1];
2681
+ field[p * 3 + 2] = art.tintable ? b : px[2];
2682
+ }
2467
2683
  }
2468
- /** Strand kinds: a chain of masses behind each particle. */
2469
- get strands() {
2470
- return this.s.kind === "string" || this.s.kind === "elastic" || this.s.kind === "fuzzy";
2684
+ medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
2685
+ return rect;
2686
+ }
2687
+ function jitterColor(hex2, amount, seed, i) {
2688
+ const [r, g, b] = hexRgb(hex2);
2689
+ const lch = oklabToOklch(rgbToOklab({ r, g, b, a: 1 }));
2690
+ const l = Math.min(1, Math.max(0, lch.l + (rand01(seed, i, 61) - 0.5) * amount * 0.3));
2691
+ const h = lch.h + (rand01(seed, i, 63) - 0.5) * amount * 80;
2692
+ const at = (c) => oklabToRgb(oklchToOklab({ l, c, h, alpha: 1 }));
2693
+ const inGamut = (o) => [o.r, o.g, o.b].every((v) => v >= -1e-4 && v <= 1 + 1e-4);
2694
+ let out = at(lch.c);
2695
+ if (!inGamut(out)) {
2696
+ let lo = 0;
2697
+ let hi = lch.c;
2698
+ for (let k = 0; k < 12; k++) {
2699
+ const mid = (lo + hi) / 2;
2700
+ if (inGamut(at(mid))) lo = mid;
2701
+ else hi = mid;
2702
+ }
2703
+ out = at(lo);
2471
2704
  }
2472
- newParticle(x, y, ox, oy, k, phase, i, copy) {
2473
- const s = this.s;
2474
- const fuzzy = s.kind === "fuzzy";
2475
- const jitter = fuzzy ? 1 : s.wavePhaseJitter;
2476
- return {
2477
- x,
2478
- y,
2479
- vx: 0,
2480
- vy: 0,
2481
- ox,
2482
- oy,
2483
- k,
2484
- phase,
2485
- age: 0,
2486
- run: 0,
2487
- bead: 0,
2488
- alive: true,
2489
- wavePhase: jitter > 0 ? jitter * Math.PI * 2 * rand01(this.seed, i, 43 + copy) : 0,
2490
- waveScale: fuzzy ? 0.6 + 0.8 * rand01(this.seed, i, 47 + copy) : 1,
2491
- dx: x,
2492
- dy: y,
2493
- px: NaN,
2494
- py: NaN,
2495
- fade: 1
2496
- };
2705
+ return "#" + [out.r, out.g, out.b].map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join("");
2706
+ }
2707
+ var StickerSpray = class {
2708
+ constructor(init) {
2709
+ this.init = init;
2710
+ const b = init.brush;
2711
+ this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);
2712
+ this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
2713
+ const spacing = Math.max(0.05, 1 / Math.max(0.05, b.sticker.density * 4));
2714
+ const walk = { ...b, tip: { ...b.tip, spacing }, dynamics: { ...b.dynamics, scatter: 0, sizeJitter: 0, angleJitter: 0 } };
2715
+ this.walkers = this.copies.map((c, i) => new PenDabWalker(walk, init.seed + i * 7919 | 0, c.linear));
2716
+ this.medium = new TileMedium(init.target, {});
2717
+ this.next = init.nextId;
2497
2718
  }
2498
- init(copy, d) {
2499
- const s = this.s;
2500
- const list = this.sets[copy];
2501
- const size = Math.max(1, d.radius * 2);
2502
- for (let i = 0; i < (s.kind === "flow" ? 0 : s.count); i++) {
2503
- const r = rand01(this.seed, i, 3 + copy) * s.spread * size * 0.5;
2504
- const a = rand01(this.seed, i, 5 + copy) * Math.PI * 2;
2505
- const ox = Math.cos(a) * r;
2506
- const oy = Math.sin(a) * r;
2507
- const p = this.newParticle(d.x + ox, d.y + oy, ox, oy, 0.6 + 0.8 * rand01(this.seed, i, 7), a, i, copy);
2508
- if (this.strands) p.strand = Array.from({ length: 6 }, () => ({ x: d.x + ox, y: d.y + oy, vx: 0, vy: 0 }));
2509
- if (s.kind === "orbit") {
2510
- p.ox = Math.max(1, r + size * 0.15);
2511
- p.oy = 0;
2512
- }
2513
- list.push(p);
2719
+ init;
2720
+ dabCount = 0;
2721
+ stickers = [];
2722
+ /** Refs the spray picked but could not draw because their art had not loaded. */
2723
+ skipped = /* @__PURE__ */ new Set();
2724
+ smoother;
2725
+ walkers;
2726
+ copies;
2727
+ medium;
2728
+ lastRaw = null;
2729
+ lastFed = null;
2730
+ next;
2731
+ push(points) {
2732
+ const rects = [];
2733
+ for (const raw of points) {
2734
+ this.lastRaw = raw;
2735
+ const p = this.smoother.push(raw);
2736
+ this.lastFed = p;
2737
+ this.copies.forEach((c, i) => {
2738
+ const dabs = [];
2739
+ this.walkers[i].push(c.map(p), dabs);
2740
+ for (const d of dabs) this.drop(d, rects);
2741
+ });
2514
2742
  }
2515
- this.orders[copy] = list.map((_, i) => i).sort((i, j) => list[i].phase - list[j].phase || i - j);
2743
+ return rects;
2516
2744
  }
2517
- /** The sideways wave of particle `p` at pen dab `d` (px along the normal to the pen's direction). */
2518
- wave(p, d, size) {
2519
- const s = this.s;
2520
- const amp = s.kind === "fuzzy" ? Math.max(FUZZY_AMPLITUDE, s.waveAmplitude) : s.waveAmplitude;
2521
- if (!(amp > 0)) return 0;
2522
- const ms = d.time ?? d.travel * NOMINAL_MS_PER_PX;
2523
- const phase = 2 * Math.PI * d.travel / (s.waveLength * p.waveScale) - s.waveSpeed * 0.025 * ms + p.wavePhase;
2524
- return amp * size * Math.sin(phase);
2745
+ finish() {
2746
+ const rects = [];
2747
+ const raw = this.lastRaw;
2748
+ const fed = this.lastFed;
2749
+ this.copies.forEach((c, i) => {
2750
+ const dabs = [];
2751
+ if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
2752
+ this.walkers[i].finish(dabs);
2753
+ for (const d of dabs) this.drop(d, rects);
2754
+ });
2755
+ return rects;
2525
2756
  }
2526
- advance(copy, d, rects) {
2527
- const s = this.s;
2528
- let list = this.sets[copy];
2529
- if (d.index === 0 && !list.length) this.init(copy, d);
2530
- const size = Math.max(1, d.radius * 2);
2531
- const markR = Math.max(0.5, size * s.size / 2);
2532
- const k = 0.02 + 0.3 * s.stiffness;
2533
- const damp = 0.6 + 0.38 * (1 - s.damping);
2534
- const g = s.gravity * 0.15;
2535
- const op = d.opacity;
2536
- const al = Math.max(0.05, d.alpha);
2537
- const nx = Math.sin(d.direction);
2538
- const ny = Math.cos(d.direction);
2539
- if (s.kind === "flow") {
2540
- this.spawn += s.count / 8;
2541
- while (this.spawn >= 1) {
2542
- this.spawn -= 1;
2543
- const i = this.dabCount + list.length * 31 + Math.floor(d.index);
2544
- const a = rand01(this.seed, i, 11 + copy) * Math.PI * 2;
2545
- const r = rand01(this.seed, i, 13 + copy) * s.spread * size * 0.5;
2546
- list.push(this.newParticle(d.x + Math.cos(a) * r, d.y + Math.sin(a) * r, 0, 0, 1, a, i, copy));
2547
- }
2757
+ drop(d, rects) {
2758
+ const st = this.init.brush.sticker;
2759
+ const i = this.next;
2760
+ const seed = this.init.seed;
2761
+ const size = d.radius * 2 * (1 - st.sizeJitter * 0.7 * rand01(seed, i, 51));
2762
+ const off = st.scatter * d.radius * 2;
2763
+ const a = rand01(seed, i, 53) * Math.PI * 2;
2764
+ const dist = rand01(seed, i, 55) * off;
2765
+ const base = this.colorAt(d, Math.cos(a) * dist, Math.sin(a) * dist);
2766
+ if (!(base.alpha > 0)) {
2767
+ this.next++;
2768
+ return;
2548
2769
  }
2549
- const lifeSteps = 8 + 120 * s.life;
2550
- for (let i = 0; i < list.length; i++) {
2551
- const p = list[i];
2552
- if (!p.alive) continue;
2553
- switch (s.kind) {
2554
- case "spring": {
2555
- p.vx = (p.vx + (d.x + p.ox - p.x) * k * p.k) * damp;
2556
- p.vy = (p.vy + (d.y + p.oy - p.y) * k * p.k + g) * damp;
2557
- p.x += p.vx;
2558
- p.y += p.vy;
2559
- break;
2560
- }
2561
- case "string":
2562
- case "fuzzy": {
2563
- const strand = p.strand;
2564
- const dm = s.kind === "fuzzy" ? chainDamp(damp, k * p.k) : damp;
2565
- let ax = d.x + p.ox;
2566
- let ay = d.y + p.oy;
2567
- for (const m of strand) {
2568
- m.vx = (m.vx + (ax - m.x) * k * p.k + 0) * dm;
2569
- m.vy = (m.vy + (ay - m.y) * k * p.k + g) * dm;
2570
- m.x += m.vx;
2571
- m.y += m.vy;
2572
- ax = m.x;
2573
- ay = m.y;
2574
- }
2575
- p.x = strand[strand.length - 1].x;
2576
- p.y = strand[strand.length - 1].y;
2577
- break;
2578
- }
2579
- case "elastic": {
2580
- const strand = p.strand;
2581
- const rest = size * 0.12;
2582
- const ke = Math.min(0.9, (0.1 + 0.6 * s.stiffness) * p.k);
2583
- const dm = chainDamp(damp, ke);
2584
- let ax = d.x + p.ox;
2585
- let ay = d.y + p.oy;
2586
- for (const m of strand) {
2587
- const ex = ax - m.x;
2588
- const ey = ay - m.y;
2589
- const len = Math.hypot(ex, ey);
2590
- const pull = len > rest ? (len - rest) / len : 0;
2591
- m.vx = (m.vx + ex * pull * ke) * dm;
2592
- m.vy = (m.vy + ey * pull * ke + g) * dm;
2593
- m.x += m.vx;
2594
- m.y += m.vy;
2595
- ax = m.x;
2596
- ay = m.y;
2597
- }
2598
- p.x = strand[strand.length - 1].x;
2599
- p.y = strand[strand.length - 1].y;
2600
- break;
2601
- }
2602
- case "orbit": {
2603
- p.phase += (0.05 + 0.3 * s.speed) * (i % 2 ? -1 : 1) * (0.7 + 0.6 * rand01(this.seed, i, 17));
2604
- p.x = d.x + Math.cos(p.phase) * p.ox;
2605
- p.y = d.y + Math.sin(p.phase) * p.ox + g * p.ox;
2606
- break;
2607
- }
2608
- case "flow": {
2609
- const [fx, fy] = curl(p.x / Math.max(8, size * 1.5), p.y / Math.max(8, size * 1.5), this.seed);
2610
- const push = size * 0.012 * (0.3 + 1.5 * s.speed);
2611
- const fl = Math.hypot(fx, fy) || 1;
2612
- p.vx = p.vx * 0.8 + fx / fl * push;
2613
- p.vy = p.vy * 0.8 + fy / fl * push + g * 0.2;
2614
- p.x += p.vx;
2615
- p.y += p.vy;
2616
- p.age++;
2617
- if (p.age > lifeSteps) p.alive = false;
2618
- break;
2619
- }
2620
- }
2621
- const w = this.wave(p, d, size);
2622
- const qx = w ? p.x + nx * w : p.x;
2623
- const qy = w ? p.y + ny * w : p.y;
2624
- const px = p.dx;
2625
- const py = p.dy;
2626
- p.px = p.dx;
2627
- p.py = p.dy;
2628
- p.dx = qx;
2629
- p.dy = qy;
2630
- const fade = s.kind === "flow" ? 1 - p.age / lifeSteps : 1;
2631
- p.fade = fade;
2632
- const moved = Math.hypot(qx - px, qy - py);
2633
- if (d.index === 0 && s.kind !== "flow") continue;
2634
- if (s.render === "dot") this.dot(copy, qx, qy, qx - px, qy - py, markR * fade, al * fade, op, d, rects);
2635
- else if (s.render === "line" || s.render === "ribbon") this.segment(px, py, qx, qy, markR * fade, al * fade, op, d.pressure, rects);
2636
- else {
2637
- p.run += moved;
2638
- const gap = Math.max(markR * 2, size * (0.05 + s.interval));
2639
- while (p.run >= gap) {
2640
- p.run -= gap;
2641
- p.bead++;
2642
- const t = moved > 0 ? 1 - p.run / Math.max(moved, 1e-6) : 1;
2643
- const bx = px + (qx - px) * Math.min(1, Math.max(0, t));
2644
- const by = py + (qy - py) * Math.min(1, Math.max(0, t));
2645
- this.dot(copy, bx, by, qx - px, qy - py, markR * (0.7 + 0.6 * (p.bead % 3 / 2)) * fade, al * fade, op, d, rects);
2646
- }
2647
- }
2770
+ const ref2 = st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "editor:dot";
2771
+ const stamp = stickerStamp({ ref: ref2 });
2772
+ if (!stamp && !this.init.art?.(ref2)) {
2773
+ this.skipped.add(ref2);
2774
+ this.next++;
2775
+ return;
2648
2776
  }
2649
- if (s.render === "ribbon" && !(d.index === 0 && s.kind !== "flow")) this.ribbon(copy, list, op, al, d, rects);
2650
- if (s.kind === "flow" && list.length > 512) {
2651
- list = list.filter((p) => p.alive);
2652
- this.sets[copy] = list;
2777
+ const sticker = {
2778
+ id: `st${this.next++}`,
2779
+ ref: ref2,
2780
+ ...stamp ? { stamp } : {},
2781
+ x: Math.round((d.x + Math.cos(a) * dist) * 100) / 100,
2782
+ y: Math.round((d.y + Math.sin(a) * dist) * 100) / 100,
2783
+ size: Math.max(2, Math.round(size * 10) / 10),
2784
+ rotation: Math.round((-d.angle * 180 / Math.PI + (rand01(seed, i, 59) * 2 - 1) * 180 * st.rotationJitter) * 10) / 10,
2785
+ color: st.colorJitter > 0 ? jitterColor(base.color, st.colorJitter, seed, i) : base.color,
2786
+ opacity: Math.round(Math.min(1, d.opacity * this.init.brush.tip.opacity * base.alpha) * 1e3) / 1e3,
2787
+ seed: Math.floor(rand01(seed, i, 65) * 1e9)
2788
+ };
2789
+ this.stickers.push(sticker);
2790
+ rects.push(drawSticker(this.medium, sticker, this.init.art));
2791
+ this.dabCount++;
2792
+ }
2793
+ sampler;
2794
+ /** A sticker's colour: the paint colour, or gradient paint at its dab (`dx`, `dy` from the dab's centre). */
2795
+ colorAt(d, dx, dy) {
2796
+ const effect = this.init.effect;
2797
+ if (effect) {
2798
+ const rgb = [0, 0, 0];
2799
+ const alpha = sampleEffectAt(effect, d.x + dx, d.y + dy, rgb, d);
2800
+ return { color: "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join(""), alpha };
2653
2801
  }
2802
+ const g = this.init.paint;
2803
+ if (!g) return { color: this.init.color, alpha: 1 };
2804
+ this.sampler ??= new PaintGradientSampler(g, this.init.seed);
2805
+ const s = g.map.by === "across-tip" ? this.sampler.sample(((dx * Math.sin(d.direction) + dy * Math.cos(d.direction)) / Math.max(0.5, d.radius) + 1) / 2) : this.sampler.dab(d);
2806
+ return { color: "#" + s.rgb.map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join(""), alpha: s.alpha };
2807
+ }
2808
+ };
2809
+ function renderStickers(stickers, target, art) {
2810
+ const medium = new TileMedium(target, {});
2811
+ for (const s of stickers) drawSticker(medium, s, art);
2812
+ }
2813
+ function normalizeStickerUpdate(u) {
2814
+ const out = {};
2815
+ if (u.ref !== void 0 && u.stamp !== void 0) throw new Error("Give ref or stamp, not both (stamp is the older name for an editor stamp).");
2816
+ if (u.stamp !== void 0) {
2817
+ if (!STAMP_KINDS.includes(u.stamp)) throw new Error(`stamp ${JSON.stringify(u.stamp)} is not a stamp; use ${STAMP_KINDS.join(", ")}, or ref for any sticker (find_stickers).`);
2818
+ out.ref = `editor:${u.stamp}`;
2819
+ out.stamp = u.stamp;
2820
+ }
2821
+ if (u.ref !== void 0) {
2822
+ out.ref = normalizeStickerRef(u.ref);
2823
+ const stamp = stickerStamp({ ref: out.ref });
2824
+ if (stamp) out.stamp = stamp;
2825
+ else out.stamp = void 0;
2654
2826
  }
2655
- /** A dot (or bead): round, or a textured stamp turned by the particle's motion (mx, my), at random or to the tip angle. */
2656
- dot(copy, x, y, mx, my, r, alpha, opacity, d, rects) {
2657
- const s = this.s;
2658
- const textures = s.dotTextures;
2659
- if (!textures.length) return this.mark(x, y, r, alpha, opacity, d.pressure, rects);
2660
- const n = this.dots[copy]++;
2661
- const pick = s.dotTextureOrder === "random" ? Math.min(textures.length - 1, Math.floor(rand01(this.seed, n, 31 + copy) * textures.length)) : n % textures.length;
2662
- const rotation = s.dotRotation === "random" ? rand01(this.seed, n, 37 + copy) * Math.PI * 2 : s.dotRotation === "follow" ? mx || my ? Math.atan2(my, mx) : -d.direction : -this.brush.tip.angle * Math.PI / 180;
2663
- this.stamp(x, y, r, textures[pick], rotation, this.seed + n * 2654435761 >>> 0, alpha, opacity, d.pressure, rects);
2827
+ for (const k of ["x", "y"]) if (u[k] !== void 0) {
2828
+ if (!Number.isFinite(u[k])) throw new Error(`${k} must be a number of pixels.`);
2829
+ out[k] = u[k];
2664
2830
  }
2665
- /**
2666
- * Ribbon fill for one step: the band between each pair of joined particles, from where they
2667
- * were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
2668
- * share edges exactly, so the fill has no seams; it is telescoped into the coverage
2669
- * (`telescope`), so it does not build up at the joins.
2670
- */
2671
- ribbon(copy, list, opacity, alpha, d, rects) {
2672
- const s = this.s;
2673
- if (!(s.ribbonFill > 0)) return;
2674
- const live = (p) => !!p && p.alive && !Number.isNaN(p.px);
2675
- let order;
2676
- if (s.kind === "flow") order = list.filter(live);
2677
- else order = this.orders[copy].map((i) => list[i]).filter(live);
2678
- if (order.length < 2) return;
2679
- const pairs = [];
2680
- if (s.ribbonPairs === "first-last") pairs.push([order[0], order[order.length - 1]]);
2681
- else for (let i = 0; i + 1 < order.length; i++) pairs.push([order[i], order[i + 1]]);
2682
- const pts = this.pts;
2683
- const ceil = Math.min(1, opacity * s.ribbonFill);
2684
- for (const [a, b] of pairs) {
2685
- pts.length = 0;
2686
- pts.push(a.px, a.py, a.dx, a.dy, b.dx, b.dy, b.px, b.py);
2687
- let x0 = Infinity;
2688
- let y0 = Infinity;
2689
- let x1 = -Infinity;
2690
- let y1 = -Infinity;
2691
- for (let i = 0; i < 8; i += 2) {
2692
- x0 = Math.min(x0, pts[i]);
2693
- x1 = Math.max(x1, pts[i]);
2694
- y0 = Math.min(y0, pts[i + 1]);
2695
- y1 = Math.max(y1, pts[i + 1]);
2696
- }
2697
- const rect = { x: Math.floor(x0), y: Math.floor(y0), width: Math.floor(x1) - Math.floor(x0) + 1, height: Math.floor(y1) - Math.floor(y0) + 1 };
2698
- const area = this.scratch(rect.width * rect.height);
2699
- addPolygonArea(pts, rect, area, Math.min(a.fade, b.fade) * alpha);
2700
- if (this.telescope(rect, area)) this.lay(rect, area, ceil, d.pressure, (a.dx + b.dx) / 2, (a.dy + b.dy) / 2, rects);
2701
- }
2831
+ if (u.size !== void 0) {
2832
+ if (!(u.size >= 2 && u.size <= 2048)) throw new Error(`size ${u.size} is out of range; use 2..2048 px.`);
2833
+ out.size = u.size;
2702
2834
  }
2703
- /**
2704
- * Turns the area newly filled over `rect` into coverage to lay towards the fill's ceiling: the
2705
- * share of what was left unfilled, so after any number of bands (in any grouping) a pixel's
2706
- * coverage is the ceiling times the area filled there. False when nothing was added.
2707
- */
2708
- telescope(rect, area) {
2709
- let any = false;
2710
- for (let yy = 0, k = 0; yy < rect.height; yy++) {
2711
- const gy = rect.y + yy;
2712
- const ty = Math.floor(gy / TILE);
2713
- let t;
2714
- let tx = NaN;
2715
- for (let xx = 0; xx < rect.width; xx++, k++) {
2716
- const add = area[k];
2717
- if (!(add > 0)) continue;
2718
- const gx = rect.x + xx;
2719
- const ntx = Math.floor(gx / TILE);
2720
- if (ntx !== tx || !t) {
2721
- tx = ntx;
2722
- const key = tileKey(tx, ty);
2723
- t = this.filled.get(key);
2724
- if (!t) {
2725
- t = new Float32Array(TILE * TILE);
2726
- this.filled.set(key, t);
2727
- }
2728
- }
2729
- const ti = (gy - ty * TILE) * TILE + (gx - tx * TILE);
2730
- const before = t[ti];
2731
- const after = Math.min(1, before + add);
2732
- t[ti] = after;
2733
- area[k] = before < 1 ? (after - before) / (1 - before) : 0;
2734
- any = true;
2735
- }
2835
+ if (u.rotation !== void 0) {
2836
+ if (!Number.isFinite(u.rotation)) throw new Error("rotation must be degrees.");
2837
+ out.rotation = u.rotation;
2838
+ }
2839
+ if (u.color !== void 0) {
2840
+ if (!/^#[0-9a-f]{6}$/i.test(u.color)) throw new Error(`color ${JSON.stringify(u.color)} must be #rrggbb.`);
2841
+ out.color = u.color.toLowerCase();
2842
+ }
2843
+ if (u.opacity !== void 0) {
2844
+ if (!(u.opacity >= 0 && u.opacity <= 1)) throw new Error(`opacity ${u.opacity} is out of range; use 0..1.`);
2845
+ out.opacity = u.opacity;
2846
+ }
2847
+ return out;
2848
+ }
2849
+ function stickerSurface(stickers, base, dirty, art) {
2850
+ const only = base && dirty ? new Set(dirty.flatMap((r) => tilesInRect(r).map(({ tx, ty }) => tileKey(tx, ty)))) : null;
2851
+ const live = /* @__PURE__ */ new Map();
2852
+ let scratch = null;
2853
+ const target = {
2854
+ offset: { x: 0, y: 0 },
2855
+ lockTransparency: false,
2856
+ selection: null,
2857
+ readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? null,
2858
+ writeTile: (tx, ty) => {
2859
+ const k = tileKey(tx, ty);
2860
+ if (only && !only.has(k)) return (scratch ??= new Uint8ClampedArray(TILE * TILE * 4)).fill(0);
2861
+ let t = live.get(k);
2862
+ if (!t) live.set(k, t = new Uint8ClampedArray(TILE * TILE * 4));
2863
+ return t;
2736
2864
  }
2737
- return any;
2865
+ };
2866
+ const reaches = (s) => !only || tilesInRect(stickerRect(s)).some(({ tx, ty }) => only.has(tileKey(tx, ty)));
2867
+ renderStickers(stickers.filter(reaches), target, art);
2868
+ const tiles = /* @__PURE__ */ new Map();
2869
+ if (only && base) {
2870
+ for (const [k, t] of base.tiles) if (!only.has(k)) tiles.set(k, t);
2738
2871
  }
2739
- };
2740
- var BristleStroke = class extends MarkStroke {
2741
- tufts;
2742
- s;
2743
- constructor(brush, options) {
2744
- super(brush, options);
2745
- this.s = brush.bristle;
2746
- const s = this.s;
2747
- this.tufts = this.copies.map(
2748
- (_, c) => Array.from({ length: s.count }, (_2, i) => {
2749
- const a = rand01(this.seed, i, 21 + c) * Math.PI * 2;
2750
- const r = Math.sqrt(rand01(this.seed, i, 23 + c));
2751
- return {
2752
- u: Math.cos(a) * r,
2753
- v: Math.sin(a) * r,
2754
- x: 0,
2755
- y: 0,
2756
- paint: 1 - s.jitter * 0.4 * rand01(this.seed, i, 25 + c),
2757
- thirst: 1 + s.jitter * (rand01(this.seed, i, 27 + c) * 2 - 1),
2758
- w: 1 + s.jitter * 0.6 * (rand01(this.seed, i, 29 + c) - 0.5),
2759
- started: false
2760
- };
2761
- })
2762
- );
2872
+ for (const [k, d] of live) {
2873
+ const t = finishTile(d, 4, 0);
2874
+ if (t) tiles.set(k, t);
2763
2875
  }
2764
- advance(copy, d, rects) {
2765
- const s = this.s;
2766
- const tuft = this.tufts[copy];
2767
- const spread = d.radius * (0.55 + 0.9 * s.splay * d.pressure);
2768
- const ca = Math.cos(d.angle);
2769
- const sa = Math.sin(d.angle);
2770
- const ro = Math.max(0.15, d.roundness);
2771
- const tx = Math.cos(d.direction);
2772
- const ty = -Math.sin(d.direction);
2773
- const follow = 0.15 + 0.85 * s.stiffness;
2774
- const bend = d.radius * (1 - s.stiffness) * d.pressure * 0.8;
2775
- const lineR = Math.max(0.5, d.radius * 2 * s.thickness * 0.5);
2776
- const dryRate = 4e-3 * (1 - s.load);
2777
- for (const b of tuft) {
2778
- const lu = b.u * spread;
2779
- const lv = b.v * spread * ro;
2780
- const ox = lu * ca - lv * sa;
2781
- const oy = -(lu * sa + lv * ca);
2782
- const lag = bend * (1 - 0.5 * b.v);
2783
- const wx = d.x + ox - tx * lag;
2784
- const wy = d.y + oy - ty * lag;
2785
- if (!b.started) {
2786
- b.x = wx;
2787
- b.y = wy;
2788
- b.started = true;
2789
- continue;
2790
- }
2791
- const nx = b.x + (wx - b.x) * follow;
2792
- const ny = b.y + (wy - b.y) * follow;
2793
- const paint = Math.max(0, b.paint);
2794
- if (paint > 0.02) this.segment(b.x, b.y, nx, ny, lineR * b.w * (0.6 + 0.4 * d.pressure), d.alpha * Math.min(1, paint * 1.4), d.opacity, d.pressure, rects);
2795
- b.paint -= dryRate * b.thirst * Math.hypot(nx - b.x, ny - b.y);
2796
- b.x = nx;
2797
- b.y = ny;
2876
+ return new TiledSurface(4, tiles, 0);
2877
+ }
2878
+ function stickerAt(stickers, x, y, art) {
2879
+ for (let i = stickers.length - 1; i >= 0; i--) {
2880
+ const s = stickers[i];
2881
+ const rad2 = s.size / 2;
2882
+ const a = s.rotation * Math.PI / 180;
2883
+ const dx = x - s.x;
2884
+ const dy = y - s.y;
2885
+ const u = (dx * Math.cos(a) + dy * Math.sin(a)) / rad2;
2886
+ const v = (-dx * Math.sin(a) + dy * Math.cos(a)) / rad2;
2887
+ if (Math.abs(u) > 1.05 || Math.abs(v) > 1.05) continue;
2888
+ if (s.size < 16) return s;
2889
+ const kind = stickerStamp(s);
2890
+ if (kind) {
2891
+ if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
2892
+ continue;
2798
2893
  }
2894
+ const pic = art?.(s.ref);
2895
+ if (!pic) return s;
2896
+ const scale = Math.max(pic.width, pic.height) / 2;
2897
+ if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
2799
2898
  }
2800
- };
2899
+ return null;
2900
+ }
2801
2901
 
2802
2902
  // src/paint-editor/stroke.ts
2803
2903
  function defaultTile(surface) {
@@ -2861,7 +2961,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
2861
2961
  effect
2862
2962
  });
2863
2963
  } else {
2864
- const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect };
2964
+ const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect, stampArt: init.stampArt };
2865
2965
  this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
2866
2966
  }
2867
2967
  }
@@ -2955,6 +3055,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
2955
3055
  if (filter) points = points.map(filter);
2956
3056
  for (const p of points) this.points.push(p);
2957
3057
  this.lastRaw = points[points.length - 1];
3058
+ this.host.sampled?.(this, points);
2958
3059
  this.lightTwin?.push(points);
2959
3060
  if (this.wetDriver) {
2960
3061
  this.wetUpdate(this.wetDriver.push(points));
@@ -5754,6 +5855,7 @@ var PaintEditorController = class _PaintEditorController {
5754
5855
  encodeImage;
5755
5856
  listeners = /* @__PURE__ */ new Set();
5756
5857
  commitListeners = /* @__PURE__ */ new Set();
5858
+ sampleListeners = /* @__PURE__ */ new Set();
5757
5859
  /** Origins set by `withOrigin`, innermost last. */
5758
5860
  origins = [];
5759
5861
  /** `group` in progress: commits fold into one step, recorded when it ends. */
@@ -5831,6 +5933,7 @@ var PaintEditorController = class _PaintEditorController {
5831
5933
  void this.refreshBrushes().catch(() => void 0);
5832
5934
  this.pendingBrushId = start.brush.id === wanted ? null : wanted;
5833
5935
  this.color = options.color ? parseColor(options.color) : "#1f2937";
5936
+ this.colorScheme = options.colorScheme === "dark" ? "dark" : "light";
5834
5937
  this.hand = options.hand === "left" ? "left" : "right";
5835
5938
  this.encodeImage = options.encodeImage;
5836
5939
  this.guides = defaultGuides(this.doc.canvas);
@@ -5867,6 +5970,27 @@ var PaintEditorController = class _PaintEditorController {
5867
5970
  this.listeners.add(listener);
5868
5971
  return () => this.listeners.delete(listener);
5869
5972
  }
5973
+ /**
5974
+ * Hears the user's live strokes as they are painted (`beginStroke` sessions, not strokes painted
5975
+ * from data): when the pen goes down, each batch of samples, and the end or cancel. For agents
5976
+ * that paint along in real time. Returns an unsubscribe function.
5977
+ */
5978
+ onStrokeSample(listener) {
5979
+ this.sampleListeners.add(listener);
5980
+ return () => {
5981
+ this.sampleListeners.delete(listener);
5982
+ };
5983
+ }
5984
+ notifySample(phase, session, added = [], strokeId) {
5985
+ if (!this.sampleListeners.size || this.previewing) return;
5986
+ const sample = { phase, layerId: session.layerId, brush: session.init.brush, color: session.init.colorHex, points: session.points, added, ...strokeId !== void 0 ? { strokeId } : {} };
5987
+ for (const l of [...this.sampleListeners]) l(sample);
5988
+ }
5989
+ /** The stroke being painted now, or `null`: its layer, brush, colour and samples so far. */
5990
+ getLiveStroke() {
5991
+ const s = this.session;
5992
+ return s ? { layerId: s.layerId, brushId: s.init.brush.id, brushName: s.init.brush.name, color: s.init.colorHex, points: s.points } : null;
5993
+ }
5870
5994
  /** Hears each undo step recorded, merged, undone or redone, with who made it. Returns an unsubscribe function. */
5871
5995
  onCommit(listener) {
5872
5996
  this.commitListeners.add(listener);
@@ -6002,6 +6126,8 @@ var PaintEditorController = class _PaintEditorController {
6002
6126
  brush: this.brush,
6003
6127
  brushes: this.library ??= this.brushEntries.map((e) => e.brush),
6004
6128
  color: this.color,
6129
+ colorScheme: this.colorScheme,
6130
+ colorLocked: this.colorLocked,
6005
6131
  secondaryColor: this.secondaryColor,
6006
6132
  paint: this.gradient ?? { type: "color", color: this.color },
6007
6133
  effectPaint: this.effectPaintPending ?? this.effectPaint,
@@ -7139,6 +7265,7 @@ var PaintEditorController = class _PaintEditorController {
7139
7265
  if (changes.paint !== void 0 && changes.paint !== null) this.readStrokePaint(changes.paint);
7140
7266
  const unchanged = !Object.keys(changes).length;
7141
7267
  const next = normalizePaintBrush({ ...changes, id: base.id }, base);
7268
+ if (entry) this.takeBrushColour(entry.spec);
7142
7269
  this.useBrush(next, unchanged ? baseSpec : brushSpecOf(next, baseSpec), unchanged && base.builtin === true);
7143
7270
  return this.brush;
7144
7271
  }
@@ -7155,6 +7282,7 @@ var PaintEditorController = class _PaintEditorController {
7155
7282
  const base = copyJSON(entry ? entry.spec : this.brushSpec);
7156
7283
  for (const k of unset) delete base[k];
7157
7284
  const spec = this.readBrushPatch(base, patch, problems);
7285
+ if (entry || patch.colour?.default !== void 0) this.takeBrushColour(spec);
7158
7286
  this.useBrush(unchanged && entry ? entry.brush : resolveBrush(spec), spec, unchanged && entry?.builtin === true);
7159
7287
  return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
7160
7288
  }
@@ -7204,7 +7332,8 @@ var PaintEditorController = class _PaintEditorController {
7204
7332
  this.brushSpec = { ...copyJSON(spec), id: next.id, name: next.name };
7205
7333
  this.pendingBrushId = null;
7206
7334
  this.emit([]);
7207
- if (next.engine === "sticker" && next.sticker.stamps.some((r) => !editorStampOf(r))) void this.prepareStickers(next.sticker.stamps).catch(() => this.emit([]));
7335
+ const refs = brushStickerRefs(next);
7336
+ if (refs.length) void this.prepareStickers(refs).catch(() => this.emit([]));
7208
7337
  }
7209
7338
  /**
7210
7339
  * Saves the active brush (or `brush`, engine settings on top of it) into the brush library under
@@ -7249,9 +7378,54 @@ var PaintEditorController = class _PaintEditorController {
7249
7378
  return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
7250
7379
  }
7251
7380
  // --- Colour --------------------------------------------------------------------
7381
+ colorScheme;
7382
+ colorLocked = false;
7383
+ /** The brush default colour the paint colour came from (resolved again when the scheme changes), or `null`. */
7384
+ brushColour = null;
7385
+ /**
7386
+ * The host's colour scheme: a paint colour that came from a brush's light / dark default
7387
+ * resolves to the new side. Editor state, not an undo step.
7388
+ */
7389
+ setColorScheme(scheme) {
7390
+ const next = scheme === "dark" ? "dark" : "light";
7391
+ if (next === this.colorScheme) return;
7392
+ this.colorScheme = next;
7393
+ const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
7394
+ if (hex2) this.color = hex2;
7395
+ this.emit([]);
7396
+ }
7397
+ /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
7398
+ setColorLocked(locked) {
7399
+ if (this.colorLocked === locked) return;
7400
+ this.colorLocked = locked;
7401
+ this.emit([]);
7402
+ }
7403
+ /**
7404
+ * After the user chooses a brush: its solid or light / dark default colour becomes the paint
7405
+ * colour (resolved for the colour scheme), unless the colour is locked. Gradient defaults are the
7406
+ * brush's own paint already; effect defaults stay a suggestion.
7407
+ */
7408
+ takeBrushColour(spec) {
7409
+ if (this.colorLocked) return;
7410
+ const d = spec.colour?.default;
7411
+ const hex2 = d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
7412
+ if (!hex2) return;
7413
+ this.color = hex2;
7414
+ this.gradient = null;
7415
+ this.effectPaint = this.effectPaintPending = null;
7416
+ this.paintEpoch++;
7417
+ this.brushColour = d;
7418
+ }
7419
+ /** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
7420
+ ownColour(brushId) {
7421
+ const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
7422
+ const d = spec?.colour?.own ? spec.colour.default : void 0;
7423
+ return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
7424
+ }
7252
7425
  /** Sets the paint colour (and goes back to one colour from gradient paint). */
7253
7426
  setColor(color) {
7254
7427
  this.color = parseColor(color);
7428
+ this.brushColour = null;
7255
7429
  this.gradient = null;
7256
7430
  this.effectPaint = this.effectPaintPending = null;
7257
7431
  this.paintEpoch++;
@@ -7435,7 +7609,7 @@ var PaintEditorController = class _PaintEditorController {
7435
7609
  this.beginWetOp();
7436
7610
  const world = this.wet.ensureLane(layer.id, brush.wet.medium);
7437
7611
  const id = layer.id;
7438
- const wetColor = init.color === void 0 ? this.color : parseColor(init.color);
7612
+ const wetColor = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
7439
7613
  const [r, g, b] = hexToRgb8(wetColor);
7440
7614
  const wetGradient = this.strokeGradient(init, brush, wetColor);
7441
7615
  const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);
@@ -7463,7 +7637,7 @@ var PaintEditorController = class _PaintEditorController {
7463
7637
  const dried = findLayer(this.doc.layers, layer.id).layer;
7464
7638
  layer = dried;
7465
7639
  }
7466
- let color = init.color === void 0 ? this.color : parseColor(init.color);
7640
+ let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
7467
7641
  const tracing = this.guides.tracing;
7468
7642
  if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
7469
7643
  const perspective = this.guides.perspective;
@@ -7502,6 +7676,7 @@ var PaintEditorController = class _PaintEditorController {
7502
7676
  // Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
7503
7677
  light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
7504
7678
  sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
7679
+ stampArt: this.stickerArt.get,
7505
7680
  paint: effect ? null : this.strokeGradient(init, brush, color),
7506
7681
  effect
7507
7682
  });
@@ -7548,6 +7723,7 @@ var PaintEditorController = class _PaintEditorController {
7548
7723
  this.liveThick = ch ? { layerId, channels: ch } : null;
7549
7724
  this.emit(rects);
7550
7725
  },
7726
+ sampled: (s, points) => this.notifySample("points", s, points),
7551
7727
  commit: (s, surface, cancelled) => {
7552
7728
  this.session = null;
7553
7729
  this.live = null;
@@ -7567,6 +7743,7 @@ var PaintEditorController = class _PaintEditorController {
7567
7743
  this.wet.syncDisplays();
7568
7744
  }
7569
7745
  this.emit();
7746
+ this.notifySample(cancelled ? "cancel" : "end", s, [], cancelled ? void 0 : null);
7570
7747
  return;
7571
7748
  }
7572
7749
  const stroke = this.record(s);
@@ -7586,6 +7763,7 @@ var PaintEditorController = class _PaintEditorController {
7586
7763
  const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
7587
7764
  const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
7588
7765
  this.commit(`${brush.name} stroke`, this.withWetEvents(withLiveStroke({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {}, ...light ? { light } : {} }, stroke)), wetStroke ? before : this.doc);
7766
+ this.notifySample("end", s, [], stroke.id);
7589
7767
  }
7590
7768
  };
7591
7769
  let session;
@@ -7597,6 +7775,7 @@ var PaintEditorController = class _PaintEditorController {
7597
7775
  }
7598
7776
  this.session = session;
7599
7777
  this.emit([]);
7778
+ this.notifySample("begin", session);
7600
7779
  return session;
7601
7780
  }
7602
7781
  /** The current live stroke, if any. */
@@ -11276,18 +11455,24 @@ function createPaintEditorToolProvider(controller, options = {}) {
11276
11455
  add({
11277
11456
  name: "set_color",
11278
11457
  title: "Set the paint colour",
11279
- description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours.",
11458
+ description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default, and whether it is locked. Choosing a brush with a default colour (set_brush id, e.g. the highlighter's yellow) makes that the paint colour unless the colour is locked; a brush with colour.own always paints its own. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours and locked.",
11280
11459
  inputSchema: {
11281
11460
  type: "object",
11282
- properties: { color: { ...COLOR3, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." } },
11461
+ properties: {
11462
+ color: { ...COLOR3, description: "Paint colour #rrggbb." },
11463
+ secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
11464
+ locked: { type: "boolean", description: "Lock the paint colour: brushes chosen later keep it instead of taking their default colour (default: unchanged; the editor starts unlocked)." }
11465
+ },
11283
11466
  additionalProperties: false
11284
11467
  },
11285
11468
  annotations: { title: "Set the paint colour", ...SET4 },
11286
- handler: ({ color, secondaryColor }) => {
11469
+ handler: ({ color, secondaryColor, locked }) => {
11470
+ if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
11287
11471
  if (color !== void 0) c.setColor(color);
11288
11472
  if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
11473
+ if (locked !== void 0) c.setColorLocked(locked);
11289
11474
  const s = c.getState();
11290
- return { color: s.color, secondaryColor: s.secondaryColor };
11475
+ return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
11291
11476
  }
11292
11477
  });
11293
11478
  add({
@@ -11369,12 +11554,32 @@ function createPaintEditorToolProvider(controller, options = {}) {
11369
11554
  await c.wetIdle();
11370
11555
  const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
11371
11556
  const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
11372
- if ((brush?.engine ?? base?.engine) === "sticker") await c.prepareStickers(brush?.sticker?.stamps ?? base?.sticker.stamps ?? []);
11557
+ if (base) {
11558
+ const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
11559
+ if (refs.length) await c.prepareStickers(refs);
11560
+ }
11373
11561
  const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
11374
11562
  const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
11375
11563
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
11376
11564
  }
11377
11565
  });
11566
+ add({
11567
+ name: "get_live_stroke",
11568
+ title: "Get the stroke being painted",
11569
+ description: "Reads the stroke the user is painting right now, if any: its layer, brush id and name, colour, and the pen samples so far (document pixels; at most the last `limit`). Returns live: false when no stroke is in progress. For agents that paint along with the user: hosts stream the same samples to them live (onStrokeSample); this is a snapshot.",
11570
+ inputSchema: {
11571
+ type: "object",
11572
+ properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
11573
+ additionalProperties: false
11574
+ },
11575
+ annotations: { title: "Get the stroke being painted", ...READ6 },
11576
+ handler: (args) => {
11577
+ const live = c.getLiveStroke();
11578
+ if (!live) return { live: false };
11579
+ const limit = args.limit ?? 500;
11580
+ return { live: true, layerId: live.layerId, brushId: live.brushId, brushName: live.brushName, color: live.color, total: live.points.length, points: live.points.slice(-limit) };
11581
+ }
11582
+ });
11378
11583
  add({
11379
11584
  name: "list_strokes",
11380
11585
  title: "List recorded strokes",
@@ -11746,6 +11951,7 @@ function strokeSummary(s, withPoints, live) {
11746
11951
  function createPaintBrushLibraryToolProvider(controller, options = {}) {
11747
11952
  return createBrushLibraryToolProvider(controller.brushLibrary, {
11748
11953
  profile: PAINT_PROFILE,
11954
+ stickers: () => controller.stickers.provider,
11749
11955
  ...options,
11750
11956
  renderPreview: (brush, { width, height, color }) => {
11751
11957
  const t = renderBrushThumbnail(resolveBrush(brush), { paper: controller.getState().document.paper, width, height, color: color.toLowerCase() });
@@ -12805,6 +13011,37 @@ function paintProposalGhost(proposal) {
12805
13011
  if (!b || !b.width || !b.height || proposal.ghost.length !== b.width * b.height * 4) return null;
12806
13012
  return { x: b.x, y: b.y, width: b.width, height: b.height, data: proposal.ghost };
12807
13013
  }
13014
+ function createPaintAgentStrokes() {
13015
+ let state = { strokes: [] };
13016
+ const listeners = /* @__PURE__ */ new Set();
13017
+ const set = (strokes) => {
13018
+ state = { strokes };
13019
+ listeners.forEach((l) => l(state));
13020
+ };
13021
+ return {
13022
+ getState: () => state,
13023
+ subscribe(listener) {
13024
+ listeners.add(listener);
13025
+ return () => {
13026
+ listeners.delete(listener);
13027
+ };
13028
+ },
13029
+ set(stroke) {
13030
+ const i = state.strokes.findIndex((s) => s.id === stroke.id);
13031
+ set(i < 0 ? [...state.strokes, stroke] : state.strokes.map((s, j) => j === i ? stroke : s));
13032
+ },
13033
+ remove(id) {
13034
+ if (state.strokes.some((s) => s.id === id)) set(state.strokes.filter((s) => s.id !== id));
13035
+ },
13036
+ clear(origin) {
13037
+ const keep = origin ? state.strokes.filter((s) => s.origin !== origin) : [];
13038
+ if (keep.length !== state.strokes.length) set(keep);
13039
+ }
13040
+ };
13041
+ }
13042
+ function paintAgentStrokePoints(stroke) {
13043
+ return stroke.points.map((p) => `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}`).join(" ");
13044
+ }
12808
13045
 
12809
13046
  // src/paint-editor/collaboration-tools.ts
12810
13047
  var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
@@ -12958,6 +13195,7 @@ export {
12958
13195
  DEFAULT_PAINT_GRADIENT_MAPPING,
12959
13196
  DEFAULT_PAPER,
12960
13197
  DEFAULT_PARTICLE,
13198
+ DEFAULT_SPRAY,
12961
13199
  DEFAULT_STICKER,
12962
13200
  DEFAULT_SWATCHES,
12963
13201
  DEFAULT_VIEW,
@@ -13093,6 +13331,7 @@ export {
13093
13331
  colorHarmony2 as colorHarmony,
13094
13332
  composeAffine,
13095
13333
  contactSheet,
13334
+ createPaintAgentStrokes,
13096
13335
  createPaintBrushLibraryToolProvider,
13097
13336
  createPaintChangeFeed,
13098
13337
  createPaintCompositor,
@@ -13185,6 +13424,7 @@ export {
13185
13424
  normalizeWetParams,
13186
13425
  overPixels,
13187
13426
  padSampleRadius,
13427
+ paintAgentStrokePoints,
13188
13428
  paintGradientMappingFor,
13189
13429
  paintPlaybackPanel,
13190
13430
  paintProposalGhost,