@pixerate/editor 0.250.0 → 0.251.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 (123) hide show
  1. package/dist/agent/index.cjs +40 -41
  2. package/dist/agent/index.js +9 -10
  3. package/dist/animation/index.cjs +6 -7
  4. package/dist/animation/index.js +5 -6
  5. package/dist/animation-editor/index.cjs +70 -70
  6. package/dist/animation-editor/index.js +12 -12
  7. package/dist/brushes/index.cjs +1406 -0
  8. package/dist/brushes/index.d.cts +12 -0
  9. package/dist/brushes/index.d.ts +12 -0
  10. package/dist/brushes/index.js +1406 -0
  11. package/dist/brushes/migrate.d.cts +17 -0
  12. package/dist/brushes/migrate.d.ts +17 -0
  13. package/dist/brushes/normalize.d.cts +15 -0
  14. package/dist/brushes/normalize.d.ts +15 -0
  15. package/dist/brushes/presets.d.cts +12 -0
  16. package/dist/brushes/presets.d.ts +12 -0
  17. package/dist/brushes/resolve.d.cts +56 -0
  18. package/dist/brushes/resolve.d.ts +56 -0
  19. package/dist/brushes/schema.d.cts +29 -0
  20. package/dist/brushes/schema.d.ts +29 -0
  21. package/dist/brushes/types.d.cts +323 -0
  22. package/dist/brushes/types.d.ts +323 -0
  23. package/dist/{brushstrokeRender-2FHFLSEG.js → brushstrokeRender-5PYSKZIJ.js} +8 -8
  24. package/dist/{brushstrokeRender-QGRXOKDA.cjs → brushstrokeRender-7FLLSQEC.cjs} +14 -14
  25. package/dist/canvas/index.cjs +57 -58
  26. package/dist/canvas/index.js +31 -32
  27. package/dist/chart/index.cjs +159 -160
  28. package/dist/chart/index.js +9 -10
  29. package/dist/{chunk-NOHJP5TK.js → chunk-2ZJ4DT3M.js} +1 -1
  30. package/dist/{chunk-XZNU4DUX.cjs → chunk-3NS5C7FB.cjs} +76 -76
  31. package/dist/{chunk-UTI5IITZ.cjs → chunk-4ZC5QJE2.cjs} +3 -3
  32. package/dist/{chunk-SMSGP2HQ.js → chunk-5JNA6LLY.js} +1 -1
  33. package/dist/{chunk-RD7RX52G.cjs → chunk-5UEFBM2M.cjs} +207 -18
  34. package/dist/{chunk-WA44NKPF.js → chunk-62Z5WLRA.js} +1 -1
  35. package/dist/{chunk-BVRRIW7F.js → chunk-6GRUVGKU.js} +3 -3
  36. package/dist/{chunk-QARCM66V.js → chunk-6ZZKRH7R.js} +3 -3
  37. package/dist/{chunk-BP6KSGVD.js → chunk-7272D3K2.js} +4 -6
  38. package/dist/{chunk-NTRAVM2I.js → chunk-7P6VF3FG.js} +2 -2
  39. package/dist/chunk-APX63ILU.js +173 -0
  40. package/dist/{chunk-W7L4HUT2.cjs → chunk-CJ6TBL2N.cjs} +16 -16
  41. package/dist/{chunk-7PT4RJL5.cjs → chunk-EUIDTZNQ.cjs} +111 -113
  42. package/dist/{chunk-CXXXAZ7H.js → chunk-F6R67I3A.js} +1 -1
  43. package/dist/{chunk-I7NJPHWV.cjs → chunk-FQPUF2W2.cjs} +35 -606
  44. package/dist/{chunk-D5KGAO4Z.js → chunk-GCVFEPJG.js} +2 -573
  45. package/dist/{chunk-LEQKZGQI.js → chunk-GOBOCZZI.js} +1 -1
  46. package/dist/{chunk-KDLBJ6QR.js → chunk-GYMMNCFT.js} +3 -3
  47. package/dist/{chunk-NUTMINJZ.cjs → chunk-H7ET2ISK.cjs} +52 -52
  48. package/dist/chunk-HS6QDWDF.cjs +1029 -0
  49. package/dist/{chunk-T7KQEMAE.cjs → chunk-HUAISCUX.cjs} +18 -18
  50. package/dist/{chunk-YGQVCQTG.js → chunk-ILP77UD6.js} +1 -1
  51. package/dist/{chunk-S6OT6CTS.cjs → chunk-IWEVV63O.cjs} +31 -33
  52. package/dist/{chunk-M2VKFE5L.cjs → chunk-JFHBXNR2.cjs} +88 -88
  53. package/dist/{chunk-WPUXGDH4.cjs → chunk-JPEHXIB5.cjs} +10 -10
  54. package/dist/chunk-JSDND7DF.cjs +173 -0
  55. package/dist/{chunk-36ZTV7WO.js → chunk-L6Y7TSQ4.js} +1 -1
  56. package/dist/{chunk-YW43M56C.cjs → chunk-LFMKWGFL.cjs} +1 -1
  57. package/dist/{chunk-V6DT2CDH.js → chunk-M2TPMYJE.js} +1 -1
  58. package/dist/{chunk-LJZRHVZV.cjs → chunk-M5GLQQWG.cjs} +2 -2
  59. package/dist/{chunk-JEGJP4OW.js → chunk-MHLU6GRJ.js} +196 -7
  60. package/dist/{chunk-KMPM5JM5.js → chunk-MONXNVZK.js} +4 -6
  61. package/dist/chunk-MTKY6K3Q.js +1029 -0
  62. package/dist/{chunk-MOW3JMNG.cjs → chunk-N76AMOKE.cjs} +42 -42
  63. package/dist/{chunk-75K3W4D6.cjs → chunk-NKGB64CW.cjs} +16 -16
  64. package/dist/{chunk-SGPIVHJB.js → chunk-P3K2JCJA.js} +3 -3
  65. package/dist/chunk-PXDIAYB6.cjs +594 -0
  66. package/dist/{chunk-3TWOCSCY.js → chunk-QC3OXRE4.js} +9 -9
  67. package/dist/{chunk-YM6ZBTXG.cjs → chunk-QLSGTCYT.cjs} +23 -23
  68. package/dist/{chunk-7YW7T5TJ.js → chunk-RHPSE2YR.js} +2 -2
  69. package/dist/{chunk-44BMJJVA.cjs → chunk-RUONPPMC.cjs} +8 -8
  70. package/dist/{chunk-ATK6ABN3.cjs → chunk-SABJMH6M.cjs} +145 -12
  71. package/dist/{chunk-XWOLPNBW.cjs → chunk-SOSBOS5T.cjs} +22 -22
  72. package/dist/{chunk-PPFJHTW5.cjs → chunk-TFF2GTGN.cjs} +22 -24
  73. package/dist/{chunk-X7WRL5ZZ.cjs → chunk-TLOGFIIV.cjs} +37 -37
  74. package/dist/chunk-TXFWZ5ZJ.js +594 -0
  75. package/dist/{chunk-GXAK6Q5J.js → chunk-UFEDHACQ.js} +2 -2
  76. package/dist/{chunk-GA4EU4P3.cjs → chunk-UXETV2NC.cjs} +4 -4
  77. package/dist/{chunk-N4DFDI73.js → chunk-WKMUBATU.js} +1 -1
  78. package/dist/{chunk-3R5P3DVY.cjs → chunk-WRZ6C4TU.cjs} +6 -6
  79. package/dist/{chunk-U4NUOKBT.js → chunk-WZOGDKCQ.js} +134 -1
  80. package/dist/{chunk-6ADHGQ73.js → chunk-YBAPT7NL.js} +2 -4
  81. package/dist/color/index.cjs +4 -6
  82. package/dist/color/index.js +7 -9
  83. package/dist/export/index.cjs +7 -8
  84. package/dist/export/index.js +6 -7
  85. package/dist/extensions/index.cjs +10 -11
  86. package/dist/extensions/index.js +9 -10
  87. package/dist/gradient/index.cjs +33 -34
  88. package/dist/gradient/index.js +4 -5
  89. package/dist/image-editor/index.cjs +845 -844
  90. package/dist/image-editor/index.js +39 -38
  91. package/dist/index.cjs +16 -17
  92. package/dist/index.js +10 -11
  93. package/dist/paint/index.cjs +6 -4
  94. package/dist/paint/index.js +21 -19
  95. package/dist/paint-editor/brushes.d.cts +1 -2
  96. package/dist/paint-editor/brushes.d.ts +1 -2
  97. package/dist/paint-editor/index.cjs +2193 -694
  98. package/dist/paint-editor/index.js +1575 -76
  99. package/dist/paint-editor/mark-settings.d.cts +104 -0
  100. package/dist/paint-editor/mark-settings.d.ts +104 -0
  101. package/dist/paint-editor/procedural.d.cts +2 -75
  102. package/dist/paint-editor/procedural.d.ts +2 -75
  103. package/dist/paint-editor/stickers.d.cts +1 -22
  104. package/dist/paint-editor/stickers.d.ts +1 -22
  105. package/dist/raster/index.cjs +2 -2
  106. package/dist/raster/index.js +9 -9
  107. package/dist/stickers/index.cjs +10 -10
  108. package/dist/stickers/index.js +9 -9
  109. package/dist/tokens/index.cjs +3 -4
  110. package/dist/tokens/index.js +2 -3
  111. package/dist/typography/index.cjs +7 -8
  112. package/dist/typography/index.js +2 -3
  113. package/dist/vector/index.cjs +974 -974
  114. package/dist/vector/index.js +12 -12
  115. package/dist/video-editor/index.cjs +6 -7
  116. package/dist/video-editor/index.js +5 -6
  117. package/package.json +11 -1
  118. package/dist/chunk-4JNWA3VT.cjs +0 -302
  119. package/dist/chunk-7AHBW2CQ.cjs +0 -200
  120. package/dist/chunk-F67XMVDI.js +0 -200
  121. package/dist/chunk-GT77U7QG.cjs +0 -2637
  122. package/dist/chunk-OTEPAQJX.js +0 -2637
  123. package/dist/chunk-WKH5ZAUA.js +0 -302
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  StickerBrowser,
3
+ createStickerTools,
4
+ describeStickerRef,
3
5
  editorStickers
4
- } from "../chunk-U4NUOKBT.js";
6
+ } from "../chunk-WZOGDKCQ.js";
5
7
  import {
6
8
  toVideoFfmpeg
7
9
  } from "../chunk-5E2Q7ZHI.js";
@@ -12,7 +14,7 @@ import {
12
14
  resolveExternalAssets,
13
15
  serializeLayers,
14
16
  serializeLayersExternal
15
- } from "../chunk-3TWOCSCY.js";
17
+ } from "../chunk-QC3OXRE4.js";
16
18
  import "../chunk-3CI7FRMK.js";
17
19
  import "../chunk-ZNA3IQZF.js";
18
20
  import {
@@ -68,12 +70,11 @@ import {
68
70
  realPresenceClock,
69
71
  tweenPresenceCursor
70
72
  } from "../chunk-IVSWRVB4.js";
71
- import "../chunk-KDLBJ6QR.js";
73
+ import "../chunk-GYMMNCFT.js";
72
74
  import {
73
75
  BRUSH_ID,
74
76
  BRUSH_ROTATIONS,
75
77
  BUILTIN_BRUSHES,
76
- BristleStroke,
77
78
  DEFAULT_BEHAVIOR,
78
79
  DEFAULT_BRISTLE,
79
80
  DEFAULT_BRUSH_ID,
@@ -81,49 +82,26 @@ import {
81
82
  DEFAULT_IMPASTO,
82
83
  DEFAULT_LIGHT,
83
84
  DEFAULT_MULTI_COLOR,
84
- DEFAULT_PAPER,
85
85
  DEFAULT_PARTICLE,
86
86
  DEFAULT_STICKER,
87
87
  DEFAULT_WET_BRUSH,
88
88
  DOT_ROTATIONS,
89
89
  DOT_TEXTURE_ORDERS,
90
- DryStroke,
91
- EFFECT_MAP_REPEATS,
92
- EFFECT_STRIP_HEIGHT,
93
- IDENTITY_COPY,
94
90
  IMPASTO_TOOLS,
95
91
  MAX_DOT_TEXTURES,
96
92
  MAX_STICKER_STAMPS,
97
93
  MULTI_COLOR_MODES,
98
94
  PAINT_BRUSH_CATEGORIES,
99
95
  PAINT_BRUSH_ENGINES,
100
- PAPER_FAMILIES,
101
- PAPER_LIBRARY,
102
96
  PARTICLE_KINDS,
103
97
  PARTICLE_RENDERS,
104
- ParticleStroke,
105
- PenDabWalker,
106
- PenSmoother,
107
98
  RIBBON_PAIRS,
108
- STICKER_ART_SIZE,
109
- StickerArtCache,
110
- StickerSpray,
111
99
  WET_MEDIA,
112
100
  WET_TOOLS,
113
- addPolygonArea,
114
101
  brushTakesPaint,
115
102
  brushToJSON,
116
- clonePaper,
117
- defaultStickerRasterizer,
118
- drawSticker,
119
- effectDabField,
120
- effectPaintKey,
121
- effectPaintSize,
122
- hexToRgb8,
123
- isPaintEffectPaint,
124
103
  isWetBrush,
125
104
  isWetCapable,
126
- mixerTip,
127
105
  multiColorToPaint,
128
106
  normalizeBehavior,
129
107
  normalizeBristle,
@@ -132,42 +110,27 @@ import {
132
110
  normalizeLight,
133
111
  normalizeMultiColor,
134
112
  normalizePaintBrush,
135
- normalizePaper,
136
113
  normalizeParticle,
137
114
  normalizeStickerSettings,
138
- normalizeStickerUpdate,
139
115
  normalizeWetBrush,
140
116
  paintToMultiColor,
141
- paperPreset,
142
117
  parseBrushLibrary,
143
- penOrientation,
144
- readEffectStrokeMap,
145
- readPaintEffectPaint,
146
- renderPaper,
147
- renderStickers,
148
- sampleArt,
149
- sampleEffectAt,
150
- sampleEffectField,
151
- stickerAt,
152
- stickerRect,
153
- stickerStamp,
154
- stickerSurface,
155
- tiltToOrientation,
156
- toStrokePoint,
157
118
  usesPaintExchange
158
- } from "../chunk-OTEPAQJX.js";
119
+ } from "../chunk-MTKY6K3Q.js";
159
120
  import {
160
121
  STAMP_KINDS,
161
122
  StickersSlot,
162
- createStickerTools,
163
123
  editorStampOf,
164
124
  normalizeStickerRef,
165
- stampCoverage
166
- } from "../chunk-WKH5ZAUA.js";
125
+ stampCoverage,
126
+ stickerIdFor
127
+ } from "../chunk-APX63ILU.js";
167
128
  import "../chunk-M6DNZNVY.js";
168
129
  import "../chunk-WEIK3UYB.js";
169
130
  import "../chunk-PWBZH3FK.js";
170
- import "../chunk-SZTI6EER.js";
131
+ import {
132
+ StickerError
133
+ } from "../chunk-SZTI6EER.js";
171
134
  import {
172
135
  EffectRendererPool
173
136
  } from "../chunk-EHLOCVWB.js";
@@ -178,13 +141,18 @@ import {
178
141
  resolveEffectValues
179
142
  } from "../chunk-JA57JIT5.js";
180
143
  import "../chunk-V2ZCJRDM.js";
144
+ import {
145
+ GRAIN_PATTERNS,
146
+ GRAIN_TILE,
147
+ PaperGrain,
148
+ grainCatch,
149
+ grainHeights
150
+ } from "../chunk-TXFWZ5ZJ.js";
181
151
  import {
182
152
  CoverageMap,
183
153
  DEFAULT_PAINT_GRADIENT,
184
154
  DEFAULT_PAINT_GRADIENT_MAPPING,
185
155
  EMPTY_MIXER_LOAD,
186
- GRAIN_PATTERNS,
187
- GRAIN_TILE,
188
156
  MAX_BRUSH_SIZE,
189
157
  MIXING_MODELS,
190
158
  PAINT_COLOR_JSON_SCHEMA,
@@ -202,8 +170,8 @@ import {
202
170
  PaintExchange,
203
171
  PaintGradientSampler,
204
172
  PaintSourceError,
205
- PaperGrain,
206
173
  SmudgeTip,
174
+ TIP_GRADIENT_STEPS,
207
175
  TileMedium,
208
176
  addPaintGradientStop,
209
177
  applyStrokeToTile,
@@ -212,8 +180,6 @@ import {
212
180
  createBlenderScratch,
213
181
  dabRect,
214
182
  exchangeRates,
215
- grainCatch,
216
- grainHeights,
217
183
  hexToPaintRgb,
218
184
  isGradientPaint,
219
185
  loadMixer,
@@ -231,7 +197,7 @@ import {
231
197
  scalePaintGradient,
232
198
  solidPaint,
233
199
  updatePaintGradientStop
234
- } from "../chunk-D5KGAO4Z.js";
200
+ } from "../chunk-GCVFEPJG.js";
235
201
  import {
236
202
  TileableNoise,
237
203
  hash2,
@@ -241,15 +207,6 @@ import {
241
207
  seedFromString
242
208
  } from "../chunk-4D6KP5BV.js";
243
209
  import "../chunk-ODBT55Q7.js";
244
- import {
245
- BLEND_MODES,
246
- PAINTERLY_BLEND_MODES,
247
- isBlendMode,
248
- isPainterlyBlendMode,
249
- mixPigment8,
250
- mixPigments,
251
- standardBlendMode
252
- } from "../chunk-IRSTHLQQ.js";
253
210
  import {
254
211
  base64ToBytes,
255
212
  bytesToBase64,
@@ -259,6 +216,15 @@ import {
259
216
  parseDataUrl,
260
217
  pngDataUrl
261
218
  } from "../chunk-OTTT5N3K.js";
219
+ import {
220
+ BLEND_MODES,
221
+ PAINTERLY_BLEND_MODES,
222
+ isBlendMode,
223
+ isPainterlyBlendMode,
224
+ mixPigment8,
225
+ mixPigments,
226
+ standardBlendMode
227
+ } from "../chunk-IRSTHLQQ.js";
262
228
  import {
263
229
  TILE,
264
230
  TiledSurface,
@@ -283,22 +249,156 @@ import {
283
249
  defineEditorDocument
284
250
  } from "../chunk-XBQX76CT.js";
285
251
  import "../chunk-HF5QZS7Z.js";
286
- import "../chunk-V6DT2CDH.js";
252
+ import "../chunk-M2TPMYJE.js";
287
253
  import "../chunk-2WPNAGHI.js";
288
254
  import {
289
255
  HARMONY_KINDS,
290
256
  colorHarmony,
291
257
  hueToWheel,
292
258
  wheelToHue
293
- } from "../chunk-BP6KSGVD.js";
294
- import "../chunk-6ADHGQ73.js";
295
- import "../chunk-JEGJP4OW.js";
259
+ } from "../chunk-7272D3K2.js";
260
+ import "../chunk-YBAPT7NL.js";
296
261
  import {
297
262
  linearToSrgb,
263
+ oklabToOklch,
264
+ oklabToRgb,
265
+ oklchToOklab,
266
+ rgbToOklab,
298
267
  srgbToLinear
299
- } from "../chunk-F67XMVDI.js";
268
+ } from "../chunk-MHLU6GRJ.js";
300
269
  import "../chunk-6ONMQ6WF.js";
301
270
 
271
+ // src/paint-editor/paper.ts
272
+ var PAPER_FAMILIES = ["watercolor", "drawing", "pastel", "canvas", "panel", "smooth"];
273
+ var unit = (v, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : d;
274
+ var clampNum = (v, lo, hi, d) => typeof v === "number" && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : d;
275
+ var HEX = /^#[0-9a-f]{6}$/i;
276
+ var tuning = (contrast = 1, invert = false) => ({ contrast, invert });
277
+ function preset(id, name, family, pattern, color, o) {
278
+ return {
279
+ preset: id,
280
+ name,
281
+ family,
282
+ pattern,
283
+ seed: o.seed ?? 1,
284
+ color,
285
+ scale: o.scale ?? 1,
286
+ tiling: "mirror",
287
+ depth: o.depth,
288
+ shading: o.shading ?? 0.5,
289
+ dry: tuning(o.contrast ?? 1),
290
+ wet: tuning(1),
291
+ absorption: o.absorption,
292
+ permeability: o.permeability,
293
+ roughness: o.roughness,
294
+ ...o.backlight ? { backlight: o.backlight, backlightColor: o.backlightColor ?? "#fff1d6" } : {}
295
+ };
296
+ }
297
+ var PAPER_LIBRARY = [
298
+ preset("cold-pressed", "Cold pressed watercolour", "watercolor", "cold-pressed", "#f7f3ea", { depth: 0.5, absorption: 0.7, permeability: 0.5, roughness: 0.6 }),
299
+ preset("hot-pressed", "Hot pressed watercolour", "watercolor", "hot-pressed", "#f8f6ef", { depth: 0.25, absorption: 0.55, permeability: 0.4, roughness: 0.25 }),
300
+ preset("rough-watercolor", "Rough watercolour", "watercolor", "rough", "#f5f0e4", { depth: 0.75, absorption: 0.8, permeability: 0.6, roughness: 0.85 }),
301
+ preset("cartridge", "Cartridge", "drawing", "vellum", "#fbfaf6", { depth: 0.3, absorption: 0.4, permeability: 0.3, roughness: 0.35 }),
302
+ preset("laid", "Laid paper", "drawing", "laid", "#f3ecdc", { depth: 0.45, absorption: 0.45, permeability: 0.35, roughness: 0.45 }),
303
+ preset("ingres", "Charcoal paper", "drawing", "ingres", "#ede6d6", { depth: 0.6, absorption: 0.4, permeability: 0.3, roughness: 0.65 }),
304
+ preset("sanded-pastel", "Sanded pastel paper", "pastel", "sanded", "#d8cfc0", { depth: 0.7, absorption: 0.3, permeability: 0.2, roughness: 0.9, shading: 0.35 }),
305
+ preset("pastel-card", "Grey pastel card", "pastel", "sanded", "#9c978e", { depth: 0.55, absorption: 0.3, permeability: 0.2, roughness: 0.75, scale: 1.5, shading: 0.3 }),
306
+ preset("cotton-canvas", "Cotton canvas", "canvas", "canvas", "#efe9dc", { depth: 0.55, absorption: 0.35, permeability: 0.25, roughness: 0.6 }),
307
+ preset("fine-linen", "Fine linen", "canvas", "linen", "#e9e2d2", { depth: 0.45, absorption: 0.3, permeability: 0.25, roughness: 0.5 }),
308
+ preset("gesso", "Gesso panel", "panel", "gesso", "#f6f4ef", { depth: 0.35, absorption: 0.2, permeability: 0.15, roughness: 0.4 }),
309
+ preset("smooth", "Smooth bristol", "smooth", "smooth", "#ffffff", { depth: 0, absorption: 0.3, permeability: 0.2, roughness: 0.05, shading: 0 }),
310
+ // Backlit: smooth paper on a lightbox, for glazes that glow like stained glass.
311
+ preset("stained-glass", "Stained glass (backlit)", "smooth", "smooth", "#ffffff", { depth: 0, absorption: 0.35, permeability: 0.25, roughness: 0.05, shading: 0, backlight: 0.8, backlightColor: "#fff1d6" })
312
+ ];
313
+ var DEFAULT_PAPER = PAPER_LIBRARY[0];
314
+ function paperPreset(id) {
315
+ const p = PAPER_LIBRARY.find((x) => x.preset === id);
316
+ return p ? clonePaper(p) : void 0;
317
+ }
318
+ function clonePaper(p) {
319
+ return { ...p, dry: { ...p.dry }, wet: { ...p.wet }, ...p.offset ? { offset: { ...p.offset } } : {} };
320
+ }
321
+ function readOffset(raw, d) {
322
+ const o = raw && typeof raw === "object" ? raw : null;
323
+ const x = o && typeof o.x === "number" && Number.isFinite(o.x) ? Math.round(o.x) : d?.x ?? 0;
324
+ const y = o && typeof o.y === "number" && Number.isFinite(o.y) ? Math.round(o.y) : d?.y ?? 0;
325
+ return x || y ? { offset: { x, y } } : {};
326
+ }
327
+ function readTuning(raw, d) {
328
+ const r = raw && typeof raw === "object" ? raw : {};
329
+ return { contrast: clampNum(r.contrast, 0, 3, d.contrast), invert: typeof r.invert === "boolean" ? r.invert : d.invert };
330
+ }
331
+ function normalizePaper(raw = {}, base) {
332
+ const r = raw;
333
+ const b = base ?? (typeof r.preset === "string" ? paperPreset(r.preset) : void 0) ?? clonePaper(DEFAULT_PAPER);
334
+ return {
335
+ preset: typeof r.preset === "string" && r.preset ? r.preset : b.preset,
336
+ name: typeof r.name === "string" && r.name.trim() ? r.name.trim() : b.name,
337
+ family: PAPER_FAMILIES.includes(r.family) ? r.family : b.family,
338
+ pattern: GRAIN_PATTERNS.includes(r.pattern) ? r.pattern : b.pattern,
339
+ seed: typeof r.seed === "number" && Number.isFinite(r.seed) ? Math.round(Math.abs(r.seed)) % 2147483647 : b.seed,
340
+ color: typeof r.color === "string" && HEX.test(r.color) ? r.color.toLowerCase() : b.color,
341
+ scale: clampNum(r.scale, 0.25, 8, b.scale),
342
+ tiling: r.tiling === "repeat" || r.tiling === "mirror" ? r.tiling : b.tiling,
343
+ depth: unit(r.depth, b.depth),
344
+ shading: unit(r.shading, b.shading),
345
+ dry: readTuning(r.dry, b.dry),
346
+ wet: readTuning(r.wet, b.wet),
347
+ absorption: unit(r.absorption, b.absorption),
348
+ permeability: unit(r.permeability, b.permeability),
349
+ roughness: unit(r.roughness, b.roughness),
350
+ ...unit(r.metallic, b.metallic ?? 0) > 0 ? { metallic: unit(r.metallic, b.metallic ?? 0) } : {},
351
+ ...unit(r.backlight, b.backlight ?? 0) > 0 ? { backlight: unit(r.backlight, b.backlight ?? 0) } : {},
352
+ ...typeof r.backlightColor === "string" && HEX.test(r.backlightColor) ? { backlightColor: r.backlightColor.toLowerCase() } : b.backlightColor ? { backlightColor: b.backlightColor } : {},
353
+ ...readOffset(r.offset, b.offset)
354
+ };
355
+ }
356
+ function hexToRgb8(hex) {
357
+ const n = parseInt(hex.slice(1, 7), 16);
358
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
359
+ }
360
+ function renderPaper(paper, rect, grain = new PaperGrain(paper), light) {
361
+ const [r, g, b] = hexToRgb8(paper.color);
362
+ const out = new Uint8ClampedArray(rect.width * rect.height * 4);
363
+ const shade = grain.flat ? 0 : paper.shading * 0.16;
364
+ const metal = light ? paper.metallic ?? 0 : 0;
365
+ let hx = 0;
366
+ let hy = 0;
367
+ let hz = 1;
368
+ if (metal > 0 && light) {
369
+ const az = light.azimuth * Math.PI / 180;
370
+ const el = light.elevation * Math.PI / 180;
371
+ const lx = Math.cos(el) * Math.sin(az);
372
+ const ly = -Math.cos(el) * Math.cos(az);
373
+ const lz = Math.sin(el) + 1;
374
+ const n = Math.hypot(lx, ly, lz);
375
+ hx = lx / n;
376
+ hy = ly / n;
377
+ hz = lz / n;
378
+ }
379
+ for (let y = 0; y < rect.height; y++) {
380
+ for (let x = 0; x < rect.width; x++) {
381
+ const i = (y * rect.width + x) * 4;
382
+ const px = rect.x + x + 0.5;
383
+ const py = rect.y + y + 0.5;
384
+ const k = shade ? 1 - shade * (1 - grain.at(px, py)) : 1;
385
+ let spec = 0;
386
+ if (metal > 0) {
387
+ const gx = (grain.at(px + 1, py) - grain.at(px - 1, py)) * 4;
388
+ const gy = (grain.at(px, py + 1) - grain.at(px, py - 1)) * 4;
389
+ const nl = Math.hypot(gx, gy, 1);
390
+ const nh = Math.max(0, (-gx * hx - gy * hy + hz) / nl);
391
+ spec = metal * Math.pow(nh, 24) * 0.9;
392
+ }
393
+ out[i] = Math.min(255, r * k * (1 - 0.25 * metal) + spec * (r * 0.6 + 102));
394
+ out[i + 1] = Math.min(255, g * k * (1 - 0.25 * metal) + spec * (g * 0.6 + 102));
395
+ out[i + 2] = Math.min(255, b * k * (1 - 0.25 * metal) + spec * (b * 0.6 + 102));
396
+ out[i + 3] = 255;
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+
302
402
  // src/paint-editor/document.ts
303
403
  var MIN_CANVAS_SIDE = 1;
304
404
  var MAX_CANVAS_SIDE = 16384;
@@ -333,6 +433,426 @@ function createPaintDocument(options = {}) {
333
433
  return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
334
434
  }
335
435
 
436
+ // src/paint-editor/effect-paint.ts
437
+ var EFFECT_STRIP_HEIGHT = 256;
438
+ var EFFECT_MAP_REPEATS = ["repeat", "mirror", "clamp"];
439
+ function effectPaintSize(p, canvas) {
440
+ return p.map ? { width: p.map.length, height: EFFECT_STRIP_HEIGHT } : { width: canvas.width, height: canvas.height };
441
+ }
442
+ function readEffectStrokeMap(raw) {
443
+ if (raw === void 0 || raw === null || raw.by === "canvas") return void 0;
444
+ const r = raw;
445
+ if (r.by !== "stroke") throw new Error(`map.by ${JSON.stringify(r.by)} is not a mapping; use "canvas" (fixed to the canvas) or "stroke" (along each stroke).`);
446
+ const length = r.length === void 0 ? 300 : Number(r.length);
447
+ if (!(Number.isFinite(length) && length >= 16 && length <= 4096)) throw new Error(`map.length ${JSON.stringify(r.length)} is out of range; use 16..4096 px of stroke per repeat.`);
448
+ const repeat = r.repeat === void 0 ? "repeat" : r.repeat;
449
+ if (!EFFECT_MAP_REPEATS.includes(repeat)) throw new Error(`map.repeat ${JSON.stringify(r.repeat)} is not a repeat mode; use ${EFFECT_MAP_REPEATS.join(", ")}.`);
450
+ return { by: "stroke", length: Math.round(length), repeat };
451
+ }
452
+ function stripIndex(img, map, dab, x, y) {
453
+ const ca = Math.cos(dab.direction);
454
+ const sa = Math.sin(dab.direction);
455
+ const dx = x - dab.x;
456
+ const dy = y - dab.y;
457
+ let u = (dab.travel + dx * ca - dy * sa) / map.length;
458
+ if (map.repeat === "clamp") u = Math.min(1, Math.max(0, u));
459
+ else if (map.repeat === "mirror") {
460
+ const m = (u % 2 + 2) % 2;
461
+ u = m > 1 ? 2 - m : m;
462
+ } else u = (u % 1 + 1) % 1;
463
+ const v = Math.min(1, Math.max(0, ((dx * sa + dy * ca) / Math.max(0.5, dab.radius) + 1) / 2));
464
+ const px = Math.min(img.width - 1, Math.floor(u * img.width));
465
+ const py = Math.min(img.height - 1, Math.floor(v * img.height));
466
+ return (py * img.width + px) * 4;
467
+ }
468
+ function effectPaintKey(p) {
469
+ return JSON.stringify({ type: "effect", effect: p.effect, provider: p.provider, values: p.values, time: p.time, ...p.map ? { map: p.map } : {} });
470
+ }
471
+ function isPaintEffectPaint(raw) {
472
+ return Boolean(raw) && typeof raw === "object" && raw.type === "effect";
473
+ }
474
+ function readMapLeniently(raw) {
475
+ try {
476
+ return readEffectStrokeMap(raw);
477
+ } catch {
478
+ return void 0;
479
+ }
480
+ }
481
+ function readPaintEffectPaint(raw) {
482
+ if (!isPaintEffectPaint(raw)) return null;
483
+ const r = raw;
484
+ if (typeof r.effect !== "string" || !r.effect || !r.values || typeof r.values !== "object" || Array.isArray(r.values)) return null;
485
+ return {
486
+ type: "effect",
487
+ effect: r.effect,
488
+ provider: typeof r.provider === "string" ? r.provider : "",
489
+ values: JSON.parse(JSON.stringify(r.values)),
490
+ time: typeof r.time === "number" && Number.isFinite(r.time) && r.time > 0 ? r.time : 0,
491
+ ...readMapLeniently(r.map) ? { map: readMapLeniently(r.map) } : {},
492
+ ...r.live === true ? { live: true } : {}
493
+ };
494
+ }
495
+ function sampleEffectField(img, rect, vals, field, dab) {
496
+ const { width, height, data } = img;
497
+ if (img.map && dab) {
498
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
499
+ for (let xx = 0; xx < rect.width; xx++, k++) {
500
+ const i = stripIndex(img, img.map, dab, rect.x + xx + 0.5, rect.y + yy + 0.5);
501
+ const f = k * 3;
502
+ const a = data[i + 3];
503
+ if (a < 255) vals[k] *= a / 255;
504
+ field[f] = data[i] / 255;
505
+ field[f + 1] = data[i + 1] / 255;
506
+ field[f + 2] = data[i + 2] / 255;
507
+ }
508
+ }
509
+ return;
510
+ }
511
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
512
+ const y = rect.y + yy;
513
+ const inY = y >= 0 && y < height;
514
+ for (let xx = 0; xx < rect.width; xx++, k++) {
515
+ const x = rect.x + xx;
516
+ const f = k * 3;
517
+ if (!inY || x < 0 || x >= width) {
518
+ vals[k] = 0;
519
+ field[f] = field[f + 1] = field[f + 2] = 0;
520
+ continue;
521
+ }
522
+ const i = (y * width + x) * 4;
523
+ const a = data[i + 3];
524
+ if (a < 255) vals[k] *= a / 255;
525
+ field[f] = data[i] / 255;
526
+ field[f + 1] = data[i + 1] / 255;
527
+ field[f + 2] = data[i + 2] / 255;
528
+ }
529
+ }
530
+ }
531
+ function sampleEffectAt(img, x, y, out, dab) {
532
+ if (img.map && dab) {
533
+ const i2 = stripIndex(img, img.map, dab, x, y);
534
+ out[0] = img.data[i2] / 255;
535
+ out[1] = img.data[i2 + 1] / 255;
536
+ out[2] = img.data[i2 + 2] / 255;
537
+ return img.data[i2 + 3] / 255;
538
+ }
539
+ const px = Math.floor(x);
540
+ const py = Math.floor(y);
541
+ if (px < 0 || py < 0 || px >= img.width || py >= img.height) return 0;
542
+ const i = (py * img.width + px) * 4;
543
+ out[0] = img.data[i] / 255;
544
+ out[1] = img.data[i + 1] / 255;
545
+ out[2] = img.data[i + 2] / 255;
546
+ return img.data[i + 3] / 255;
547
+ }
548
+ function effectDabField(img, rect, cov, centre2, mixed, field, steps = 0, dab = null) {
549
+ sampleEffectField(img, rect, cov, field, dab);
550
+ const d0 = mixed[0] - centre2[0];
551
+ const d1 = mixed[1] - centre2[1];
552
+ const d2 = mixed[2] - centre2[2];
553
+ const n = rect.width * rect.height;
554
+ for (let k = 0; k < n; k++) {
555
+ if (steps) cov[k] = Math.round(cov[k] * steps) / steps;
556
+ const f = k * 3;
557
+ field[f] = Math.min(1, Math.max(0, field[f] + d0));
558
+ field[f + 1] = Math.min(1, Math.max(0, field[f + 1] + d1));
559
+ field[f + 2] = Math.min(1, Math.max(0, field[f + 2] + d2));
560
+ }
561
+ }
562
+
563
+ // src/paint-editor/engine.ts
564
+ function penOrientation(p) {
565
+ if (typeof p.altitude === "number" && Number.isFinite(p.altitude)) {
566
+ const altitude = Math.min(Math.PI / 2, Math.max(0, p.altitude));
567
+ const azimuth = typeof p.azimuth === "number" && Number.isFinite(p.azimuth) ? p.azimuth : 0;
568
+ return { altitude, azimuth };
569
+ }
570
+ const hasX = typeof p.tiltX === "number" && Number.isFinite(p.tiltX);
571
+ const hasY = typeof p.tiltY === "number" && Number.isFinite(p.tiltY);
572
+ if (!hasX && !hasY) return null;
573
+ return tiltToOrientation(hasX ? p.tiltX : 0, hasY ? p.tiltY : 0);
574
+ }
575
+ function tiltToOrientation(tiltX, tiltY) {
576
+ const clampT = (v) => Math.min(89.999, Math.max(-89.999, v));
577
+ const tx = Math.tan(clampT(tiltX) * Math.PI / 180);
578
+ const ty = Math.tan(clampT(tiltY) * Math.PI / 180);
579
+ const r = Math.hypot(tx, ty);
580
+ if (r < 1e-9) return { altitude: Math.PI / 2, azimuth: 0 };
581
+ let azimuth = Math.atan2(ty, tx);
582
+ if (azimuth < 0) azimuth += 2 * Math.PI;
583
+ return { altitude: Math.atan(1 / r), azimuth };
584
+ }
585
+ var pressureOf = (p) => typeof p.pressure === "number" && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1;
586
+ var PenSmoother = class {
587
+ pos = null;
588
+ pressure = null;
589
+ a;
590
+ ap;
591
+ constructor(position, pressure) {
592
+ this.a = 1 - Math.min(0.95, Math.max(0, position));
593
+ this.ap = 1 - Math.min(0.95, Math.max(0, pressure));
594
+ }
595
+ push(p) {
596
+ const pr = pressureOf(p);
597
+ if (!this.pos) {
598
+ this.pos = { x: p.x, y: p.y };
599
+ this.pressure = pr;
600
+ return p;
601
+ }
602
+ this.pos = { x: this.pos.x + (p.x - this.pos.x) * this.a, y: this.pos.y + (p.y - this.pos.y) * this.a };
603
+ this.pressure = this.pressure + (pr - this.pressure) * this.ap;
604
+ return { ...p, x: this.pos.x, y: this.pos.y, pressure: this.pressure };
605
+ }
606
+ /** Where the smoothed position is now (or `null` before the first sample). */
607
+ get position() {
608
+ return this.pos;
609
+ }
610
+ };
611
+ var IDENTITY_COPY = { map: (p) => p, linear: [1, 0, 0, 1] };
612
+ var FULL_SPEED = 2.5;
613
+ var PenDabWalker = class {
614
+ constructor(brush, seed = 0, linear = [1, 0, 0, 1]) {
615
+ this.brush = brush;
616
+ this.seed = seed;
617
+ this.linear = linear;
618
+ }
619
+ brush;
620
+ seed;
621
+ linear;
622
+ last = null;
623
+ nextAt = 0;
624
+ pendingFirst = null;
625
+ index = 0;
626
+ dir = 0;
627
+ lastT = null;
628
+ firstT = null;
629
+ speed = null;
630
+ travelled = 0;
631
+ sample(p) {
632
+ const o = penOrientation(p);
633
+ let speed = null;
634
+ let time = null;
635
+ if (this.last) this.travelled += Math.hypot(p.x - this.last.x, p.y - this.last.y);
636
+ if (typeof p.t === "number" && Number.isFinite(p.t)) {
637
+ if (this.firstT === null) this.firstT = p.t;
638
+ time = p.t - this.firstT;
639
+ if (this.last && this.lastT !== null && p.t > this.lastT) {
640
+ const v = Math.hypot(p.x - this.last.x, p.y - this.last.y) / (p.t - this.lastT);
641
+ this.speed = this.speed === null ? v : this.speed + (v - this.speed) * 0.35;
642
+ }
643
+ this.lastT = p.t;
644
+ speed = this.speed ?? 0;
645
+ }
646
+ return {
647
+ x: p.x,
648
+ y: p.y,
649
+ pressure: pressureOf(p),
650
+ lean: o ? 1 - o.altitude / (Math.PI / 2) : 0,
651
+ azimuth: o ? o.azimuth : 0,
652
+ twist: typeof p.twist === "number" && Number.isFinite(p.twist) ? p.twist : null,
653
+ speed,
654
+ travel: this.travelled,
655
+ time
656
+ };
657
+ }
658
+ diameter(s) {
659
+ const tip = this.brush.tip;
660
+ const dyn = this.brush.dynamics;
661
+ let d = tip.size;
662
+ if (tip.pressureSize) d *= dyn.minSize + (1 - dyn.minSize) * s.pressure;
663
+ d *= 1 + dyn.tiltSize * s.lean * 1.5;
664
+ if (s.speed !== null && dyn.velocitySize) d *= Math.max(0.05, 1 + dyn.velocitySize * Math.min(1, s.speed / FULL_SPEED) * (dyn.velocitySize < 0 ? 0.85 : 1));
665
+ return d;
666
+ }
667
+ stepLen(s) {
668
+ return Math.max(0.5, Math.max(1, this.diameter(s)) * Math.max(0.01, this.brush.tip.spacing));
669
+ }
670
+ /** A rasterizer angle (CCW, y up) mapped through this copy's linear part. */
671
+ mapAngle(theta) {
672
+ const [a, b, c, d] = this.linear;
673
+ if (a === 1 && b === 0 && c === 0 && d === 1) return theta;
674
+ const vx = Math.cos(theta);
675
+ const vy = -Math.sin(theta);
676
+ return Math.atan2(-(c * vx + d * vy), a * vx + b * vy);
677
+ }
678
+ dab(s) {
679
+ const { tip, dynamics: dyn, rotation } = this.brush;
680
+ const i = this.index++;
681
+ const seed = this.seed;
682
+ let diameter = this.diameter(s);
683
+ if (dyn.sizeJitter) diameter *= Math.max(0.05, 1 + dyn.sizeJitter * randSigned(seed, i, 1 /* Size */));
684
+ const base = tip.angle * Math.PI / 180;
685
+ let angle;
686
+ if (rotation === "stroke") angle = this.dir + base;
687
+ else if (rotation === "barrel" && s.twist !== null) angle = this.mapAngle(base - s.twist * Math.PI / 180);
688
+ else if (dyn.tiltRoundness > 0 && s.lean > 0.02) angle = this.mapAngle(base - s.azimuth);
689
+ else angle = this.mapAngle(base);
690
+ if (dyn.angleJitter) angle += dyn.angleJitter * Math.PI * randSigned(seed, i, 2 /* Angle */);
691
+ let opacity = tip.pressureOpacity ? s.pressure : 1;
692
+ opacity *= 1 - dyn.tiltOpacity * s.lean * 0.8;
693
+ if (s.speed !== null && dyn.velocityOpacity) opacity *= Math.max(0, 1 + dyn.velocityOpacity * (Math.min(1, s.speed / FULL_SPEED) - 0.5));
694
+ if (dyn.opacityJitter) opacity *= 1 - dyn.opacityJitter * (randSigned(seed, i, 3 /* Opacity */) * 0.5 + 0.5);
695
+ const flow = tip.flow * (1 - dyn.pressureFlow + dyn.pressureFlow * s.pressure);
696
+ let { x, y } = s;
697
+ if (dyn.scatter) {
698
+ const off = dyn.scatter * diameter * randSigned(seed, i, 4 /* Scatter */);
699
+ x += Math.sin(this.dir) * off;
700
+ y += Math.cos(this.dir) * off;
701
+ }
702
+ return {
703
+ x,
704
+ y,
705
+ radius: Math.max(0.5, diameter / 2),
706
+ alpha: Math.min(1, Math.max(0, flow)),
707
+ opacity: Math.min(1, Math.max(0, opacity)),
708
+ angle,
709
+ roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),
710
+ pressure: s.pressure,
711
+ direction: this.dir,
712
+ index: i,
713
+ travel: s.travel,
714
+ time: s.time,
715
+ lean: s.lean,
716
+ speed: s.speed
717
+ };
718
+ }
719
+ push(p, out) {
720
+ const a = this.last;
721
+ const s = this.sample(p);
722
+ if (!a) {
723
+ this.last = s;
724
+ this.pendingFirst = s;
725
+ this.nextAt = this.stepLen(s);
726
+ return;
727
+ }
728
+ const dx = s.x - a.x;
729
+ const dy = s.y - a.y;
730
+ const len = Math.hypot(dx, dy);
731
+ if (len > 1e-9) this.dir = Math.atan2(-dy, dx);
732
+ if (this.pendingFirst) {
733
+ out.push(this.dab(this.pendingFirst));
734
+ this.pendingFirst = null;
735
+ }
736
+ if (len > 1e-9) {
737
+ while (this.nextAt <= len + 1e-9) {
738
+ const f = Math.min(1, this.nextAt / len);
739
+ const lerp = (u, v) => u + (v - u) * f;
740
+ const q = {
741
+ x: a.x + dx * f,
742
+ y: a.y + dy * f,
743
+ pressure: lerp(a.pressure, s.pressure),
744
+ lean: lerp(a.lean, s.lean),
745
+ azimuth: lerpAngle(a.azimuth, s.azimuth, f),
746
+ twist: a.twist !== null && s.twist !== null ? lerpDegrees(a.twist, s.twist, f) : s.twist,
747
+ speed: a.speed !== null && s.speed !== null ? lerp(a.speed, s.speed) : s.speed,
748
+ travel: a.travel + this.nextAt,
749
+ time: a.time !== null && s.time !== null ? lerp(a.time, s.time) : s.time
750
+ };
751
+ out.push(this.dab(q));
752
+ this.nextAt += this.stepLen(q);
753
+ }
754
+ this.nextAt -= len;
755
+ }
756
+ this.last = s;
757
+ }
758
+ finish(out) {
759
+ if (this.pendingFirst) {
760
+ out.push(this.dab(this.pendingFirst));
761
+ this.pendingFirst = null;
762
+ }
763
+ }
764
+ };
765
+ function lerpAngle(a, b, t) {
766
+ let d = b - a;
767
+ while (d > Math.PI) d -= 2 * Math.PI;
768
+ while (d < -Math.PI) d += 2 * Math.PI;
769
+ return a + d * t;
770
+ }
771
+ function lerpDegrees(a, b, t) {
772
+ let d = b - a;
773
+ while (d > 180) d -= 360;
774
+ while (d < -180) d += 360;
775
+ return a + d * t;
776
+ }
777
+ var DryStroke = class {
778
+ constructor(brush, options) {
779
+ this.brush = brush;
780
+ this.options = options;
781
+ this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
782
+ this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
783
+ this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear));
784
+ this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
785
+ }
786
+ brush;
787
+ options;
788
+ coverage = new CoverageMap();
789
+ dabCount = 0;
790
+ smoother;
791
+ walkers;
792
+ copies;
793
+ depth;
794
+ buf = new Float32Array(0);
795
+ field = new Float32Array(0);
796
+ lastRaw = null;
797
+ lastFed = null;
798
+ /** Feeds raw pen samples; returns the document rectangles the new dabs touched. */
799
+ push(points) {
800
+ const dabs = [];
801
+ for (const raw of points) {
802
+ this.lastRaw = raw;
803
+ const p = this.smoother.push(raw);
804
+ this.lastFed = p;
805
+ this.copies.forEach((c, i) => this.walkers[i].push(c.map(p), dabs));
806
+ }
807
+ return this.paint(dabs);
808
+ }
809
+ /** Ends the stroke: the smoothed path catches up with the last raw sample; a single tap paints one dab. */
810
+ finish() {
811
+ const dabs = [];
812
+ const raw = this.lastRaw;
813
+ const fed = this.lastFed;
814
+ if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.copies.forEach((c, i) => this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs));
815
+ for (const w of this.walkers) w.finish(dabs);
816
+ return this.paint(dabs);
817
+ }
818
+ paint(dabs) {
819
+ const rects = [];
820
+ const hardness = this.brush.tip.hardness;
821
+ const grain = this.options.grain;
822
+ for (const d of dabs) {
823
+ const rect = dabRect(d, hardness);
824
+ const n = rect.width * rect.height;
825
+ if (this.buf.length < n) this.buf = new Float32Array(n);
826
+ const vals = this.buf.subarray(0, n);
827
+ rasterizeDab(d, hardness, rect, vals);
828
+ if (this.depth > 0 && grain) {
829
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
830
+ const y = rect.y + yy + 0.5;
831
+ for (let xx = 0; xx < rect.width; xx++, k++) {
832
+ if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
833
+ }
834
+ }
835
+ }
836
+ let field = null;
837
+ if (this.options.effect) {
838
+ if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
839
+ field = this.field.subarray(0, n * 3);
840
+ sampleEffectField(this.options.effect, rect, vals, field, d);
841
+ }
842
+ this.coverage.accumulate(rect, vals, d.opacity, null, field);
843
+ rects.push(rect);
844
+ this.dabCount++;
845
+ }
846
+ return rects;
847
+ }
848
+ };
849
+ function mixerTip(brush) {
850
+ return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };
851
+ }
852
+ function toStrokePoint(p) {
853
+ return { x: p.x, y: p.y, pressure: pressureOf(p), ...typeof p.t === "number" ? { t: p.t } : {} };
854
+ }
855
+
336
856
  // src/paint-editor/dab-stroke.ts
337
857
  var GRADIENT_STEPS = 32;
338
858
  function multiColorPalette(mc, color, secondary) {
@@ -1283,6 +1803,985 @@ function lightFromJSON(raw) {
1283
1803
  return surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
1284
1804
  }
1285
1805
 
1806
+ // src/paint-editor/sticker-art.ts
1807
+ var STICKER_ART_SIZE = 256;
1808
+ var defaultStickerRasterizer = async (asset, size) => {
1809
+ if (asset.kind === "image") {
1810
+ const parsed = parseDataUrl(asset.url);
1811
+ if (parsed && parsed.mime === "image/png") {
1812
+ const png = decodePng(parsed.bytes);
1813
+ return { width: png.width, height: png.height, data: new Uint8ClampedArray(png.data) };
1814
+ }
1815
+ }
1816
+ const g = globalThis;
1817
+ if (!g.createImageBitmap || !g.OffscreenCanvas) {
1818
+ 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).`);
1819
+ }
1820
+ const blob = asset.kind === "svg" ? new Blob([asset.svg], { type: "image/svg+xml" }) : await (await g.fetch(asset.url)).blob();
1821
+ const bitmap = await g.createImageBitmap(blob);
1822
+ const scale = size / Math.max(bitmap.width, bitmap.height, 1);
1823
+ const w = Math.max(1, Math.round(bitmap.width * scale));
1824
+ const h = Math.max(1, Math.round(bitmap.height * scale));
1825
+ const canvas = new g.OffscreenCanvas(w, h);
1826
+ const ctx = canvas.getContext("2d");
1827
+ ctx.drawImage(bitmap, 0, 0, w, h);
1828
+ const img = ctx.getImageData(0, 0, w, h);
1829
+ return { width: w, height: h, data: img.data };
1830
+ };
1831
+ var StickerArtCache = class {
1832
+ entries = /* @__PURE__ */ new Map();
1833
+ /** The art for `ref` when it has loaded (editor stamps never need any). */
1834
+ get = (ref2) => {
1835
+ const e = this.entries.get(ref2);
1836
+ return e?.status === "ready" ? e.art : void 0;
1837
+ };
1838
+ /** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
1839
+ status(ref2) {
1840
+ if (editorStampOf(ref2)) return { status: "ready" };
1841
+ const e = this.entries.get(ref2);
1842
+ if (!e) return { status: "none" };
1843
+ return e.status === "failed" ? { status: "failed", error: e.error } : { status: e.status };
1844
+ }
1845
+ /**
1846
+ * Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
1847
+ * reject with the closest known ones; failures are kept so they are reported, and retried on
1848
+ * the next call.
1849
+ */
1850
+ ensure(provider, ref2, rasterize) {
1851
+ if (editorStampOf(ref2)) return Promise.resolve(null);
1852
+ const e = this.entries.get(ref2);
1853
+ if (e?.status === "ready") return Promise.resolve(e.art);
1854
+ if (e?.status === "loading") return e.promise;
1855
+ const promise = (async () => {
1856
+ const d = await describeStickerRef(provider, ref2);
1857
+ const id = stickerIdFor(provider, ref2);
1858
+ const max = provider.capabilities.maxSize ?? STICKER_ART_SIZE;
1859
+ const size = Math.min(STICKER_ART_SIZE, max);
1860
+ const asset = await provider.load(id, d.formats.includes("png") ? { format: "png", size } : { format: d.formats[0] ?? "svg", size });
1861
+ const px = await rasterize(asset, size);
1862
+ 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.`);
1863
+ return { ...px, tintable: d.tintable };
1864
+ })();
1865
+ this.entries.set(ref2, { status: "loading", promise });
1866
+ return promise.then(
1867
+ (art) => {
1868
+ this.entries.set(ref2, { status: "ready", art });
1869
+ return art;
1870
+ },
1871
+ (err) => {
1872
+ const message = err instanceof Error ? err.message : String(err);
1873
+ this.entries.set(ref2, { status: "failed", error: message });
1874
+ throw err;
1875
+ }
1876
+ );
1877
+ }
1878
+ /** Forgets every load (a new provider). */
1879
+ clear() {
1880
+ this.entries.clear();
1881
+ }
1882
+ };
1883
+ function sampleArt(art, x, y, out) {
1884
+ const { width: w, height: h, data } = art;
1885
+ const fx = x - 0.5;
1886
+ const fy = y - 0.5;
1887
+ const x0 = Math.floor(fx);
1888
+ const y0 = Math.floor(fy);
1889
+ const tx = fx - x0;
1890
+ const ty = fy - y0;
1891
+ let r = 0;
1892
+ let g = 0;
1893
+ let b = 0;
1894
+ let a = 0;
1895
+ for (let k = 0; k < 4; k++) {
1896
+ const xx = x0 + (k & 1);
1897
+ const yy = y0 + (k >> 1);
1898
+ if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
1899
+ const wgt = (k & 1 ? tx : 1 - tx) * (k >> 1 ? ty : 1 - ty);
1900
+ const i = (yy * w + xx) * 4;
1901
+ const al = data[i + 3] * wgt;
1902
+ r += data[i] * al;
1903
+ g += data[i + 1] * al;
1904
+ b += data[i + 2] * al;
1905
+ a += al;
1906
+ }
1907
+ out[0] = a > 0 ? r / a / 255 : 0;
1908
+ out[1] = a > 0 ? g / a / 255 : 0;
1909
+ out[2] = a > 0 ? b / a / 255 : 0;
1910
+ out[3] = a / 255;
1911
+ return out;
1912
+ }
1913
+
1914
+ // src/paint-editor/stickers.ts
1915
+ function hexRgb(hex) {
1916
+ const n = parseInt(hex.slice(1, 7), 16);
1917
+ return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
1918
+ }
1919
+ function stickerRect(s) {
1920
+ const r = s.size / 2 + 1;
1921
+ const x0 = Math.floor(s.x - r);
1922
+ const y0 = Math.floor(s.y - r);
1923
+ return { x: x0, y: y0, width: Math.ceil(s.x + r) - x0, height: Math.ceil(s.y + r) - y0 };
1924
+ }
1925
+ function stickerStamp(s) {
1926
+ if (s.ref) {
1927
+ try {
1928
+ return editorStampOf(s.ref);
1929
+ } catch {
1930
+ return null;
1931
+ }
1932
+ }
1933
+ return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
1934
+ }
1935
+ function drawSticker(medium, s, art) {
1936
+ const kind = stickerStamp(s);
1937
+ if (!kind) {
1938
+ const a2 = art?.(s.ref);
1939
+ return a2 ? drawArtSticker(medium, s, a2) : stickerRect(s);
1940
+ }
1941
+ const rect = stickerRect(s);
1942
+ const n = rect.width * rect.height;
1943
+ const cov = new Float32Array(n);
1944
+ const field = new Float32Array(n * 3);
1945
+ const rad2 = s.size / 2;
1946
+ const a = s.rotation * Math.PI / 180;
1947
+ const ca = Math.cos(a);
1948
+ const sa = Math.sin(a);
1949
+ const [r, g, b] = hexRgb(s.color);
1950
+ for (let yy = 0, p = 0; yy < rect.height; yy++) {
1951
+ for (let xx = 0, px = rect.x; xx < rect.width; xx++, p++, px++) {
1952
+ const dx = px + 0.5 - s.x;
1953
+ const dy = rect.y + yy + 0.5 - s.y;
1954
+ const u = (dx * ca + dy * sa) / rad2;
1955
+ const v = (-dx * sa + dy * ca) / rad2;
1956
+ if (u < -1.05 || u > 1.05 || v < -1.05 || v > 1.05) continue;
1957
+ let c = 0;
1958
+ 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);
1959
+ c /= 4;
1960
+ if (c <= 0) continue;
1961
+ cov[p] = c;
1962
+ const shade = 1 + 0.12 * (-(u + v) / 2) + 0.06 * (hash2(px, rect.y + yy, s.seed) - 0.5);
1963
+ field[p * 3] = Math.min(1, Math.max(0, r * shade));
1964
+ field[p * 3 + 1] = Math.min(1, Math.max(0, g * shade));
1965
+ field[p * 3 + 2] = Math.min(1, Math.max(0, b * shade));
1966
+ }
1967
+ }
1968
+ medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
1969
+ return rect;
1970
+ }
1971
+ function drawArtSticker(medium, s, art) {
1972
+ const rect = stickerRect(s);
1973
+ const n = rect.width * rect.height;
1974
+ const cov = new Float32Array(n);
1975
+ const field = new Float32Array(n * 3);
1976
+ const a = s.rotation * Math.PI / 180;
1977
+ const ca = Math.cos(a);
1978
+ const sa = Math.sin(a);
1979
+ const scale = Math.max(art.width, art.height) / s.size;
1980
+ const [r, g, b] = hexRgb(s.color);
1981
+ const px = [0, 0, 0, 0];
1982
+ for (let yy = 0, p = 0; yy < rect.height; yy++) {
1983
+ for (let xx = 0; xx < rect.width; xx++, p++) {
1984
+ const dx = rect.x + xx + 0.5 - s.x;
1985
+ const dy = rect.y + yy + 0.5 - s.y;
1986
+ const ax = (dx * ca + dy * sa) * scale + art.width / 2;
1987
+ const ay = (-dx * sa + dy * ca) * scale + art.height / 2;
1988
+ if (ax < -1 || ay < -1 || ax > art.width + 1 || ay > art.height + 1) continue;
1989
+ sampleArt(art, ax, ay, px);
1990
+ if (!(px[3] > 0)) continue;
1991
+ cov[p] = px[3];
1992
+ field[p * 3] = art.tintable ? r : px[0];
1993
+ field[p * 3 + 1] = art.tintable ? g : px[1];
1994
+ field[p * 3 + 2] = art.tintable ? b : px[2];
1995
+ }
1996
+ }
1997
+ medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
1998
+ return rect;
1999
+ }
2000
+ function jitterColor(hex, amount, seed, i) {
2001
+ const [r, g, b] = hexRgb(hex);
2002
+ const lch = oklabToOklch(rgbToOklab({ r, g, b, a: 1 }));
2003
+ const l = Math.min(1, Math.max(0, lch.l + (rand01(seed, i, 61) - 0.5) * amount * 0.3));
2004
+ const h = lch.h + (rand01(seed, i, 63) - 0.5) * amount * 80;
2005
+ const at = (c) => oklabToRgb(oklchToOklab({ l, c, h, alpha: 1 }));
2006
+ const inGamut = (o) => [o.r, o.g, o.b].every((v) => v >= -1e-4 && v <= 1 + 1e-4);
2007
+ let out = at(lch.c);
2008
+ if (!inGamut(out)) {
2009
+ let lo = 0;
2010
+ let hi = lch.c;
2011
+ for (let k = 0; k < 12; k++) {
2012
+ const mid = (lo + hi) / 2;
2013
+ if (inGamut(at(mid))) lo = mid;
2014
+ else hi = mid;
2015
+ }
2016
+ out = at(lo);
2017
+ }
2018
+ 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("");
2019
+ }
2020
+ var StickerSpray = class {
2021
+ constructor(init) {
2022
+ this.init = init;
2023
+ const b = init.brush;
2024
+ this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);
2025
+ this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
2026
+ const spacing = Math.max(0.05, 1 / Math.max(0.05, b.sticker.density * 4));
2027
+ const walk = { ...b, tip: { ...b.tip, spacing }, dynamics: { ...b.dynamics, scatter: 0, sizeJitter: 0, angleJitter: 0 } };
2028
+ this.walkers = this.copies.map((c, i) => new PenDabWalker(walk, init.seed + i * 7919 | 0, c.linear));
2029
+ this.medium = new TileMedium(init.target, {});
2030
+ this.next = init.nextId;
2031
+ }
2032
+ init;
2033
+ dabCount = 0;
2034
+ stickers = [];
2035
+ /** Refs the spray picked but could not draw because their art had not loaded. */
2036
+ skipped = /* @__PURE__ */ new Set();
2037
+ smoother;
2038
+ walkers;
2039
+ copies;
2040
+ medium;
2041
+ lastRaw = null;
2042
+ lastFed = null;
2043
+ next;
2044
+ push(points) {
2045
+ const rects = [];
2046
+ for (const raw of points) {
2047
+ this.lastRaw = raw;
2048
+ const p = this.smoother.push(raw);
2049
+ this.lastFed = p;
2050
+ this.copies.forEach((c, i) => {
2051
+ const dabs = [];
2052
+ this.walkers[i].push(c.map(p), dabs);
2053
+ for (const d of dabs) this.drop(d, rects);
2054
+ });
2055
+ }
2056
+ return rects;
2057
+ }
2058
+ finish() {
2059
+ const rects = [];
2060
+ const raw = this.lastRaw;
2061
+ const fed = this.lastFed;
2062
+ this.copies.forEach((c, i) => {
2063
+ const dabs = [];
2064
+ if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
2065
+ this.walkers[i].finish(dabs);
2066
+ for (const d of dabs) this.drop(d, rects);
2067
+ });
2068
+ return rects;
2069
+ }
2070
+ drop(d, rects) {
2071
+ const st = this.init.brush.sticker;
2072
+ const i = this.next;
2073
+ const seed = this.init.seed;
2074
+ const size = d.radius * 2 * (1 - st.sizeJitter * 0.7 * rand01(seed, i, 51));
2075
+ const off = st.scatter * d.radius * 2;
2076
+ const a = rand01(seed, i, 53) * Math.PI * 2;
2077
+ const dist = rand01(seed, i, 55) * off;
2078
+ const base = this.colorAt(d, Math.cos(a) * dist, Math.sin(a) * dist);
2079
+ if (!(base.alpha > 0)) {
2080
+ this.next++;
2081
+ return;
2082
+ }
2083
+ const ref2 = st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "editor:dot";
2084
+ const stamp = stickerStamp({ ref: ref2 });
2085
+ if (!stamp && !this.init.art?.(ref2)) {
2086
+ this.skipped.add(ref2);
2087
+ this.next++;
2088
+ return;
2089
+ }
2090
+ const sticker = {
2091
+ id: `st${this.next++}`,
2092
+ ref: ref2,
2093
+ ...stamp ? { stamp } : {},
2094
+ x: Math.round((d.x + Math.cos(a) * dist) * 100) / 100,
2095
+ y: Math.round((d.y + Math.sin(a) * dist) * 100) / 100,
2096
+ size: Math.max(2, Math.round(size * 10) / 10),
2097
+ rotation: Math.round((-d.angle * 180 / Math.PI + (rand01(seed, i, 59) * 2 - 1) * 180 * st.rotationJitter) * 10) / 10,
2098
+ color: st.colorJitter > 0 ? jitterColor(base.color, st.colorJitter, seed, i) : base.color,
2099
+ opacity: Math.round(Math.min(1, d.opacity * this.init.brush.tip.opacity * base.alpha) * 1e3) / 1e3,
2100
+ seed: Math.floor(rand01(seed, i, 65) * 1e9)
2101
+ };
2102
+ this.stickers.push(sticker);
2103
+ rects.push(drawSticker(this.medium, sticker, this.init.art));
2104
+ this.dabCount++;
2105
+ }
2106
+ sampler;
2107
+ /** A sticker's colour: the paint colour, or gradient paint at its dab (`dx`, `dy` from the dab's centre). */
2108
+ colorAt(d, dx, dy) {
2109
+ const effect = this.init.effect;
2110
+ if (effect) {
2111
+ const rgb = [0, 0, 0];
2112
+ const alpha = sampleEffectAt(effect, d.x + dx, d.y + dy, rgb, d);
2113
+ return { color: "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join(""), alpha };
2114
+ }
2115
+ const g = this.init.paint;
2116
+ if (!g) return { color: this.init.color, alpha: 1 };
2117
+ this.sampler ??= new PaintGradientSampler(g, this.init.seed);
2118
+ 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);
2119
+ 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 };
2120
+ }
2121
+ };
2122
+ function renderStickers(stickers, target, art) {
2123
+ const medium = new TileMedium(target, {});
2124
+ for (const s of stickers) drawSticker(medium, s, art);
2125
+ }
2126
+ function normalizeStickerUpdate(u) {
2127
+ const out = {};
2128
+ 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).");
2129
+ if (u.stamp !== void 0) {
2130
+ 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).`);
2131
+ out.ref = `editor:${u.stamp}`;
2132
+ out.stamp = u.stamp;
2133
+ }
2134
+ if (u.ref !== void 0) {
2135
+ out.ref = normalizeStickerRef(u.ref);
2136
+ const stamp = stickerStamp({ ref: out.ref });
2137
+ if (stamp) out.stamp = stamp;
2138
+ else out.stamp = void 0;
2139
+ }
2140
+ for (const k of ["x", "y"]) if (u[k] !== void 0) {
2141
+ if (!Number.isFinite(u[k])) throw new Error(`${k} must be a number of pixels.`);
2142
+ out[k] = u[k];
2143
+ }
2144
+ if (u.size !== void 0) {
2145
+ if (!(u.size >= 2 && u.size <= 2048)) throw new Error(`size ${u.size} is out of range; use 2..2048 px.`);
2146
+ out.size = u.size;
2147
+ }
2148
+ if (u.rotation !== void 0) {
2149
+ if (!Number.isFinite(u.rotation)) throw new Error("rotation must be degrees.");
2150
+ out.rotation = u.rotation;
2151
+ }
2152
+ if (u.color !== void 0) {
2153
+ if (!/^#[0-9a-f]{6}$/i.test(u.color)) throw new Error(`color ${JSON.stringify(u.color)} must be #rrggbb.`);
2154
+ out.color = u.color.toLowerCase();
2155
+ }
2156
+ if (u.opacity !== void 0) {
2157
+ if (!(u.opacity >= 0 && u.opacity <= 1)) throw new Error(`opacity ${u.opacity} is out of range; use 0..1.`);
2158
+ out.opacity = u.opacity;
2159
+ }
2160
+ return out;
2161
+ }
2162
+ function stickerSurface(stickers, base, dirty, art) {
2163
+ const only = base && dirty ? new Set(dirty.flatMap((r) => tilesInRect(r).map(({ tx, ty }) => tileKey(tx, ty)))) : null;
2164
+ const live = /* @__PURE__ */ new Map();
2165
+ let scratch = null;
2166
+ const target = {
2167
+ offset: { x: 0, y: 0 },
2168
+ lockTransparency: false,
2169
+ selection: null,
2170
+ readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? null,
2171
+ writeTile: (tx, ty) => {
2172
+ const k = tileKey(tx, ty);
2173
+ if (only && !only.has(k)) return (scratch ??= new Uint8ClampedArray(TILE * TILE * 4)).fill(0);
2174
+ let t = live.get(k);
2175
+ if (!t) live.set(k, t = new Uint8ClampedArray(TILE * TILE * 4));
2176
+ return t;
2177
+ }
2178
+ };
2179
+ const reaches = (s) => !only || tilesInRect(stickerRect(s)).some(({ tx, ty }) => only.has(tileKey(tx, ty)));
2180
+ renderStickers(stickers.filter(reaches), target, art);
2181
+ const tiles = /* @__PURE__ */ new Map();
2182
+ if (only && base) {
2183
+ for (const [k, t] of base.tiles) if (!only.has(k)) tiles.set(k, t);
2184
+ }
2185
+ for (const [k, d] of live) {
2186
+ const t = finishTile(d, 4, 0);
2187
+ if (t) tiles.set(k, t);
2188
+ }
2189
+ return new TiledSurface(4, tiles, 0);
2190
+ }
2191
+ function stickerAt(stickers, x, y, art) {
2192
+ for (let i = stickers.length - 1; i >= 0; i--) {
2193
+ const s = stickers[i];
2194
+ const rad2 = s.size / 2;
2195
+ const a = s.rotation * Math.PI / 180;
2196
+ const dx = x - s.x;
2197
+ const dy = y - s.y;
2198
+ const u = (dx * Math.cos(a) + dy * Math.sin(a)) / rad2;
2199
+ const v = (-dx * Math.sin(a) + dy * Math.cos(a)) / rad2;
2200
+ if (Math.abs(u) > 1.05 || Math.abs(v) > 1.05) continue;
2201
+ if (s.size < 16) return s;
2202
+ const kind = stickerStamp(s);
2203
+ if (kind) {
2204
+ if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
2205
+ continue;
2206
+ }
2207
+ const pic = art?.(s.ref);
2208
+ if (!pic) return s;
2209
+ const scale = Math.max(pic.width, pic.height) / 2;
2210
+ if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
2211
+ }
2212
+ return null;
2213
+ }
2214
+
2215
+ // src/paint-editor/procedural.ts
2216
+ var STEP_PX = 1.5;
2217
+ var MarkStroke = class {
2218
+ constructor(brush, options) {
2219
+ this.brush = brush;
2220
+ this.options = options;
2221
+ this.sampler = options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
2222
+ this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;
2223
+ this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
2224
+ this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
2225
+ this.seed = options.seed ?? 0;
2226
+ const fine = { ...brush, tip: { ...brush.tip, spacing: Math.max(0.01, Math.min(0.25, STEP_PX / Math.max(1, brush.tip.size))) } };
2227
+ this.walkers = this.copies.map((c, i) => new PenDabWalker(fine, (options.seed ?? 0) + i * 7919 | 0, c.linear));
2228
+ this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
2229
+ }
2230
+ brush;
2231
+ options;
2232
+ coverage = new CoverageMap();
2233
+ dabCount = 0;
2234
+ smoother;
2235
+ copies;
2236
+ walkers;
2237
+ depth;
2238
+ buf = new Float32Array(0);
2239
+ field = new Float32Array(0);
2240
+ lastRaw = null;
2241
+ lastFed = null;
2242
+ seed;
2243
+ sampler;
2244
+ tipTable;
2245
+ /** The pen dab the marks being made belong to, and its gradient colour. */
2246
+ pen = null;
2247
+ penColor = { rgb: [0, 0, 0], alpha: 1 };
2248
+ push(points) {
2249
+ const rects = [];
2250
+ for (const raw of points) {
2251
+ this.lastRaw = raw;
2252
+ const p = this.smoother.push(raw);
2253
+ this.lastFed = p;
2254
+ this.copies.forEach((c, i) => {
2255
+ const dabs = [];
2256
+ this.walkers[i].push(c.map(p), dabs);
2257
+ for (const d of dabs) this.step(i, d, rects);
2258
+ });
2259
+ }
2260
+ return rects;
2261
+ }
2262
+ step(copy, d, rects) {
2263
+ this.pen = d;
2264
+ if (this.sampler && !this.tipTable) this.sampler.dab(d, this.penColor);
2265
+ this.advance(copy, d, rects);
2266
+ }
2267
+ /** The colour of a mark at (x, y): its pen dab's, or across the tip by its offset from the pen. */
2268
+ markColor(x, y) {
2269
+ if (!this.sampler || !this.pen) return null;
2270
+ if (!this.tipTable) return this.penColor;
2271
+ const d = this.pen;
2272
+ const u = ((x - d.x) * Math.sin(d.direction) + (y - d.y) * Math.cos(d.direction)) / Math.max(0.5, d.radius);
2273
+ const last = this.tipTable.length - 1;
2274
+ return this.tipTable[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
2275
+ }
2276
+ finish() {
2277
+ const rects = [];
2278
+ const raw = this.lastRaw;
2279
+ const fed = this.lastFed;
2280
+ this.copies.forEach((c, i) => {
2281
+ const dabs = [];
2282
+ if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
2283
+ this.walkers[i].finish(dabs);
2284
+ for (const d of dabs) this.step(i, d, rects);
2285
+ });
2286
+ return rects;
2287
+ }
2288
+ /** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
2289
+ mark(x, y, r, alpha, opacity, pressure, rects) {
2290
+ if (!(r > 0.05) || !(alpha > 0)) return;
2291
+ const dab = { x, y, radius: Math.max(0.5, r), alpha: Math.min(1, alpha), opacity: Math.min(1, opacity), angle: 0, roundness: 1 };
2292
+ const hardness = this.brush.tip.hardness;
2293
+ const rect = dabRect(dab, hardness);
2294
+ const n = rect.width * rect.height;
2295
+ if (n <= 0) return;
2296
+ const vals = this.scratch(n);
2297
+ rasterizeDab(dab, hardness, rect, vals);
2298
+ this.lay(rect, vals, dab.opacity, pressure, x, y, rects);
2299
+ }
2300
+ /**
2301
+ * Stamps a textured mark: `kind` from the sticker stamps over a square of half-size `r`, turned
2302
+ * `rotation` radians clockwise, with flow `alpha` towards `opacity`. Small marks are supersampled.
2303
+ */
2304
+ stamp(x, y, r, kind, rotation, seed, alpha, opacity, pressure, rects) {
2305
+ if (!(r > 0.05) || !(alpha > 0)) return;
2306
+ const reach = Math.max(0.5, r) * Math.SQRT2 + 1;
2307
+ const rect = { x: Math.floor(x - reach), y: Math.floor(y - reach), width: 0, height: 0 };
2308
+ rect.width = Math.ceil(x + reach) - rect.x;
2309
+ rect.height = Math.ceil(y + reach) - rect.y;
2310
+ const n = rect.width * rect.height;
2311
+ const vals = this.scratch(n);
2312
+ const ss = r < 10 ? 3 : 1;
2313
+ const c = Math.cos(rotation);
2314
+ const s = Math.sin(rotation);
2315
+ const inv = 1 / Math.max(0.5, r);
2316
+ const a = Math.min(1, alpha);
2317
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
2318
+ for (let xx = 0; xx < rect.width; xx++, k++) {
2319
+ let sum = 0;
2320
+ for (let sy = 0; sy < ss; sy++) {
2321
+ for (let sx = 0; sx < ss; sx++) {
2322
+ const dx = rect.x + xx + (sx + 0.5) / ss - x;
2323
+ const dy = rect.y + yy + (sy + 0.5) / ss - y;
2324
+ sum += stampCoverage(kind, (dx * c + dy * s) * inv, (dy * c - dx * s) * inv, seed);
2325
+ }
2326
+ }
2327
+ vals[k] = sum / (ss * ss) * a;
2328
+ }
2329
+ }
2330
+ this.lay(rect, vals, Math.min(1, opacity), pressure, x, y, rects);
2331
+ }
2332
+ /** A scratch buffer of `n` values (reused; valid until the next mark). */
2333
+ scratch(n) {
2334
+ if (this.buf.length < n) this.buf = new Float32Array(n);
2335
+ const vals = this.buf.subarray(0, n);
2336
+ vals.fill(0);
2337
+ return vals;
2338
+ }
2339
+ /**
2340
+ * Lays coverage `vals` over `rect` towards `opacity`: paper grain, then effect paint or the
2341
+ * colour of a mark at (x, y) (gradient paint), into the stroke's coverage.
2342
+ */
2343
+ lay(rect, vals, opacity, pressure, x, y, rects) {
2344
+ const n = rect.width * rect.height;
2345
+ if (n <= 0) return;
2346
+ const grain = this.options.grain;
2347
+ if (this.depth > 0 && grain) {
2348
+ for (let yy = 0, k = 0; yy < rect.height; yy++)
2349
+ 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);
2350
+ }
2351
+ if (this.options.effect) {
2352
+ if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
2353
+ const field = this.field.subarray(0, n * 3);
2354
+ sampleEffectField(this.options.effect, rect, vals, field, this.pen);
2355
+ this.coverage.accumulate(rect, vals, opacity, null, field);
2356
+ } else {
2357
+ const color = this.markColor(x, y);
2358
+ if (color && color.alpha < 1) for (let k = 0; k < n; k++) vals[k] *= color.alpha;
2359
+ this.coverage.accumulate(rect, vals, opacity, color ? color.rgb : null);
2360
+ }
2361
+ rects.push(rect);
2362
+ this.dabCount++;
2363
+ }
2364
+ /** Marks along a segment, spaced by a fraction of the radius. */
2365
+ segment(x0, y0, x1, y1, r, alpha, opacity, pressure, rects) {
2366
+ const len = Math.hypot(x1 - x0, y1 - y0);
2367
+ const step = Math.max(0.35, r * 0.5);
2368
+ const n = Math.max(1, Math.ceil(len / step));
2369
+ for (let i = 1; i <= n; i++) this.mark(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, r, alpha, opacity, pressure, rects);
2370
+ }
2371
+ };
2372
+ var FUZZY_AMPLITUDE = 0.12;
2373
+ var NOMINAL_MS_PER_PX = 2;
2374
+ var RIBBON_SUB = 4;
2375
+ function chainDamp(damp, k) {
2376
+ return Math.min(damp, 1 / (1 + 0.8 * Math.sqrt(Math.max(0, k))) ** 2);
2377
+ }
2378
+ function addPolygonArea(pts, rect, out, weight = 1) {
2379
+ const n = pts.length >> 1;
2380
+ if (n < 3) return;
2381
+ let y0 = Infinity;
2382
+ let y1 = -Infinity;
2383
+ for (let i = 1; i < pts.length; i += 2) {
2384
+ if (pts[i] < y0) y0 = pts[i];
2385
+ if (pts[i] > y1) y1 = pts[i];
2386
+ }
2387
+ const r0 = Math.max(0, Math.floor(y0) - rect.y);
2388
+ const r1 = Math.min(rect.height - 1, Math.floor(y1) - rect.y);
2389
+ const xs = [];
2390
+ const w = weight / RIBBON_SUB;
2391
+ for (let row = r0; row <= r1; row++) {
2392
+ const base = row * rect.width;
2393
+ for (let sub = 0; sub < RIBBON_SUB; sub++) {
2394
+ const y = rect.y + row + (sub + 0.5) / RIBBON_SUB;
2395
+ xs.length = 0;
2396
+ for (let i = 0, j = n - 1; i < n; j = i++) {
2397
+ const yi = pts[i * 2 + 1];
2398
+ const yj = pts[j * 2 + 1];
2399
+ if (yi <= y !== yj <= y) xs.push(pts[i * 2] + (y - yi) / (yj - yi) * (pts[j * 2] - pts[i * 2]));
2400
+ }
2401
+ if (xs.length < 2) continue;
2402
+ xs.sort((a, b) => a - b);
2403
+ for (let k = 0; k + 1 < xs.length; k += 2) {
2404
+ const a = Math.max(rect.x, xs[k]);
2405
+ const b = Math.min(rect.x + rect.width, xs[k + 1]);
2406
+ if (!(b > a)) continue;
2407
+ for (let px = Math.floor(a); px < b; px++) {
2408
+ const cover = Math.min(b, px + 1) - Math.max(a, px);
2409
+ if (cover > 0) out[base + px - rect.x] += cover * w;
2410
+ }
2411
+ }
2412
+ }
2413
+ }
2414
+ }
2415
+ function curl(x, y, seed) {
2416
+ const n = (u, v) => {
2417
+ const x0 = Math.floor(u);
2418
+ const y0 = Math.floor(v);
2419
+ const fx = u - x0;
2420
+ const fy = v - y0;
2421
+ const sx = fx * fx * (3 - 2 * fx);
2422
+ const sy = fy * fy * (3 - 2 * fy);
2423
+ const h = (a2, b2) => rand01(seed, a2 * 73856093 + b2 * 19349663, 41);
2424
+ const a = h(x0, y0);
2425
+ const b = h(x0 + 1, y0);
2426
+ const c = h(x0, y0 + 1);
2427
+ const d = h(x0 + 1, y0 + 1);
2428
+ return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy;
2429
+ };
2430
+ const e = 0.01;
2431
+ return [(n(x, y + e) - n(x, y - e)) / (2 * e), -(n(x + e, y) - n(x - e, y)) / (2 * e)];
2432
+ }
2433
+ var ParticleStroke = class extends MarkStroke {
2434
+ sets;
2435
+ /** Ribbon pairs per copy: particles in their order around the pen (non-flow kinds). */
2436
+ orders;
2437
+ s;
2438
+ spawn = 0;
2439
+ /** Textured dots drawn so far, per copy (sequence order, random picks). */
2440
+ dots;
2441
+ /** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
2442
+ filled = /* @__PURE__ */ new Map();
2443
+ pts = [];
2444
+ constructor(brush, options) {
2445
+ super(brush, options);
2446
+ this.s = brush.particle;
2447
+ this.sets = this.copies.map(() => []);
2448
+ this.orders = this.copies.map(() => []);
2449
+ this.dots = this.copies.map(() => 0);
2450
+ }
2451
+ /** Strand kinds: a chain of masses behind each particle. */
2452
+ get strands() {
2453
+ return this.s.kind === "string" || this.s.kind === "elastic" || this.s.kind === "fuzzy";
2454
+ }
2455
+ newParticle(x, y, ox, oy, k, phase, i, copy) {
2456
+ const s = this.s;
2457
+ const fuzzy = s.kind === "fuzzy";
2458
+ const jitter = fuzzy ? 1 : s.wavePhaseJitter;
2459
+ return {
2460
+ x,
2461
+ y,
2462
+ vx: 0,
2463
+ vy: 0,
2464
+ ox,
2465
+ oy,
2466
+ k,
2467
+ phase,
2468
+ age: 0,
2469
+ run: 0,
2470
+ bead: 0,
2471
+ alive: true,
2472
+ wavePhase: jitter > 0 ? jitter * Math.PI * 2 * rand01(this.seed, i, 43 + copy) : 0,
2473
+ waveScale: fuzzy ? 0.6 + 0.8 * rand01(this.seed, i, 47 + copy) : 1,
2474
+ dx: x,
2475
+ dy: y,
2476
+ px: NaN,
2477
+ py: NaN,
2478
+ fade: 1
2479
+ };
2480
+ }
2481
+ init(copy, d) {
2482
+ const s = this.s;
2483
+ const list = this.sets[copy];
2484
+ const size = Math.max(1, d.radius * 2);
2485
+ for (let i = 0; i < (s.kind === "flow" ? 0 : s.count); i++) {
2486
+ const r = rand01(this.seed, i, 3 + copy) * s.spread * size * 0.5;
2487
+ const a = rand01(this.seed, i, 5 + copy) * Math.PI * 2;
2488
+ const ox = Math.cos(a) * r;
2489
+ const oy = Math.sin(a) * r;
2490
+ const p = this.newParticle(d.x + ox, d.y + oy, ox, oy, 0.6 + 0.8 * rand01(this.seed, i, 7), a, i, copy);
2491
+ if (this.strands) p.strand = Array.from({ length: 6 }, () => ({ x: d.x + ox, y: d.y + oy, vx: 0, vy: 0 }));
2492
+ if (s.kind === "orbit") {
2493
+ p.ox = Math.max(1, r + size * 0.15);
2494
+ p.oy = 0;
2495
+ }
2496
+ list.push(p);
2497
+ }
2498
+ this.orders[copy] = list.map((_, i) => i).sort((i, j) => list[i].phase - list[j].phase || i - j);
2499
+ }
2500
+ /** The sideways wave of particle `p` at pen dab `d` (px along the normal to the pen's direction). */
2501
+ wave(p, d, size) {
2502
+ const s = this.s;
2503
+ const amp = s.kind === "fuzzy" ? Math.max(FUZZY_AMPLITUDE, s.waveAmplitude) : s.waveAmplitude;
2504
+ if (!(amp > 0)) return 0;
2505
+ const ms = d.time ?? d.travel * NOMINAL_MS_PER_PX;
2506
+ const phase = 2 * Math.PI * d.travel / (s.waveLength * p.waveScale) - s.waveSpeed * 0.025 * ms + p.wavePhase;
2507
+ return amp * size * Math.sin(phase);
2508
+ }
2509
+ advance(copy, d, rects) {
2510
+ const s = this.s;
2511
+ let list = this.sets[copy];
2512
+ if (d.index === 0 && !list.length) this.init(copy, d);
2513
+ const size = Math.max(1, d.radius * 2);
2514
+ const markR = Math.max(0.5, size * s.size / 2);
2515
+ const k = 0.02 + 0.3 * s.stiffness;
2516
+ const damp = 0.6 + 0.38 * (1 - s.damping);
2517
+ const g = s.gravity * 0.15;
2518
+ const op = d.opacity;
2519
+ const al = Math.max(0.05, d.alpha);
2520
+ const nx = Math.sin(d.direction);
2521
+ const ny = Math.cos(d.direction);
2522
+ if (s.kind === "flow") {
2523
+ this.spawn += s.count / 8;
2524
+ while (this.spawn >= 1) {
2525
+ this.spawn -= 1;
2526
+ const i = this.dabCount + list.length * 31 + Math.floor(d.index);
2527
+ const a = rand01(this.seed, i, 11 + copy) * Math.PI * 2;
2528
+ const r = rand01(this.seed, i, 13 + copy) * s.spread * size * 0.5;
2529
+ list.push(this.newParticle(d.x + Math.cos(a) * r, d.y + Math.sin(a) * r, 0, 0, 1, a, i, copy));
2530
+ }
2531
+ }
2532
+ const lifeSteps = 8 + 120 * s.life;
2533
+ for (let i = 0; i < list.length; i++) {
2534
+ const p = list[i];
2535
+ if (!p.alive) continue;
2536
+ switch (s.kind) {
2537
+ case "spring": {
2538
+ p.vx = (p.vx + (d.x + p.ox - p.x) * k * p.k) * damp;
2539
+ p.vy = (p.vy + (d.y + p.oy - p.y) * k * p.k + g) * damp;
2540
+ p.x += p.vx;
2541
+ p.y += p.vy;
2542
+ break;
2543
+ }
2544
+ case "string":
2545
+ case "fuzzy": {
2546
+ const strand = p.strand;
2547
+ const dm = s.kind === "fuzzy" ? chainDamp(damp, k * p.k) : damp;
2548
+ let ax = d.x + p.ox;
2549
+ let ay = d.y + p.oy;
2550
+ for (const m of strand) {
2551
+ m.vx = (m.vx + (ax - m.x) * k * p.k + 0) * dm;
2552
+ m.vy = (m.vy + (ay - m.y) * k * p.k + g) * dm;
2553
+ m.x += m.vx;
2554
+ m.y += m.vy;
2555
+ ax = m.x;
2556
+ ay = m.y;
2557
+ }
2558
+ p.x = strand[strand.length - 1].x;
2559
+ p.y = strand[strand.length - 1].y;
2560
+ break;
2561
+ }
2562
+ case "elastic": {
2563
+ const strand = p.strand;
2564
+ const rest = size * 0.12;
2565
+ const ke = Math.min(0.9, (0.1 + 0.6 * s.stiffness) * p.k);
2566
+ const dm = chainDamp(damp, ke);
2567
+ let ax = d.x + p.ox;
2568
+ let ay = d.y + p.oy;
2569
+ for (const m of strand) {
2570
+ const ex = ax - m.x;
2571
+ const ey = ay - m.y;
2572
+ const len = Math.hypot(ex, ey);
2573
+ const pull = len > rest ? (len - rest) / len : 0;
2574
+ m.vx = (m.vx + ex * pull * ke) * dm;
2575
+ m.vy = (m.vy + ey * pull * ke + g) * dm;
2576
+ m.x += m.vx;
2577
+ m.y += m.vy;
2578
+ ax = m.x;
2579
+ ay = m.y;
2580
+ }
2581
+ p.x = strand[strand.length - 1].x;
2582
+ p.y = strand[strand.length - 1].y;
2583
+ break;
2584
+ }
2585
+ case "orbit": {
2586
+ p.phase += (0.05 + 0.3 * s.speed) * (i % 2 ? -1 : 1) * (0.7 + 0.6 * rand01(this.seed, i, 17));
2587
+ p.x = d.x + Math.cos(p.phase) * p.ox;
2588
+ p.y = d.y + Math.sin(p.phase) * p.ox + g * p.ox;
2589
+ break;
2590
+ }
2591
+ case "flow": {
2592
+ const [fx, fy] = curl(p.x / Math.max(8, size * 1.5), p.y / Math.max(8, size * 1.5), this.seed);
2593
+ const push = size * 0.012 * (0.3 + 1.5 * s.speed);
2594
+ const fl = Math.hypot(fx, fy) || 1;
2595
+ p.vx = p.vx * 0.8 + fx / fl * push;
2596
+ p.vy = p.vy * 0.8 + fy / fl * push + g * 0.2;
2597
+ p.x += p.vx;
2598
+ p.y += p.vy;
2599
+ p.age++;
2600
+ if (p.age > lifeSteps) p.alive = false;
2601
+ break;
2602
+ }
2603
+ }
2604
+ const w = this.wave(p, d, size);
2605
+ const qx = w ? p.x + nx * w : p.x;
2606
+ const qy = w ? p.y + ny * w : p.y;
2607
+ const px = p.dx;
2608
+ const py = p.dy;
2609
+ p.px = p.dx;
2610
+ p.py = p.dy;
2611
+ p.dx = qx;
2612
+ p.dy = qy;
2613
+ const fade = s.kind === "flow" ? 1 - p.age / lifeSteps : 1;
2614
+ p.fade = fade;
2615
+ const moved = Math.hypot(qx - px, qy - py);
2616
+ if (d.index === 0 && s.kind !== "flow") continue;
2617
+ if (s.render === "dot") this.dot(copy, qx, qy, qx - px, qy - py, markR * fade, al * fade, op, d, rects);
2618
+ else if (s.render === "line" || s.render === "ribbon") this.segment(px, py, qx, qy, markR * fade, al * fade, op, d.pressure, rects);
2619
+ else {
2620
+ p.run += moved;
2621
+ const gap = Math.max(markR * 2, size * (0.05 + s.interval));
2622
+ while (p.run >= gap) {
2623
+ p.run -= gap;
2624
+ p.bead++;
2625
+ const t = moved > 0 ? 1 - p.run / Math.max(moved, 1e-6) : 1;
2626
+ const bx = px + (qx - px) * Math.min(1, Math.max(0, t));
2627
+ const by = py + (qy - py) * Math.min(1, Math.max(0, t));
2628
+ this.dot(copy, bx, by, qx - px, qy - py, markR * (0.7 + 0.6 * (p.bead % 3 / 2)) * fade, al * fade, op, d, rects);
2629
+ }
2630
+ }
2631
+ }
2632
+ if (s.render === "ribbon" && !(d.index === 0 && s.kind !== "flow")) this.ribbon(copy, list, op, al, d, rects);
2633
+ if (s.kind === "flow" && list.length > 512) {
2634
+ list = list.filter((p) => p.alive);
2635
+ this.sets[copy] = list;
2636
+ }
2637
+ }
2638
+ /** A dot (or bead): round, or a textured stamp turned by the particle's motion (mx, my), at random or to the tip angle. */
2639
+ dot(copy, x, y, mx, my, r, alpha, opacity, d, rects) {
2640
+ const s = this.s;
2641
+ const textures = s.dotTextures;
2642
+ if (!textures.length) return this.mark(x, y, r, alpha, opacity, d.pressure, rects);
2643
+ const n = this.dots[copy]++;
2644
+ const pick = s.dotTextureOrder === "random" ? Math.min(textures.length - 1, Math.floor(rand01(this.seed, n, 31 + copy) * textures.length)) : n % textures.length;
2645
+ 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;
2646
+ this.stamp(x, y, r, textures[pick], rotation, this.seed + n * 2654435761 >>> 0, alpha, opacity, d.pressure, rects);
2647
+ }
2648
+ /**
2649
+ * Ribbon fill for one step: the band between each pair of joined particles, from where they
2650
+ * were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
2651
+ * share edges exactly, so the fill has no seams; it is telescoped into the coverage
2652
+ * (`telescope`), so it does not build up at the joins.
2653
+ */
2654
+ ribbon(copy, list, opacity, alpha, d, rects) {
2655
+ const s = this.s;
2656
+ if (!(s.ribbonFill > 0)) return;
2657
+ const live = (p) => !!p && p.alive && !Number.isNaN(p.px);
2658
+ let order;
2659
+ if (s.kind === "flow") order = list.filter(live);
2660
+ else order = this.orders[copy].map((i) => list[i]).filter(live);
2661
+ if (order.length < 2) return;
2662
+ const pairs = [];
2663
+ if (s.ribbonPairs === "first-last") pairs.push([order[0], order[order.length - 1]]);
2664
+ else for (let i = 0; i + 1 < order.length; i++) pairs.push([order[i], order[i + 1]]);
2665
+ const pts = this.pts;
2666
+ const ceil = Math.min(1, opacity * s.ribbonFill);
2667
+ for (const [a, b] of pairs) {
2668
+ pts.length = 0;
2669
+ pts.push(a.px, a.py, a.dx, a.dy, b.dx, b.dy, b.px, b.py);
2670
+ let x0 = Infinity;
2671
+ let y0 = Infinity;
2672
+ let x1 = -Infinity;
2673
+ let y1 = -Infinity;
2674
+ for (let i = 0; i < 8; i += 2) {
2675
+ x0 = Math.min(x0, pts[i]);
2676
+ x1 = Math.max(x1, pts[i]);
2677
+ y0 = Math.min(y0, pts[i + 1]);
2678
+ y1 = Math.max(y1, pts[i + 1]);
2679
+ }
2680
+ 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 };
2681
+ const area = this.scratch(rect.width * rect.height);
2682
+ addPolygonArea(pts, rect, area, Math.min(a.fade, b.fade) * alpha);
2683
+ if (this.telescope(rect, area)) this.lay(rect, area, ceil, d.pressure, (a.dx + b.dx) / 2, (a.dy + b.dy) / 2, rects);
2684
+ }
2685
+ }
2686
+ /**
2687
+ * Turns the area newly filled over `rect` into coverage to lay towards the fill's ceiling: the
2688
+ * share of what was left unfilled, so after any number of bands (in any grouping) a pixel's
2689
+ * coverage is the ceiling times the area filled there. False when nothing was added.
2690
+ */
2691
+ telescope(rect, area) {
2692
+ let any = false;
2693
+ for (let yy = 0, k = 0; yy < rect.height; yy++) {
2694
+ const gy = rect.y + yy;
2695
+ const ty = Math.floor(gy / TILE);
2696
+ let t;
2697
+ let tx = NaN;
2698
+ for (let xx = 0; xx < rect.width; xx++, k++) {
2699
+ const add = area[k];
2700
+ if (!(add > 0)) continue;
2701
+ const gx = rect.x + xx;
2702
+ const ntx = Math.floor(gx / TILE);
2703
+ if (ntx !== tx || !t) {
2704
+ tx = ntx;
2705
+ const key = tileKey(tx, ty);
2706
+ t = this.filled.get(key);
2707
+ if (!t) {
2708
+ t = new Float32Array(TILE * TILE);
2709
+ this.filled.set(key, t);
2710
+ }
2711
+ }
2712
+ const ti = (gy - ty * TILE) * TILE + (gx - tx * TILE);
2713
+ const before = t[ti];
2714
+ const after = Math.min(1, before + add);
2715
+ t[ti] = after;
2716
+ area[k] = before < 1 ? (after - before) / (1 - before) : 0;
2717
+ any = true;
2718
+ }
2719
+ }
2720
+ return any;
2721
+ }
2722
+ };
2723
+ var BristleStroke = class extends MarkStroke {
2724
+ tufts;
2725
+ s;
2726
+ constructor(brush, options) {
2727
+ super(brush, options);
2728
+ this.s = brush.bristle;
2729
+ const s = this.s;
2730
+ this.tufts = this.copies.map(
2731
+ (_, c) => Array.from({ length: s.count }, (_2, i) => {
2732
+ const a = rand01(this.seed, i, 21 + c) * Math.PI * 2;
2733
+ const r = Math.sqrt(rand01(this.seed, i, 23 + c));
2734
+ return {
2735
+ u: Math.cos(a) * r,
2736
+ v: Math.sin(a) * r,
2737
+ x: 0,
2738
+ y: 0,
2739
+ paint: 1 - s.jitter * 0.4 * rand01(this.seed, i, 25 + c),
2740
+ thirst: 1 + s.jitter * (rand01(this.seed, i, 27 + c) * 2 - 1),
2741
+ w: 1 + s.jitter * 0.6 * (rand01(this.seed, i, 29 + c) - 0.5),
2742
+ started: false
2743
+ };
2744
+ })
2745
+ );
2746
+ }
2747
+ advance(copy, d, rects) {
2748
+ const s = this.s;
2749
+ const tuft = this.tufts[copy];
2750
+ const spread = d.radius * (0.55 + 0.9 * s.splay * d.pressure);
2751
+ const ca = Math.cos(d.angle);
2752
+ const sa = Math.sin(d.angle);
2753
+ const ro = Math.max(0.15, d.roundness);
2754
+ const tx = Math.cos(d.direction);
2755
+ const ty = -Math.sin(d.direction);
2756
+ const follow = 0.15 + 0.85 * s.stiffness;
2757
+ const bend = d.radius * (1 - s.stiffness) * d.pressure * 0.8;
2758
+ const lineR = Math.max(0.5, d.radius * 2 * s.thickness * 0.5);
2759
+ const dryRate = 4e-3 * (1 - s.load);
2760
+ for (const b of tuft) {
2761
+ const lu = b.u * spread;
2762
+ const lv = b.v * spread * ro;
2763
+ const ox = lu * ca - lv * sa;
2764
+ const oy = -(lu * sa + lv * ca);
2765
+ const lag = bend * (1 - 0.5 * b.v);
2766
+ const wx = d.x + ox - tx * lag;
2767
+ const wy = d.y + oy - ty * lag;
2768
+ if (!b.started) {
2769
+ b.x = wx;
2770
+ b.y = wy;
2771
+ b.started = true;
2772
+ continue;
2773
+ }
2774
+ const nx = b.x + (wx - b.x) * follow;
2775
+ const ny = b.y + (wy - b.y) * follow;
2776
+ const paint = Math.max(0, b.paint);
2777
+ 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);
2778
+ b.paint -= dryRate * b.thirst * Math.hypot(nx - b.x, ny - b.y);
2779
+ b.x = nx;
2780
+ b.y = ny;
2781
+ }
2782
+ }
2783
+ };
2784
+
1286
2785
  // src/paint-editor/stroke.ts
1287
2786
  function defaultTile(surface) {
1288
2787
  const data = new Uint8ClampedArray(TILE * TILE * surface.channels);
@@ -3232,9 +4731,9 @@ function perspectiveGuideLines(g, size, count = 12) {
3232
4731
  }
3233
4732
 
3234
4733
  // src/paint-editor/color.ts
3235
- var HEX = /^#?([0-9a-f]{6})$/i;
4734
+ var HEX2 = /^#?([0-9a-f]{6})$/i;
3236
4735
  function parseHexColor(value, what = "color") {
3237
- const m = typeof value === "string" ? HEX.exec(value.trim()) : null;
4736
+ const m = typeof value === "string" ? HEX2.exec(value.trim()) : null;
3238
4737
  if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
3239
4738
  const n = parseInt(m[1], 16);
3240
4739
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
@@ -4084,10 +5583,10 @@ var PaintEditorError = class extends Error {
4084
5583
  this.name = "PaintEditorError";
4085
5584
  }
4086
5585
  };
4087
- var HEX2 = /^#[0-9a-f]{6}$/i;
5586
+ var HEX3 = /^#[0-9a-f]{6}$/i;
4088
5587
  var MAX_ADD_STICKERS = 500;
4089
5588
  function parseColor(value, what = "color") {
4090
- if (typeof value !== "string" || !HEX2.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
5589
+ if (typeof value !== "string" || !HEX3.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
4091
5590
  return value.trim().toLowerCase();
4092
5591
  }
4093
5592
  function rgbaOf(hex) {
@@ -9479,7 +10978,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
9479
10978
  annotations: { title: "Get the paper", ...READ6 },
9480
10979
  handler: () => JSON.parse(JSON.stringify(c.document.paper))
9481
10980
  });
9482
- const tuning = {
10981
+ const tuning2 = {
9483
10982
  type: "object",
9484
10983
  properties: {
9485
10984
  contrast: { type: "number", minimum: 0, maximum: 3, description: "Height contrast around the middle (default 1)." },
@@ -9502,8 +11001,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
9502
11001
  pattern: { type: "string", enum: [...GRAIN_PATTERNS], description: "Grain pattern." },
9503
11002
  tiling: { type: "string", enum: ["repeat", "mirror"], description: "How the grain tile repeats (mirror hides seams)." },
9504
11003
  shading: { type: "number", minimum: 0, maximum: 1, description: "How much the grain shades the paper colour." },
9505
- dry: { ...tuning, description: "Height map tuning for dry media." },
9506
- wet: { ...tuning, description: "Height map tuning for wet media (the grain water and pigment follow)." },
11004
+ dry: { ...tuning2, description: "Height map tuning for dry media." },
11005
+ wet: { ...tuning2, description: "Height map tuning for wet media (the grain water and pigment follow)." },
9507
11006
  absorption: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how fast the paper soaks up water (high: washes stop spreading sooner)." },
9508
11007
  permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how freely water flows over the fibres (low: more ragged, slower spreading)." },
9509
11008
  roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain pins wet edges and catches granulating pigment." },