@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.
- package/dist/agent/index.cjs +40 -41
- package/dist/agent/index.js +9 -10
- package/dist/animation/index.cjs +6 -7
- package/dist/animation/index.js +5 -6
- package/dist/animation-editor/index.cjs +124 -123
- package/dist/animation-editor/index.js +15 -14
- package/dist/brushes/index.cjs +1406 -0
- package/dist/brushes/index.d.cts +12 -0
- package/dist/brushes/index.d.ts +12 -0
- package/dist/brushes/index.js +1406 -0
- package/dist/brushes/migrate.d.cts +17 -0
- package/dist/brushes/migrate.d.ts +17 -0
- package/dist/brushes/normalize.d.cts +15 -0
- package/dist/brushes/normalize.d.ts +15 -0
- package/dist/brushes/presets.d.cts +12 -0
- package/dist/brushes/presets.d.ts +12 -0
- package/dist/brushes/resolve.d.cts +56 -0
- package/dist/brushes/resolve.d.ts +56 -0
- package/dist/brushes/schema.d.cts +29 -0
- package/dist/brushes/schema.d.ts +29 -0
- package/dist/brushes/types.d.cts +323 -0
- package/dist/brushes/types.d.ts +323 -0
- package/dist/{brushstrokeRender-T3JZST44.js → brushstrokeRender-5PYSKZIJ.js} +9 -8
- package/dist/{brushstrokeRender-GCKK7V2I.cjs → brushstrokeRender-7FLLSQEC.cjs} +15 -14
- package/dist/canvas/index.cjs +120 -65
- package/dist/canvas/index.d.cts +1 -0
- package/dist/canvas/index.d.ts +1 -0
- package/dist/canvas/index.js +88 -33
- package/dist/canvas/stickers.d.cts +35 -0
- package/dist/canvas/stickers.d.ts +35 -0
- package/dist/chart/index.cjs +159 -160
- package/dist/chart/index.js +9 -10
- package/dist/{chunk-RXZL4NG5.js → chunk-226QDRO3.js} +1 -1
- package/dist/{chunk-DOIU7VJG.js → chunk-2ZJ4DT3M.js} +1 -1
- package/dist/{chunk-XZNU4DUX.cjs → chunk-3NS5C7FB.cjs} +76 -76
- package/dist/{chunk-UTI5IITZ.cjs → chunk-4ZC5QJE2.cjs} +3 -3
- package/dist/{chunk-L3JFOTBQ.js → chunk-5E2Q7ZHI.js} +1 -1
- package/dist/{chunk-6BIDVPED.js → chunk-5JNA6LLY.js} +1 -1
- package/dist/{chunk-RD7RX52G.cjs → chunk-5UEFBM2M.cjs} +207 -18
- package/dist/{chunk-WA44NKPF.js → chunk-62Z5WLRA.js} +1 -1
- package/dist/{chunk-BVRRIW7F.js → chunk-6GRUVGKU.js} +3 -3
- package/dist/{chunk-QARCM66V.js → chunk-6ZZKRH7R.js} +3 -3
- package/dist/{chunk-BP6KSGVD.js → chunk-7272D3K2.js} +4 -6
- package/dist/{chunk-O75V4ZS3.js → chunk-7P6VF3FG.js} +2 -2
- package/dist/{chunk-AU6MWQKZ.js → chunk-ABL7IWFZ.js} +1 -1
- package/dist/{chunk-5RH3VXAK.js → chunk-APX63ILU.js} +4 -100
- package/dist/{chunk-W7L4HUT2.cjs → chunk-CJ6TBL2N.cjs} +16 -16
- package/dist/{chunk-7PT4RJL5.cjs → chunk-EUIDTZNQ.cjs} +111 -113
- package/dist/{chunk-CXXXAZ7H.js → chunk-F6R67I3A.js} +1 -1
- package/dist/{chunk-I7NJPHWV.cjs → chunk-FQPUF2W2.cjs} +35 -606
- package/dist/{chunk-D5KGAO4Z.js → chunk-GCVFEPJG.js} +2 -573
- package/dist/{chunk-LEQKZGQI.js → chunk-GOBOCZZI.js} +1 -1
- package/dist/{chunk-PZP2SZIZ.js → chunk-GYMMNCFT.js} +3 -3
- package/dist/{chunk-NUTMINJZ.cjs → chunk-H7ET2ISK.cjs} +52 -52
- package/dist/chunk-HS6QDWDF.cjs +1029 -0
- package/dist/{chunk-T7KQEMAE.cjs → chunk-HUAISCUX.cjs} +18 -18
- package/dist/{chunk-YGQVCQTG.js → chunk-ILP77UD6.js} +1 -1
- package/dist/{chunk-GHCSC2WP.cjs → chunk-ISIO63Q5.cjs} +21 -15
- package/dist/{chunk-S6OT6CTS.cjs → chunk-IWEVV63O.cjs} +31 -33
- package/dist/{chunk-OFH6Q67U.js → chunk-JA57JIT5.js} +9 -3
- package/dist/{chunk-LP2MCSBE.cjs → chunk-JFHBXNR2.cjs} +88 -88
- package/dist/{chunk-IOFVM3L3.cjs → chunk-JPEHXIB5.cjs} +10 -10
- package/dist/chunk-JSDND7DF.cjs +173 -0
- package/dist/{chunk-36ZTV7WO.js → chunk-L6Y7TSQ4.js} +1 -1
- package/dist/{chunk-KYX7NZ5V.cjs → chunk-LFMKWGFL.cjs} +1 -1
- package/dist/{chunk-V6DT2CDH.js → chunk-M2TPMYJE.js} +1 -1
- package/dist/{chunk-LJZRHVZV.cjs → chunk-M5GLQQWG.cjs} +2 -2
- package/dist/{chunk-JEGJP4OW.js → chunk-MHLU6GRJ.js} +196 -7
- package/dist/{chunk-KMPM5JM5.js → chunk-MONXNVZK.js} +4 -6
- package/dist/chunk-MTKY6K3Q.js +1029 -0
- package/dist/{chunk-MOW3JMNG.cjs → chunk-N76AMOKE.cjs} +42 -42
- package/dist/{chunk-75K3W4D6.cjs → chunk-NKGB64CW.cjs} +16 -16
- package/dist/{chunk-SGPIVHJB.js → chunk-P3K2JCJA.js} +3 -3
- package/dist/chunk-PXDIAYB6.cjs +594 -0
- package/dist/{chunk-UBRJOIR5.js → chunk-QC3OXRE4.js} +9 -9
- package/dist/{chunk-QOBCBGGI.cjs → chunk-QKXO6JPP.cjs} +14 -14
- package/dist/{chunk-WLJZZHUZ.cjs → chunk-QLSGTCYT.cjs} +23 -23
- package/dist/{chunk-7YW7T5TJ.js → chunk-RHPSE2YR.js} +2 -2
- package/dist/{chunk-44BMJJVA.cjs → chunk-RUONPPMC.cjs} +8 -8
- package/dist/{chunk-LJRCU3BJ.cjs → chunk-SABJMH6M.cjs} +150 -15
- package/dist/{chunk-XWOLPNBW.cjs → chunk-SOSBOS5T.cjs} +22 -22
- package/dist/chunk-SZTI6EER.js +13 -0
- package/dist/{chunk-PPFJHTW5.cjs → chunk-TFF2GTGN.cjs} +22 -24
- package/dist/{chunk-2SMMZDMG.cjs → chunk-TLOGFIIV.cjs} +37 -37
- package/dist/chunk-TXFWZ5ZJ.js +594 -0
- package/dist/chunk-U67EN7GG.cjs +13 -0
- package/dist/{chunk-GXAK6Q5J.js → chunk-UFEDHACQ.js} +2 -2
- package/dist/{chunk-GA4EU4P3.cjs → chunk-UXETV2NC.cjs} +4 -4
- package/dist/{chunk-N4DFDI73.js → chunk-WKMUBATU.js} +1 -1
- package/dist/{chunk-3R5P3DVY.cjs → chunk-WRZ6C4TU.cjs} +6 -6
- package/dist/{chunk-EJLRBS5Q.js → chunk-WZOGDKCQ.js} +137 -2
- package/dist/{chunk-6ADHGQ73.js → chunk-YBAPT7NL.js} +2 -4
- package/dist/{chunk-K5DS2JTA.cjs → chunk-Z3BRH4JK.cjs} +6 -6
- package/dist/{chunk-XNZFVYC2.cjs → chunk-ZBCT5MEA.cjs} +7 -7
- package/dist/color/index.cjs +4 -6
- package/dist/color/index.js +7 -9
- package/dist/effects/helpers.d.cts +6 -0
- package/dist/effects/helpers.d.ts +6 -0
- package/dist/effects/index.cjs +62 -60
- package/dist/effects/index.js +5 -3
- package/dist/effects/tools.d.cts +6 -0
- package/dist/effects/tools.d.ts +6 -0
- package/dist/export/index.cjs +7 -8
- package/dist/export/index.js +6 -7
- package/dist/extensions/index.cjs +10 -11
- package/dist/extensions/index.js +9 -10
- package/dist/gradient/index.cjs +33 -34
- package/dist/gradient/index.js +4 -5
- package/dist/image-editor/effects.d.cts +1 -1
- package/dist/image-editor/effects.d.ts +1 -1
- package/dist/image-editor/index.cjs +887 -863
- package/dist/image-editor/index.d.cts +1 -0
- package/dist/image-editor/index.d.ts +1 -0
- package/dist/image-editor/index.js +69 -45
- package/dist/image-editor/stickers.d.cts +9 -0
- package/dist/image-editor/stickers.d.ts +9 -0
- package/dist/index.cjs +16 -17
- package/dist/index.js +10 -11
- package/dist/paint/index.cjs +6 -4
- package/dist/paint/index.js +21 -19
- package/dist/paint-editor/brushes.d.cts +1 -2
- package/dist/paint-editor/brushes.d.ts +1 -2
- package/dist/paint-editor/index.cjs +2199 -699
- package/dist/paint-editor/index.js +1577 -77
- package/dist/paint-editor/mark-settings.d.cts +104 -0
- package/dist/paint-editor/mark-settings.d.ts +104 -0
- package/dist/paint-editor/procedural.d.cts +2 -75
- package/dist/paint-editor/procedural.d.ts +2 -75
- package/dist/paint-editor/stickers.d.cts +1 -22
- package/dist/paint-editor/stickers.d.ts +1 -22
- package/dist/raster/index.cjs +2 -2
- package/dist/raster/index.js +9 -9
- package/dist/stickers/index.cjs +22 -16
- package/dist/stickers/index.d.cts +1 -1
- package/dist/stickers/index.d.ts +1 -1
- package/dist/stickers/index.js +14 -8
- package/dist/stickers/tools.d.cts +57 -1
- package/dist/stickers/tools.d.ts +57 -1
- package/dist/tokens/index.cjs +3 -4
- package/dist/tokens/index.js +2 -3
- package/dist/typography/index.cjs +7 -8
- package/dist/typography/index.js +2 -3
- package/dist/vector/index.cjs +1011 -977
- package/dist/vector/index.d.cts +1 -0
- package/dist/vector/index.d.ts +1 -0
- package/dist/vector/index.js +47 -13
- package/dist/vector/stickers.d.cts +11 -0
- package/dist/vector/stickers.d.ts +11 -0
- package/dist/video-editor/index.cjs +349 -350
- package/dist/video-editor/index.js +9 -10
- package/package.json +11 -1
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-stickers/SKILL.md +13 -1
- package/dist/chunk-7AHBW2CQ.cjs +0 -200
- package/dist/chunk-F67XMVDI.js +0 -200
- package/dist/chunk-H4AOUUBE.cjs +0 -2635
- package/dist/chunk-J7T5QZ45.js +0 -2635
- 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-
|
|
6
|
+
} from "../chunk-WZOGDKCQ.js";
|
|
5
7
|
import {
|
|
6
8
|
toVideoFfmpeg
|
|
7
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
293
|
-
import "../chunk-
|
|
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-
|
|
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
|
|
4734
|
+
var HEX2 = /^#?([0-9a-f]{6})$/i;
|
|
3235
4735
|
function parseHexColor(value, what = "color") {
|
|
3236
|
-
const m = typeof value === "string" ?
|
|
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
|
|
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" || !
|
|
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
|
|
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: { ...
|
|
9505
|
-
wet: { ...
|
|
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." },
|