@pixerate/editor 0.249.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 (158) 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 +124 -123
  6. package/dist/animation-editor/index.js +15 -14
  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-T3JZST44.js → brushstrokeRender-5PYSKZIJ.js} +9 -8
  24. package/dist/{brushstrokeRender-GCKK7V2I.cjs → brushstrokeRender-7FLLSQEC.cjs} +15 -14
  25. package/dist/canvas/index.cjs +120 -65
  26. package/dist/canvas/index.d.cts +1 -0
  27. package/dist/canvas/index.d.ts +1 -0
  28. package/dist/canvas/index.js +88 -33
  29. package/dist/canvas/stickers.d.cts +35 -0
  30. package/dist/canvas/stickers.d.ts +35 -0
  31. package/dist/chart/index.cjs +159 -160
  32. package/dist/chart/index.js +9 -10
  33. package/dist/{chunk-RXZL4NG5.js → chunk-226QDRO3.js} +1 -1
  34. package/dist/{chunk-DOIU7VJG.js → chunk-2ZJ4DT3M.js} +1 -1
  35. package/dist/{chunk-XZNU4DUX.cjs → chunk-3NS5C7FB.cjs} +76 -76
  36. package/dist/{chunk-UTI5IITZ.cjs → chunk-4ZC5QJE2.cjs} +3 -3
  37. package/dist/{chunk-L3JFOTBQ.js → chunk-5E2Q7ZHI.js} +1 -1
  38. package/dist/{chunk-6BIDVPED.js → chunk-5JNA6LLY.js} +1 -1
  39. package/dist/{chunk-RD7RX52G.cjs → chunk-5UEFBM2M.cjs} +207 -18
  40. package/dist/{chunk-WA44NKPF.js → chunk-62Z5WLRA.js} +1 -1
  41. package/dist/{chunk-BVRRIW7F.js → chunk-6GRUVGKU.js} +3 -3
  42. package/dist/{chunk-QARCM66V.js → chunk-6ZZKRH7R.js} +3 -3
  43. package/dist/{chunk-BP6KSGVD.js → chunk-7272D3K2.js} +4 -6
  44. package/dist/{chunk-O75V4ZS3.js → chunk-7P6VF3FG.js} +2 -2
  45. package/dist/{chunk-AU6MWQKZ.js → chunk-ABL7IWFZ.js} +1 -1
  46. package/dist/{chunk-5RH3VXAK.js → chunk-APX63ILU.js} +4 -100
  47. package/dist/{chunk-W7L4HUT2.cjs → chunk-CJ6TBL2N.cjs} +16 -16
  48. package/dist/{chunk-7PT4RJL5.cjs → chunk-EUIDTZNQ.cjs} +111 -113
  49. package/dist/{chunk-CXXXAZ7H.js → chunk-F6R67I3A.js} +1 -1
  50. package/dist/{chunk-I7NJPHWV.cjs → chunk-FQPUF2W2.cjs} +35 -606
  51. package/dist/{chunk-D5KGAO4Z.js → chunk-GCVFEPJG.js} +2 -573
  52. package/dist/{chunk-LEQKZGQI.js → chunk-GOBOCZZI.js} +1 -1
  53. package/dist/{chunk-PZP2SZIZ.js → chunk-GYMMNCFT.js} +3 -3
  54. package/dist/{chunk-NUTMINJZ.cjs → chunk-H7ET2ISK.cjs} +52 -52
  55. package/dist/chunk-HS6QDWDF.cjs +1029 -0
  56. package/dist/{chunk-T7KQEMAE.cjs → chunk-HUAISCUX.cjs} +18 -18
  57. package/dist/{chunk-YGQVCQTG.js → chunk-ILP77UD6.js} +1 -1
  58. package/dist/{chunk-GHCSC2WP.cjs → chunk-ISIO63Q5.cjs} +21 -15
  59. package/dist/{chunk-S6OT6CTS.cjs → chunk-IWEVV63O.cjs} +31 -33
  60. package/dist/{chunk-OFH6Q67U.js → chunk-JA57JIT5.js} +9 -3
  61. package/dist/{chunk-LP2MCSBE.cjs → chunk-JFHBXNR2.cjs} +88 -88
  62. package/dist/{chunk-IOFVM3L3.cjs → chunk-JPEHXIB5.cjs} +10 -10
  63. package/dist/chunk-JSDND7DF.cjs +173 -0
  64. package/dist/{chunk-36ZTV7WO.js → chunk-L6Y7TSQ4.js} +1 -1
  65. package/dist/{chunk-KYX7NZ5V.cjs → chunk-LFMKWGFL.cjs} +1 -1
  66. package/dist/{chunk-V6DT2CDH.js → chunk-M2TPMYJE.js} +1 -1
  67. package/dist/{chunk-LJZRHVZV.cjs → chunk-M5GLQQWG.cjs} +2 -2
  68. package/dist/{chunk-JEGJP4OW.js → chunk-MHLU6GRJ.js} +196 -7
  69. package/dist/{chunk-KMPM5JM5.js → chunk-MONXNVZK.js} +4 -6
  70. package/dist/chunk-MTKY6K3Q.js +1029 -0
  71. package/dist/{chunk-MOW3JMNG.cjs → chunk-N76AMOKE.cjs} +42 -42
  72. package/dist/{chunk-75K3W4D6.cjs → chunk-NKGB64CW.cjs} +16 -16
  73. package/dist/{chunk-SGPIVHJB.js → chunk-P3K2JCJA.js} +3 -3
  74. package/dist/chunk-PXDIAYB6.cjs +594 -0
  75. package/dist/{chunk-UBRJOIR5.js → chunk-QC3OXRE4.js} +9 -9
  76. package/dist/{chunk-QOBCBGGI.cjs → chunk-QKXO6JPP.cjs} +14 -14
  77. package/dist/{chunk-WLJZZHUZ.cjs → chunk-QLSGTCYT.cjs} +23 -23
  78. package/dist/{chunk-7YW7T5TJ.js → chunk-RHPSE2YR.js} +2 -2
  79. package/dist/{chunk-44BMJJVA.cjs → chunk-RUONPPMC.cjs} +8 -8
  80. package/dist/{chunk-LJRCU3BJ.cjs → chunk-SABJMH6M.cjs} +150 -15
  81. package/dist/{chunk-XWOLPNBW.cjs → chunk-SOSBOS5T.cjs} +22 -22
  82. package/dist/chunk-SZTI6EER.js +13 -0
  83. package/dist/{chunk-PPFJHTW5.cjs → chunk-TFF2GTGN.cjs} +22 -24
  84. package/dist/{chunk-2SMMZDMG.cjs → chunk-TLOGFIIV.cjs} +37 -37
  85. package/dist/chunk-TXFWZ5ZJ.js +594 -0
  86. package/dist/chunk-U67EN7GG.cjs +13 -0
  87. package/dist/{chunk-GXAK6Q5J.js → chunk-UFEDHACQ.js} +2 -2
  88. package/dist/{chunk-GA4EU4P3.cjs → chunk-UXETV2NC.cjs} +4 -4
  89. package/dist/{chunk-N4DFDI73.js → chunk-WKMUBATU.js} +1 -1
  90. package/dist/{chunk-3R5P3DVY.cjs → chunk-WRZ6C4TU.cjs} +6 -6
  91. package/dist/{chunk-EJLRBS5Q.js → chunk-WZOGDKCQ.js} +137 -2
  92. package/dist/{chunk-6ADHGQ73.js → chunk-YBAPT7NL.js} +2 -4
  93. package/dist/{chunk-K5DS2JTA.cjs → chunk-Z3BRH4JK.cjs} +6 -6
  94. package/dist/{chunk-XNZFVYC2.cjs → chunk-ZBCT5MEA.cjs} +7 -7
  95. package/dist/color/index.cjs +4 -6
  96. package/dist/color/index.js +7 -9
  97. package/dist/effects/helpers.d.cts +6 -0
  98. package/dist/effects/helpers.d.ts +6 -0
  99. package/dist/effects/index.cjs +62 -60
  100. package/dist/effects/index.js +5 -3
  101. package/dist/effects/tools.d.cts +6 -0
  102. package/dist/effects/tools.d.ts +6 -0
  103. package/dist/export/index.cjs +7 -8
  104. package/dist/export/index.js +6 -7
  105. package/dist/extensions/index.cjs +10 -11
  106. package/dist/extensions/index.js +9 -10
  107. package/dist/gradient/index.cjs +33 -34
  108. package/dist/gradient/index.js +4 -5
  109. package/dist/image-editor/effects.d.cts +1 -1
  110. package/dist/image-editor/effects.d.ts +1 -1
  111. package/dist/image-editor/index.cjs +887 -863
  112. package/dist/image-editor/index.d.cts +1 -0
  113. package/dist/image-editor/index.d.ts +1 -0
  114. package/dist/image-editor/index.js +69 -45
  115. package/dist/image-editor/stickers.d.cts +9 -0
  116. package/dist/image-editor/stickers.d.ts +9 -0
  117. package/dist/index.cjs +16 -17
  118. package/dist/index.js +10 -11
  119. package/dist/paint/index.cjs +6 -4
  120. package/dist/paint/index.js +21 -19
  121. package/dist/paint-editor/brushes.d.cts +1 -2
  122. package/dist/paint-editor/brushes.d.ts +1 -2
  123. package/dist/paint-editor/index.cjs +2199 -699
  124. package/dist/paint-editor/index.js +1577 -77
  125. package/dist/paint-editor/mark-settings.d.cts +104 -0
  126. package/dist/paint-editor/mark-settings.d.ts +104 -0
  127. package/dist/paint-editor/procedural.d.cts +2 -75
  128. package/dist/paint-editor/procedural.d.ts +2 -75
  129. package/dist/paint-editor/stickers.d.cts +1 -22
  130. package/dist/paint-editor/stickers.d.ts +1 -22
  131. package/dist/raster/index.cjs +2 -2
  132. package/dist/raster/index.js +9 -9
  133. package/dist/stickers/index.cjs +22 -16
  134. package/dist/stickers/index.d.cts +1 -1
  135. package/dist/stickers/index.d.ts +1 -1
  136. package/dist/stickers/index.js +14 -8
  137. package/dist/stickers/tools.d.cts +57 -1
  138. package/dist/stickers/tools.d.ts +57 -1
  139. package/dist/tokens/index.cjs +3 -4
  140. package/dist/tokens/index.js +2 -3
  141. package/dist/typography/index.cjs +7 -8
  142. package/dist/typography/index.js +2 -3
  143. package/dist/vector/index.cjs +1011 -977
  144. package/dist/vector/index.d.cts +1 -0
  145. package/dist/vector/index.d.ts +1 -0
  146. package/dist/vector/index.js +47 -13
  147. package/dist/vector/stickers.d.cts +11 -0
  148. package/dist/vector/stickers.d.ts +11 -0
  149. package/dist/video-editor/index.cjs +349 -350
  150. package/dist/video-editor/index.js +9 -10
  151. package/package.json +11 -1
  152. package/skills/pixerate-image-editor/SKILL.md +1 -1
  153. package/skills/pixerate-stickers/SKILL.md +13 -1
  154. package/dist/chunk-7AHBW2CQ.cjs +0 -200
  155. package/dist/chunk-F67XMVDI.js +0 -200
  156. package/dist/chunk-H4AOUUBE.cjs +0 -2635
  157. package/dist/chunk-J7T5QZ45.js +0 -2635
  158. package/dist/chunk-ZEYMKDYS.cjs +0 -269
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  StickerBrowser,
3
+ createStickerTools,
4
+ describeStickerRef,
3
5
  editorStickers
4
- } from "../chunk-EJLRBS5Q.js";
6
+ } from "../chunk-WZOGDKCQ.js";
5
7
  import {
6
8
  toVideoFfmpeg
7
- } from "../chunk-L3JFOTBQ.js";
9
+ } from "../chunk-5E2Q7ZHI.js";
8
10
  import {
9
11
  createLayerEffectTools,
10
12
  deserializeLayers,
@@ -12,7 +14,7 @@ import {
12
14
  resolveExternalAssets,
13
15
  serializeLayers,
14
16
  serializeLayersExternal
15
- } from "../chunk-UBRJOIR5.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-PZP2SZIZ.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,41 +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-J7T5QZ45.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-5RH3VXAK.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";
131
+ import {
132
+ StickerError
133
+ } from "../chunk-SZTI6EER.js";
170
134
  import {
171
135
  EffectRendererPool
172
136
  } from "../chunk-EHLOCVWB.js";
@@ -175,15 +139,20 @@ import {
175
139
  EffectsSlot,
176
140
  createEffectToolProvider,
177
141
  resolveEffectValues
178
- } from "../chunk-OFH6Q67U.js";
142
+ } from "../chunk-JA57JIT5.js";
179
143
  import "../chunk-V2ZCJRDM.js";
144
+ import {
145
+ GRAIN_PATTERNS,
146
+ GRAIN_TILE,
147
+ PaperGrain,
148
+ grainCatch,
149
+ grainHeights
150
+ } from "../chunk-TXFWZ5ZJ.js";
180
151
  import {
181
152
  CoverageMap,
182
153
  DEFAULT_PAINT_GRADIENT,
183
154
  DEFAULT_PAINT_GRADIENT_MAPPING,
184
155
  EMPTY_MIXER_LOAD,
185
- GRAIN_PATTERNS,
186
- GRAIN_TILE,
187
156
  MAX_BRUSH_SIZE,
188
157
  MIXING_MODELS,
189
158
  PAINT_COLOR_JSON_SCHEMA,
@@ -201,8 +170,8 @@ import {
201
170
  PaintExchange,
202
171
  PaintGradientSampler,
203
172
  PaintSourceError,
204
- PaperGrain,
205
173
  SmudgeTip,
174
+ TIP_GRADIENT_STEPS,
206
175
  TileMedium,
207
176
  addPaintGradientStop,
208
177
  applyStrokeToTile,
@@ -211,8 +180,6 @@ import {
211
180
  createBlenderScratch,
212
181
  dabRect,
213
182
  exchangeRates,
214
- grainCatch,
215
- grainHeights,
216
183
  hexToPaintRgb,
217
184
  isGradientPaint,
218
185
  loadMixer,
@@ -230,7 +197,7 @@ import {
230
197
  scalePaintGradient,
231
198
  solidPaint,
232
199
  updatePaintGradientStop
233
- } from "../chunk-D5KGAO4Z.js";
200
+ } from "../chunk-GCVFEPJG.js";
234
201
  import {
235
202
  TileableNoise,
236
203
  hash2,
@@ -240,15 +207,6 @@ import {
240
207
  seedFromString
241
208
  } from "../chunk-4D6KP5BV.js";
242
209
  import "../chunk-ODBT55Q7.js";
243
- import {
244
- BLEND_MODES,
245
- PAINTERLY_BLEND_MODES,
246
- isBlendMode,
247
- isPainterlyBlendMode,
248
- mixPigment8,
249
- mixPigments,
250
- standardBlendMode
251
- } from "../chunk-IRSTHLQQ.js";
252
210
  import {
253
211
  base64ToBytes,
254
212
  bytesToBase64,
@@ -258,6 +216,15 @@ import {
258
216
  parseDataUrl,
259
217
  pngDataUrl
260
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";
261
228
  import {
262
229
  TILE,
263
230
  TiledSurface,
@@ -282,22 +249,156 @@ import {
282
249
  defineEditorDocument
283
250
  } from "../chunk-XBQX76CT.js";
284
251
  import "../chunk-HF5QZS7Z.js";
285
- import "../chunk-V6DT2CDH.js";
252
+ import "../chunk-M2TPMYJE.js";
286
253
  import "../chunk-2WPNAGHI.js";
287
254
  import {
288
255
  HARMONY_KINDS,
289
256
  colorHarmony,
290
257
  hueToWheel,
291
258
  wheelToHue
292
- } from "../chunk-BP6KSGVD.js";
293
- import "../chunk-6ADHGQ73.js";
294
- import "../chunk-JEGJP4OW.js";
259
+ } from "../chunk-7272D3K2.js";
260
+ import "../chunk-YBAPT7NL.js";
295
261
  import {
296
262
  linearToSrgb,
263
+ oklabToOklch,
264
+ oklabToRgb,
265
+ oklchToOklab,
266
+ rgbToOklab,
297
267
  srgbToLinear
298
- } from "../chunk-F67XMVDI.js";
268
+ } from "../chunk-MHLU6GRJ.js";
299
269
  import "../chunk-6ONMQ6WF.js";
300
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
+
301
402
  // src/paint-editor/document.ts
302
403
  var MIN_CANVAS_SIDE = 1;
303
404
  var MAX_CANVAS_SIDE = 16384;
@@ -332,6 +433,426 @@ function createPaintDocument(options = {}) {
332
433
  return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
333
434
  }
334
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
+
335
856
  // src/paint-editor/dab-stroke.ts
336
857
  var GRADIENT_STEPS = 32;
337
858
  function multiColorPalette(mc, color, secondary) {
@@ -1282,6 +1803,985 @@ function lightFromJSON(raw) {
1282
1803
  return surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
1283
1804
  }
1284
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
+
1285
2785
  // src/paint-editor/stroke.ts
1286
2786
  function defaultTile(surface) {
1287
2787
  const data = new Uint8ClampedArray(TILE * TILE * surface.channels);
@@ -3231,9 +4731,9 @@ function perspectiveGuideLines(g, size, count = 12) {
3231
4731
  }
3232
4732
 
3233
4733
  // src/paint-editor/color.ts
3234
- var HEX = /^#?([0-9a-f]{6})$/i;
4734
+ var HEX2 = /^#?([0-9a-f]{6})$/i;
3235
4735
  function parseHexColor(value, what = "color") {
3236
- const m = typeof value === "string" ? HEX.exec(value.trim()) : null;
4736
+ const m = typeof value === "string" ? HEX2.exec(value.trim()) : null;
3237
4737
  if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
3238
4738
  const n = parseInt(m[1], 16);
3239
4739
  return [n >> 16 & 255, n >> 8 & 255, n & 255];
@@ -4083,10 +5583,10 @@ var PaintEditorError = class extends Error {
4083
5583
  this.name = "PaintEditorError";
4084
5584
  }
4085
5585
  };
4086
- var HEX2 = /^#[0-9a-f]{6}$/i;
5586
+ var HEX3 = /^#[0-9a-f]{6}$/i;
4087
5587
  var MAX_ADD_STICKERS = 500;
4088
5588
  function parseColor(value, what = "color") {
4089
- 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".`);
4090
5590
  return value.trim().toLowerCase();
4091
5591
  }
4092
5592
  function rgbaOf(hex) {
@@ -9478,7 +10978,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
9478
10978
  annotations: { title: "Get the paper", ...READ6 },
9479
10979
  handler: () => JSON.parse(JSON.stringify(c.document.paper))
9480
10980
  });
9481
- const tuning = {
10981
+ const tuning2 = {
9482
10982
  type: "object",
9483
10983
  properties: {
9484
10984
  contrast: { type: "number", minimum: 0, maximum: 3, description: "Height contrast around the middle (default 1)." },
@@ -9501,8 +11001,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
9501
11001
  pattern: { type: "string", enum: [...GRAIN_PATTERNS], description: "Grain pattern." },
9502
11002
  tiling: { type: "string", enum: ["repeat", "mirror"], description: "How the grain tile repeats (mirror hides seams)." },
9503
11003
  shading: { type: "number", minimum: 0, maximum: 1, description: "How much the grain shades the paper colour." },
9504
- dry: { ...tuning, description: "Height map tuning for dry media." },
9505
- 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)." },
9506
11006
  absorption: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how fast the paper soaks up water (high: washes stop spreading sooner)." },
9507
11007
  permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how freely water flows over the fibres (low: more ragged, slower spreading)." },
9508
11008
  roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain pins wet edges and catches granulating pigment." },