@pixerate/editor 0.266.2 → 0.268.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 +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/animation-editor/controller.d.cts +8 -0
- package/dist/animation-editor/controller.d.ts +8 -0
- package/dist/animation-editor/effect-tools.d.cts +1 -1
- package/dist/animation-editor/effect-tools.d.ts +1 -1
- package/dist/animation-editor/index.cjs +175 -108
- package/dist/animation-editor/index.js +94 -27
- package/dist/brushes/index.cjs +201 -75
- package/dist/brushes/index.d.cts +4 -3
- package/dist/brushes/index.d.ts +4 -3
- package/dist/brushes/index.js +138 -12
- package/dist/brushes/library.d.cts +27 -1
- package/dist/brushes/library.d.ts +27 -1
- package/dist/brushes/profiles.d.cts +10 -1
- package/dist/brushes/profiles.d.ts +10 -1
- package/dist/brushes/schema.d.cts +4 -0
- package/dist/brushes/schema.d.ts +4 -0
- package/dist/brushes/sources.d.cts +114 -0
- package/dist/brushes/sources.d.ts +114 -0
- package/dist/brushes/types.d.cts +37 -5
- package/dist/brushes/types.d.ts +37 -5
- package/dist/brushes/ui.d.cts +40 -4
- package/dist/brushes/ui.d.ts +40 -4
- package/dist/{brushstrokeRender-KD453OBX.js → brushstrokeRender-A727TTNQ.js} +7 -7
- package/dist/{brushstrokeRender-FSUWXUZY.cjs → brushstrokeRender-WR2N5HHY.cjs} +23 -23
- package/dist/canvas/brushTypes.d.cts +12 -0
- package/dist/canvas/brushTypes.d.ts +12 -0
- package/dist/canvas/brushstroke.d.cts +1 -1
- package/dist/canvas/brushstroke.d.ts +1 -1
- package/dist/canvas/drawController.d.cts +19 -4
- package/dist/canvas/drawController.d.ts +19 -4
- package/dist/canvas/index.cjs +144 -105
- package/dist/canvas/index.js +46 -7
- package/dist/{chunk-TOID45I2.cjs → chunk-2WOGXEDX.cjs} +1 -1
- package/dist/{chunk-XORJ7UZO.cjs → chunk-3VHH6V67.cjs} +13 -12
- package/dist/{chunk-3C33YTNN.js → chunk-3XDSNIGF.js} +73 -6
- package/dist/chunk-62MFUHSI.cjs +802 -0
- package/dist/{chunk-R44Y3EFS.js → chunk-74ODPZLX.js} +17 -345
- package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
- package/dist/chunk-ADMRNWR7.js +340 -0
- package/dist/{chunk-MVUQSK5J.cjs → chunk-B3G7BKUV.cjs} +111 -439
- package/dist/{chunk-DXYHQ2VN.js → chunk-BAZMCFVL.js} +1 -1
- package/dist/{chunk-VRK322ZC.cjs → chunk-BPJSI4NT.cjs} +2 -2
- package/dist/{chunk-RFJZGTHA.cjs → chunk-EU2ZZA6L.cjs} +400 -69
- package/dist/{chunk-XC6DPRYI.js → chunk-GSTSZPSR.js} +154 -949
- package/dist/{chunk-EZOJBP4D.js → chunk-HFY5LG5J.js} +1 -1
- package/dist/chunk-HVO6XCI4.js +802 -0
- package/dist/{chunk-YHDHFJUC.cjs → chunk-J5VQYK7T.cjs} +217 -1012
- package/dist/{chunk-XRXFVRGY.cjs → chunk-JBB3GDBB.cjs} +2483 -208
- package/dist/{chunk-ACFSFUQH.js → chunk-JL6YH5WD.js} +4 -0
- package/dist/{chunk-CIJBSUM5.js → chunk-KZI3YWKW.js} +353 -22
- package/dist/{chunk-4ODRXQRW.js → chunk-LETF75CA.js} +1 -1
- package/dist/{chunk-FQPUF2W2.cjs → chunk-NIBFKXMD.cjs} +217 -35
- package/dist/{chunk-AQZQWXEU.cjs → chunk-NPWWSCWT.cjs} +35 -35
- package/dist/{chunk-DOZ6F2K2.js → chunk-NSETJI74.js} +3 -2
- package/dist/{chunk-PE6YJMQT.cjs → chunk-PFHJS4HE.cjs} +20 -19
- package/dist/{chunk-7KNW3WXZ.cjs → chunk-QEVLRLUN.cjs} +63 -0
- package/dist/{chunk-LK4SXL3V.js → chunk-QNY7BDFP.js} +1 -1
- package/dist/{chunk-OTN6VB5S.cjs → chunk-RDFHWAHL.cjs} +5 -1
- package/dist/{chunk-PXDIAYB6.cjs → chunk-STLTOISW.cjs} +34 -214
- package/dist/{chunk-T2LDTEXK.js → chunk-UVTO3D4E.js} +2333 -58
- package/dist/{chunk-6JL2D5SG.js → chunk-VE6UKYQ2.js} +3 -2
- package/dist/{chunk-TXFWZ5ZJ.js → chunk-W6JDUXP3.js} +4 -184
- package/dist/{chunk-3SBLVGOM.cjs → chunk-YOVXQHA3.cjs} +23 -23
- package/dist/{chunk-WLUVZKVD.cjs → chunk-YXP6X3SY.cjs} +103 -36
- package/dist/chunk-YZWM6ZIP.cjs +340 -0
- package/dist/{chunk-472INMWH.js → chunk-ZA7L5YJB.js} +63 -0
- package/dist/commands/builtins.d.cts +12 -1
- package/dist/commands/builtins.d.ts +12 -1
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/image-editor/controller.d.cts +1 -1
- package/dist/image-editor/controller.d.ts +1 -1
- package/dist/image-editor/index.cjs +868 -864
- package/dist/image-editor/index.js +123 -119
- package/dist/paint/index.cjs +8 -6
- package/dist/paint/index.js +56 -54
- package/dist/paint-editor/brush-library-tools.d.cts +12 -2
- package/dist/paint-editor/brush-library-tools.d.ts +12 -2
- package/dist/paint-editor/brush-model.d.cts +29 -1
- package/dist/paint-editor/brush-model.d.ts +29 -1
- package/dist/paint-editor/brush-source-art.d.cts +32 -0
- package/dist/paint-editor/brush-source-art.d.ts +32 -0
- package/dist/paint-editor/controller.d.cts +11 -4
- package/dist/paint-editor/controller.d.ts +11 -4
- package/dist/paint-editor/dab-stroke.d.cts +2 -0
- package/dist/paint-editor/dab-stroke.d.ts +2 -0
- package/dist/paint-editor/engine.d.cts +11 -2
- package/dist/paint-editor/engine.d.ts +11 -2
- package/dist/paint-editor/index.cjs +967 -2335
- package/dist/paint-editor/index.d.cts +3 -1
- package/dist/paint-editor/index.d.ts +3 -1
- package/dist/paint-editor/index.js +394 -1762
- package/dist/paint-editor/path-samples.d.cts +2 -0
- package/dist/paint-editor/path-samples.d.ts +2 -0
- package/dist/paint-editor/presence.d.cts +20 -1
- package/dist/paint-editor/presence.d.ts +20 -1
- package/dist/paint-editor/stamp-tip.d.cts +8 -5
- package/dist/paint-editor/stamp-tip.d.ts +8 -5
- package/dist/paint-editor/thumbnails.d.cts +6 -0
- package/dist/paint-editor/thumbnails.d.ts +6 -0
- package/dist/paint-editor/tip-grain.d.cts +34 -0
- package/dist/paint-editor/tip-grain.d.ts +34 -0
- package/dist/raster/index.cjs +2 -2
- package/dist/raster/index.js +9 -9
- package/dist/stickers/helpers.d.cts +2 -0
- package/dist/stickers/helpers.d.ts +2 -0
- package/dist/stickers/index.cjs +41 -35
- package/dist/stickers/index.js +22 -16
- package/dist/stickers/types.d.cts +4 -2
- package/dist/stickers/types.d.ts +4 -2
- package/dist/vector/backdrop.d.cts +13 -0
- package/dist/vector/backdrop.d.ts +13 -0
- package/dist/vector/brushstroke.d.cts +54 -7
- package/dist/vector/brushstroke.d.ts +54 -7
- package/dist/vector/brushstrokeMedia.d.cts +96 -0
- package/dist/vector/brushstrokeMedia.d.ts +96 -0
- package/dist/vector/controller.d.cts +17 -3
- package/dist/vector/controller.d.ts +17 -3
- package/dist/vector/index.cjs +1850 -1390
- package/dist/vector/index.d.cts +2 -0
- package/dist/vector/index.d.ts +2 -0
- package/dist/vector/index.js +520 -60
- package/dist/vector/render.d.cts +6 -0
- package/dist/vector/render.d.ts +6 -0
- package/dist/vector/types.d.cts +14 -0
- package/dist/vector/types.d.ts +14 -0
- package/package.json +1 -1
- package/skills/pixerate-animation-editor/SKILL.md +1 -0
- package/skills/pixerate-brushes/SKILL.md +37 -10
- package/skills/pixerate-brushes-design/SKILL.md +15 -5
- package/skills/pixerate-brushes-design/evals/port-to-vector.json +8 -8
- package/skills/pixerate-canvas/SKILL.md +11 -1
- package/skills/pixerate-image-editor/SKILL.md +19 -64
- package/skills/pixerate-image-editor/references/color-management.md +2 -1
- package/skills/pixerate-image-editor/references/free-transform.md +12 -0
- package/skills/pixerate-image-editor/references/layer-effects.md +10 -0
- package/skills/pixerate-image-editor/references/painting.md +17 -0
- package/skills/pixerate-image-editor/references/photoshop-files.md +1 -1
- package/skills/pixerate-image-editor/references/shapes-and-type.md +14 -0
- package/skills/pixerate-image-editor/references/smart-objects.md +10 -0
- package/skills/pixerate-paint-editor/SKILL.md +14 -4
- package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
- package/skills/pixerate-paint-editor/references/wet-media.md +9 -0
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
- package/skills/pixerate-vector/SKILL.md +16 -6
- package/skills/pixerate-vector/references/best-practices.md +1 -0
- package/skills/pixerate-vector/references/commands.md +19 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StickerBrowser,
|
|
3
3
|
editorStickers
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-NSETJI74.js";
|
|
5
5
|
import {
|
|
6
6
|
BRUSH_PATCH_PROPERTIES,
|
|
7
7
|
BRUSH_SLIDERS,
|
|
@@ -11,15 +11,15 @@ import {
|
|
|
11
11
|
mergeBrushPatch,
|
|
12
12
|
multiColorSliders,
|
|
13
13
|
patchBrushValue
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-3XDSNIGF.js";
|
|
15
15
|
import {
|
|
16
16
|
createStickerTools,
|
|
17
17
|
describeStickerRef
|
|
18
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-VE6UKYQ2.js";
|
|
19
19
|
import {
|
|
20
20
|
BRUSH_PREVIEW_DARK_PAPER,
|
|
21
21
|
brushPreviewsOnDark
|
|
22
|
-
} from "../chunk-
|
|
22
|
+
} from "../chunk-LETF75CA.js";
|
|
23
23
|
import {
|
|
24
24
|
toVideoFfmpeg
|
|
25
25
|
} from "../chunk-MWUZCEFT.js";
|
|
@@ -34,7 +34,8 @@ import {
|
|
|
34
34
|
resolveExternalAssets,
|
|
35
35
|
serializeLayers,
|
|
36
36
|
serializeLayersExternal
|
|
37
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-74ODPZLX.js";
|
|
38
|
+
import "../chunk-ADMRNWR7.js";
|
|
38
39
|
import "../chunk-WJ4WQJCC.js";
|
|
39
40
|
import {
|
|
40
41
|
LAYER_EFFECT_KINDS,
|
|
@@ -57,30 +58,9 @@ import {
|
|
|
57
58
|
import "../chunk-OHKOFKBY.js";
|
|
58
59
|
import "../chunk-ZIF4UJJ6.js";
|
|
59
60
|
import {
|
|
60
|
-
DEFAULT_FLOW,
|
|
61
|
-
DEFAULT_WET_PARAMS,
|
|
62
|
-
FLAT,
|
|
63
|
-
MAX_WET_LANES,
|
|
64
|
-
PIGMENT_UNIT,
|
|
65
|
-
SRGB_TO_LINEAR,
|
|
66
|
-
WET_FLOW_KINDS,
|
|
67
|
-
WET_PARAM_KEYS,
|
|
68
|
-
WET_TILE,
|
|
69
|
-
WetMedium,
|
|
70
|
-
WetWorld,
|
|
71
61
|
createPigmentTools,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
flowField,
|
|
75
|
-
linearToSrgb8,
|
|
76
|
-
moveWetSnapshot,
|
|
77
|
-
normalizeFlow,
|
|
78
|
-
normalizeTilt,
|
|
79
|
-
normalizeWetParams,
|
|
80
|
-
pigmentOf,
|
|
81
|
-
pointerPressure,
|
|
82
|
-
wetSupport
|
|
83
|
-
} from "../chunk-XC6DPRYI.js";
|
|
62
|
+
pointerPressure
|
|
63
|
+
} from "../chunk-HVO6XCI4.js";
|
|
84
64
|
import "../chunk-VWEWY4FY.js";
|
|
85
65
|
import "../chunk-HVQVHUTV.js";
|
|
86
66
|
import {
|
|
@@ -92,13 +72,55 @@ import {
|
|
|
92
72
|
realPresenceClock,
|
|
93
73
|
tweenPresenceCursor
|
|
94
74
|
} from "../chunk-IVSWRVB4.js";
|
|
95
|
-
import
|
|
75
|
+
import {
|
|
76
|
+
DEFAULT_PAPER,
|
|
77
|
+
DryStroke,
|
|
78
|
+
EFFECT_MAP_REPEATS,
|
|
79
|
+
EFFECT_STRIP_HEIGHT,
|
|
80
|
+
GPU_LEAD,
|
|
81
|
+
IDENTITY_COPY,
|
|
82
|
+
PAPER_FAMILIES,
|
|
83
|
+
PAPER_LIBRARY,
|
|
84
|
+
PaintDabStroke,
|
|
85
|
+
PaintWetLayers,
|
|
86
|
+
PenDabWalker,
|
|
87
|
+
PenSmoother,
|
|
88
|
+
TipGrain,
|
|
89
|
+
WET_GPU_MODES,
|
|
90
|
+
WET_MEDIUM_PARAMS,
|
|
91
|
+
WetStrokeDriver,
|
|
92
|
+
brushStickerRefs,
|
|
93
|
+
clonePaper,
|
|
94
|
+
effectDabField,
|
|
95
|
+
effectPaintKey,
|
|
96
|
+
effectPaintSize,
|
|
97
|
+
hexToRgb8,
|
|
98
|
+
isPaintEffectPaint,
|
|
99
|
+
mixerTip,
|
|
100
|
+
multiColorForDab,
|
|
101
|
+
multiColorGradient,
|
|
102
|
+
multiColorPalette,
|
|
103
|
+
normalizePaper,
|
|
104
|
+
paperPreset,
|
|
105
|
+
penOrientation,
|
|
106
|
+
readEffectStrokeMap,
|
|
107
|
+
readPaintEffectPaint,
|
|
108
|
+
renderPaper,
|
|
109
|
+
sampleEffectAt,
|
|
110
|
+
sampleEffectField,
|
|
111
|
+
stampSampler,
|
|
112
|
+
tiltToOrientation,
|
|
113
|
+
toStrokePoint,
|
|
114
|
+
wetPaperInput,
|
|
115
|
+
wetPigmentOf
|
|
116
|
+
} from "../chunk-UVTO3D4E.js";
|
|
96
117
|
import {
|
|
97
118
|
BRUSH_ID,
|
|
98
119
|
BRUSH_PRESETS,
|
|
99
120
|
BRUSH_ROTATIONS,
|
|
100
121
|
BUILTIN_BRUSHES,
|
|
101
122
|
BrushLibraryError,
|
|
123
|
+
DAB_FLIPS,
|
|
102
124
|
DEFAULT_BEHAVIOR,
|
|
103
125
|
DEFAULT_BRISTLE,
|
|
104
126
|
DEFAULT_BRUSH_ID,
|
|
@@ -109,6 +131,7 @@ import {
|
|
|
109
131
|
DEFAULT_PARTICLE,
|
|
110
132
|
DEFAULT_SPRAY,
|
|
111
133
|
DEFAULT_STICKER,
|
|
134
|
+
DEFAULT_TIP_GRAIN,
|
|
112
135
|
DEFAULT_WET_BRUSH,
|
|
113
136
|
DOT_ROTATIONS,
|
|
114
137
|
DOT_TEXTURE_ORDERS,
|
|
@@ -122,12 +145,17 @@ import {
|
|
|
122
145
|
PARTICLE_KINDS,
|
|
123
146
|
PARTICLE_RENDERS,
|
|
124
147
|
RIBBON_PAIRS,
|
|
148
|
+
TIP_GRAIN_MODES,
|
|
125
149
|
WET_MEDIA,
|
|
126
150
|
WET_TOOLS,
|
|
151
|
+
brushSourceIdOf,
|
|
152
|
+
brushSourceIds,
|
|
127
153
|
brushTakesPaint,
|
|
128
154
|
brushToJSON,
|
|
129
155
|
closest,
|
|
130
156
|
createBrushLibrary,
|
|
157
|
+
decodeBrushSource,
|
|
158
|
+
isBrushSourceRef,
|
|
131
159
|
isWetBrush,
|
|
132
160
|
isWetCapable,
|
|
133
161
|
migrateBrushV1,
|
|
@@ -145,9 +173,10 @@ import {
|
|
|
145
173
|
normalizeWetBrush,
|
|
146
174
|
paintToMultiColor,
|
|
147
175
|
parseBrushLibrary,
|
|
176
|
+
readBrushSource,
|
|
148
177
|
resolveBrush,
|
|
149
178
|
usesPaintExchange
|
|
150
|
-
} from "../chunk-
|
|
179
|
+
} from "../chunk-KZI3YWKW.js";
|
|
151
180
|
import {
|
|
152
181
|
STAMP_KINDS,
|
|
153
182
|
StickersSlot,
|
|
@@ -156,7 +185,7 @@ import {
|
|
|
156
185
|
stampCoverage,
|
|
157
186
|
stickerIdFor,
|
|
158
187
|
stickerVariantFor
|
|
159
|
-
} from "../chunk-
|
|
188
|
+
} from "../chunk-JL6YH5WD.js";
|
|
160
189
|
import "../chunk-M6DNZNVY.js";
|
|
161
190
|
import {
|
|
162
191
|
bezierPathToSvg,
|
|
@@ -181,17 +210,28 @@ import {
|
|
|
181
210
|
} from "../chunk-MV7CYFMJ.js";
|
|
182
211
|
import "../chunk-V2ZCJRDM.js";
|
|
183
212
|
import {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
213
|
+
DEFAULT_FLOW,
|
|
214
|
+
DEFAULT_WET_PARAMS,
|
|
215
|
+
MAX_WET_LANES,
|
|
216
|
+
SRGB_TO_LINEAR,
|
|
217
|
+
WET_FLOW_KINDS,
|
|
218
|
+
WET_PARAM_KEYS,
|
|
219
|
+
WetWorld,
|
|
220
|
+
linearToSrgb8,
|
|
221
|
+
normalizeFlow,
|
|
222
|
+
normalizeTilt,
|
|
223
|
+
normalizeWetParams,
|
|
224
|
+
pigmentOf,
|
|
225
|
+
wetSupport
|
|
226
|
+
} from "../chunk-GSTSZPSR.js";
|
|
227
|
+
import "../chunk-W6JDUXP3.js";
|
|
190
228
|
import {
|
|
191
229
|
CoverageMap,
|
|
192
230
|
DEFAULT_PAINT_GRADIENT,
|
|
193
231
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
194
232
|
EMPTY_MIXER_LOAD,
|
|
233
|
+
GRAIN_PATTERNS,
|
|
234
|
+
GRAIN_TILE,
|
|
195
235
|
MAX_BRUSH_SIZE,
|
|
196
236
|
PAINT_COLOR_JSON_SCHEMA,
|
|
197
237
|
PAINT_GRADIENT_FULL_SPEED,
|
|
@@ -208,20 +248,20 @@ import {
|
|
|
208
248
|
PaintExchange,
|
|
209
249
|
PaintGradientSampler,
|
|
210
250
|
PaintSourceError,
|
|
251
|
+
PaperGrain,
|
|
211
252
|
SmudgeTip,
|
|
212
253
|
TIP_GRADIENT_STEPS,
|
|
213
254
|
TileMedium,
|
|
214
255
|
addPaintGradientStop,
|
|
215
256
|
applyStrokeToTile,
|
|
216
|
-
blenderDab,
|
|
217
257
|
cleanMixer,
|
|
218
|
-
createBlenderScratch,
|
|
219
258
|
dabRect,
|
|
220
259
|
exchangeRates,
|
|
260
|
+
grainCatch,
|
|
261
|
+
grainHeights,
|
|
221
262
|
hexToPaintRgb,
|
|
222
263
|
isGradientPaint,
|
|
223
264
|
loadMixer,
|
|
224
|
-
mixerRates,
|
|
225
265
|
normalizeHexColor,
|
|
226
266
|
paintGradientMappingFor,
|
|
227
267
|
paintSourceColor,
|
|
@@ -235,7 +275,7 @@ import {
|
|
|
235
275
|
scalePaintGradient,
|
|
236
276
|
solidPaint,
|
|
237
277
|
updatePaintGradientStop
|
|
238
|
-
} from "../chunk-
|
|
278
|
+
} from "../chunk-ACTHSQOI.js";
|
|
239
279
|
import {
|
|
240
280
|
TileableNoise,
|
|
241
281
|
hash2,
|
|
@@ -245,6 +285,14 @@ import {
|
|
|
245
285
|
seedFromString
|
|
246
286
|
} from "../chunk-4D6KP5BV.js";
|
|
247
287
|
import "../chunk-ODBT55Q7.js";
|
|
288
|
+
import {
|
|
289
|
+
BLEND_MODES,
|
|
290
|
+
PAINTERLY_BLEND_MODES,
|
|
291
|
+
isBlendMode,
|
|
292
|
+
isPainterlyBlendMode,
|
|
293
|
+
mixPigments,
|
|
294
|
+
standardBlendMode
|
|
295
|
+
} from "../chunk-IRSTHLQQ.js";
|
|
248
296
|
import {
|
|
249
297
|
base64ToBytes,
|
|
250
298
|
bytesToBase64,
|
|
@@ -254,15 +302,6 @@ import {
|
|
|
254
302
|
parseDataUrl,
|
|
255
303
|
pngDataUrl
|
|
256
304
|
} from "../chunk-OTTT5N3K.js";
|
|
257
|
-
import {
|
|
258
|
-
BLEND_MODES,
|
|
259
|
-
PAINTERLY_BLEND_MODES,
|
|
260
|
-
isBlendMode,
|
|
261
|
-
isPainterlyBlendMode,
|
|
262
|
-
mixPigment8,
|
|
263
|
-
mixPigments,
|
|
264
|
-
standardBlendMode
|
|
265
|
-
} from "../chunk-IRSTHLQQ.js";
|
|
266
305
|
import {
|
|
267
306
|
TILE,
|
|
268
307
|
TiledSurface,
|
|
@@ -288,985 +327,61 @@ import {
|
|
|
288
327
|
defineEditorDocument,
|
|
289
328
|
toEditorDocumentFile
|
|
290
329
|
} from "../chunk-XBQX76CT.js";
|
|
291
|
-
import "../chunk-HF5QZS7Z.js";
|
|
292
|
-
import "../chunk-M2TPMYJE.js";
|
|
293
|
-
import "../chunk-2WPNAGHI.js";
|
|
294
|
-
import {
|
|
295
|
-
HARMONY_KINDS,
|
|
296
|
-
colorHarmony,
|
|
297
|
-
hueToWheel,
|
|
298
|
-
wheelToHue
|
|
299
|
-
} from "../chunk-2EUW2XZX.js";
|
|
300
|
-
import "../chunk-E2V5DA3W.js";
|
|
301
|
-
import "../chunk-VR6OGHU5.js";
|
|
302
|
-
import "../chunk-JC2SHA4A.js";
|
|
303
|
-
import {
|
|
304
|
-
linearToSrgb,
|
|
305
|
-
oklabToOklch,
|
|
306
|
-
oklabToRgb,
|
|
307
|
-
oklchToOklab,
|
|
308
|
-
rgbToOklab,
|
|
309
|
-
srgbToLinear
|
|
310
|
-
} from "../chunk-MHLU6GRJ.js";
|
|
311
|
-
import "../chunk-6ONMQ6WF.js";
|
|
312
|
-
|
|
313
|
-
// src/paint-editor/
|
|
314
|
-
var
|
|
315
|
-
var
|
|
316
|
-
var
|
|
317
|
-
var
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
tiling: "mirror",
|
|
329
|
-
depth: o.depth,
|
|
330
|
-
shading: o.shading ?? 0.5,
|
|
331
|
-
dry: tuning(o.contrast ?? 1),
|
|
332
|
-
wet: tuning(1),
|
|
333
|
-
absorption: o.absorption,
|
|
334
|
-
permeability: o.permeability,
|
|
335
|
-
roughness: o.roughness,
|
|
336
|
-
...o.backlight ? { backlight: o.backlight, backlightColor: o.backlightColor ?? "#fff1d6" } : {}
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
var PAPER_LIBRARY = [
|
|
340
|
-
preset("cold-pressed", "Cold pressed watercolour", "watercolor", "cold-pressed", "#f7f3ea", { depth: 0.5, absorption: 0.7, permeability: 0.5, roughness: 0.6 }),
|
|
341
|
-
preset("hot-pressed", "Hot pressed watercolour", "watercolor", "hot-pressed", "#f8f6ef", { depth: 0.25, absorption: 0.55, permeability: 0.4, roughness: 0.25 }),
|
|
342
|
-
preset("rough-watercolor", "Rough watercolour", "watercolor", "rough", "#f5f0e4", { depth: 0.75, absorption: 0.8, permeability: 0.6, roughness: 0.85 }),
|
|
343
|
-
preset("cartridge", "Cartridge", "drawing", "vellum", "#fbfaf6", { depth: 0.3, absorption: 0.4, permeability: 0.3, roughness: 0.35 }),
|
|
344
|
-
preset("laid", "Laid paper", "drawing", "laid", "#f3ecdc", { depth: 0.45, absorption: 0.45, permeability: 0.35, roughness: 0.45 }),
|
|
345
|
-
preset("ingres", "Charcoal paper", "drawing", "ingres", "#ede6d6", { depth: 0.6, absorption: 0.4, permeability: 0.3, roughness: 0.65 }),
|
|
346
|
-
preset("sanded-pastel", "Sanded pastel paper", "pastel", "sanded", "#d8cfc0", { depth: 0.7, absorption: 0.3, permeability: 0.2, roughness: 0.9, shading: 0.35 }),
|
|
347
|
-
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 }),
|
|
348
|
-
preset("cotton-canvas", "Cotton canvas", "canvas", "canvas", "#efe9dc", { depth: 0.55, absorption: 0.35, permeability: 0.25, roughness: 0.6 }),
|
|
349
|
-
preset("fine-linen", "Fine linen", "canvas", "linen", "#e9e2d2", { depth: 0.45, absorption: 0.3, permeability: 0.25, roughness: 0.5 }),
|
|
350
|
-
preset("gesso", "Gesso panel", "panel", "gesso", "#f6f4ef", { depth: 0.35, absorption: 0.2, permeability: 0.15, roughness: 0.4 }),
|
|
351
|
-
preset("smooth", "Smooth bristol", "smooth", "smooth", "#ffffff", { depth: 0, absorption: 0.3, permeability: 0.2, roughness: 0.05, shading: 0 }),
|
|
352
|
-
// Backlit: smooth paper on a lightbox, for glazes that glow like stained glass.
|
|
353
|
-
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" })
|
|
354
|
-
];
|
|
355
|
-
var DEFAULT_PAPER = PAPER_LIBRARY[0];
|
|
356
|
-
function paperPreset(id) {
|
|
357
|
-
const p = PAPER_LIBRARY.find((x) => x.preset === id);
|
|
358
|
-
return p ? clonePaper(p) : void 0;
|
|
359
|
-
}
|
|
360
|
-
function clonePaper(p) {
|
|
361
|
-
return { ...p, dry: { ...p.dry }, wet: { ...p.wet }, ...p.offset ? { offset: { ...p.offset } } : {} };
|
|
362
|
-
}
|
|
363
|
-
function readOffset(raw, d) {
|
|
364
|
-
const o = raw && typeof raw === "object" ? raw : null;
|
|
365
|
-
const x = o && typeof o.x === "number" && Number.isFinite(o.x) ? Math.round(o.x) : d?.x ?? 0;
|
|
366
|
-
const y = o && typeof o.y === "number" && Number.isFinite(o.y) ? Math.round(o.y) : d?.y ?? 0;
|
|
367
|
-
return x || y ? { offset: { x, y } } : {};
|
|
368
|
-
}
|
|
369
|
-
function readTuning(raw, d) {
|
|
370
|
-
const r = raw && typeof raw === "object" ? raw : {};
|
|
371
|
-
return { contrast: clampNum(r.contrast, 0, 3, d.contrast), invert: typeof r.invert === "boolean" ? r.invert : d.invert };
|
|
372
|
-
}
|
|
373
|
-
function normalizePaper(raw = {}, base) {
|
|
374
|
-
const r = raw;
|
|
375
|
-
const b = base ?? (typeof r.preset === "string" ? paperPreset(r.preset) : void 0) ?? clonePaper(DEFAULT_PAPER);
|
|
376
|
-
return {
|
|
377
|
-
preset: typeof r.preset === "string" && r.preset ? r.preset : b.preset,
|
|
378
|
-
name: typeof r.name === "string" && r.name.trim() ? r.name.trim() : b.name,
|
|
379
|
-
family: PAPER_FAMILIES.includes(r.family) ? r.family : b.family,
|
|
380
|
-
pattern: GRAIN_PATTERNS.includes(r.pattern) ? r.pattern : b.pattern,
|
|
381
|
-
seed: typeof r.seed === "number" && Number.isFinite(r.seed) ? Math.round(Math.abs(r.seed)) % 2147483647 : b.seed,
|
|
382
|
-
color: typeof r.color === "string" && HEX.test(r.color) ? r.color.toLowerCase() : b.color,
|
|
383
|
-
scale: clampNum(r.scale, 0.25, 8, b.scale),
|
|
384
|
-
tiling: r.tiling === "repeat" || r.tiling === "mirror" ? r.tiling : b.tiling,
|
|
385
|
-
depth: unit(r.depth, b.depth),
|
|
386
|
-
shading: unit(r.shading, b.shading),
|
|
387
|
-
dry: readTuning(r.dry, b.dry),
|
|
388
|
-
wet: readTuning(r.wet, b.wet),
|
|
389
|
-
absorption: unit(r.absorption, b.absorption),
|
|
390
|
-
permeability: unit(r.permeability, b.permeability),
|
|
391
|
-
roughness: unit(r.roughness, b.roughness),
|
|
392
|
-
...unit(r.metallic, b.metallic ?? 0) > 0 ? { metallic: unit(r.metallic, b.metallic ?? 0) } : {},
|
|
393
|
-
...unit(r.backlight, b.backlight ?? 0) > 0 ? { backlight: unit(r.backlight, b.backlight ?? 0) } : {},
|
|
394
|
-
...typeof r.backlightColor === "string" && HEX.test(r.backlightColor) ? { backlightColor: r.backlightColor.toLowerCase() } : b.backlightColor ? { backlightColor: b.backlightColor } : {},
|
|
395
|
-
...readOffset(r.offset, b.offset)
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
function hexToRgb8(hex2) {
|
|
399
|
-
const n = parseInt(hex2.slice(1, 7), 16);
|
|
400
|
-
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
401
|
-
}
|
|
402
|
-
function renderPaper(paper, rect, grain = new PaperGrain(paper), light) {
|
|
403
|
-
const [r, g, b] = hexToRgb8(paper.color);
|
|
404
|
-
const out = new Uint8ClampedArray(rect.width * rect.height * 4);
|
|
405
|
-
const shade = grain.flat ? 0 : paper.shading * 0.16;
|
|
406
|
-
const metal = light ? paper.metallic ?? 0 : 0;
|
|
407
|
-
let hx = 0;
|
|
408
|
-
let hy = 0;
|
|
409
|
-
let hz = 1;
|
|
410
|
-
if (metal > 0 && light) {
|
|
411
|
-
const az = light.azimuth * Math.PI / 180;
|
|
412
|
-
const el = light.elevation * Math.PI / 180;
|
|
413
|
-
const lx = Math.cos(el) * Math.sin(az);
|
|
414
|
-
const ly = -Math.cos(el) * Math.cos(az);
|
|
415
|
-
const lz = Math.sin(el) + 1;
|
|
416
|
-
const n = Math.hypot(lx, ly, lz);
|
|
417
|
-
hx = lx / n;
|
|
418
|
-
hy = ly / n;
|
|
419
|
-
hz = lz / n;
|
|
420
|
-
}
|
|
421
|
-
for (let y = 0; y < rect.height; y++) {
|
|
422
|
-
for (let x = 0; x < rect.width; x++) {
|
|
423
|
-
const i = (y * rect.width + x) * 4;
|
|
424
|
-
const px = rect.x + x + 0.5;
|
|
425
|
-
const py = rect.y + y + 0.5;
|
|
426
|
-
const k = shade ? 1 - shade * (1 - grain.at(px, py)) : 1;
|
|
427
|
-
let spec = 0;
|
|
428
|
-
if (metal > 0) {
|
|
429
|
-
const gx = (grain.at(px + 1, py) - grain.at(px - 1, py)) * 4;
|
|
430
|
-
const gy = (grain.at(px, py + 1) - grain.at(px, py - 1)) * 4;
|
|
431
|
-
const nl = Math.hypot(gx, gy, 1);
|
|
432
|
-
const nh = Math.max(0, (-gx * hx - gy * hy + hz) / nl);
|
|
433
|
-
spec = metal * Math.pow(nh, 24) * 0.9;
|
|
434
|
-
}
|
|
435
|
-
out[i] = Math.min(255, r * k * (1 - 0.25 * metal) + spec * (r * 0.6 + 102));
|
|
436
|
-
out[i + 1] = Math.min(255, g * k * (1 - 0.25 * metal) + spec * (g * 0.6 + 102));
|
|
437
|
-
out[i + 2] = Math.min(255, b * k * (1 - 0.25 * metal) + spec * (b * 0.6 + 102));
|
|
438
|
-
out[i + 3] = 255;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return out;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// src/paint-editor/document.ts
|
|
445
|
-
var MIN_CANVAS_SIDE = 1;
|
|
446
|
-
var MAX_CANVAS_SIDE = 16384;
|
|
447
|
-
var DEFAULT_DPI = 300;
|
|
448
|
-
var nextId = 1;
|
|
449
|
-
function newPaintLayerId() {
|
|
450
|
-
return `pl${Date.now().toString(36)}${(nextId++).toString(36)}`;
|
|
451
|
-
}
|
|
452
|
-
function createPaintLayer(name, id = newPaintLayerId()) {
|
|
453
|
-
return { type: "pixel", id, name, visible: true, opacity: 1, blend: "normal", surface: emptySurface(4), offset: { x: 0, y: 0 } };
|
|
454
|
-
}
|
|
455
|
-
var PaintDocumentError = class extends Error {
|
|
456
|
-
constructor(message) {
|
|
457
|
-
super(message);
|
|
458
|
-
this.name = "PaintDocumentError";
|
|
459
|
-
}
|
|
460
|
-
};
|
|
461
|
-
function normalizeCanvas(raw) {
|
|
462
|
-
const side = (v, what) => {
|
|
463
|
-
if (typeof v !== "number" || !Number.isFinite(v)) throw new PaintDocumentError(`${what} must be a number of pixels (${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE}).`);
|
|
464
|
-
const n = Math.round(v);
|
|
465
|
-
if (n < MIN_CANVAS_SIDE || n > MAX_CANVAS_SIDE) throw new PaintDocumentError(`${what} ${n} is out of range; use ${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE} pixels.`);
|
|
466
|
-
return n;
|
|
467
|
-
};
|
|
468
|
-
const dpi = raw.dpi === void 0 ? DEFAULT_DPI : raw.dpi;
|
|
469
|
-
if (typeof dpi !== "number" || !Number.isFinite(dpi) || dpi < 1 || dpi > 1e4) throw new PaintDocumentError(`dpi ${dpi} is out of range; use 1..10000.`);
|
|
470
|
-
return { width: side(raw.width, "width"), height: side(raw.height, "height"), dpi: Math.round(dpi * 100) / 100 };
|
|
471
|
-
}
|
|
472
|
-
function createPaintDocument(options = {}) {
|
|
473
|
-
const canvas = normalizeCanvas({ width: options.width ?? 2048, height: options.height ?? 1536, dpi: options.dpi });
|
|
474
|
-
const layer = createPaintLayer("Layer 1");
|
|
475
|
-
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// src/paint-editor/effect-paint.ts
|
|
479
|
-
var EFFECT_STRIP_HEIGHT = 256;
|
|
480
|
-
var EFFECT_MAP_REPEATS = ["repeat", "mirror", "clamp"];
|
|
481
|
-
function effectPaintSize(p, canvas) {
|
|
482
|
-
return p.map ? { width: p.map.length, height: EFFECT_STRIP_HEIGHT } : { width: canvas.width, height: canvas.height };
|
|
483
|
-
}
|
|
484
|
-
function readEffectStrokeMap(raw) {
|
|
485
|
-
if (raw === void 0 || raw === null || raw.by === "canvas") return void 0;
|
|
486
|
-
const r = raw;
|
|
487
|
-
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).`);
|
|
488
|
-
const length = r.length === void 0 ? 300 : Number(r.length);
|
|
489
|
-
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.`);
|
|
490
|
-
const repeat = r.repeat === void 0 ? "repeat" : r.repeat;
|
|
491
|
-
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(", ")}.`);
|
|
492
|
-
return { by: "stroke", length: Math.round(length), repeat };
|
|
493
|
-
}
|
|
494
|
-
function stripIndex(img, map, dab, x, y) {
|
|
495
|
-
const ca = Math.cos(dab.direction);
|
|
496
|
-
const sa = Math.sin(dab.direction);
|
|
497
|
-
const dx = x - dab.x;
|
|
498
|
-
const dy = y - dab.y;
|
|
499
|
-
let u = (dab.travel + dx * ca - dy * sa) / map.length;
|
|
500
|
-
if (map.repeat === "clamp") u = Math.min(1, Math.max(0, u));
|
|
501
|
-
else if (map.repeat === "mirror") {
|
|
502
|
-
const m = (u % 2 + 2) % 2;
|
|
503
|
-
u = m > 1 ? 2 - m : m;
|
|
504
|
-
} else u = (u % 1 + 1) % 1;
|
|
505
|
-
const v = Math.min(1, Math.max(0, ((dx * sa + dy * ca) / Math.max(0.5, dab.radius) + 1) / 2));
|
|
506
|
-
const px = Math.min(img.width - 1, Math.floor(u * img.width));
|
|
507
|
-
const py = Math.min(img.height - 1, Math.floor(v * img.height));
|
|
508
|
-
return (py * img.width + px) * 4;
|
|
509
|
-
}
|
|
510
|
-
function effectPaintKey(p) {
|
|
511
|
-
return JSON.stringify({ type: "effect", effect: p.effect, provider: p.provider, values: p.values, time: p.time, ...p.map ? { map: p.map } : {} });
|
|
512
|
-
}
|
|
513
|
-
function isPaintEffectPaint(raw) {
|
|
514
|
-
return Boolean(raw) && typeof raw === "object" && raw.type === "effect";
|
|
515
|
-
}
|
|
516
|
-
function readMapLeniently(raw) {
|
|
517
|
-
try {
|
|
518
|
-
return readEffectStrokeMap(raw);
|
|
519
|
-
} catch {
|
|
520
|
-
return void 0;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
function readPaintEffectPaint(raw) {
|
|
524
|
-
if (!isPaintEffectPaint(raw)) return null;
|
|
525
|
-
const r = raw;
|
|
526
|
-
if (typeof r.effect !== "string" || !r.effect || !r.values || typeof r.values !== "object" || Array.isArray(r.values)) return null;
|
|
527
|
-
return {
|
|
528
|
-
type: "effect",
|
|
529
|
-
effect: r.effect,
|
|
530
|
-
provider: typeof r.provider === "string" ? r.provider : "",
|
|
531
|
-
values: JSON.parse(JSON.stringify(r.values)),
|
|
532
|
-
time: typeof r.time === "number" && Number.isFinite(r.time) && r.time > 0 ? r.time : 0,
|
|
533
|
-
...readMapLeniently(r.map) ? { map: readMapLeniently(r.map) } : {},
|
|
534
|
-
...r.live === true ? { live: true } : {}
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
function sampleEffectField(img, rect, vals, field, dab) {
|
|
538
|
-
const { width, height, data } = img;
|
|
539
|
-
if (img.map && dab) {
|
|
540
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
541
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
542
|
-
const i = stripIndex(img, img.map, dab, rect.x + xx + 0.5, rect.y + yy + 0.5);
|
|
543
|
-
const f = k * 3;
|
|
544
|
-
const a = data[i + 3];
|
|
545
|
-
if (a < 255) vals[k] *= a / 255;
|
|
546
|
-
field[f] = data[i] / 255;
|
|
547
|
-
field[f + 1] = data[i + 1] / 255;
|
|
548
|
-
field[f + 2] = data[i + 2] / 255;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
554
|
-
const y = rect.y + yy;
|
|
555
|
-
const inY = y >= 0 && y < height;
|
|
556
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
557
|
-
const x = rect.x + xx;
|
|
558
|
-
const f = k * 3;
|
|
559
|
-
if (!inY || x < 0 || x >= width) {
|
|
560
|
-
vals[k] = 0;
|
|
561
|
-
field[f] = field[f + 1] = field[f + 2] = 0;
|
|
562
|
-
continue;
|
|
563
|
-
}
|
|
564
|
-
const i = (y * width + x) * 4;
|
|
565
|
-
const a = data[i + 3];
|
|
566
|
-
if (a < 255) vals[k] *= a / 255;
|
|
567
|
-
field[f] = data[i] / 255;
|
|
568
|
-
field[f + 1] = data[i + 1] / 255;
|
|
569
|
-
field[f + 2] = data[i + 2] / 255;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function sampleEffectAt(img, x, y, out, dab) {
|
|
574
|
-
if (img.map && dab) {
|
|
575
|
-
const i2 = stripIndex(img, img.map, dab, x, y);
|
|
576
|
-
out[0] = img.data[i2] / 255;
|
|
577
|
-
out[1] = img.data[i2 + 1] / 255;
|
|
578
|
-
out[2] = img.data[i2 + 2] / 255;
|
|
579
|
-
return img.data[i2 + 3] / 255;
|
|
580
|
-
}
|
|
581
|
-
const px = Math.floor(x);
|
|
582
|
-
const py = Math.floor(y);
|
|
583
|
-
if (px < 0 || py < 0 || px >= img.width || py >= img.height) return 0;
|
|
584
|
-
const i = (py * img.width + px) * 4;
|
|
585
|
-
out[0] = img.data[i] / 255;
|
|
586
|
-
out[1] = img.data[i + 1] / 255;
|
|
587
|
-
out[2] = img.data[i + 2] / 255;
|
|
588
|
-
return img.data[i + 3] / 255;
|
|
589
|
-
}
|
|
590
|
-
function effectDabField(img, rect, cov, centre2, mixed, field, steps = 0, dab = null) {
|
|
591
|
-
sampleEffectField(img, rect, cov, field, dab);
|
|
592
|
-
const d0 = mixed[0] - centre2[0];
|
|
593
|
-
const d1 = mixed[1] - centre2[1];
|
|
594
|
-
const d2 = mixed[2] - centre2[2];
|
|
595
|
-
const n = rect.width * rect.height;
|
|
596
|
-
for (let k = 0; k < n; k++) {
|
|
597
|
-
if (steps) cov[k] = Math.round(cov[k] * steps) / steps;
|
|
598
|
-
const f = k * 3;
|
|
599
|
-
field[f] = Math.min(1, Math.max(0, field[f] + d0));
|
|
600
|
-
field[f + 1] = Math.min(1, Math.max(0, field[f + 1] + d1));
|
|
601
|
-
field[f + 2] = Math.min(1, Math.max(0, field[f + 2] + d2));
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// src/paint-editor/stamp-tip.ts
|
|
606
|
-
var samplers = /* @__PURE__ */ new WeakMap();
|
|
607
|
-
function stampSampler(ref2, art) {
|
|
608
|
-
let kind = null;
|
|
609
|
-
try {
|
|
610
|
-
kind = editorStampOf(ref2.includes(":") ? ref2 : `editor:${ref2}`);
|
|
611
|
-
} catch {
|
|
612
|
-
return null;
|
|
613
|
-
}
|
|
614
|
-
if (kind) {
|
|
615
|
-
const k = kind;
|
|
616
|
-
return (u, v, seed) => stampCoverage(k, u, v, seed);
|
|
617
|
-
}
|
|
618
|
-
const a = art?.(ref2);
|
|
619
|
-
if (!a || !(a.width > 0 && a.height > 0)) return null;
|
|
620
|
-
const hit = samplers.get(a);
|
|
621
|
-
if (hit) return hit;
|
|
622
|
-
const { width: w, height: h, data } = a;
|
|
623
|
-
const scale = Math.max(w, h) / 2;
|
|
624
|
-
const alpha = (x, y) => x < 0 || y < 0 || x >= w || y >= h ? 0 : data[(y * w + x) * 4 + 3] / 255;
|
|
625
|
-
const sampler = (u, v) => {
|
|
626
|
-
const x = u * scale + w / 2 - 0.5;
|
|
627
|
-
const y = v * scale + h / 2 - 0.5;
|
|
628
|
-
const x0 = Math.floor(x);
|
|
629
|
-
const y0 = Math.floor(y);
|
|
630
|
-
const fx = x - x0;
|
|
631
|
-
const fy = y - y0;
|
|
632
|
-
return (alpha(x0, y0) * (1 - fx) + alpha(x0 + 1, y0) * fx) * (1 - fy) + (alpha(x0, y0 + 1) * (1 - fx) + alpha(x0 + 1, y0 + 1) * fx) * fy;
|
|
633
|
-
};
|
|
634
|
-
samplers.set(a, sampler);
|
|
635
|
-
return sampler;
|
|
636
|
-
}
|
|
637
|
-
function stampDabRect(d) {
|
|
638
|
-
const r = Math.max(0.5, d.radius) * Math.SQRT2 + 1.5;
|
|
639
|
-
const x0 = Math.floor(d.x - r);
|
|
640
|
-
const y0 = Math.floor(d.y - r);
|
|
641
|
-
return { x: x0, y: y0, width: Math.ceil(d.x + r) - x0, height: Math.ceil(d.y + r) - y0 };
|
|
642
|
-
}
|
|
643
|
-
function rasterizeStampDab(d, sample, seed, rect, out) {
|
|
644
|
-
const w = rect.width;
|
|
645
|
-
const h = rect.height;
|
|
646
|
-
out.fill(0, 0, w * h);
|
|
647
|
-
const r = Math.max(0.5, d.radius);
|
|
648
|
-
const ro = Math.min(1, Math.max(0.05, d.roundness));
|
|
649
|
-
const sn = Math.sin(d.angle);
|
|
650
|
-
const cs = Math.cos(d.angle);
|
|
651
|
-
const ss = r < 6 ? 3 : 1;
|
|
652
|
-
for (let yy = 0; yy < h; yy++) {
|
|
653
|
-
for (let xx = 0; xx < w; xx++) {
|
|
654
|
-
let sum = 0;
|
|
655
|
-
for (let sy = 0; sy < ss; sy++) {
|
|
656
|
-
for (let sx = 0; sx < ss; sx++) {
|
|
657
|
-
const dx = rect.x + xx + (sx + 0.5) / ss - d.x;
|
|
658
|
-
const dy = rect.y + yy + (sy + 0.5) / ss - d.y;
|
|
659
|
-
const u = (dx * cs - dy * sn) / r;
|
|
660
|
-
const v = (dx * sn + dy * cs) / (r * ro);
|
|
661
|
-
if (u > -1.05 && u < 1.05 && v > -1.05 && v < 1.05) sum += sample(u, v, seed);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const val = sum / (ss * ss);
|
|
665
|
-
if (val > 0) out[yy * w + xx] = Math.min(1, val) * d.alpha;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
function brushStickerRefs(brush) {
|
|
670
|
-
const refs = brush.engine === "sticker" ? [...brush.sticker.stamps] : brush.engine === "particle" ? [...brush.particle.dotTextures] : brush.engine === "dab" && brush.stamp ? [brush.stamp] : [];
|
|
671
|
-
return [...new Set(refs.filter((r) => r.includes(":") && !r.startsWith("editor:")))];
|
|
672
|
-
}
|
|
673
|
-
function sprayDroplets(d, spray, seed) {
|
|
674
|
-
const count = 1 + Math.round(spray.density * 31);
|
|
675
|
-
const key = d.index * 131 + Math.round(d.travel * 8);
|
|
676
|
-
const out = [];
|
|
677
|
-
for (let i = 0; i < count; i++) {
|
|
678
|
-
const n = key * 64 + i;
|
|
679
|
-
const a = rand01(seed, n, 61) * Math.PI * 2;
|
|
680
|
-
const rr = Math.sqrt(rand01(seed, n, 67)) * d.radius;
|
|
681
|
-
out.push({ ...d, x: d.x + Math.cos(a) * rr, y: d.y + Math.sin(a) * rr, radius: Math.max(0.5, d.radius * spray.size * (0.6 + 0.8 * rand01(seed, n, 71))), roundness: 1, angle: rand01(seed, n, 73) * Math.PI * 2 });
|
|
682
|
-
}
|
|
683
|
-
return out;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/paint-editor/engine.ts
|
|
687
|
-
function penOrientation(p) {
|
|
688
|
-
if (typeof p.altitude === "number" && Number.isFinite(p.altitude)) {
|
|
689
|
-
const altitude = Math.min(Math.PI / 2, Math.max(0, p.altitude));
|
|
690
|
-
const azimuth = typeof p.azimuth === "number" && Number.isFinite(p.azimuth) ? p.azimuth : 0;
|
|
691
|
-
return { altitude, azimuth };
|
|
692
|
-
}
|
|
693
|
-
const hasX = typeof p.tiltX === "number" && Number.isFinite(p.tiltX);
|
|
694
|
-
const hasY = typeof p.tiltY === "number" && Number.isFinite(p.tiltY);
|
|
695
|
-
if (!hasX && !hasY) return null;
|
|
696
|
-
return tiltToOrientation(hasX ? p.tiltX : 0, hasY ? p.tiltY : 0);
|
|
697
|
-
}
|
|
698
|
-
function tiltToOrientation(tiltX, tiltY) {
|
|
699
|
-
const clampT = (v) => Math.min(89.999, Math.max(-89.999, v));
|
|
700
|
-
const tx = Math.tan(clampT(tiltX) * Math.PI / 180);
|
|
701
|
-
const ty = Math.tan(clampT(tiltY) * Math.PI / 180);
|
|
702
|
-
const r = Math.hypot(tx, ty);
|
|
703
|
-
if (r < 1e-9) return { altitude: Math.PI / 2, azimuth: 0 };
|
|
704
|
-
let azimuth = Math.atan2(ty, tx);
|
|
705
|
-
if (azimuth < 0) azimuth += 2 * Math.PI;
|
|
706
|
-
return { altitude: Math.atan(1 / r), azimuth };
|
|
707
|
-
}
|
|
708
|
-
var pressureOf = (p) => typeof p.pressure === "number" && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1;
|
|
709
|
-
var PenSmoother = class {
|
|
710
|
-
pos = null;
|
|
711
|
-
pressure = null;
|
|
712
|
-
a;
|
|
713
|
-
ap;
|
|
714
|
-
constructor(position, pressure) {
|
|
715
|
-
this.a = 1 - Math.min(0.95, Math.max(0, position));
|
|
716
|
-
this.ap = 1 - Math.min(0.95, Math.max(0, pressure));
|
|
717
|
-
}
|
|
718
|
-
push(p) {
|
|
719
|
-
const pr = pressureOf(p);
|
|
720
|
-
if (!this.pos) {
|
|
721
|
-
this.pos = { x: p.x, y: p.y };
|
|
722
|
-
this.pressure = pr;
|
|
723
|
-
return p;
|
|
724
|
-
}
|
|
725
|
-
this.pos = { x: this.pos.x + (p.x - this.pos.x) * this.a, y: this.pos.y + (p.y - this.pos.y) * this.a };
|
|
726
|
-
this.pressure = this.pressure + (pr - this.pressure) * this.ap;
|
|
727
|
-
return { ...p, x: this.pos.x, y: this.pos.y, pressure: this.pressure };
|
|
728
|
-
}
|
|
729
|
-
/** Where the smoothed position is now (or `null` before the first sample). */
|
|
730
|
-
get position() {
|
|
731
|
-
return this.pos;
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
var IDENTITY_COPY = { map: (p) => p, linear: [1, 0, 0, 1] };
|
|
735
|
-
var FULL_SPEED = 2.5;
|
|
736
|
-
var PenDabWalker = class {
|
|
737
|
-
constructor(brush, seed = 0, linear = [1, 0, 0, 1]) {
|
|
738
|
-
this.brush = brush;
|
|
739
|
-
this.seed = seed;
|
|
740
|
-
this.linear = linear;
|
|
741
|
-
}
|
|
742
|
-
brush;
|
|
743
|
-
seed;
|
|
744
|
-
linear;
|
|
745
|
-
last = null;
|
|
746
|
-
nextAt = 0;
|
|
747
|
-
pendingFirst = null;
|
|
748
|
-
index = 0;
|
|
749
|
-
dir = 0;
|
|
750
|
-
lastT = null;
|
|
751
|
-
firstT = null;
|
|
752
|
-
speed = null;
|
|
753
|
-
travelled = 0;
|
|
754
|
-
sample(p) {
|
|
755
|
-
const o = penOrientation(p);
|
|
756
|
-
let speed = null;
|
|
757
|
-
let time = null;
|
|
758
|
-
if (this.last) this.travelled += Math.hypot(p.x - this.last.x, p.y - this.last.y);
|
|
759
|
-
if (typeof p.t === "number" && Number.isFinite(p.t)) {
|
|
760
|
-
if (this.firstT === null) this.firstT = p.t;
|
|
761
|
-
time = p.t - this.firstT;
|
|
762
|
-
if (this.last && this.lastT !== null && p.t > this.lastT) {
|
|
763
|
-
const v = Math.hypot(p.x - this.last.x, p.y - this.last.y) / (p.t - this.lastT);
|
|
764
|
-
this.speed = this.speed === null ? v : this.speed + (v - this.speed) * 0.35;
|
|
765
|
-
}
|
|
766
|
-
this.lastT = p.t;
|
|
767
|
-
speed = this.speed ?? 0;
|
|
768
|
-
}
|
|
769
|
-
return {
|
|
770
|
-
x: p.x,
|
|
771
|
-
y: p.y,
|
|
772
|
-
pressure: pressureOf(p),
|
|
773
|
-
lean: o ? 1 - o.altitude / (Math.PI / 2) : 0,
|
|
774
|
-
azimuth: o ? o.azimuth : 0,
|
|
775
|
-
twist: typeof p.twist === "number" && Number.isFinite(p.twist) ? p.twist : null,
|
|
776
|
-
speed,
|
|
777
|
-
travel: this.travelled,
|
|
778
|
-
time
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
diameter(s) {
|
|
782
|
-
const tip = this.brush.tip;
|
|
783
|
-
const dyn = this.brush.dynamics;
|
|
784
|
-
let d = tip.size;
|
|
785
|
-
if (tip.pressureSize) d *= dyn.minSize + (1 - dyn.minSize) * s.pressure;
|
|
786
|
-
d *= 1 + dyn.tiltSize * s.lean * 1.5;
|
|
787
|
-
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));
|
|
788
|
-
return d;
|
|
789
|
-
}
|
|
790
|
-
stepLen(s) {
|
|
791
|
-
return Math.max(0.5, Math.max(1, this.diameter(s)) * Math.max(0.01, this.brush.tip.spacing));
|
|
792
|
-
}
|
|
793
|
-
/** A rasterizer angle (CCW, y up) mapped through this copy's linear part. */
|
|
794
|
-
mapAngle(theta) {
|
|
795
|
-
const [a, b, c, d] = this.linear;
|
|
796
|
-
if (a === 1 && b === 0 && c === 0 && d === 1) return theta;
|
|
797
|
-
const vx = Math.cos(theta);
|
|
798
|
-
const vy = -Math.sin(theta);
|
|
799
|
-
return Math.atan2(-(c * vx + d * vy), a * vx + b * vy);
|
|
800
|
-
}
|
|
801
|
-
dab(s) {
|
|
802
|
-
const { tip, dynamics: dyn, rotation } = this.brush;
|
|
803
|
-
const i = this.index++;
|
|
804
|
-
const seed = this.seed;
|
|
805
|
-
let diameter = this.diameter(s);
|
|
806
|
-
if (dyn.sizeJitter) diameter *= Math.max(0.05, 1 + dyn.sizeJitter * randSigned(seed, i, 1 /* Size */));
|
|
807
|
-
const base = tip.angle * Math.PI / 180;
|
|
808
|
-
let angle;
|
|
809
|
-
if (rotation === "stroke") angle = this.dir + base;
|
|
810
|
-
else if (rotation === "barrel" && s.twist !== null) angle = this.mapAngle(base - s.twist * Math.PI / 180);
|
|
811
|
-
else if (dyn.tiltRoundness > 0 && s.lean > 0.02) angle = this.mapAngle(base - s.azimuth);
|
|
812
|
-
else angle = this.mapAngle(base);
|
|
813
|
-
if (dyn.angleJitter) angle += dyn.angleJitter * Math.PI * randSigned(seed, i, 2 /* Angle */);
|
|
814
|
-
let opacity = tip.pressureOpacity ? s.pressure : 1;
|
|
815
|
-
opacity *= 1 - dyn.tiltOpacity * s.lean * 0.8;
|
|
816
|
-
if (s.speed !== null && dyn.velocityOpacity) opacity *= Math.max(0, 1 + dyn.velocityOpacity * (Math.min(1, s.speed / FULL_SPEED) - 0.5));
|
|
817
|
-
if (dyn.opacityJitter) opacity *= 1 - dyn.opacityJitter * (randSigned(seed, i, 3 /* Opacity */) * 0.5 + 0.5);
|
|
818
|
-
const flow = tip.flow * (1 - dyn.pressureFlow + dyn.pressureFlow * s.pressure);
|
|
819
|
-
let { x, y } = s;
|
|
820
|
-
if (dyn.scatter) {
|
|
821
|
-
const off = dyn.scatter * diameter * randSigned(seed, i, 4 /* Scatter */);
|
|
822
|
-
x += Math.sin(this.dir) * off;
|
|
823
|
-
y += Math.cos(this.dir) * off;
|
|
824
|
-
}
|
|
825
|
-
return {
|
|
826
|
-
x,
|
|
827
|
-
y,
|
|
828
|
-
radius: Math.max(0.5, diameter / 2),
|
|
829
|
-
alpha: Math.min(1, Math.max(0, flow)),
|
|
830
|
-
opacity: Math.min(1, Math.max(0, opacity)),
|
|
831
|
-
angle,
|
|
832
|
-
roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),
|
|
833
|
-
pressure: s.pressure,
|
|
834
|
-
direction: this.dir,
|
|
835
|
-
index: i,
|
|
836
|
-
travel: s.travel,
|
|
837
|
-
time: s.time,
|
|
838
|
-
lean: s.lean,
|
|
839
|
-
speed: s.speed
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
push(p, out) {
|
|
843
|
-
const a = this.last;
|
|
844
|
-
const s = this.sample(p);
|
|
845
|
-
if (!a) {
|
|
846
|
-
this.last = s;
|
|
847
|
-
this.pendingFirst = s;
|
|
848
|
-
this.nextAt = this.stepLen(s);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
const dx = s.x - a.x;
|
|
852
|
-
const dy = s.y - a.y;
|
|
853
|
-
const len = Math.hypot(dx, dy);
|
|
854
|
-
if (len > 1e-9) this.dir = Math.atan2(-dy, dx);
|
|
855
|
-
if (this.pendingFirst) {
|
|
856
|
-
out.push(this.dab(this.pendingFirst));
|
|
857
|
-
this.pendingFirst = null;
|
|
858
|
-
}
|
|
859
|
-
if (len > 1e-9) {
|
|
860
|
-
while (this.nextAt <= len + 1e-9) {
|
|
861
|
-
const f = Math.min(1, this.nextAt / len);
|
|
862
|
-
const lerp = (u, v) => u + (v - u) * f;
|
|
863
|
-
const q = {
|
|
864
|
-
x: a.x + dx * f,
|
|
865
|
-
y: a.y + dy * f,
|
|
866
|
-
pressure: lerp(a.pressure, s.pressure),
|
|
867
|
-
lean: lerp(a.lean, s.lean),
|
|
868
|
-
azimuth: lerpAngle(a.azimuth, s.azimuth, f),
|
|
869
|
-
twist: a.twist !== null && s.twist !== null ? lerpDegrees(a.twist, s.twist, f) : s.twist,
|
|
870
|
-
speed: a.speed !== null && s.speed !== null ? lerp(a.speed, s.speed) : s.speed,
|
|
871
|
-
travel: a.travel + this.nextAt,
|
|
872
|
-
time: a.time !== null && s.time !== null ? lerp(a.time, s.time) : s.time
|
|
873
|
-
};
|
|
874
|
-
out.push(this.dab(q));
|
|
875
|
-
this.nextAt += this.stepLen(q);
|
|
876
|
-
}
|
|
877
|
-
this.nextAt -= len;
|
|
878
|
-
}
|
|
879
|
-
this.last = s;
|
|
880
|
-
}
|
|
881
|
-
finish(out) {
|
|
882
|
-
if (this.pendingFirst) {
|
|
883
|
-
out.push(this.dab(this.pendingFirst));
|
|
884
|
-
this.pendingFirst = null;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
function lerpAngle(a, b, t) {
|
|
889
|
-
let d = b - a;
|
|
890
|
-
while (d > Math.PI) d -= 2 * Math.PI;
|
|
891
|
-
while (d < -Math.PI) d += 2 * Math.PI;
|
|
892
|
-
return a + d * t;
|
|
893
|
-
}
|
|
894
|
-
function lerpDegrees(a, b, t) {
|
|
895
|
-
let d = b - a;
|
|
896
|
-
while (d > 180) d -= 360;
|
|
897
|
-
while (d < -180) d += 360;
|
|
898
|
-
return a + d * t;
|
|
899
|
-
}
|
|
900
|
-
var DryStroke = class {
|
|
901
|
-
constructor(brush, options) {
|
|
902
|
-
this.brush = brush;
|
|
903
|
-
this.options = options;
|
|
904
|
-
this.stamp = brush.stamp ? stampSampler(brush.stamp, options.stampArt) : null;
|
|
905
|
-
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
906
|
-
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
907
|
-
this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear));
|
|
908
|
-
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
909
|
-
}
|
|
910
|
-
brush;
|
|
911
|
-
options;
|
|
912
|
-
coverage = new CoverageMap();
|
|
913
|
-
dabCount = 0;
|
|
914
|
-
smoother;
|
|
915
|
-
walkers;
|
|
916
|
-
copies;
|
|
917
|
-
depth;
|
|
918
|
-
buf = new Float32Array(0);
|
|
919
|
-
field = new Float32Array(0);
|
|
920
|
-
lastRaw = null;
|
|
921
|
-
lastFed = null;
|
|
922
|
-
/** The stamp tip's shape, or `null` for the round computed tip. */
|
|
923
|
-
stamp;
|
|
924
|
-
/** Feeds raw pen samples; returns the document rectangles the new dabs touched. */
|
|
925
|
-
push(points) {
|
|
926
|
-
const dabs = [];
|
|
927
|
-
for (const raw of points) {
|
|
928
|
-
this.lastRaw = raw;
|
|
929
|
-
const p = this.smoother.push(raw);
|
|
930
|
-
this.lastFed = p;
|
|
931
|
-
this.copies.forEach((c, i) => this.walkers[i].push(c.map(p), dabs));
|
|
932
|
-
}
|
|
933
|
-
return this.paint(dabs);
|
|
934
|
-
}
|
|
935
|
-
/** Ends the stroke: the smoothed path catches up with the last raw sample; a single tap paints one dab. */
|
|
936
|
-
finish() {
|
|
937
|
-
const dabs = [];
|
|
938
|
-
const raw = this.lastRaw;
|
|
939
|
-
const fed = this.lastFed;
|
|
940
|
-
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));
|
|
941
|
-
for (const w of this.walkers) w.finish(dabs);
|
|
942
|
-
return this.paint(dabs);
|
|
943
|
-
}
|
|
944
|
-
paint(dabs) {
|
|
945
|
-
const rects = [];
|
|
946
|
-
const spray = this.brush.spray;
|
|
947
|
-
const seed = this.options.seed ?? 0;
|
|
948
|
-
for (const d of dabs) {
|
|
949
|
-
if (!spray) {
|
|
950
|
-
this.mark(d, d, rects);
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
for (const drop of sprayDroplets(d, spray, seed)) this.mark(drop, d, rects);
|
|
954
|
-
}
|
|
955
|
-
return rects;
|
|
956
|
-
}
|
|
957
|
-
/** Lays one mark (a dab, or a spray droplet of dab `d`): the round tip or the stamp, the paper grain, effect paint. */
|
|
958
|
-
mark(m, d, rects) {
|
|
959
|
-
const hardness = this.brush.tip.hardness;
|
|
960
|
-
const grain = this.options.grain;
|
|
961
|
-
const rect = this.stamp ? stampDabRect(m) : dabRect(m, hardness);
|
|
962
|
-
const n = rect.width * rect.height;
|
|
963
|
-
if (this.buf.length < n) this.buf = new Float32Array(n);
|
|
964
|
-
const vals = this.buf.subarray(0, n);
|
|
965
|
-
if (this.stamp) rasterizeStampDab(m, this.stamp, d.index, rect, vals);
|
|
966
|
-
else rasterizeDab(m, hardness, rect, vals);
|
|
967
|
-
if (this.depth > 0 && grain) {
|
|
968
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
969
|
-
const y = rect.y + yy + 0.5;
|
|
970
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
971
|
-
if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
let field = null;
|
|
976
|
-
if (this.options.effect) {
|
|
977
|
-
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
978
|
-
field = this.field.subarray(0, n * 3);
|
|
979
|
-
sampleEffectField(this.options.effect, rect, vals, field, d);
|
|
980
|
-
}
|
|
981
|
-
this.coverage.accumulate(rect, vals, m.opacity, null, field);
|
|
982
|
-
rects.push(rect);
|
|
983
|
-
this.dabCount++;
|
|
984
|
-
}
|
|
985
|
-
};
|
|
986
|
-
function mixerTip(brush) {
|
|
987
|
-
return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };
|
|
988
|
-
}
|
|
989
|
-
function toStrokePoint(p) {
|
|
990
|
-
return { x: p.x, y: p.y, pressure: pressureOf(p), ...typeof p.t === "number" ? { t: p.t } : {} };
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// src/paint-editor/dab-stroke.ts
|
|
994
|
-
var GRADIENT_STEPS = 32;
|
|
995
|
-
function multiColorPalette(mc, color, secondary) {
|
|
996
|
-
if (mc.colors.length >= 2) {
|
|
997
|
-
return mc.colors.map((h) => {
|
|
998
|
-
const n = parseInt(h.slice(1), 16);
|
|
999
|
-
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
return [[...color], [...secondary]];
|
|
1003
|
-
}
|
|
1004
|
-
function soften(f, softness) {
|
|
1005
|
-
if (softness >= 1) return f;
|
|
1006
|
-
const w = Math.max(1e-3, softness);
|
|
1007
|
-
return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));
|
|
1008
|
-
}
|
|
1009
|
-
function mixRgb(a, b, t, mixing) {
|
|
1010
|
-
if (t <= 0) return [...a];
|
|
1011
|
-
if (t >= 1) return [...b];
|
|
1012
|
-
if (mixing === "pigment") {
|
|
1013
|
-
const o = [0, 0, 0];
|
|
1014
|
-
const q = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255);
|
|
1015
|
-
mixPigment8(q(a[0]), q(a[1]), q(a[2]), q(b[0]), q(b[1]), q(b[2]), t, o);
|
|
1016
|
-
return [o[0], o[1], o[2]];
|
|
1017
|
-
}
|
|
1018
|
-
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
1019
|
-
}
|
|
1020
|
-
function multiColorGradient(palette, softness, mixing) {
|
|
1021
|
-
const n = palette.length;
|
|
1022
|
-
const out = [];
|
|
1023
|
-
for (let j = 0; j < GRADIENT_STEPS; j++) {
|
|
1024
|
-
const f = j / (GRADIENT_STEPS - 1) * (n - 1);
|
|
1025
|
-
const i = Math.min(n - 2, Math.floor(f));
|
|
1026
|
-
out.push(mixRgb(palette[i], palette[i + 1], soften(f - i, softness), mixing));
|
|
1027
|
-
}
|
|
1028
|
-
return out;
|
|
1029
|
-
}
|
|
1030
|
-
function multiColorForDab(mc, palette, index, seed, mixing) {
|
|
1031
|
-
const n = palette.length;
|
|
1032
|
-
if (mc.order === "random") {
|
|
1033
|
-
const run = Math.floor(index / mc.length);
|
|
1034
|
-
return [...palette[Math.min(n - 1, Math.floor(rand01(seed, run, 11) * n))]];
|
|
1035
|
-
}
|
|
1036
|
-
const f = index / mc.length % n;
|
|
1037
|
-
const i = Math.floor(f);
|
|
1038
|
-
return mixRgb(palette[i], palette[(i + 1) % n], soften(f - i, mc.softness), mixing);
|
|
1039
|
-
}
|
|
1040
|
-
var PaintDabStroke = class {
|
|
1041
|
-
constructor(brush, options) {
|
|
1042
|
-
this.brush = brush;
|
|
1043
|
-
this.options = options;
|
|
1044
|
-
this.stamp = brush.engine === "dab" && brush.stamp ? stampSampler(brush.stamp, options.stampArt) : null;
|
|
1045
|
-
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
1046
|
-
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
1047
|
-
const exchanging = brush.engine === "mixer" || brush.engine === "dab";
|
|
1048
|
-
const rates = brush.engine === "mixer" ? mixerRates(brush.mixer) : exchangeRates(brush.behavior.pickup, brush.behavior.mix, 1, brush.behavior.mixing);
|
|
1049
|
-
this.mixing = brush.engine === "mixer" ? brush.mixer.mixing : brush.behavior.mixing;
|
|
1050
|
-
this.states = this.copies.map((c, i) => ({
|
|
1051
|
-
walker: new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear),
|
|
1052
|
-
exchange: exchanging ? new PaintExchange(rates, options.load) : null,
|
|
1053
|
-
smudge: brush.engine === "smudge" ? new SmudgeTip(brush.smudge, brush.tip.size * (1 + brush.dynamics.tiltSize * 1.5)) : null
|
|
1054
|
-
}));
|
|
1055
|
-
this.medium = new TileMedium(options.target, brush.engine === "dab" ? { blend: brush.tip.blend } : {});
|
|
1056
|
-
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
1057
|
-
this.coverage = brush.engine === "dab" ? new CoverageMap() : null;
|
|
1058
|
-
this.sampler = exchanging && options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
|
|
1059
|
-
const table = this.sampler?.acrossTip ? this.sampler.tipTable(GRADIENT_STEPS) : null;
|
|
1060
|
-
this.gradient = table ? table.map((e) => e.rgb) : null;
|
|
1061
|
-
this.gradientAlpha = table && table.some((e) => e.alpha < 1) ? table.map((e) => e.alpha) : null;
|
|
1062
|
-
this.scratch = brush.engine === "blender" ? createBlenderScratch() : null;
|
|
1063
|
-
}
|
|
1064
|
-
brush;
|
|
1065
|
-
options;
|
|
1066
|
-
dabCount = 0;
|
|
1067
|
-
smoother;
|
|
1068
|
-
copies;
|
|
1069
|
-
states;
|
|
1070
|
-
medium;
|
|
1071
|
-
depth;
|
|
1072
|
-
coverage;
|
|
1073
|
-
sampler;
|
|
1074
|
-
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
1075
|
-
gradient;
|
|
1076
|
-
/** Opacity across the tip (`across-tip` gradients with opacity stops), or `null`. */
|
|
1077
|
-
gradientAlpha;
|
|
1078
|
-
alphaCov = new Float32Array(0);
|
|
1079
|
-
fieldLut = [];
|
|
1080
|
-
scratch;
|
|
1081
|
-
mixing;
|
|
1082
|
-
tip = new Float32Array(0);
|
|
1083
|
-
cov = new Float32Array(0);
|
|
1084
|
-
field = new Float32Array(0);
|
|
1085
|
-
paint = [0, 0, 0];
|
|
1086
|
-
lastRaw = null;
|
|
1087
|
-
lastFed = null;
|
|
1088
|
-
/** The stamp tip's shape (dab brushes), or `null` for the round computed tip. */
|
|
1089
|
-
stamp;
|
|
1090
|
-
/** What copy 0's brush carries after the dabs so far (mixer and wet-capable dab brushes). */
|
|
1091
|
-
get load() {
|
|
1092
|
-
return this.states[0].exchange?.load ?? null;
|
|
1093
|
-
}
|
|
1094
|
-
push(points) {
|
|
1095
|
-
const rects = [];
|
|
1096
|
-
for (const raw of points) {
|
|
1097
|
-
this.lastRaw = raw;
|
|
1098
|
-
const p = this.smoother.push(raw);
|
|
1099
|
-
this.lastFed = p;
|
|
1100
|
-
this.copies.forEach((c, i) => {
|
|
1101
|
-
const dabs = [];
|
|
1102
|
-
this.states[i].walker.push(c.map(p), dabs);
|
|
1103
|
-
this.paintDabs(i, dabs, rects);
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
return rects;
|
|
1107
|
-
}
|
|
1108
|
-
/** Ends the stroke: the smoothed path catches up with the last raw sample; a tap paints one dab. */
|
|
1109
|
-
finish() {
|
|
1110
|
-
const rects = [];
|
|
1111
|
-
const raw = this.lastRaw;
|
|
1112
|
-
const fed = this.lastFed;
|
|
1113
|
-
this.copies.forEach((c, i) => {
|
|
1114
|
-
const dabs = [];
|
|
1115
|
-
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
1116
|
-
this.states[i].walker.finish(dabs);
|
|
1117
|
-
this.paintDabs(i, dabs, rects);
|
|
1118
|
-
});
|
|
1119
|
-
return rects;
|
|
1120
|
-
}
|
|
1121
|
-
paintDabs(copy, dabs, rects) {
|
|
1122
|
-
const spray = this.brush.engine === "dab" ? this.brush.spray : null;
|
|
1123
|
-
for (const d of spray ? dabs.flatMap((dab) => sprayDroplets(dab, spray, (this.options.seed ?? 0) + copy * 7919)) : dabs) {
|
|
1124
|
-
const rect = this.stamp ? stampDabRect(d) : dabRect(d, this.brush.tip.hardness);
|
|
1125
|
-
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1126
|
-
this.dab(copy, d, rect);
|
|
1127
|
-
rects.push(rect);
|
|
1128
|
-
this.dabCount++;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
/** The tip's pure coverage (`tip`) and, with grain, what the paper catches of it (`cov`). */
|
|
1132
|
-
rasterize(d, rect) {
|
|
1133
|
-
const n = rect.width * rect.height;
|
|
1134
|
-
if (this.tip.length < n) {
|
|
1135
|
-
this.tip = new Float32Array(n);
|
|
1136
|
-
this.cov = new Float32Array(n);
|
|
1137
|
-
}
|
|
1138
|
-
const tip = this.tip.subarray(0, n);
|
|
1139
|
-
tip.fill(0);
|
|
1140
|
-
if (this.stamp) rasterizeStampDab({ ...d, alpha: 1 }, this.stamp, d.index, rect, tip);
|
|
1141
|
-
else rasterizeDab({ ...d, alpha: 1 }, this.brush.tip.hardness, rect, tip);
|
|
1142
|
-
if (!(this.depth > 0 && this.options.grain)) return { tip, cov: tip };
|
|
1143
|
-
const cov = this.cov.subarray(0, n);
|
|
1144
|
-
const grain = this.options.grain;
|
|
1145
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1146
|
-
const y = rect.y + yy + 0.5;
|
|
1147
|
-
for (let xx = 0; xx < rect.width; xx++, k++) cov[k] = tip[k] > 0 ? tip[k] * grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure) : 0;
|
|
1148
|
-
}
|
|
1149
|
-
return { tip, cov };
|
|
1150
|
-
}
|
|
1151
|
-
dab(copy, d, rect) {
|
|
1152
|
-
const brush = this.brush;
|
|
1153
|
-
const { tip, cov } = this.rasterize(d, rect);
|
|
1154
|
-
const st = this.states[copy];
|
|
1155
|
-
if (brush.engine === "blender") {
|
|
1156
|
-
blenderDab(this.medium, rect, cov, d.opacity * d.alpha * brush.tip.opacity, d.radius, brush.blender, this.scratch);
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
if (brush.engine === "smudge") {
|
|
1160
|
-
st.smudge.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: d.opacity * d.alpha * brush.tip.opacity }, rect, cov);
|
|
1161
|
-
return;
|
|
1162
|
-
}
|
|
1163
|
-
const ex = st.exchange;
|
|
1164
|
-
ex.pickUp(this.medium.sample(rect, tip));
|
|
1165
|
-
let flow = 1;
|
|
1166
|
-
if (this.options.effect) {
|
|
1167
|
-
const rgb = [0, 0, 0];
|
|
1168
|
-
flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb, d);
|
|
1169
|
-
ex.setReservoirColor(rgb[0], rgb[1], rgb[2]);
|
|
1170
|
-
} else if (this.sampler && !this.gradient) {
|
|
1171
|
-
const g = this.sampler.dab(d, this.sample);
|
|
1172
|
-
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
1173
|
-
flow = g.alpha;
|
|
1174
|
-
}
|
|
1175
|
-
let field = null;
|
|
1176
|
-
let total;
|
|
1177
|
-
let laid = cov;
|
|
1178
|
-
if (this.gradient) {
|
|
1179
|
-
total = this.tipField(ex, d, rect);
|
|
1180
|
-
field = this.field;
|
|
1181
|
-
if (this.gradientAlpha) laid = this.tipAlpha(d, rect, cov);
|
|
1182
|
-
} else total = ex.paint(this.paint);
|
|
1183
|
-
if (total > 0 && flow > 0) {
|
|
1184
|
-
if (this.coverage) this.depositWithCeiling(d, rect, laid, field, ex, flow);
|
|
1185
|
-
else {
|
|
1186
|
-
const dep = Math.min(1, total) * d.alpha * brush.tip.opacity * d.opacity * flow;
|
|
1187
|
-
if (dep > 0) this.medium.deposit(rect, laid, dep, this.paint, ex.paintPigment, field);
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
ex.drain();
|
|
1191
|
-
}
|
|
1192
|
-
/** Fills `this.field` with the tip gradient (mixed with the carried paint) per pixel; returns the paint weight. */
|
|
1193
|
-
tipField(ex, d, rect) {
|
|
1194
|
-
const g = this.gradient;
|
|
1195
|
-
const lut = this.fieldLut;
|
|
1196
|
-
let total = 0;
|
|
1197
|
-
for (let j = 0; j < g.length; j++) {
|
|
1198
|
-
ex.setReservoirColor(g[j][0], g[j][1], g[j][2]);
|
|
1199
|
-
const out = lut[j] ?? [0, 0, 0];
|
|
1200
|
-
total = ex.paint(out);
|
|
1201
|
-
lut[j] = out;
|
|
1202
|
-
}
|
|
1203
|
-
const n = rect.width * rect.height;
|
|
1204
|
-
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
1205
|
-
const f = this.field;
|
|
1206
|
-
const nx = Math.sin(d.direction);
|
|
1207
|
-
const ny = Math.cos(d.direction);
|
|
1208
|
-
const r = Math.max(0.5, d.radius);
|
|
1209
|
-
const last = g.length - 1;
|
|
1210
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1211
|
-
const py = rect.y + yy + 0.5 - d.y;
|
|
1212
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1213
|
-
const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;
|
|
1214
|
-
const j = Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)));
|
|
1215
|
-
f[k * 3] = lut[j][0];
|
|
1216
|
-
f[k * 3 + 1] = lut[j][1];
|
|
1217
|
-
f[k * 3 + 2] = lut[j][2];
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
return total;
|
|
1221
|
-
}
|
|
1222
|
-
/** `cov` scaled by the gradient's opacity across the tip. */
|
|
1223
|
-
tipAlpha(d, rect, cov) {
|
|
1224
|
-
const a = this.gradientAlpha;
|
|
1225
|
-
const n = rect.width * rect.height;
|
|
1226
|
-
if (this.alphaCov.length < n) this.alphaCov = new Float32Array(n);
|
|
1227
|
-
const out = this.alphaCov.subarray(0, n);
|
|
1228
|
-
const nx = Math.sin(d.direction);
|
|
1229
|
-
const ny = Math.cos(d.direction);
|
|
1230
|
-
const r = Math.max(0.5, d.radius);
|
|
1231
|
-
const last = a.length - 1;
|
|
1232
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1233
|
-
const py = rect.y + yy + 0.5 - d.y;
|
|
1234
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1235
|
-
const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;
|
|
1236
|
-
out[k] = cov[k] * a[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
return out;
|
|
1240
|
-
}
|
|
1241
|
-
/** A dab brush's deposit: only what this dab adds to the stroke's coverage, under its opacity ceiling. */
|
|
1242
|
-
depositWithCeiling(d, rect, cov, field, ex, flow = 1) {
|
|
1243
|
-
const map = this.coverage;
|
|
1244
|
-
const n = rect.width * rect.height;
|
|
1245
|
-
const before = new Float32Array(n);
|
|
1246
|
-
const vals = new Float32Array(n);
|
|
1247
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1248
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1249
|
-
if (cov[k] <= 0) continue;
|
|
1250
|
-
before[k] = map.get(rect.x + xx, rect.y + yy);
|
|
1251
|
-
vals[k] = cov[k] * d.alpha * flow;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
map.accumulate(rect, vals, d.opacity);
|
|
1255
|
-
const o = this.brush.tip.opacity;
|
|
1256
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1257
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1258
|
-
if (cov[k] <= 0) {
|
|
1259
|
-
vals[k] = 0;
|
|
1260
|
-
continue;
|
|
1261
|
-
}
|
|
1262
|
-
const c0 = before[k] * o;
|
|
1263
|
-
const c1 = map.get(rect.x + xx, rect.y + yy) * o;
|
|
1264
|
-
vals[k] = c1 > c0 && c0 < 1 ? (c1 - c0) / (1 - c0) : 0;
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
this.medium.deposit(rect, vals, 1, this.paint, ex.paintPigment, field);
|
|
330
|
+
import "../chunk-HF5QZS7Z.js";
|
|
331
|
+
import "../chunk-M2TPMYJE.js";
|
|
332
|
+
import "../chunk-2WPNAGHI.js";
|
|
333
|
+
import {
|
|
334
|
+
HARMONY_KINDS,
|
|
335
|
+
colorHarmony,
|
|
336
|
+
hueToWheel,
|
|
337
|
+
wheelToHue
|
|
338
|
+
} from "../chunk-2EUW2XZX.js";
|
|
339
|
+
import "../chunk-E2V5DA3W.js";
|
|
340
|
+
import "../chunk-VR6OGHU5.js";
|
|
341
|
+
import "../chunk-JC2SHA4A.js";
|
|
342
|
+
import {
|
|
343
|
+
linearToSrgb,
|
|
344
|
+
oklabToOklch,
|
|
345
|
+
oklabToRgb,
|
|
346
|
+
oklchToOklab,
|
|
347
|
+
rgbToOklab,
|
|
348
|
+
srgbToLinear
|
|
349
|
+
} from "../chunk-MHLU6GRJ.js";
|
|
350
|
+
import "../chunk-6ONMQ6WF.js";
|
|
351
|
+
|
|
352
|
+
// src/paint-editor/document.ts
|
|
353
|
+
var MIN_CANVAS_SIDE = 1;
|
|
354
|
+
var MAX_CANVAS_SIDE = 16384;
|
|
355
|
+
var DEFAULT_DPI = 300;
|
|
356
|
+
var nextId = 1;
|
|
357
|
+
function newPaintLayerId() {
|
|
358
|
+
return `pl${Date.now().toString(36)}${(nextId++).toString(36)}`;
|
|
359
|
+
}
|
|
360
|
+
function createPaintLayer(name, id = newPaintLayerId()) {
|
|
361
|
+
return { type: "pixel", id, name, visible: true, opacity: 1, blend: "normal", surface: emptySurface(4), offset: { x: 0, y: 0 } };
|
|
362
|
+
}
|
|
363
|
+
var PaintDocumentError = class extends Error {
|
|
364
|
+
constructor(message) {
|
|
365
|
+
super(message);
|
|
366
|
+
this.name = "PaintDocumentError";
|
|
1268
367
|
}
|
|
1269
368
|
};
|
|
369
|
+
function normalizeCanvas(raw) {
|
|
370
|
+
const side = (v, what) => {
|
|
371
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new PaintDocumentError(`${what} must be a number of pixels (${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE}).`);
|
|
372
|
+
const n = Math.round(v);
|
|
373
|
+
if (n < MIN_CANVAS_SIDE || n > MAX_CANVAS_SIDE) throw new PaintDocumentError(`${what} ${n} is out of range; use ${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE} pixels.`);
|
|
374
|
+
return n;
|
|
375
|
+
};
|
|
376
|
+
const dpi = raw.dpi === void 0 ? DEFAULT_DPI : raw.dpi;
|
|
377
|
+
if (typeof dpi !== "number" || !Number.isFinite(dpi) || dpi < 1 || dpi > 1e4) throw new PaintDocumentError(`dpi ${dpi} is out of range; use 1..10000.`);
|
|
378
|
+
return { width: side(raw.width, "width"), height: side(raw.height, "height"), dpi: Math.round(dpi * 100) / 100 };
|
|
379
|
+
}
|
|
380
|
+
function createPaintDocument(options = {}) {
|
|
381
|
+
const canvas = normalizeCanvas({ width: options.width ?? 2048, height: options.height ?? 1536, dpi: options.dpi });
|
|
382
|
+
const layer = createPaintLayer("Layer 1");
|
|
383
|
+
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
384
|
+
}
|
|
1270
385
|
|
|
1271
386
|
// src/paint-editor/thick.ts
|
|
1272
387
|
var DEFAULT_LIGHTING = { azimuth: 315, elevation: 40, relief: 0.7 };
|
|
@@ -3937,696 +3052,6 @@ function downloadBytes(bytes, fileName, mimeType) {
|
|
|
3937
3052
|
setTimeout(() => g.URL.revokeObjectURL(url), 1e3);
|
|
3938
3053
|
}
|
|
3939
3054
|
|
|
3940
|
-
// src/paint-editor/wet.ts
|
|
3941
|
-
var STROKE_WATER = 0.9;
|
|
3942
|
-
var STROKE_MASS = PIGMENT_UNIT * 2.5;
|
|
3943
|
-
var STROKE_LIGHT = 1.5;
|
|
3944
|
-
var COVERAGE_STEPS = 4096;
|
|
3945
|
-
function union2(a, b) {
|
|
3946
|
-
if (!a) return { ...b };
|
|
3947
|
-
const x = Math.min(a.x, b.x);
|
|
3948
|
-
const y = Math.min(a.y, b.y);
|
|
3949
|
-
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
3950
|
-
}
|
|
3951
|
-
function wetPaperInput(paper) {
|
|
3952
|
-
const grain = new PaperGrain(paper, "wet");
|
|
3953
|
-
const flat = grain.flat;
|
|
3954
|
-
return {
|
|
3955
|
-
absorption: paper.absorption,
|
|
3956
|
-
permeability: paper.permeability,
|
|
3957
|
-
roughness: flat ? 0 : paper.roughness,
|
|
3958
|
-
color: hexToRgb8(paper.color),
|
|
3959
|
-
heightAt: flat ? () => 0.5 : (x, y) => grain.at(x, y)
|
|
3960
|
-
};
|
|
3961
|
-
}
|
|
3962
|
-
var WetStrokeDriver = class {
|
|
3963
|
-
constructor(init) {
|
|
3964
|
-
this.init = init;
|
|
3965
|
-
const brush = init.brush;
|
|
3966
|
-
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
3967
|
-
const copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
3968
|
-
const wet = brush.wet;
|
|
3969
|
-
const spacing = Math.max(0.02, brush.tip.spacing);
|
|
3970
|
-
const perDab = Math.min(1, spacing * 1.2);
|
|
3971
|
-
const thin = wet.thinner;
|
|
3972
|
-
this.medium = new WetMedium(init.world, init.laneId, {
|
|
3973
|
-
water: STROKE_WATER * Math.min(1, wet.water * (1 + thin)) * perDab,
|
|
3974
|
-
mass: STROKE_MASS * wet.pigment * (1 - 0.6 * thin) * perDab,
|
|
3975
|
-
body: wet.body * (1 - thin),
|
|
3976
|
-
light: 0
|
|
3977
|
-
});
|
|
3978
|
-
this.lightBase = wet.tool === "paint" ? STROKE_LIGHT * brush.light.amount * (1 - 0.6 * thin) * perDab : 0;
|
|
3979
|
-
const rates = exchangeRates(brush.behavior.pickup, brush.behavior.mix, wet.load, brush.behavior.mixing);
|
|
3980
|
-
this.states = copies.map((c, i) => ({
|
|
3981
|
-
walker: new PenDabWalker(brush, init.seed + i * 7919 | 0, c.linear),
|
|
3982
|
-
exchange: wet.tool === "paint" ? new PaintExchange(rates, init.load) : null,
|
|
3983
|
-
last: null
|
|
3984
|
-
}));
|
|
3985
|
-
this.copies = copies;
|
|
3986
|
-
this.depth = init.grain && !init.grain.flat ? Math.min(1, Math.max(0, init.paperDepth * brush.grain)) : 0;
|
|
3987
|
-
const g = init.paint ?? null;
|
|
3988
|
-
this.sampler = g ? new PaintGradientSampler(g.map.by === "across-tip" ? { ...g, map: { by: "distance", length: 300, repeat: "mirror" } } : g, init.seed) : null;
|
|
3989
|
-
}
|
|
3990
|
-
init;
|
|
3991
|
-
dabCount = 0;
|
|
3992
|
-
bounds = null;
|
|
3993
|
-
/** The world's clock at which each raw point was applied (recorded with the stroke). */
|
|
3994
|
-
steps = [];
|
|
3995
|
-
smoother;
|
|
3996
|
-
states;
|
|
3997
|
-
medium;
|
|
3998
|
-
queue = [];
|
|
3999
|
-
finishing = false;
|
|
4000
|
-
finished = false;
|
|
4001
|
-
lastRaw = null;
|
|
4002
|
-
lastFed = null;
|
|
4003
|
-
tip = new Float32Array(0);
|
|
4004
|
-
cov = new Float32Array(0);
|
|
4005
|
-
paint = [0, 0, 0];
|
|
4006
|
-
centre = [0, 0, 0];
|
|
4007
|
-
depth;
|
|
4008
|
-
lightBase;
|
|
4009
|
-
sampler;
|
|
4010
|
-
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
4011
|
-
copies;
|
|
4012
|
-
/** What copy 0's brush carries after the dabs so far. */
|
|
4013
|
-
get load() {
|
|
4014
|
-
return this.states[0].exchange?.load ?? null;
|
|
4015
|
-
}
|
|
4016
|
-
/** Whether every point (and the end) has been applied. */
|
|
4017
|
-
get done() {
|
|
4018
|
-
return this.finished;
|
|
4019
|
-
}
|
|
4020
|
-
/** Adds raw points; applies them now unless a batch is running. Returns where pixels changed. */
|
|
4021
|
-
push(points) {
|
|
4022
|
-
for (const p of points) this.queue.push(p);
|
|
4023
|
-
return this.flush();
|
|
4024
|
-
}
|
|
4025
|
-
/** Asks for the end; it happens now unless a batch is running (then at the next `flush`). */
|
|
4026
|
-
finish() {
|
|
4027
|
-
this.finishing = true;
|
|
4028
|
-
return this.flush();
|
|
4029
|
-
}
|
|
4030
|
-
/** Applies waiting points (and the end, when asked) if the world is between batches. */
|
|
4031
|
-
flush() {
|
|
4032
|
-
const rects = [];
|
|
4033
|
-
if (this.init.world.busy || this.finished) return rects;
|
|
4034
|
-
while (this.queue.length) {
|
|
4035
|
-
const raw = this.queue.shift();
|
|
4036
|
-
this.steps.push(this.init.world.clock);
|
|
4037
|
-
this.lastRaw = raw;
|
|
4038
|
-
const p = this.smoother.push(raw);
|
|
4039
|
-
this.lastFed = p;
|
|
4040
|
-
this.copies.forEach((c, i) => {
|
|
4041
|
-
const dabs = [];
|
|
4042
|
-
this.states[i].walker.push(c.map(p), dabs);
|
|
4043
|
-
this.applyDabs(i, dabs, rects);
|
|
4044
|
-
});
|
|
4045
|
-
}
|
|
4046
|
-
if (this.finishing) {
|
|
4047
|
-
const raw = this.lastRaw;
|
|
4048
|
-
const fed = this.lastFed;
|
|
4049
|
-
this.copies.forEach((c, i) => {
|
|
4050
|
-
const dabs = [];
|
|
4051
|
-
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
4052
|
-
this.states[i].walker.finish(dabs);
|
|
4053
|
-
this.applyDabs(i, dabs, rects);
|
|
4054
|
-
});
|
|
4055
|
-
this.finished = true;
|
|
4056
|
-
}
|
|
4057
|
-
for (const r of rects) this.bounds = union2(this.bounds, r);
|
|
4058
|
-
return rects;
|
|
4059
|
-
}
|
|
4060
|
-
applyDabs(copy, dabs, rects) {
|
|
4061
|
-
for (const d of dabs) {
|
|
4062
|
-
const rect = dabRect(d, this.init.brush.tip.hardness);
|
|
4063
|
-
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
4064
|
-
this.dab(copy, d, rect);
|
|
4065
|
-
rects.push(rect);
|
|
4066
|
-
this.dabCount++;
|
|
4067
|
-
}
|
|
4068
|
-
}
|
|
4069
|
-
/** The tip's coverage over `rect`, with the grain, quantised. */
|
|
4070
|
-
coverage(d, rect) {
|
|
4071
|
-
const n = rect.width * rect.height;
|
|
4072
|
-
if (this.tip.length < n) {
|
|
4073
|
-
this.tip = new Float32Array(n);
|
|
4074
|
-
this.cov = new Float32Array(n);
|
|
4075
|
-
}
|
|
4076
|
-
const tip = this.tip.subarray(0, n);
|
|
4077
|
-
const cov = this.cov.subarray(0, n);
|
|
4078
|
-
tip.fill(0);
|
|
4079
|
-
rasterizeDab({ ...d, alpha: 1 }, this.init.brush.tip.hardness, rect, tip);
|
|
4080
|
-
const grain = this.depth > 0 ? this.init.grain : null;
|
|
4081
|
-
const mask = this.init.mask;
|
|
4082
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
4083
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
4084
|
-
if (mask && tip[k] > 0) {
|
|
4085
|
-
const mx = rect.x + xx;
|
|
4086
|
-
const my = rect.y + yy;
|
|
4087
|
-
tip[k] *= mx >= 0 && my >= 0 && mx < mask.width && my < mask.height ? mask.data[my * mask.width + mx] / 255 : 0;
|
|
4088
|
-
}
|
|
4089
|
-
let v = tip[k];
|
|
4090
|
-
if (v > 0 && grain) v *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, d.pressure);
|
|
4091
|
-
tip[k] = Math.round(tip[k] * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
4092
|
-
cov[k] = Math.round(v * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
4093
|
-
}
|
|
4094
|
-
}
|
|
4095
|
-
return { tip, cov };
|
|
4096
|
-
}
|
|
4097
|
-
dab(copy, d, rect) {
|
|
4098
|
-
const { world, laneId, brush } = this.init;
|
|
4099
|
-
const wet = brush.wet;
|
|
4100
|
-
const st = this.states[copy];
|
|
4101
|
-
const { tip, cov } = this.coverage(d, rect);
|
|
4102
|
-
const k = Math.round(Math.min(1, d.alpha * d.opacity * brush.tip.opacity) * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
4103
|
-
if (!(k > 0)) return;
|
|
4104
|
-
switch (wet.tool) {
|
|
4105
|
-
case "paint": {
|
|
4106
|
-
const ex = st.exchange;
|
|
4107
|
-
this.medium.options.light = this.lightBase * (brush.light.pressure ? d.pressure : 1);
|
|
4108
|
-
let flow = 1;
|
|
4109
|
-
const effect = this.init.effect;
|
|
4110
|
-
if (effect) {
|
|
4111
|
-
sampleEffectAt(effect, d.x, d.y, this.centre, d);
|
|
4112
|
-
ex.setReservoirColor(this.centre[0], this.centre[1], this.centre[2]);
|
|
4113
|
-
} else if (this.sampler) {
|
|
4114
|
-
const g = this.sampler.dab(d, this.sample);
|
|
4115
|
-
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
4116
|
-
flow = g.alpha;
|
|
4117
|
-
}
|
|
4118
|
-
ex.pickUp(this.medium.sample(rect, tip));
|
|
4119
|
-
const total = ex.paint(this.paint);
|
|
4120
|
-
if (total > 0 && flow > 0 && effect) {
|
|
4121
|
-
const ec = cov.slice();
|
|
4122
|
-
const field = new Float32Array(ec.length * 3);
|
|
4123
|
-
effectDabField(effect, rect, ec, this.centre, this.paint, field, COVERAGE_STEPS, d);
|
|
4124
|
-
this.medium.deposit(rect, ec, Math.min(1, total) * k, this.paint, null, field);
|
|
4125
|
-
} else if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);
|
|
4126
|
-
else world.paint(laneId, rect, cov, this.medium.options.water * k, null);
|
|
4127
|
-
ex.drain();
|
|
4128
|
-
if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);
|
|
4129
|
-
break;
|
|
4130
|
-
}
|
|
4131
|
-
case "water": {
|
|
4132
|
-
world.paint(laneId, rect, cov, this.medium.options.water * k * (wet.water > 0 ? 1 : 0), null);
|
|
4133
|
-
if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);
|
|
4134
|
-
break;
|
|
4135
|
-
}
|
|
4136
|
-
case "dry":
|
|
4137
|
-
world.remove(laneId, rect, cov, wet.strength * 0.35 * k, 0, 0, wet.strength * 0.35 * k);
|
|
4138
|
-
break;
|
|
4139
|
-
case "blow": {
|
|
4140
|
-
const s = wet.strength * 0.08 * k;
|
|
4141
|
-
world.push(laneId, rect, cov, Math.cos(d.direction) * s, -Math.sin(d.direction) * s);
|
|
4142
|
-
break;
|
|
4143
|
-
}
|
|
4144
|
-
case "blend":
|
|
4145
|
-
world.blend(laneId, rect, cov, wet.strength * k);
|
|
4146
|
-
break;
|
|
4147
|
-
case "smudge": {
|
|
4148
|
-
if (st.last) world.drag(laneId, rect, cov, d.x - st.last.x, d.y - st.last.y, wet.strength * k);
|
|
4149
|
-
break;
|
|
4150
|
-
}
|
|
4151
|
-
case "blot":
|
|
4152
|
-
world.remove(laneId, rect, cov, wet.strength * 0.8 * k, wet.strength * 0.6 * k, wet.strength * 0.15 * k, wet.strength * 0.5 * k);
|
|
4153
|
-
break;
|
|
4154
|
-
}
|
|
4155
|
-
st.last = d;
|
|
4156
|
-
}
|
|
4157
|
-
};
|
|
4158
|
-
var WET_MEDIUM_PARAMS = {
|
|
4159
|
-
watercolor: {},
|
|
4160
|
-
gouache: { viscosity: 0.45, evaporation: 0.1, edgeDarkening: 0.15, granulation: 0.1, bloom: 0.1, bleed: 0.15, pickup: 0.5, pigmentWeight: 0.5 },
|
|
4161
|
-
oil: { viscosity: 0.95, evaporation: 0.03, edgeDarkening: 0, granulation: 0, bloom: 0, bleed: 0.08, pickup: 0.6, fixture: 0.02, pigmentWeight: 0.8 }
|
|
4162
|
-
};
|
|
4163
|
-
var WET_GPU_MODES = ["auto", "off"];
|
|
4164
|
-
var GPU_LEAD = 16;
|
|
4165
|
-
var DISPLAY_TILES_PER_TICK = 24;
|
|
4166
|
-
var PaintWetLayers = class {
|
|
4167
|
-
constructor(host, options = {}) {
|
|
4168
|
-
this.host = host;
|
|
4169
|
-
this.options = options;
|
|
4170
|
-
this.speed = Math.max(1, Math.min(2e3, options.speed ?? 120));
|
|
4171
|
-
this.undoMemory = Math.max(0, options.undoMemory ?? 512 * 1024 * 1024);
|
|
4172
|
-
this.gpuMode = options.gpu === "off" ? "off" : "auto";
|
|
4173
|
-
this.gpuMinTiles = Math.max(1, Math.floor(options.gpuMinTiles ?? 16));
|
|
4174
|
-
}
|
|
4175
|
-
host;
|
|
4176
|
-
options;
|
|
4177
|
-
world = null;
|
|
4178
|
-
tilt = { ...FLAT };
|
|
4179
|
-
settings = /* @__PURE__ */ new Map();
|
|
4180
|
-
paused = false;
|
|
4181
|
-
/** Wet strokes dry as soon as they end (one undo step with the stroke). */
|
|
4182
|
-
instantDry = false;
|
|
4183
|
-
speed;
|
|
4184
|
-
overlay = false;
|
|
4185
|
-
undoMemory;
|
|
4186
|
-
/** The live wet stroke, if any. */
|
|
4187
|
-
driver = null;
|
|
4188
|
-
lastTick = null;
|
|
4189
|
-
ticking = null;
|
|
4190
|
-
stepsOwed = 0;
|
|
4191
|
-
displaying = null;
|
|
4192
|
-
leadAt = 0;
|
|
4193
|
-
gpuMode;
|
|
4194
|
-
gpuMinTiles;
|
|
4195
|
-
gpu = null;
|
|
4196
|
-
gpuSupport = null;
|
|
4197
|
-
gpuStarting = null;
|
|
4198
|
-
// --- Engine B ------------------------------------------------------------------
|
|
4199
|
-
/** Starts the GPU engine for the world (once; it may turn out unavailable). */
|
|
4200
|
-
startGpu() {
|
|
4201
|
-
if (this.gpuMode === "off" || this.gpu || this.gpuStarting || this.gpuSupport) return;
|
|
4202
|
-
const provider = this.options.gpuProvider === void 0 ? findWebGpu() : this.options.gpuProvider;
|
|
4203
|
-
this.gpuStarting = createWetGpuEngine(provider).then(({ engine, support }) => {
|
|
4204
|
-
this.gpuSupport = support;
|
|
4205
|
-
if (!engine) return;
|
|
4206
|
-
if (this.gpuMode === "off" || !this.world) {
|
|
4207
|
-
engine.destroy();
|
|
4208
|
-
this.gpuSupport = null;
|
|
4209
|
-
return;
|
|
4210
|
-
}
|
|
4211
|
-
this.gpu = engine;
|
|
4212
|
-
this.world.gpuMinTiles = this.gpuMinTiles;
|
|
4213
|
-
this.world.attachGpu(engine);
|
|
4214
|
-
}).finally(() => {
|
|
4215
|
-
this.gpuStarting = null;
|
|
4216
|
-
this.host.changed();
|
|
4217
|
-
});
|
|
4218
|
-
}
|
|
4219
|
-
/** Resolves once the GPU engine has started or turned out unavailable. */
|
|
4220
|
-
async gpuReady() {
|
|
4221
|
-
if (this.gpuStarting) await this.gpuStarting;
|
|
4222
|
-
return this.engineInfo();
|
|
4223
|
-
}
|
|
4224
|
-
/** `auto`: use the GPU for large wet areas when there is one; `off`: the CPU only. */
|
|
4225
|
-
setGpu(mode, minTiles) {
|
|
4226
|
-
if (minTiles !== void 0) {
|
|
4227
|
-
this.gpuMinTiles = Math.max(1, Math.floor(minTiles));
|
|
4228
|
-
if (this.world) this.world.gpuMinTiles = this.gpuMinTiles;
|
|
4229
|
-
}
|
|
4230
|
-
if (mode === this.gpuMode) return this.engineInfo();
|
|
4231
|
-
this.gpuMode = mode;
|
|
4232
|
-
if (mode === "off") {
|
|
4233
|
-
this.world?.attachGpu(null);
|
|
4234
|
-
this.gpu?.destroy();
|
|
4235
|
-
this.gpu = null;
|
|
4236
|
-
this.gpuSupport = null;
|
|
4237
|
-
} else if (this.world) this.startGpu();
|
|
4238
|
-
return this.engineInfo();
|
|
4239
|
-
}
|
|
4240
|
-
engineInfo() {
|
|
4241
|
-
const w = this.world;
|
|
4242
|
-
const base = { mode: this.gpuMode, adapter: this.gpuSupport?.adapter ?? null, maxTiles: this.gpuSupport?.maxSlots ?? 0, minTiles: this.gpuMinTiles, activeTiles: w?.activeTiles() ?? 0, cpu: this.backend() };
|
|
4243
|
-
if (this.gpuMode === "off") return { ...base, state: "off", reason: "Forced to the CPU." };
|
|
4244
|
-
if (this.gpuStarting) return { ...base, state: "starting", reason: null };
|
|
4245
|
-
if (w?.gpuError) return { ...base, state: "failed", reason: w.gpuError };
|
|
4246
|
-
if (this.gpuSupport && !this.gpuSupport.supported) return { ...base, state: "unavailable", reason: this.gpuSupport.reason };
|
|
4247
|
-
if (!this.gpu) return { ...base, state: "not-started", reason: "Starts with the first wet stroke." };
|
|
4248
|
-
if (w?.gpuActive) return { ...base, state: "active", reason: null };
|
|
4249
|
-
return { ...base, state: "standby", reason: `The GPU takes over at ${this.gpuMinTiles} wet tiles (256 px).` };
|
|
4250
|
-
}
|
|
4251
|
-
/** The world (made on first use, at the canvas size). */
|
|
4252
|
-
ensureWorld() {
|
|
4253
|
-
const { width, height } = this.host.canvas();
|
|
4254
|
-
if (this.world && (this.world.width !== width || this.world.height !== height)) {
|
|
4255
|
-
if (this.world.laneIds().length) throw new Error("The wet world is the wrong size; dry the paint before resizing the canvas.");
|
|
4256
|
-
this.dropWorld();
|
|
4257
|
-
}
|
|
4258
|
-
if (!this.world) this.world = this.makeWorld(width, height, this.host.paper());
|
|
4259
|
-
return this.world;
|
|
4260
|
-
}
|
|
4261
|
-
makeWorld(width, height, paper) {
|
|
4262
|
-
const world = new WetWorld(width, height, wetPaperInput(paper), { kernel: this.options.kernel, threads: this.options.threads });
|
|
4263
|
-
world.setTilt(this.tilt);
|
|
4264
|
-
world.gpuMinTiles = this.gpuMinTiles;
|
|
4265
|
-
if (this.gpu) world.attachGpu(this.gpu);
|
|
4266
|
-
else this.startGpu();
|
|
4267
|
-
return world;
|
|
4268
|
-
}
|
|
4269
|
-
/**
|
|
4270
|
-
* Whether `extend` can keep the wet paint when the canvas grows by `left` / `top` on those
|
|
4271
|
-
* sides: the shift has to be whole wet tiles (256 px), and no wet stroke may be live.
|
|
4272
|
-
*/
|
|
4273
|
-
canExtend(left, top) {
|
|
4274
|
-
return left % WET_TILE === 0 && top % WET_TILE === 0 && !this.driver;
|
|
4275
|
-
}
|
|
4276
|
-
/**
|
|
4277
|
-
* Keeps the wet paint while the canvas grows to `width`×`height` by `left` / `top` on those
|
|
4278
|
-
* sides (`canExtend` must hold): the world is made again at the new size with every wet layer
|
|
4279
|
-
* moved by (left, top), still wet, its base and flow map with it; `paper` is the new
|
|
4280
|
-
* document's paper (its grain moved with the paint). Wet settings' flow centres move too. The
|
|
4281
|
-
* caller shows the layers again (`syncDisplays`) once the document is the new size.
|
|
4282
|
-
*/
|
|
4283
|
-
extend(width, height, left, top, paper) {
|
|
4284
|
-
if (!this.canExtend(left, top)) throw new Error("Wet paint only moves by whole 256 px tiles, between strokes.");
|
|
4285
|
-
for (const [id, s] of this.settings) if (s.flow.center) this.settings.set(id, { ...s, flow: { ...s.flow, center: { x: s.flow.center.x + left, y: s.flow.center.y + top } } });
|
|
4286
|
-
const old = this.world;
|
|
4287
|
-
const snap = old?.laneIds().length ? old.snapshot() : null;
|
|
4288
|
-
this.dropWorld();
|
|
4289
|
-
this.lastTick = null;
|
|
4290
|
-
this.stepsOwed = 0;
|
|
4291
|
-
if (!old || !snap) return;
|
|
4292
|
-
const moved = moveWetSnapshot(snap, old, { width, height }, left, top, (l) => {
|
|
4293
|
-
const flow = this.settings.get(l.id)?.flow;
|
|
4294
|
-
return {
|
|
4295
|
-
base: translateSurface(l.base, left, top),
|
|
4296
|
-
...l.light ? { light: translateSurface(l.light, left, top) } : {},
|
|
4297
|
-
flow: l.flow && flow && flow.kind !== "none" ? flowField(flow, width, height) : l.flow
|
|
4298
|
-
};
|
|
4299
|
-
});
|
|
4300
|
-
this.world = this.makeWorld(width, height, paper);
|
|
4301
|
-
this.world.restore(moved);
|
|
4302
|
-
}
|
|
4303
|
-
/** Whether any layer (or `layerId`) has wet paint. */
|
|
4304
|
-
isWet(layerId) {
|
|
4305
|
-
const w = this.world;
|
|
4306
|
-
if (!w) return false;
|
|
4307
|
-
if (layerId !== void 0) return w.hasLane(layerId) && w.isActive(layerId);
|
|
4308
|
-
return w.laneIds().some((id) => w.isActive(id));
|
|
4309
|
-
}
|
|
4310
|
-
/** Layers holding a wet layer (wet or drying). */
|
|
4311
|
-
wetLayerIds() {
|
|
4312
|
-
return this.world?.laneIds() ?? [];
|
|
4313
|
-
}
|
|
4314
|
-
/** Whether a batch is running (edits wait). */
|
|
4315
|
-
get busy() {
|
|
4316
|
-
return Boolean(this.world?.busy);
|
|
4317
|
-
}
|
|
4318
|
-
/** Resolves when no batch is running. */
|
|
4319
|
-
async idle() {
|
|
4320
|
-
while (this.ticking) await this.ticking;
|
|
4321
|
-
}
|
|
4322
|
-
settingsOf(layerId, medium = "watercolor") {
|
|
4323
|
-
let s = this.settings.get(layerId);
|
|
4324
|
-
if (!s) {
|
|
4325
|
-
s = { params: normalizeWetParams(WET_MEDIUM_PARAMS[medium], DEFAULT_WET_PARAMS), flow: { ...DEFAULT_FLOW } };
|
|
4326
|
-
this.settings.set(layerId, s);
|
|
4327
|
-
}
|
|
4328
|
-
return s;
|
|
4329
|
-
}
|
|
4330
|
-
/** Puts a layer in the world (its current pixels become the base under the wet paint). */
|
|
4331
|
-
ensureLane(layerId, medium) {
|
|
4332
|
-
const w = this.ensureWorld();
|
|
4333
|
-
if (!w.hasLane(layerId)) {
|
|
4334
|
-
const base = this.host.layerSurface(layerId) ?? new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
|
|
4335
|
-
const s = this.settingsOf(layerId, medium);
|
|
4336
|
-
w.ensureLane(layerId, { params: s.params, base, light: this.host.layerLight(layerId) });
|
|
4337
|
-
const { width, height } = this.host.canvas();
|
|
4338
|
-
if (s.flow.kind !== "none") w.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);
|
|
4339
|
-
}
|
|
4340
|
-
return w;
|
|
4341
|
-
}
|
|
4342
|
-
setParams(layerId, update) {
|
|
4343
|
-
const s = this.settingsOf(layerId);
|
|
4344
|
-
s.params = normalizeWetParams(update, s.params);
|
|
4345
|
-
if (this.world?.hasLane(layerId)) this.world.setParams(layerId, s.params);
|
|
4346
|
-
return { ...s.params };
|
|
4347
|
-
}
|
|
4348
|
-
setFlow(layerId, update) {
|
|
4349
|
-
const s = this.settingsOf(layerId);
|
|
4350
|
-
s.flow = normalizeFlow(update, s.flow);
|
|
4351
|
-
if (this.world?.hasLane(layerId)) {
|
|
4352
|
-
const { width, height } = this.host.canvas();
|
|
4353
|
-
this.world.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);
|
|
4354
|
-
}
|
|
4355
|
-
return { ...s.flow };
|
|
4356
|
-
}
|
|
4357
|
-
setTilt(update) {
|
|
4358
|
-
this.tilt = normalizeTilt(update, this.tilt);
|
|
4359
|
-
this.world?.setTilt(this.tilt);
|
|
4360
|
-
return { ...this.tilt };
|
|
4361
|
-
}
|
|
4362
|
-
/** The paper changed. */
|
|
4363
|
-
paperChanged() {
|
|
4364
|
-
this.world?.setPaper(wetPaperInput(this.host.paper()));
|
|
4365
|
-
}
|
|
4366
|
-
/** A layer as tools describe it. */
|
|
4367
|
-
info(layerId) {
|
|
4368
|
-
const s = this.settingsOf(layerId);
|
|
4369
|
-
const st = this.world?.hasLane(layerId) ? this.world.stats(layerId) : null;
|
|
4370
|
-
return {
|
|
4371
|
-
layerId,
|
|
4372
|
-
wet: Boolean(st && st.wetCells > 0),
|
|
4373
|
-
wetCells: st?.wetCells ?? 0,
|
|
4374
|
-
wetBounds: st?.wetBounds ?? null,
|
|
4375
|
-
activeTiles: st?.activeTiles ?? 0,
|
|
4376
|
-
params: { ...s.params },
|
|
4377
|
-
flow: { ...s.flow }
|
|
4378
|
-
};
|
|
4379
|
-
}
|
|
4380
|
-
backend() {
|
|
4381
|
-
return this.world?.backend() ?? null;
|
|
4382
|
-
}
|
|
4383
|
-
/** Shows the current wet pixels (at most `maxTiles` reshaded per layer). */
|
|
4384
|
-
syncDisplays(maxTiles = Infinity) {
|
|
4385
|
-
const w = this.world;
|
|
4386
|
-
if (!w) return [];
|
|
4387
|
-
const surfaces = /* @__PURE__ */ new Map();
|
|
4388
|
-
const rects = [];
|
|
4389
|
-
for (const id of w.laneIds()) {
|
|
4390
|
-
const d = w.display(id, maxTiles);
|
|
4391
|
-
if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
|
|
4392
|
-
rects.push(...d.changed);
|
|
4393
|
-
}
|
|
4394
|
-
this.syncLights();
|
|
4395
|
-
if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
|
|
4396
|
-
return rects;
|
|
4397
|
-
}
|
|
4398
|
-
/** Like `syncDisplays`, shading on the GPU when Engine B is active (all stale tiles at once). */
|
|
4399
|
-
async syncDisplaysGpu(maxCpuTiles, lead = false) {
|
|
4400
|
-
const w = this.world;
|
|
4401
|
-
if (!w) return [];
|
|
4402
|
-
const surfaces = /* @__PURE__ */ new Map();
|
|
4403
|
-
const rects = [];
|
|
4404
|
-
for (const id of w.laneIds()) {
|
|
4405
|
-
const d = await w.displayGpu(id, maxCpuTiles, lead);
|
|
4406
|
-
if (!w.hasLane(id)) continue;
|
|
4407
|
-
if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
|
|
4408
|
-
rects.push(...d.changed);
|
|
4409
|
-
}
|
|
4410
|
-
this.syncLights();
|
|
4411
|
-
if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
|
|
4412
|
-
return rects;
|
|
4413
|
-
}
|
|
4414
|
-
/** Shows each wet layer's live light paint (the light under it with the wet light shaded in). */
|
|
4415
|
-
syncLights() {
|
|
4416
|
-
const w = this.world;
|
|
4417
|
-
if (!w) return;
|
|
4418
|
-
const lights = /* @__PURE__ */ new Map();
|
|
4419
|
-
for (const id of w.laneIds()) {
|
|
4420
|
-
const l = w.lightDisplay(id);
|
|
4421
|
-
if (l && this.host.layerLight(id) !== l) lights.set(id, l);
|
|
4422
|
-
}
|
|
4423
|
-
if (lights.size) this.host.updateLights(lights);
|
|
4424
|
-
}
|
|
4425
|
-
/** Drops the wet layers whose paint has all dried (they become ordinary pixel layers). */
|
|
4426
|
-
retireDry() {
|
|
4427
|
-
const w = this.world;
|
|
4428
|
-
if (!w) return;
|
|
4429
|
-
for (const id of w.laneIds()) {
|
|
4430
|
-
if (w.isActive(id) || this.driver?.init.laneId === id) continue;
|
|
4431
|
-
const base = w.getBase(id);
|
|
4432
|
-
const light = w.isLit(id) ? w.getLight(id) : null;
|
|
4433
|
-
w.removeLane(id);
|
|
4434
|
-
if (light && this.host.layerLight(id) !== light) this.host.updateLights(/* @__PURE__ */ new Map([[id, light]]));
|
|
4435
|
-
this.host.updateSurfaces(/* @__PURE__ */ new Map([[id, base]]), []);
|
|
4436
|
-
}
|
|
4437
|
-
}
|
|
4438
|
-
/**
|
|
4439
|
-
* Advances time by the wall clock (`now` in ms, e.g. `performance.now()`): `speed` steps per
|
|
4440
|
-
* second, at most `maxSteps` per call. Call it every frame; it does nothing while paused or dry.
|
|
4441
|
-
* Resolves when the steps are done and the display is up to date.
|
|
4442
|
-
*/
|
|
4443
|
-
tick(now, maxSteps = 8) {
|
|
4444
|
-
if (this.ticking) {
|
|
4445
|
-
this.leadFrame(now);
|
|
4446
|
-
return this.ticking;
|
|
4447
|
-
}
|
|
4448
|
-
if (this.displaying) return this.displaying;
|
|
4449
|
-
const last = this.lastTick;
|
|
4450
|
-
this.lastTick = now;
|
|
4451
|
-
const w = this.world;
|
|
4452
|
-
if (!w || this.paused || last === null) {
|
|
4453
|
-
this.flushDriver();
|
|
4454
|
-
return Promise.resolve();
|
|
4455
|
-
}
|
|
4456
|
-
if (!w.isActive()) {
|
|
4457
|
-
this.flushDriver();
|
|
4458
|
-
this.retireDry();
|
|
4459
|
-
return Promise.resolve();
|
|
4460
|
-
}
|
|
4461
|
-
const lead = w.gpuActive && !this.driver;
|
|
4462
|
-
w.gpuLeadMode = lead;
|
|
4463
|
-
const most = lead ? Math.max(maxSteps, GPU_LEAD) : maxSteps;
|
|
4464
|
-
this.stepsOwed = Math.min(most * 4, this.stepsOwed + (now - last) / 1e3 * this.speed);
|
|
4465
|
-
const n = Math.min(most, Math.floor(this.stepsOwed));
|
|
4466
|
-
this.leadAt = now;
|
|
4467
|
-
if (n < 1) {
|
|
4468
|
-
this.flushDriver();
|
|
4469
|
-
return Promise.resolve();
|
|
4470
|
-
}
|
|
4471
|
-
this.stepsOwed -= n;
|
|
4472
|
-
const run = (async () => {
|
|
4473
|
-
try {
|
|
4474
|
-
await w.step(n);
|
|
4475
|
-
} finally {
|
|
4476
|
-
this.ticking = null;
|
|
4477
|
-
}
|
|
4478
|
-
this.flushDriver();
|
|
4479
|
-
if (w.gpuActive) {
|
|
4480
|
-
const shading = this.syncDisplaysGpu(DISPLAY_TILES_PER_TICK).then(() => void 0);
|
|
4481
|
-
this.displaying = shading;
|
|
4482
|
-
try {
|
|
4483
|
-
await shading;
|
|
4484
|
-
} finally {
|
|
4485
|
-
this.displaying = null;
|
|
4486
|
-
}
|
|
4487
|
-
} else this.syncDisplays(DISPLAY_TILES_PER_TICK);
|
|
4488
|
-
this.retireDry();
|
|
4489
|
-
this.host.changed();
|
|
4490
|
-
})();
|
|
4491
|
-
this.ticking = run;
|
|
4492
|
-
return run;
|
|
4493
|
-
}
|
|
4494
|
-
/** Lead mode: steps the GPU on by the time since the last frame (within the running batch) and shows it. */
|
|
4495
|
-
leadFrame(now) {
|
|
4496
|
-
const w = this.world;
|
|
4497
|
-
if (!w || this.displaying || this.driver || !w.gpuLeading) return;
|
|
4498
|
-
const k = Math.max(1, Math.round((now - this.leadAt) / 1e3 * this.speed));
|
|
4499
|
-
if (!w.gpuLead(k)) return;
|
|
4500
|
-
this.leadAt = now;
|
|
4501
|
-
const shading = this.syncDisplaysGpu(0, true).then(() => this.host.changed());
|
|
4502
|
-
this.displaying = shading;
|
|
4503
|
-
void shading.finally(() => {
|
|
4504
|
-
if (this.displaying === shading) this.displaying = null;
|
|
4505
|
-
});
|
|
4506
|
-
}
|
|
4507
|
-
flushDriver() {
|
|
4508
|
-
const d = this.driver;
|
|
4509
|
-
if (!d || !this.world) return;
|
|
4510
|
-
const rects = d.flush();
|
|
4511
|
-
if (rects.length) this.syncDisplays(DISPLAY_TILES_PER_TICK);
|
|
4512
|
-
if (d.done) this.onDriverDone?.(d);
|
|
4513
|
-
}
|
|
4514
|
-
/** Called when a live stroke that ended mid-batch has finished applying. */
|
|
4515
|
-
onDriverDone = null;
|
|
4516
|
-
/** Steps `steps` times now (agents, tests). Returns the steps run. */
|
|
4517
|
-
async advance(steps) {
|
|
4518
|
-
await this.idle();
|
|
4519
|
-
const w = this.world;
|
|
4520
|
-
if (!w) return 0;
|
|
4521
|
-
await w.ready;
|
|
4522
|
-
await this.idle();
|
|
4523
|
-
const start = w.clock;
|
|
4524
|
-
const n = Math.max(0, Math.floor(steps));
|
|
4525
|
-
const job = (async () => {
|
|
4526
|
-
try {
|
|
4527
|
-
await w.step(n);
|
|
4528
|
-
} finally {
|
|
4529
|
-
this.ticking = null;
|
|
4530
|
-
}
|
|
4531
|
-
})();
|
|
4532
|
-
this.ticking = job;
|
|
4533
|
-
await job;
|
|
4534
|
-
if (w.gpuActive) await this.syncDisplaysGpu(Infinity);
|
|
4535
|
-
else this.syncDisplays();
|
|
4536
|
-
this.retireDry();
|
|
4537
|
-
return w.clock - start;
|
|
4538
|
-
}
|
|
4539
|
-
/**
|
|
4540
|
-
* Dries at once (everything settles where it is), on one layer or all, optionally only in a
|
|
4541
|
-
* rectangle. Returns the layers and rectangles that dried.
|
|
4542
|
-
*/
|
|
4543
|
-
dry(layerId, rect) {
|
|
4544
|
-
const w = this.world;
|
|
4545
|
-
if (!w) return [];
|
|
4546
|
-
const ids = layerId !== void 0 ? w.hasLane(layerId) ? [layerId] : [] : w.laneIds();
|
|
4547
|
-
const out = [];
|
|
4548
|
-
for (const id of ids) out.push({ layerId: id, bounds: w.dry(id, rect) });
|
|
4549
|
-
this.syncDisplays();
|
|
4550
|
-
this.retireDry();
|
|
4551
|
-
return out;
|
|
4552
|
-
}
|
|
4553
|
-
/** The layer's light paint with all its wet paint dried (`null` without a wet layer or light), without changing anything. */
|
|
4554
|
-
bakedLight(layerId) {
|
|
4555
|
-
const w = this.world;
|
|
4556
|
-
if (!w?.hasLane(layerId) || !w.isLit(layerId)) return null;
|
|
4557
|
-
return w.bakedLight(layerId);
|
|
4558
|
-
}
|
|
4559
|
-
/** The layer's pixels with all its wet paint dried (deterministic), without changing anything. */
|
|
4560
|
-
bakedSurface(layerId) {
|
|
4561
|
-
const w = this.world;
|
|
4562
|
-
if (!w?.hasLane(layerId)) return null;
|
|
4563
|
-
return w.bakedSurface(layerId);
|
|
4564
|
-
}
|
|
4565
|
-
/** A snapshot of the world for undo (`null` when there is none). */
|
|
4566
|
-
snapshot() {
|
|
4567
|
-
const w = this.world;
|
|
4568
|
-
if (!w || !w.laneIds().length) return null;
|
|
4569
|
-
return w.snapshot();
|
|
4570
|
-
}
|
|
4571
|
-
/** Puts a snapshot back (`null`: no wet layers). Returns the layer surfaces it implies. */
|
|
4572
|
-
restore(snap) {
|
|
4573
|
-
if (!snap) {
|
|
4574
|
-
if (this.world) for (const id of this.world.laneIds()) this.world.removeLane(id);
|
|
4575
|
-
return;
|
|
4576
|
-
}
|
|
4577
|
-
const { width, height } = this.host.canvas();
|
|
4578
|
-
if (this.world && (this.world.width !== width || this.world.height !== height)) this.dropWorld();
|
|
4579
|
-
this.ensureWorld().restore(snap);
|
|
4580
|
-
this.syncDisplays();
|
|
4581
|
-
}
|
|
4582
|
-
dropWorld() {
|
|
4583
|
-
this.world?.destroy();
|
|
4584
|
-
this.world = null;
|
|
4585
|
-
this.driver = null;
|
|
4586
|
-
}
|
|
4587
|
-
/** Forgets everything wet (a new canvas, a resize, opening a document). */
|
|
4588
|
-
reset() {
|
|
4589
|
-
this.world?.destroy();
|
|
4590
|
-
this.world = null;
|
|
4591
|
-
this.driver = null;
|
|
4592
|
-
this.settings.clear();
|
|
4593
|
-
this.lastTick = null;
|
|
4594
|
-
this.stepsOwed = 0;
|
|
4595
|
-
}
|
|
4596
|
-
/** Water overlay tiles (RGBA) over a document rectangle, for the view. */
|
|
4597
|
-
waterOverlay(rect) {
|
|
4598
|
-
const w = this.world;
|
|
4599
|
-
if (!w) return [];
|
|
4600
|
-
const out = [];
|
|
4601
|
-
const tx0 = Math.max(0, Math.floor(rect.x / TILE));
|
|
4602
|
-
const ty0 = Math.max(0, Math.floor(rect.y / TILE));
|
|
4603
|
-
const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);
|
|
4604
|
-
const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);
|
|
4605
|
-
for (let ty = ty0; ty <= ty1; ty++)
|
|
4606
|
-
for (let tx = tx0; tx <= tx1; tx++) {
|
|
4607
|
-
let merged = null;
|
|
4608
|
-
for (const id of w.laneIds()) {
|
|
4609
|
-
const t = w.waterOverlay(id, tx, ty);
|
|
4610
|
-
if (!t) continue;
|
|
4611
|
-
if (!merged) merged = t;
|
|
4612
|
-
else for (let i = 3; i < t.length; i += 4) merged[i] = Math.max(merged[i], t[i]);
|
|
4613
|
-
}
|
|
4614
|
-
if (merged) out.push({ tx, ty, data: merged });
|
|
4615
|
-
}
|
|
4616
|
-
return out;
|
|
4617
|
-
}
|
|
4618
|
-
destroy() {
|
|
4619
|
-
this.world?.destroy();
|
|
4620
|
-
this.world = null;
|
|
4621
|
-
this.gpu?.destroy();
|
|
4622
|
-
this.gpu = null;
|
|
4623
|
-
}
|
|
4624
|
-
};
|
|
4625
|
-
function wetPigmentOf(hex2, body) {
|
|
4626
|
-
const [r, g, b] = hexToRgb8(hex2);
|
|
4627
|
-
return Array.from(pigmentOf([r / 255, g / 255, b / 255], body));
|
|
4628
|
-
}
|
|
4629
|
-
|
|
4630
3055
|
// src/paint-editor/thumbnails.ts
|
|
4631
3056
|
var thumbWorld = null;
|
|
4632
3057
|
function paintWetTestStroke(brush, paper, color, width, height) {
|
|
@@ -4672,7 +3097,7 @@ function thumbnailStroke(width, height) {
|
|
|
4672
3097
|
}
|
|
4673
3098
|
return pts;
|
|
4674
3099
|
}
|
|
4675
|
-
function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
3100
|
+
function paintTestStroke(surface, brush, paper, color, width, height, stampArt) {
|
|
4676
3101
|
const [r, g, b] = hexToRgb8(color);
|
|
4677
3102
|
let result = surface;
|
|
4678
3103
|
let thick = null;
|
|
@@ -4704,7 +3129,8 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
4704
3129
|
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),
|
|
4705
3130
|
thick: brush.engine === "impasto" ? { channels: emptyThickChannels() } : void 0,
|
|
4706
3131
|
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0,
|
|
4707
|
-
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null
|
|
3132
|
+
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null,
|
|
3133
|
+
stampArt
|
|
4708
3134
|
}
|
|
4709
3135
|
);
|
|
4710
3136
|
session.push(thumbnailStroke(width, height));
|
|
@@ -4729,7 +3155,7 @@ function renderBrushThumbnail(brush, options) {
|
|
|
4729
3155
|
}
|
|
4730
3156
|
let light = null;
|
|
4731
3157
|
if (b.engine === "wet") surface = paintWetTestStroke(b, options.paper, options.color, width, height);
|
|
4732
|
-
else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height));
|
|
3158
|
+
else ({ surface, light } = paintTestStroke(surface, b, options.paper, options.color, width, height, options.stampArt));
|
|
4733
3159
|
const layer = { type: "pixel", id: "thumbnail", name: "thumbnail", visible: true, opacity: 1, blend: "normal", surface, offset: { x: 0, y: 0 } };
|
|
4734
3160
|
const paper = light && brushPreviewsOnDark(b) ? { ...options.paper, color: BRUSH_PREVIEW_DARK_PAPER } : options.paper;
|
|
4735
3161
|
const spill = light ? computeSpill(surface, light, { radius: Math.max(4, height / 6), strength: 1 }) : null;
|
|
@@ -4922,9 +3348,9 @@ function perspectiveGuideLines(g, size, count = 12) {
|
|
|
4922
3348
|
}
|
|
4923
3349
|
|
|
4924
3350
|
// src/paint-editor/color.ts
|
|
4925
|
-
var
|
|
3351
|
+
var HEX = /^#?([0-9a-f]{6})$/i;
|
|
4926
3352
|
function parseHexColor(value, what = "color") {
|
|
4927
|
-
const m = typeof value === "string" ?
|
|
3353
|
+
const m = typeof value === "string" ? HEX.exec(value.trim()) : null;
|
|
4928
3354
|
if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
4929
3355
|
const n = parseInt(m[1], 16);
|
|
4930
3356
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
@@ -5279,6 +3705,8 @@ function samplePath(d, options = {}) {
|
|
|
5279
3705
|
if (!(spacing >= 0.25 && spacing <= 100)) throw new Error(`spacing ${spacing} is out of range; use 0.25..100 document pixels.`);
|
|
5280
3706
|
const profile = options.pressure;
|
|
5281
3707
|
if (profile && (!profile.length || profile.some((p) => !(p >= 0 && p <= 1)))) throw new Error("pressure needs one or more values from 0 to 1, e.g. [0.2, 1, 0.2] for a taper.");
|
|
3708
|
+
const speed = options.speed;
|
|
3709
|
+
if (speed !== void 0 && !(speed >= 0.01 && speed <= 100)) throw new Error(`speed ${speed} is out of range; use 0.01..100 document pixels per millisecond.`);
|
|
5282
3710
|
let contours;
|
|
5283
3711
|
try {
|
|
5284
3712
|
contours = pathToContours(d);
|
|
@@ -5306,7 +3734,10 @@ function samplePath(d, options = {}) {
|
|
|
5306
3734
|
const b = line[Math.min(j + 1, line.length - 1)];
|
|
5307
3735
|
const x = Math.round((a[0] + (b[0] - a[0]) * f) * 1e3) / 1e3;
|
|
5308
3736
|
const y = Math.round((a[1] + (b[1] - a[1]) * f) * 1e3) / 1e3;
|
|
5309
|
-
|
|
3737
|
+
const p = { x, y };
|
|
3738
|
+
if (profile) p.pressure = Math.round(profileAt(profile, k / n) * 1e3) / 1e3;
|
|
3739
|
+
if (speed !== void 0) p.t = Math.round(at / speed * 1e3) / 1e3;
|
|
3740
|
+
points.push(p);
|
|
5310
3741
|
}
|
|
5311
3742
|
out.push(points);
|
|
5312
3743
|
}
|
|
@@ -5314,6 +3745,59 @@ function samplePath(d, options = {}) {
|
|
|
5314
3745
|
return out;
|
|
5315
3746
|
}
|
|
5316
3747
|
|
|
3748
|
+
// src/paint-editor/brush-source-art.ts
|
|
3749
|
+
var BrushSourceArtCache = class {
|
|
3750
|
+
sources = /* @__PURE__ */ new Map();
|
|
3751
|
+
art = /* @__PURE__ */ new Map();
|
|
3752
|
+
/** Adds or replaces sources (a replaced image is decoded again). */
|
|
3753
|
+
set(sources) {
|
|
3754
|
+
for (const s of sources) {
|
|
3755
|
+
const was = this.sources.get(s.id);
|
|
3756
|
+
if (was && was.image === s.image) continue;
|
|
3757
|
+
this.sources.set(s.id, s);
|
|
3758
|
+
this.art.delete(s.id);
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
/** The source of an id, if known. */
|
|
3762
|
+
source(id) {
|
|
3763
|
+
return this.sources.get(id);
|
|
3764
|
+
}
|
|
3765
|
+
/** Every known source. */
|
|
3766
|
+
all() {
|
|
3767
|
+
return [...this.sources.values()];
|
|
3768
|
+
}
|
|
3769
|
+
/** The art of a `source:<id>` ref (alpha = the mask), or `undefined` when the source is unknown or its image can't be read. */
|
|
3770
|
+
get = (ref2) => {
|
|
3771
|
+
const id = brushSourceIdOf(ref2);
|
|
3772
|
+
if (id === null) return void 0;
|
|
3773
|
+
if (this.art.has(id)) return this.art.get(id) ?? void 0;
|
|
3774
|
+
const source = this.sources.get(id);
|
|
3775
|
+
if (!source) return void 0;
|
|
3776
|
+
let art = null;
|
|
3777
|
+
try {
|
|
3778
|
+
const { width, height, mask } = decodeBrushSource(source);
|
|
3779
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
3780
|
+
for (let i = 0; i < mask.length; i++) {
|
|
3781
|
+
data[i * 4] = data[i * 4 + 1] = data[i * 4 + 2] = 255;
|
|
3782
|
+
data[i * 4 + 3] = mask[i];
|
|
3783
|
+
}
|
|
3784
|
+
art = { width, height, data, tintable: true };
|
|
3785
|
+
} catch {
|
|
3786
|
+
art = null;
|
|
3787
|
+
}
|
|
3788
|
+
this.art.set(id, art);
|
|
3789
|
+
return art ?? void 0;
|
|
3790
|
+
};
|
|
3791
|
+
/** A lookup that reads sources from here and every other ref from `stickers`. */
|
|
3792
|
+
with(stickers) {
|
|
3793
|
+
return (ref2, variant) => isBrushSourceRef(ref2) ? this.get(ref2) : stickers(ref2, variant);
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
function engineBrushSourceIds(brush) {
|
|
3797
|
+
const refs = [brush.stamp, brush.tipGrain?.source, ...brush.particle?.dotTextures ?? []];
|
|
3798
|
+
return [...new Set(refs.filter((r) => !!r && isBrushSourceRef(r)).map((r) => brushSourceIdOf(r)))];
|
|
3799
|
+
}
|
|
3800
|
+
|
|
5317
3801
|
// src/paint-editor/live-strokes.ts
|
|
5318
3802
|
var LIVE_STROKE_ENGINES = ["dab", "particle", "bristle", "mixer"];
|
|
5319
3803
|
function liveStrokeRefusal(brush) {
|
|
@@ -5904,10 +4388,10 @@ async function libraryCall(call) {
|
|
|
5904
4388
|
throw e instanceof BrushLibraryError ? new PaintEditorError(e.message) : e;
|
|
5905
4389
|
}
|
|
5906
4390
|
}
|
|
5907
|
-
var
|
|
4391
|
+
var HEX2 = /^#[0-9a-f]{6}$/i;
|
|
5908
4392
|
var MAX_ADD_STICKERS = 500;
|
|
5909
4393
|
function parseColor(value, what = "color") {
|
|
5910
|
-
if (typeof value !== "string" || !
|
|
4394
|
+
if (typeof value !== "string" || !HEX2.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
5911
4395
|
return value.trim().toLowerCase();
|
|
5912
4396
|
}
|
|
5913
4397
|
function rgbaOf(hex2) {
|
|
@@ -6023,6 +4507,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6023
4507
|
/** The sticker library slot (see `setStickersProvider`). */
|
|
6024
4508
|
stickers = new StickersSlot(editorStickers());
|
|
6025
4509
|
stickerArt = new StickerArtCache();
|
|
4510
|
+
/** Image sources strokes draw with: the brush library's and the open painting's (saved with it). */
|
|
4511
|
+
brushSources = new BrushSourceArtCache();
|
|
4512
|
+
/** Art for stamp tips and tip grain: image sources from `brushSources`, stickers from the sticker library. */
|
|
4513
|
+
stampArt = (ref2, variant) => isBrushSourceRef(ref2) ? this.brushSources.get(ref2) : this.stickerArt.get(ref2, variant);
|
|
6026
4514
|
rasterizeSticker;
|
|
6027
4515
|
effectsRenderer = new PaintEffectsRenderer();
|
|
6028
4516
|
paperEffectNodeCache = null;
|
|
@@ -6141,36 +4629,82 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6141
4629
|
}
|
|
6142
4630
|
/**
|
|
6143
4631
|
* Runs `fn` as one undo step named `label`: every change it makes (strokes, fills, new layers) is
|
|
6144
|
-
* undone together.
|
|
6145
|
-
*
|
|
4632
|
+
* undone together. When `fn` returns a promise, the group holds until it settles; until then other
|
|
4633
|
+
* changes (the user's strokes, undo) are refused, except those `fn` makes through `within`, so
|
|
4634
|
+
* nothing else lands in (or is lost with) the group's step. Nested groups join the outer one. If
|
|
4635
|
+
* `fn` throws or rejects, its changes are put back and nothing is recorded.
|
|
6146
4636
|
*/
|
|
6147
4637
|
group(label, fn) {
|
|
6148
|
-
if (this.grouping) return fn();
|
|
4638
|
+
if (this.grouping && (!this.grouping.pending || this.grouping.inside)) return fn((step) => step());
|
|
6149
4639
|
this.requireIdle("grouping changes");
|
|
6150
4640
|
const before = this.doc;
|
|
6151
4641
|
const revision = this.docRevision;
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
4642
|
+
const strokesBefore = this.strokeCounter;
|
|
4643
|
+
const origin = this.getOrigin();
|
|
4644
|
+
const g = { label, before, changed: false, pending: false, inside: 0 };
|
|
4645
|
+
this.grouping = g;
|
|
4646
|
+
const within = (step) => {
|
|
4647
|
+
if (this.grouping !== g) return step();
|
|
4648
|
+
g.inside++;
|
|
4649
|
+
try {
|
|
4650
|
+
return step();
|
|
4651
|
+
} finally {
|
|
4652
|
+
g.inside--;
|
|
4653
|
+
}
|
|
4654
|
+
};
|
|
4655
|
+
const rollback = () => {
|
|
6157
4656
|
this.grouping = null;
|
|
4657
|
+
this.strokeCounter = strokesBefore;
|
|
4658
|
+
if (this.wetOp) {
|
|
4659
|
+
const snap = this.wetOp.before;
|
|
4660
|
+
this.wetOp = null;
|
|
4661
|
+
this.pendingWetEvents = [];
|
|
4662
|
+
this.wet.restore(snap);
|
|
4663
|
+
}
|
|
6158
4664
|
if (this.doc !== before) {
|
|
6159
4665
|
this.doc = before;
|
|
6160
4666
|
this.docRevision = revision + 1;
|
|
4667
|
+
this.wet.syncDisplays();
|
|
4668
|
+
this.emit();
|
|
4669
|
+
}
|
|
4670
|
+
};
|
|
4671
|
+
const finish = () => {
|
|
4672
|
+
this.grouping = null;
|
|
4673
|
+
if (g.changed && this.doc !== before || this.wetOp) {
|
|
4674
|
+
const entry = { label: g.label, origin, before, after: this.doc };
|
|
4675
|
+
if (this.wetOp) {
|
|
4676
|
+
entry.wet = { before: this.wetOp.before, after: this.wet.snapshot(), lost: false };
|
|
4677
|
+
this.wetOp = null;
|
|
4678
|
+
}
|
|
4679
|
+
this.undoStack.push(entry);
|
|
4680
|
+
if (this.undoStack.length > this.historyLimit) this.undoStack.shift();
|
|
4681
|
+
this.redoStack = [];
|
|
4682
|
+
this.trimWetHistory();
|
|
4683
|
+
this.notifyCommit({ kind: "execute", label: g.label, origin: entry.origin });
|
|
6161
4684
|
this.emit();
|
|
6162
4685
|
}
|
|
4686
|
+
};
|
|
4687
|
+
let result;
|
|
4688
|
+
try {
|
|
4689
|
+
result = fn(within);
|
|
4690
|
+
} catch (e) {
|
|
4691
|
+
rollback();
|
|
6163
4692
|
throw e;
|
|
6164
4693
|
}
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
4694
|
+
if (result && typeof result.then === "function") {
|
|
4695
|
+
g.pending = true;
|
|
4696
|
+
return Promise.resolve(result).then(
|
|
4697
|
+
(val) => {
|
|
4698
|
+
finish();
|
|
4699
|
+
return val;
|
|
4700
|
+
},
|
|
4701
|
+
(e) => {
|
|
4702
|
+
rollback();
|
|
4703
|
+
throw e;
|
|
4704
|
+
}
|
|
4705
|
+
);
|
|
6173
4706
|
}
|
|
4707
|
+
finish();
|
|
6174
4708
|
return result;
|
|
6175
4709
|
}
|
|
6176
4710
|
/**
|
|
@@ -6297,11 +4831,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6297
4831
|
/** Why the document cannot be replaced or stepped through right now, or `null`. */
|
|
6298
4832
|
busyReason() {
|
|
6299
4833
|
if (this.session) return "A stroke is in progress; end or cancel it first.";
|
|
4834
|
+
if (this.grouping?.pending && !this.grouping.inside) return `"${this.grouping.label}" is still being painted; wait for it to finish first.`;
|
|
6300
4835
|
if (this.wet.busy) return "Wet paint is mid-step; wait a moment (wetIdle()) first.";
|
|
6301
4836
|
return null;
|
|
6302
4837
|
}
|
|
6303
4838
|
requireIdle(what) {
|
|
6304
4839
|
if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);
|
|
4840
|
+
if (this.grouping?.pending && !this.grouping.inside) throw new PaintEditorError(`"${this.grouping.label}" is still being painted (one undo step); wait for it to finish before ${what}.`);
|
|
6305
4841
|
}
|
|
6306
4842
|
commit(label, next, before = this.doc) {
|
|
6307
4843
|
if (this.grouping) {
|
|
@@ -7338,7 +5874,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7338
5874
|
}
|
|
7339
5875
|
/** Reads the library again (after a change anywhere it is shared). */
|
|
7340
5876
|
refreshBrushes() {
|
|
7341
|
-
const
|
|
5877
|
+
const library = this.brushLibrary;
|
|
5878
|
+
const sources = typeof library.listSources === "function" ? library.listSources().catch(() => []) : Promise.resolve([]);
|
|
5879
|
+
const run = Promise.all([library.list(), sources]).then(([entries, list]) => {
|
|
5880
|
+
this.brushSources.set(list);
|
|
7342
5881
|
this.brushEntries = entries.map((e) => libraryBrush(e.brush, e.builtin));
|
|
7343
5882
|
this.library = null;
|
|
7344
5883
|
const pending = this.pendingBrushId;
|
|
@@ -7485,7 +6024,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7485
6024
|
/** A thumbnail of a brush: a test stroke on the current paper. */
|
|
7486
6025
|
brushThumbnail(id, options = {}) {
|
|
7487
6026
|
const b = id === this.brush.id ? this.brush : this.requireBrush(id);
|
|
7488
|
-
return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
|
|
6027
|
+
return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color, stampArt: this.stampArt });
|
|
7489
6028
|
}
|
|
7490
6029
|
// --- Colour --------------------------------------------------------------------
|
|
7491
6030
|
colorScheme;
|
|
@@ -7788,7 +6327,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7788
6327
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
7789
6328
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
7790
6329
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
7791
|
-
stampArt: this.
|
|
6330
|
+
stampArt: this.stampArt,
|
|
7792
6331
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
7793
6332
|
effect
|
|
7794
6333
|
});
|
|
@@ -7968,7 +6507,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7968
6507
|
} catch (e) {
|
|
7969
6508
|
throw new PaintEditorError(e instanceof Error ? e.message : String(e));
|
|
7970
6509
|
}
|
|
7971
|
-
const { path: _path, spacing: _spacing, pressure: _pressure, ...stroke } = init;
|
|
6510
|
+
const { path: _path, spacing: _spacing, pressure: _pressure, speed: _speed, ...stroke } = init;
|
|
7972
6511
|
const before = this.doc;
|
|
7973
6512
|
let doc = this.doc;
|
|
7974
6513
|
let bounds = null;
|
|
@@ -8166,6 +6705,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8166
6705
|
if (w > 16384 || h > 16384) throw new PaintEditorError(`At scale ${scale} the canvas would be ${w}\xD7${h}; the limit is 16384 px per side. Use a smaller scale.`);
|
|
8167
6706
|
const target = new _PaintEditorController({ width: w, height: h, dpi: dpi * scale, brushLibrary: this.brushLibrary, wet: this.wetOptions });
|
|
8168
6707
|
target.brushEntries = this.brushEntries;
|
|
6708
|
+
target.brushSources.set(this.brushSources.all());
|
|
8169
6709
|
const paper = { ...this.doc.paper, scale: Math.min(8, this.doc.paper.scale * scale) };
|
|
8170
6710
|
target.adopt({ state: { canvas: target.doc.canvas, paper, layers: emptiedLayers(this.doc.layers), activeLayerId: this.doc.activeLayerId, strokes: [] }, extensions: {}, problems: [] });
|
|
8171
6711
|
target.lighting = { ...this.lighting };
|
|
@@ -8604,6 +7144,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8604
7144
|
for (const [id, list] of Object.entries(this.doc.liveStrokes ?? {})) if (findLayer(this.doc.layers, id)) liveStrokes[id] = list.map(strokeToJSON);
|
|
8605
7145
|
if (Object.keys(liveStrokes).length) ext.liveStrokes = liveStrokes;
|
|
8606
7146
|
else delete ext.liveStrokes;
|
|
7147
|
+
const strokes = [...this.doc.strokes, ...Object.values(this.doc.liveStrokes ?? {}).flat()];
|
|
7148
|
+
const sourceIds = [...new Set(strokes.flatMap((st) => engineBrushSourceIds(st.brush)))];
|
|
7149
|
+
const sources = sourceIds.map((id) => this.brushSources.source(id)).filter((x) => x !== void 0);
|
|
7150
|
+
if (sources.length) ext.brushSources = sources.map((x) => ({ ...x }));
|
|
7151
|
+
else delete ext.brushSources;
|
|
8607
7152
|
const light = {};
|
|
8608
7153
|
for (const [id, t] of Object.entries(this.doc.light ?? {})) {
|
|
8609
7154
|
if (!findLayer(this.doc.layers, id)) continue;
|
|
@@ -8626,6 +7171,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8626
7171
|
}
|
|
8627
7172
|
/** Reads `layerChannels` and `lighting` from loaded extensions into the document. */
|
|
8628
7173
|
readThick(state, ext, problems) {
|
|
7174
|
+
if (Array.isArray(ext.brushSources)) this.brushSources.set(ext.brushSources.map((x, i) => readBrushSource(x, `brushSources[${i}]`, problems)).filter((x) => x !== null));
|
|
8629
7175
|
this.lighting = normalizeLighting(ext.lighting ?? {}, DEFAULT_LIGHTING);
|
|
8630
7176
|
this.litCaches.clear();
|
|
8631
7177
|
if (ext.stickers && typeof ext.stickers === "object") {
|
|
@@ -8744,6 +7290,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8744
7290
|
delete this.extensions.lighting;
|
|
8745
7291
|
delete this.extensions.stickers;
|
|
8746
7292
|
delete this.extensions.liveStrokes;
|
|
7293
|
+
delete this.extensions.brushSources;
|
|
8747
7294
|
delete this.extensions.effectLayers;
|
|
8748
7295
|
delete this.extensions.paperEffect;
|
|
8749
7296
|
this.viewCompositor.clear();
|
|
@@ -10164,6 +8711,9 @@ var PaintDirector = class {
|
|
|
10164
8711
|
case "stroke":
|
|
10165
8712
|
await this.say(id, command.note, 500);
|
|
10166
8713
|
return this.stroke(id, command.stroke);
|
|
8714
|
+
case "strokes":
|
|
8715
|
+
await this.say(id, command.note, 500);
|
|
8716
|
+
return this.strokes(id, command.strokes, command.label);
|
|
10167
8717
|
case "fill": {
|
|
10168
8718
|
await this.say(id, command.note, 500);
|
|
10169
8719
|
const { width, height } = c.getState().document.canvas;
|
|
@@ -10189,11 +8739,11 @@ var PaintDirector = class {
|
|
|
10189
8739
|
}
|
|
10190
8740
|
}
|
|
10191
8741
|
/** Paints a stroke live: begins it, pushes its points frame by frame as the cursor reaches them, then ends it (one undo step). */
|
|
10192
|
-
async stroke(id, init) {
|
|
8742
|
+
async stroke(id, init, within = (step) => step()) {
|
|
10193
8743
|
const c = this.controller;
|
|
10194
8744
|
const points = c.checkPoints(init.points);
|
|
10195
8745
|
const { points: _points, ...rest } = init;
|
|
10196
|
-
const session = c.beginStroke({ snapToPerspective: false, start: points[0], ...rest });
|
|
8746
|
+
const session = within(() => c.beginStroke({ snapToPerspective: false, start: points[0], ...rest }));
|
|
10197
8747
|
try {
|
|
10198
8748
|
await this.glide(id, points[0]);
|
|
10199
8749
|
this.presence.set({ id, pressed: true });
|
|
@@ -10221,7 +8771,46 @@ var PaintDirector = class {
|
|
|
10221
8771
|
throw error;
|
|
10222
8772
|
}
|
|
10223
8773
|
}
|
|
8774
|
+
/** Paints several strokes live in turn and commits them as one undo step (e.g. a path's subpaths). */
|
|
8775
|
+
async strokes(id, inits, label) {
|
|
8776
|
+
const c = this.controller;
|
|
8777
|
+
if (!Array.isArray(inits) || !inits.length) throw new PaintEditorError("strokes is empty; give at least one stroke.");
|
|
8778
|
+
const checked = inits.map((s) => ({ ...s, points: c.checkPoints(s.points) }));
|
|
8779
|
+
const first = checked[0];
|
|
8780
|
+
const base = first.brushId ? c.getBrush(first.brushId) : c.getState().brush;
|
|
8781
|
+
const stepLabel = label ?? `${base?.name ?? "Brush"} path stroke`;
|
|
8782
|
+
return c.group(stepLabel, async (within) => {
|
|
8783
|
+
let layerId = first.layerId ?? c.document.activeLayerId ?? "";
|
|
8784
|
+
let bounds = null;
|
|
8785
|
+
let dabs = 0;
|
|
8786
|
+
const strokeIds = [];
|
|
8787
|
+
let last = null;
|
|
8788
|
+
for (const s of checked) {
|
|
8789
|
+
const r = await this.stroke(id, s, within);
|
|
8790
|
+
layerId = r.layerId;
|
|
8791
|
+
bounds = unionRect2(bounds, r.bounds);
|
|
8792
|
+
dabs += r.dabs;
|
|
8793
|
+
if (r.strokeId) strokeIds.push(r.strokeId);
|
|
8794
|
+
last = r;
|
|
8795
|
+
}
|
|
8796
|
+
return {
|
|
8797
|
+
layerId,
|
|
8798
|
+
bounds,
|
|
8799
|
+
dabs,
|
|
8800
|
+
strokeIds,
|
|
8801
|
+
...last?.mixerLoad ? { mixerLoad: last.mixerLoad } : {},
|
|
8802
|
+
...last?.wet !== void 0 ? { wet: last.wet } : {}
|
|
8803
|
+
};
|
|
8804
|
+
});
|
|
8805
|
+
}
|
|
10224
8806
|
};
|
|
8807
|
+
function unionRect2(a, b) {
|
|
8808
|
+
if (!a) return b ? { ...b } : null;
|
|
8809
|
+
if (!b) return { ...a };
|
|
8810
|
+
const x = Math.min(a.x, b.x);
|
|
8811
|
+
const y = Math.min(a.y, b.y);
|
|
8812
|
+
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
8813
|
+
}
|
|
10225
8814
|
|
|
10226
8815
|
// src/paint-editor/playback.ts
|
|
10227
8816
|
var WET = /^(advance_wet_paint|dry_paint|wet_paper|set_wet_settings|set_flow_map|set_canvas_tilt|pause_wet_paint|set_wet_engine|set_water_overlay)$/;
|
|
@@ -10253,7 +8842,7 @@ function planPaintToolPlayback(tool, _args) {
|
|
|
10253
8842
|
if (control) steps.push({ type: "target", ref: control }, { type: "press" }, { type: "release" });
|
|
10254
8843
|
return steps.length ? steps : null;
|
|
10255
8844
|
}
|
|
10256
|
-
var DIRECTED = /* @__PURE__ */ new Set(["paint_stroke", "fill_layer", "replay_strokes"]);
|
|
8845
|
+
var DIRECTED = /* @__PURE__ */ new Set(["paint_stroke", "stroke_path", "fill_layer", "replay_strokes"]);
|
|
10257
8846
|
function withPaintPlayback(provider, options) {
|
|
10258
8847
|
const plan = (tool) => {
|
|
10259
8848
|
if (tool.plan || tool.playback !== void 0) return tool;
|
|
@@ -11288,7 +9877,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11288
9877
|
const add = (t) => tools.push(t);
|
|
11289
9878
|
const act = async (command, direct) => {
|
|
11290
9879
|
if (!options.director) return direct();
|
|
11291
|
-
const
|
|
9880
|
+
const cmd = typeof command === "function" ? command() : command;
|
|
9881
|
+
const [r] = await options.director.perform(options.actor ?? { id: "agent", name: "Agent" }, [cmd]);
|
|
11292
9882
|
if (!r || !r.ok) throw new Error(r && !r.ok ? r.message : "The director did not run the command.");
|
|
11293
9883
|
return r.result;
|
|
11294
9884
|
};
|
|
@@ -11394,7 +9984,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11394
9984
|
annotations: { title: "Get the paper", ...READ6 },
|
|
11395
9985
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
11396
9986
|
});
|
|
11397
|
-
const
|
|
9987
|
+
const tuning = {
|
|
11398
9988
|
type: "object",
|
|
11399
9989
|
properties: {
|
|
11400
9990
|
contrast: { type: "number", minimum: 0, maximum: 3, description: "Height contrast around the middle (default 1)." },
|
|
@@ -11417,8 +10007,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11417
10007
|
pattern: { type: "string", enum: [...GRAIN_PATTERNS], description: "Grain pattern." },
|
|
11418
10008
|
tiling: { type: "string", enum: ["repeat", "mirror"], description: "How the grain tile repeats (mirror hides seams)." },
|
|
11419
10009
|
shading: { type: "number", minimum: 0, maximum: 1, description: "How much the grain shades the paper colour." },
|
|
11420
|
-
dry: { ...
|
|
11421
|
-
wet: { ...
|
|
10010
|
+
dry: { ...tuning, description: "Height map tuning for dry media." },
|
|
10011
|
+
wet: { ...tuning, description: "Height map tuning for wet media (the grain water and pigment follow)." },
|
|
11422
10012
|
absorption: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how fast the paper soaks up water (high: washes stop spreading sooner)." },
|
|
11423
10013
|
permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how freely water flows over the fibres (low: more ragged, slower spreading)." },
|
|
11424
10014
|
roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain pins wet edges and catches granulating pigment." },
|
|
@@ -11737,13 +10327,14 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11737
10327
|
add({
|
|
11738
10328
|
name: "stroke_path",
|
|
11739
10329
|
title: "Brush along a path",
|
|
11740
|
-
description: "Brushes vector path data (SVG path d in document pixels: M, L, H, V, C, S, Q, T, A and Z) on a pixel layer with a library brush or the active one, as if drawn by hand along it: the outline of a shape, a lettering guide, a curve from a vector tool. Each subpath becomes one recorded stroke, sampled every spacing px, with an optional pressure profile spread along each subpath (a taper such as [0.2, 1, 0.2]); closed subpaths end where they start. Brushes behave as in paint_stroke (grain, pick-up, wet and thick paint, guides off). One undo step for the whole path. Returns the layer id, changed bounds, dab count and the recorded strokeIds (for replay_strokes, update_stroke).",
|
|
10330
|
+
description: "Brushes vector path data (SVG path d in document pixels: M, L, H, V, C, S, Q, T, A and Z) on a pixel layer with a library brush or the active one, as if drawn by hand along it: the outline of a shape, a lettering guide, a curve from a vector tool. Each subpath becomes one recorded stroke, sampled every spacing px, with an optional pressure profile spread along each subpath (a taper such as [0.2, 1, 0.2]); closed subpaths end where they start. Brushes behave as in paint_stroke (grain, pick-up, wet and thick paint, guides off). With an agent cursor (a director), each subpath is painted live in turn. One undo step for the whole path. Returns the layer id, changed bounds, dab count and the recorded strokeIds (for replay_strokes, update_stroke).",
|
|
11741
10331
|
inputSchema: {
|
|
11742
10332
|
type: "object",
|
|
11743
10333
|
properties: {
|
|
11744
10334
|
path: { type: "string", minLength: 1, description: 'SVG path data in document pixels, e.g. "M 40 200 C 120 40 280 40 360 200". Several subpaths paint several strokes.' },
|
|
11745
10335
|
spacing: { type: "number", minimum: 0.25, maximum: 100, description: "Distance between samples along the path in document pixels (default 2); the brush spaces its own dabs, so this only sets how closely curves are followed." },
|
|
11746
10336
|
pressure: { type: "array", minItems: 1, maxItems: 64, items: { type: "number", minimum: 0, maximum: 1 }, description: "Pen pressure along each subpath, spread evenly from start to end (default full pressure); [0.2, 1, 0.2] tapers both ends." },
|
|
10337
|
+
speed: { type: "number", minimum: 0.01, maximum: 100, description: "Drawing speed in document pixels per millisecond (optional; gives each sample a time t so playback has a pace and speed-sensitive brushes respond)." },
|
|
11747
10338
|
layerId: LAYER_ID4,
|
|
11748
10339
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
11749
10340
|
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
@@ -11765,7 +10356,30 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11765
10356
|
if (refs.length) await c.prepareStickers(refs);
|
|
11766
10357
|
}
|
|
11767
10358
|
const path = options.origin && typeof a.path === "string" && a.path.trim() ? bezierPathToSvg(a.path, { offset: { x: -options.origin.x, y: -options.origin.y } }) : a.path;
|
|
11768
|
-
const
|
|
10359
|
+
const init = { path, spacing: a.spacing, pressure: a.pressure, speed: a.speed, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed };
|
|
10360
|
+
const r = await act(
|
|
10361
|
+
() => {
|
|
10362
|
+
if (typeof path !== "string" || !path.trim()) throw new PaintEditorError('path is empty; give SVG path data in document pixels, e.g. "M 10 10 C 40 0 80 60 120 30".');
|
|
10363
|
+
let subpaths;
|
|
10364
|
+
try {
|
|
10365
|
+
subpaths = samplePath(path, init);
|
|
10366
|
+
} catch (e) {
|
|
10367
|
+
throw new PaintEditorError(e instanceof Error ? e.message : String(e));
|
|
10368
|
+
}
|
|
10369
|
+
const { path: _path, spacing: _spacing, pressure: _pressure, speed: _speed, ...stroke } = init;
|
|
10370
|
+
return {
|
|
10371
|
+
kind: "strokes",
|
|
10372
|
+
label: `${base?.name ?? "Brush"} path stroke`,
|
|
10373
|
+
strokes: subpaths.map((points, i) => ({
|
|
10374
|
+
...stroke,
|
|
10375
|
+
...stroke.seed !== void 0 ? { seed: stroke.seed + i } : {},
|
|
10376
|
+
snapToPerspective: false,
|
|
10377
|
+
points
|
|
10378
|
+
}))
|
|
10379
|
+
};
|
|
10380
|
+
},
|
|
10381
|
+
() => c.strokePath(init)
|
|
10382
|
+
);
|
|
11769
10383
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11770
10384
|
}
|
|
11771
10385
|
});
|
|
@@ -12154,11 +10768,23 @@ function strokeSummary(s, withPoints, live) {
|
|
|
12154
10768
|
}
|
|
12155
10769
|
|
|
12156
10770
|
// src/paint-editor/brush-library-tools.ts
|
|
12157
|
-
function paintBrushPreviewRenderer(paper = () => DEFAULT_PAPER) {
|
|
12158
|
-
const
|
|
12159
|
-
|
|
10771
|
+
function paintBrushPreviewRenderer(paper = () => DEFAULT_PAPER, options = {}) {
|
|
10772
|
+
const sources = new BrushSourceArtCache();
|
|
10773
|
+
const draw = (brush, { width, height, color }) => {
|
|
10774
|
+
const stickers = options.stampArt?.();
|
|
10775
|
+
const stampArt = sources.with(stickers ?? (() => void 0));
|
|
10776
|
+
const t = renderBrushThumbnail(resolveBrush(brush), { paper: paper(), width, height, color: color.toLowerCase(), stampArt });
|
|
12160
10777
|
return { data: bytesToBase64(encodePng(t.width, t.height, t.data)), mimeType: "image/png" };
|
|
12161
10778
|
};
|
|
10779
|
+
const render = (brush, size) => {
|
|
10780
|
+
const ids = brushSourceIds(brush);
|
|
10781
|
+
if (!ids.length || !options.library) return draw(brush, size);
|
|
10782
|
+
const library = options.library;
|
|
10783
|
+
return Promise.all(ids.map((id) => library.getSource(id))).then((found) => {
|
|
10784
|
+
sources.set(found.filter((s) => s !== null));
|
|
10785
|
+
return draw(brush, size);
|
|
10786
|
+
});
|
|
10787
|
+
};
|
|
12162
10788
|
return Object.defineProperty(render, "paper", { get: () => paper().color, enumerable: true });
|
|
12163
10789
|
}
|
|
12164
10790
|
function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
@@ -12166,7 +10792,7 @@ function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
|
12166
10792
|
profile: PAINT_PROFILE,
|
|
12167
10793
|
stickers: () => controller.stickers.provider,
|
|
12168
10794
|
...options,
|
|
12169
|
-
renderPreview: paintBrushPreviewRenderer(() => controller.getState().document.paper)
|
|
10795
|
+
renderPreview: paintBrushPreviewRenderer(() => controller.getState().document.paper, { library: controller.brushLibrary, stampArt: () => controller.stampArt })
|
|
12170
10796
|
});
|
|
12171
10797
|
}
|
|
12172
10798
|
|
|
@@ -13058,7 +11684,7 @@ function createPaintChangeFeed(controller, options = {}) {
|
|
|
13058
11684
|
}
|
|
13059
11685
|
};
|
|
13060
11686
|
}
|
|
13061
|
-
var
|
|
11687
|
+
var HEX3 = /^#[0-9a-f]{6}$/i;
|
|
13062
11688
|
var DRY_ENGINES = /* @__PURE__ */ new Set(["dab", "particle", "bristle", "mixer"]);
|
|
13063
11689
|
function suggestionBrushRefusal(brush) {
|
|
13064
11690
|
if (!DRY_ENGINES.has(brush.engine)) return `The ${brush.name} brush (${brush.engine}) can't paint a suggestion: suggestions are dry paint on a layer of their own. Use a dab, particle, bristle or mixer brush.`;
|
|
@@ -13098,20 +11724,20 @@ function createPaintProposals(options) {
|
|
|
13098
11724
|
if (op.kind === "stroke") {
|
|
13099
11725
|
if (!op.points?.length) throw new PaintEditorError(`${at}: a stroke needs points in document pixels.`);
|
|
13100
11726
|
points += op.points.length;
|
|
13101
|
-
if (op.color !== void 0 && !
|
|
11727
|
+
if (op.color !== void 0 && !HEX3.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
13102
11728
|
const brush = op.brushId ? controller.getBrush(op.brushId) : controller.getState().brush;
|
|
13103
11729
|
if (!brush) throw new PaintEditorError(`${at}: unknown brush "${op.brushId}". Call list_brush_presets for the ids.`);
|
|
13104
11730
|
const refusal = suggestionBrushRefusal(brush);
|
|
13105
11731
|
if (refusal) throw new PaintEditorError(`${at}: ${refusal}`);
|
|
13106
11732
|
if (op.size !== void 0 && !(op.size > 0)) throw new PaintEditorError(`${at}: size must be above 0 (document pixels).`);
|
|
13107
11733
|
} else if (op.kind === "wash") {
|
|
13108
|
-
if (!
|
|
11734
|
+
if (!HEX3.test(op.color ?? "")) throw new PaintEditorError(`${at}: a wash needs color as #rrggbb.`);
|
|
13109
11735
|
if (op.opacity !== void 0 && !(op.opacity > 0 && op.opacity <= 1)) throw new PaintEditorError(`${at}: opacity must be in (0, 1].`);
|
|
13110
11736
|
} else if (op.kind === "echo") {
|
|
13111
11737
|
if (!op.strokeIds?.length) throw new PaintEditorError(`${at}: an echo needs strokeIds (list_strokes).`);
|
|
13112
11738
|
const unknown = op.strokeIds.filter((id) => !known.has(id));
|
|
13113
11739
|
if (unknown.length) throw new PaintEditorError(`${at}: unknown stroke ${unknown.map((u) => `"${u}"`).join(", ")}. Call list_strokes for the ids.`);
|
|
13114
|
-
if (op.color !== void 0 && !
|
|
11740
|
+
if (op.color !== void 0 && !HEX3.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
13115
11741
|
if (op.repeat !== void 0 && !(Number.isInteger(op.repeat) && op.repeat >= 1 && op.repeat <= 50)) throw new PaintEditorError(`${at}: repeat must be a whole number from 1 to 50.`);
|
|
13116
11742
|
for (const id of op.strokeIds) {
|
|
13117
11743
|
const stroke = controller.getStrokes().find((s) => s.id === id);
|
|
@@ -13537,6 +12163,8 @@ export {
|
|
|
13537
12163
|
BRUSH_SLIDERS,
|
|
13538
12164
|
BUILTIN_BRUSHES,
|
|
13539
12165
|
BristleStroke,
|
|
12166
|
+
BrushSourceArtCache,
|
|
12167
|
+
DAB_FLIPS,
|
|
13540
12168
|
DEFAULT_BACKLIGHT_COLOR,
|
|
13541
12169
|
DEFAULT_BEHAVIOR,
|
|
13542
12170
|
DEFAULT_BRISTLE,
|
|
@@ -13555,6 +12183,7 @@ export {
|
|
|
13555
12183
|
DEFAULT_SPRAY,
|
|
13556
12184
|
DEFAULT_STICKER,
|
|
13557
12185
|
DEFAULT_SWATCHES,
|
|
12186
|
+
DEFAULT_TIP_GRAIN,
|
|
13558
12187
|
DEFAULT_VIEW,
|
|
13559
12188
|
DEFAULT_WET_BRUSH,
|
|
13560
12189
|
DEFAULT_WET_PARAMS,
|
|
@@ -13660,9 +12289,11 @@ export {
|
|
|
13660
12289
|
StickerArtCache,
|
|
13661
12290
|
StickerBrowser,
|
|
13662
12291
|
StickerSpray,
|
|
12292
|
+
TIP_GRAIN_MODES,
|
|
13663
12293
|
TRACING_NODE_ID,
|
|
13664
12294
|
ThickStrokeDriver,
|
|
13665
12295
|
TileableNoise,
|
|
12296
|
+
TipGrain,
|
|
13666
12297
|
WET_FLOW_KINDS,
|
|
13667
12298
|
WET_GPU_MODES,
|
|
13668
12299
|
WET_GPU_OPTIONS,
|
|
@@ -13727,6 +12358,7 @@ export {
|
|
|
13727
12358
|
effectPaintSize,
|
|
13728
12359
|
emptiedLayers,
|
|
13729
12360
|
emptyThickChannels,
|
|
12361
|
+
engineBrushSourceIds,
|
|
13730
12362
|
fitZoom,
|
|
13731
12363
|
grainCatch,
|
|
13732
12364
|
grainHeights,
|