@pixerate/editor 0.256.0 → 0.257.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 (122) 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/controller.d.cts +29 -0
  94. package/dist/paint-editor/controller.d.ts +29 -0
  95. package/dist/paint-editor/engine.d.cts +7 -0
  96. package/dist/paint-editor/engine.d.ts +7 -0
  97. package/dist/paint-editor/index.cjs +1443 -1280
  98. package/dist/paint-editor/index.js +1117 -954
  99. package/dist/paint-editor/mark-settings.d.cts +1 -1
  100. package/dist/paint-editor/mark-settings.d.ts +1 -1
  101. package/dist/paint-editor/procedural.d.cts +4 -2
  102. package/dist/paint-editor/procedural.d.ts +4 -2
  103. package/dist/paint-editor/stamp-tip.d.cts +45 -0
  104. package/dist/paint-editor/stamp-tip.d.ts +45 -0
  105. package/dist/paint-editor/stroke.d.cts +2 -0
  106. package/dist/paint-editor/stroke.d.ts +2 -0
  107. package/dist/stickers/index.cjs +4 -2
  108. package/dist/stickers/index.js +6 -4
  109. package/dist/typography/index.cjs +3 -2
  110. package/dist/typography/index.js +2 -1
  111. package/dist/vector/index.cjs +971 -969
  112. package/dist/vector/index.js +8 -6
  113. package/dist/video-editor/index.cjs +4 -2
  114. package/dist/video-editor/index.js +4 -2
  115. package/package.json +1 -1
  116. package/skills/pixerate-brushes/SKILL.md +21 -11
  117. package/skills/pixerate-brushes-design/SKILL.md +6 -2
  118. package/skills/pixerate-brushes-design/evals/canvas-chalk.json +8 -8
  119. package/skills/pixerate-canvas/SKILL.md +3 -2
  120. package/skills/pixerate-image-editor/SKILL.md +1 -1
  121. package/skills/pixerate-paint-editor/SKILL.md +4 -1
  122. 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;
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);
2086
2216
  }
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;
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
+ }
2099
2239
  }
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;
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
+ }
2106
2339
  }
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 };
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;
2131
2344
  }
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
2345
  }
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();
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);
2172
2355
  }
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;
2356
+ /**
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.
2361
+ */
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
+ }
2176
2393
  }
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;
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;
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;
2406
+ for (let xx = 0; xx < rect.width; xx++, k++) {
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);
2418
+ }
2419
+ }
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;
2426
+ }
2194
2427
  }
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);
2428
+ return any;
2201
2429
  }
2202
- for (const [k, d] of live) {
2203
- const t = finishTile(d, 4, 0);
2204
- if (t) tiles.set(k, t);
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
+ );
2205
2454
  }
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;
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;
2223
2489
  }
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
2490
  }
2229
- return null;
2230
- }
2491
+ };
2231
2492
 
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
- });
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) };
2276
2501
  }
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
2502
  }
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;
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).`);
2304
2506
  }
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);
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 };
2316
2531
  }
2317
2532
  /**
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.
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.
2320
2536
  */
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);
2334
- for (let yy = 0, k = 0; yy < rect.height; yy++) {
2335
- 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);
2342
- }
2343
- }
2344
- vals[k] = sum / (ss * ss) * a;
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;
2345
2562
  }
2346
- }
2347
- this.lay(rect, vals, Math.min(1, opacity), pressure, x, y, rects);
2563
+ );
2348
2564
  }
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;
2565
+ /** Forgets every load (a new provider). */
2566
+ clear() {
2567
+ this.entries.clear();
2355
2568
  }
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);
2367
- }
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);
2377
- }
2378
- rects.push(rect);
2379
- this.dabCount++;
2569
+ };
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;
2380
2593
  }
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);
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;
2618
+ }
2387
2619
  }
2388
- };
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);
2620
+ return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
2394
2621
  }
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];
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);
2403
2627
  }
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
- }
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));
2429
2653
  }
2430
2654
  }
2655
+ medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
2656
+ return rect;
2431
2657
  }
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)];
2449
- }
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 };
2654
2807
  }
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);
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;
2664
2820
  }
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
- }
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;
2702
2826
  }
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
- }
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];
2830
+ }
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;
2834
+ }
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
  }
@@ -5831,6 +5931,7 @@ var PaintEditorController = class _PaintEditorController {
5831
5931
  void this.refreshBrushes().catch(() => void 0);
5832
5932
  this.pendingBrushId = start.brush.id === wanted ? null : wanted;
5833
5933
  this.color = options.color ? parseColor(options.color) : "#1f2937";
5934
+ this.colorScheme = options.colorScheme === "dark" ? "dark" : "light";
5834
5935
  this.hand = options.hand === "left" ? "left" : "right";
5835
5936
  this.encodeImage = options.encodeImage;
5836
5937
  this.guides = defaultGuides(this.doc.canvas);
@@ -6002,6 +6103,8 @@ var PaintEditorController = class _PaintEditorController {
6002
6103
  brush: this.brush,
6003
6104
  brushes: this.library ??= this.brushEntries.map((e) => e.brush),
6004
6105
  color: this.color,
6106
+ colorScheme: this.colorScheme,
6107
+ colorLocked: this.colorLocked,
6005
6108
  secondaryColor: this.secondaryColor,
6006
6109
  paint: this.gradient ?? { type: "color", color: this.color },
6007
6110
  effectPaint: this.effectPaintPending ?? this.effectPaint,
@@ -7139,6 +7242,7 @@ var PaintEditorController = class _PaintEditorController {
7139
7242
  if (changes.paint !== void 0 && changes.paint !== null) this.readStrokePaint(changes.paint);
7140
7243
  const unchanged = !Object.keys(changes).length;
7141
7244
  const next = normalizePaintBrush({ ...changes, id: base.id }, base);
7245
+ if (entry) this.takeBrushColour(entry.spec);
7142
7246
  this.useBrush(next, unchanged ? baseSpec : brushSpecOf(next, baseSpec), unchanged && base.builtin === true);
7143
7247
  return this.brush;
7144
7248
  }
@@ -7155,6 +7259,7 @@ var PaintEditorController = class _PaintEditorController {
7155
7259
  const base = copyJSON(entry ? entry.spec : this.brushSpec);
7156
7260
  for (const k of unset) delete base[k];
7157
7261
  const spec = this.readBrushPatch(base, patch, problems);
7262
+ if (entry || patch.colour?.default !== void 0) this.takeBrushColour(spec);
7158
7263
  this.useBrush(unchanged && entry ? entry.brush : resolveBrush(spec), spec, unchanged && entry?.builtin === true);
7159
7264
  return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
7160
7265
  }
@@ -7204,7 +7309,8 @@ var PaintEditorController = class _PaintEditorController {
7204
7309
  this.brushSpec = { ...copyJSON(spec), id: next.id, name: next.name };
7205
7310
  this.pendingBrushId = null;
7206
7311
  this.emit([]);
7207
- if (next.engine === "sticker" && next.sticker.stamps.some((r) => !editorStampOf(r))) void this.prepareStickers(next.sticker.stamps).catch(() => this.emit([]));
7312
+ const refs = brushStickerRefs(next);
7313
+ if (refs.length) void this.prepareStickers(refs).catch(() => this.emit([]));
7208
7314
  }
7209
7315
  /**
7210
7316
  * Saves the active brush (or `brush`, engine settings on top of it) into the brush library under
@@ -7249,9 +7355,54 @@ var PaintEditorController = class _PaintEditorController {
7249
7355
  return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
7250
7356
  }
7251
7357
  // --- Colour --------------------------------------------------------------------
7358
+ colorScheme;
7359
+ colorLocked = false;
7360
+ /** The brush default colour the paint colour came from (resolved again when the scheme changes), or `null`. */
7361
+ brushColour = null;
7362
+ /**
7363
+ * The host's colour scheme: a paint colour that came from a brush's light / dark default
7364
+ * resolves to the new side. Editor state, not an undo step.
7365
+ */
7366
+ setColorScheme(scheme) {
7367
+ const next = scheme === "dark" ? "dark" : "light";
7368
+ if (next === this.colorScheme) return;
7369
+ this.colorScheme = next;
7370
+ const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
7371
+ if (hex2) this.color = hex2;
7372
+ this.emit([]);
7373
+ }
7374
+ /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
7375
+ setColorLocked(locked) {
7376
+ if (this.colorLocked === locked) return;
7377
+ this.colorLocked = locked;
7378
+ this.emit([]);
7379
+ }
7380
+ /**
7381
+ * After the user chooses a brush: its solid or light / dark default colour becomes the paint
7382
+ * colour (resolved for the colour scheme), unless the colour is locked. Gradient defaults are the
7383
+ * brush's own paint already; effect defaults stay a suggestion.
7384
+ */
7385
+ takeBrushColour(spec) {
7386
+ if (this.colorLocked) return;
7387
+ const d = spec.colour?.default;
7388
+ const hex2 = d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
7389
+ if (!hex2) return;
7390
+ this.color = hex2;
7391
+ this.gradient = null;
7392
+ this.effectPaint = this.effectPaintPending = null;
7393
+ this.paintEpoch++;
7394
+ this.brushColour = d;
7395
+ }
7396
+ /** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
7397
+ ownColour(brushId) {
7398
+ const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
7399
+ const d = spec?.colour?.own ? spec.colour.default : void 0;
7400
+ return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
7401
+ }
7252
7402
  /** Sets the paint colour (and goes back to one colour from gradient paint). */
7253
7403
  setColor(color) {
7254
7404
  this.color = parseColor(color);
7405
+ this.brushColour = null;
7255
7406
  this.gradient = null;
7256
7407
  this.effectPaint = this.effectPaintPending = null;
7257
7408
  this.paintEpoch++;
@@ -7435,7 +7586,7 @@ var PaintEditorController = class _PaintEditorController {
7435
7586
  this.beginWetOp();
7436
7587
  const world = this.wet.ensureLane(layer.id, brush.wet.medium);
7437
7588
  const id = layer.id;
7438
- const wetColor = init.color === void 0 ? this.color : parseColor(init.color);
7589
+ const wetColor = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
7439
7590
  const [r, g, b] = hexToRgb8(wetColor);
7440
7591
  const wetGradient = this.strokeGradient(init, brush, wetColor);
7441
7592
  const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);
@@ -7463,7 +7614,7 @@ var PaintEditorController = class _PaintEditorController {
7463
7614
  const dried = findLayer(this.doc.layers, layer.id).layer;
7464
7615
  layer = dried;
7465
7616
  }
7466
- let color = init.color === void 0 ? this.color : parseColor(init.color);
7617
+ let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
7467
7618
  const tracing = this.guides.tracing;
7468
7619
  if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
7469
7620
  const perspective = this.guides.perspective;
@@ -7502,6 +7653,7 @@ var PaintEditorController = class _PaintEditorController {
7502
7653
  // Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
7503
7654
  light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
7504
7655
  sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
7656
+ stampArt: this.stickerArt.get,
7505
7657
  paint: effect ? null : this.strokeGradient(init, brush, color),
7506
7658
  effect
7507
7659
  });
@@ -11276,18 +11428,24 @@ function createPaintEditorToolProvider(controller, options = {}) {
11276
11428
  add({
11277
11429
  name: "set_color",
11278
11430
  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.",
11431
+ 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
11432
  inputSchema: {
11281
11433
  type: "object",
11282
- properties: { color: { ...COLOR3, description: "Paint colour #rrggbb." }, secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." } },
11434
+ properties: {
11435
+ color: { ...COLOR3, description: "Paint colour #rrggbb." },
11436
+ secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
11437
+ 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)." }
11438
+ },
11283
11439
  additionalProperties: false
11284
11440
  },
11285
11441
  annotations: { title: "Set the paint colour", ...SET4 },
11286
- handler: ({ color, secondaryColor }) => {
11442
+ handler: ({ color, secondaryColor, locked }) => {
11443
+ if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
11287
11444
  if (color !== void 0) c.setColor(color);
11288
11445
  if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
11446
+ if (locked !== void 0) c.setColorLocked(locked);
11289
11447
  const s = c.getState();
11290
- return { color: s.color, secondaryColor: s.secondaryColor };
11448
+ return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
11291
11449
  }
11292
11450
  });
11293
11451
  add({
@@ -11369,7 +11527,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
11369
11527
  await c.wetIdle();
11370
11528
  const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
11371
11529
  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 ?? []);
11530
+ if (base) {
11531
+ const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
11532
+ if (refs.length) await c.prepareStickers(refs);
11533
+ }
11373
11534
  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
11535
  const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
11375
11536
  return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
@@ -11746,6 +11907,7 @@ function strokeSummary(s, withPoints, live) {
11746
11907
  function createPaintBrushLibraryToolProvider(controller, options = {}) {
11747
11908
  return createBrushLibraryToolProvider(controller.brushLibrary, {
11748
11909
  profile: PAINT_PROFILE,
11910
+ stickers: () => controller.stickers.provider,
11749
11911
  ...options,
11750
11912
  renderPreview: (brush, { width, height, color }) => {
11751
11913
  const t = renderBrushThumbnail(resolveBrush(brush), { paper: controller.getState().document.paper, width, height, color: color.toLowerCase() });
@@ -12958,6 +13120,7 @@ export {
12958
13120
  DEFAULT_PAINT_GRADIENT_MAPPING,
12959
13121
  DEFAULT_PAPER,
12960
13122
  DEFAULT_PARTICLE,
13123
+ DEFAULT_SPRAY,
12961
13124
  DEFAULT_STICKER,
12962
13125
  DEFAULT_SWATCHES,
12963
13126
  DEFAULT_VIEW,