@pixerate/editor 0.266.1 → 0.267.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 +8 -4
- package/dist/agent/index.js +6 -2
- 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 +172 -105
- package/dist/animation-editor/index.js +91 -24
- package/dist/brushes/index.cjs +59 -59
- package/dist/brushes/index.js +5 -5
- package/dist/brushes/profiles.d.cts +6 -1
- package/dist/brushes/profiles.d.ts +6 -1
- package/dist/{brushstrokeRender-FSUWXUZY.cjs → brushstrokeRender-L7JYRR2R.cjs} +16 -16
- package/dist/{brushstrokeRender-KD453OBX.js → brushstrokeRender-NLFPEBGI.js} +4 -4
- 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-XORJ7UZO.cjs → chunk-3VHH6V67.cjs} +13 -12
- package/dist/{chunk-T2LDTEXK.js → chunk-4IDUYB45.js} +2217 -56
- package/dist/chunk-7UKFXXWL.js +802 -0
- package/dist/chunk-ADMRNWR7.js +340 -0
- package/dist/{chunk-CIJBSUM5.js → chunk-AGNUD5BI.js} +2 -2
- package/dist/{chunk-WLUVZKVD.cjs → chunk-DT4YQUGT.cjs} +32 -32
- package/dist/{chunk-DXYHQ2VN.js → chunk-EF7LZAXQ.js} +1 -1
- package/dist/chunk-EXB2EGBE.cjs +802 -0
- package/dist/{chunk-4ODRXQRW.js → chunk-EYSHTYLG.js} +1 -1
- package/dist/{chunk-R44Y3EFS.js → chunk-FGJHJIMN.js} +8 -336
- package/dist/{chunk-AQZQWXEU.cjs → chunk-G7NHE2VD.cjs} +35 -35
- package/dist/{chunk-XC6DPRYI.js → chunk-GSTSZPSR.js} +154 -949
- package/dist/{chunk-VRK322ZC.cjs → chunk-HIO6E7LC.cjs} +2 -2
- package/dist/{chunk-TOID45I2.cjs → chunk-J2TNIZ55.cjs} +1 -1
- package/dist/{chunk-YHDHFJUC.cjs → chunk-J5VQYK7T.cjs} +217 -1012
- package/dist/{chunk-3C33YTNN.js → chunk-JGWVZPT6.js} +2 -2
- package/dist/{chunk-ACFSFUQH.js → chunk-JL6YH5WD.js} +4 -0
- package/dist/{chunk-3SBLVGOM.cjs → chunk-LA5Y4ETA.cjs} +23 -23
- package/dist/{chunk-MVUQSK5J.cjs → chunk-LHPEG5AU.cjs} +107 -435
- package/dist/{chunk-DOZ6F2K2.js → chunk-NSETJI74.js} +3 -2
- package/dist/{chunk-XRXFVRGY.cjs → chunk-OKHKA43L.cjs} +2364 -203
- package/dist/{chunk-LK4SXL3V.js → chunk-OSEXT7JL.js} +1 -1
- package/dist/{chunk-PE6YJMQT.cjs → chunk-PFHJS4HE.cjs} +20 -19
- package/dist/{chunk-ZQI3WUPP.js → chunk-QCLY5SFS.js} +12 -0
- package/dist/{chunk-7KNW3WXZ.cjs → chunk-QEVLRLUN.cjs} +63 -0
- package/dist/{chunk-OTN6VB5S.cjs → chunk-RDFHWAHL.cjs} +5 -1
- package/dist/{chunk-6JL2D5SG.js → chunk-VE6UKYQ2.js} +3 -2
- package/dist/{chunk-RFJZGTHA.cjs → chunk-XVOP3CGQ.cjs} +9 -9
- package/dist/{chunk-EZOJBP4D.js → chunk-YIC6MAJ2.js} +1 -1
- package/dist/chunk-YZWM6ZIP.cjs +340 -0
- package/dist/{chunk-472INMWH.js → chunk-ZA7L5YJB.js} +63 -0
- package/dist/{chunk-YBC3RKT2.cjs → chunk-ZGXJ7BTR.cjs} +13 -1
- 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 +837 -833
- package/dist/image-editor/index.js +105 -101
- package/dist/paint/index.cjs +5 -3
- package/dist/paint/index.js +46 -44
- package/dist/paint-editor/controller.d.cts +5 -3
- package/dist/paint-editor/controller.d.ts +5 -3
- package/dist/paint-editor/index.cjs +832 -2295
- package/dist/paint-editor/index.js +285 -1748
- 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/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/tools/conformance.d.cts +8 -0
- package/dist/tools/conformance.d.ts +8 -0
- package/dist/tools/index.cjs +6 -2
- package/dist/tools/index.js +5 -1
- 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 +1846 -1386
- package/dist/vector/index.d.cts +2 -0
- package/dist/vector/index.d.ts +2 -0
- package/dist/vector/index.js +517 -57
- 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 +2 -2
- package/skills/pixerate-brushes-design/SKILL.md +7 -4
- 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 +8 -4
- package/skills/pixerate-paint-editor/references/wet-media.md +9 -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-JGWVZPT6.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-EYSHTYLG.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-FGJHJIMN.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-7UKFXXWL.js";
|
|
84
64
|
import "../chunk-VWEWY4FY.js";
|
|
85
65
|
import "../chunk-HVQVHUTV.js";
|
|
86
66
|
import {
|
|
@@ -92,7 +72,47 @@ 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
|
+
WET_GPU_MODES,
|
|
89
|
+
WET_MEDIUM_PARAMS,
|
|
90
|
+
WetStrokeDriver,
|
|
91
|
+
brushStickerRefs,
|
|
92
|
+
clonePaper,
|
|
93
|
+
effectDabField,
|
|
94
|
+
effectPaintKey,
|
|
95
|
+
effectPaintSize,
|
|
96
|
+
hexToRgb8,
|
|
97
|
+
isPaintEffectPaint,
|
|
98
|
+
mixerTip,
|
|
99
|
+
multiColorForDab,
|
|
100
|
+
multiColorGradient,
|
|
101
|
+
multiColorPalette,
|
|
102
|
+
normalizePaper,
|
|
103
|
+
paperPreset,
|
|
104
|
+
penOrientation,
|
|
105
|
+
readEffectStrokeMap,
|
|
106
|
+
readPaintEffectPaint,
|
|
107
|
+
renderPaper,
|
|
108
|
+
sampleEffectAt,
|
|
109
|
+
sampleEffectField,
|
|
110
|
+
stampSampler,
|
|
111
|
+
tiltToOrientation,
|
|
112
|
+
toStrokePoint,
|
|
113
|
+
wetPaperInput,
|
|
114
|
+
wetPigmentOf
|
|
115
|
+
} from "../chunk-4IDUYB45.js";
|
|
96
116
|
import {
|
|
97
117
|
BRUSH_ID,
|
|
98
118
|
BRUSH_PRESETS,
|
|
@@ -147,7 +167,7 @@ import {
|
|
|
147
167
|
parseBrushLibrary,
|
|
148
168
|
resolveBrush,
|
|
149
169
|
usesPaintExchange
|
|
150
|
-
} from "../chunk-
|
|
170
|
+
} from "../chunk-AGNUD5BI.js";
|
|
151
171
|
import {
|
|
152
172
|
STAMP_KINDS,
|
|
153
173
|
StickersSlot,
|
|
@@ -156,7 +176,7 @@ import {
|
|
|
156
176
|
stampCoverage,
|
|
157
177
|
stickerIdFor,
|
|
158
178
|
stickerVariantFor
|
|
159
|
-
} from "../chunk-
|
|
179
|
+
} from "../chunk-JL6YH5WD.js";
|
|
160
180
|
import "../chunk-M6DNZNVY.js";
|
|
161
181
|
import {
|
|
162
182
|
bezierPathToSvg,
|
|
@@ -180,6 +200,21 @@ import {
|
|
|
180
200
|
resolveEffectValues
|
|
181
201
|
} from "../chunk-MV7CYFMJ.js";
|
|
182
202
|
import "../chunk-V2ZCJRDM.js";
|
|
203
|
+
import {
|
|
204
|
+
DEFAULT_FLOW,
|
|
205
|
+
DEFAULT_WET_PARAMS,
|
|
206
|
+
MAX_WET_LANES,
|
|
207
|
+
SRGB_TO_LINEAR,
|
|
208
|
+
WET_FLOW_KINDS,
|
|
209
|
+
WET_PARAM_KEYS,
|
|
210
|
+
WetWorld,
|
|
211
|
+
linearToSrgb8,
|
|
212
|
+
normalizeFlow,
|
|
213
|
+
normalizeTilt,
|
|
214
|
+
normalizeWetParams,
|
|
215
|
+
pigmentOf,
|
|
216
|
+
wetSupport
|
|
217
|
+
} from "../chunk-GSTSZPSR.js";
|
|
183
218
|
import {
|
|
184
219
|
GRAIN_PATTERNS,
|
|
185
220
|
GRAIN_TILE,
|
|
@@ -213,15 +248,12 @@ import {
|
|
|
213
248
|
TileMedium,
|
|
214
249
|
addPaintGradientStop,
|
|
215
250
|
applyStrokeToTile,
|
|
216
|
-
blenderDab,
|
|
217
251
|
cleanMixer,
|
|
218
|
-
createBlenderScratch,
|
|
219
252
|
dabRect,
|
|
220
253
|
exchangeRates,
|
|
221
254
|
hexToPaintRgb,
|
|
222
255
|
isGradientPaint,
|
|
223
256
|
loadMixer,
|
|
224
|
-
mixerRates,
|
|
225
257
|
normalizeHexColor,
|
|
226
258
|
paintGradientMappingFor,
|
|
227
259
|
paintSourceColor,
|
|
@@ -259,7 +291,6 @@ import {
|
|
|
259
291
|
PAINTERLY_BLEND_MODES,
|
|
260
292
|
isBlendMode,
|
|
261
293
|
isPainterlyBlendMode,
|
|
262
|
-
mixPigment8,
|
|
263
294
|
mixPigments,
|
|
264
295
|
standardBlendMode
|
|
265
296
|
} from "../chunk-IRSTHLQQ.js";
|
|
@@ -277,996 +308,72 @@ import {
|
|
|
277
308
|
tilesInRect,
|
|
278
309
|
translateSurface,
|
|
279
310
|
writeRegion
|
|
280
|
-
} from "../chunk-HVVR5PZW.js";
|
|
281
|
-
import "../chunk-QGHEDC7B.js";
|
|
282
|
-
import "../chunk-SIFAIGB3.js";
|
|
283
|
-
import "../chunk-CIQIM2IP.js";
|
|
284
|
-
import "../chunk-VDGBHYC7.js";
|
|
285
|
-
import {
|
|
286
|
-
EDITOR_DOCUMENT_FORMAT,
|
|
287
|
-
createEditorAssetRegistry,
|
|
288
|
-
defineEditorDocument,
|
|
289
|
-
toEditorDocumentFile
|
|
290
|
-
} 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);
|
|
311
|
+
} from "../chunk-HVVR5PZW.js";
|
|
312
|
+
import "../chunk-QGHEDC7B.js";
|
|
313
|
+
import "../chunk-SIFAIGB3.js";
|
|
314
|
+
import "../chunk-CIQIM2IP.js";
|
|
315
|
+
import "../chunk-VDGBHYC7.js";
|
|
316
|
+
import {
|
|
317
|
+
EDITOR_DOCUMENT_FORMAT,
|
|
318
|
+
createEditorAssetRegistry,
|
|
319
|
+
defineEditorDocument,
|
|
320
|
+
toEditorDocumentFile
|
|
321
|
+
} from "../chunk-XBQX76CT.js";
|
|
322
|
+
import "../chunk-HF5QZS7Z.js";
|
|
323
|
+
import "../chunk-M2TPMYJE.js";
|
|
324
|
+
import "../chunk-2WPNAGHI.js";
|
|
325
|
+
import {
|
|
326
|
+
HARMONY_KINDS,
|
|
327
|
+
colorHarmony,
|
|
328
|
+
hueToWheel,
|
|
329
|
+
wheelToHue
|
|
330
|
+
} from "../chunk-2EUW2XZX.js";
|
|
331
|
+
import "../chunk-E2V5DA3W.js";
|
|
332
|
+
import "../chunk-VR6OGHU5.js";
|
|
333
|
+
import "../chunk-JC2SHA4A.js";
|
|
334
|
+
import {
|
|
335
|
+
linearToSrgb,
|
|
336
|
+
oklabToOklch,
|
|
337
|
+
oklabToRgb,
|
|
338
|
+
oklchToOklab,
|
|
339
|
+
rgbToOklab,
|
|
340
|
+
srgbToLinear
|
|
341
|
+
} from "../chunk-MHLU6GRJ.js";
|
|
342
|
+
import "../chunk-6ONMQ6WF.js";
|
|
343
|
+
|
|
344
|
+
// src/paint-editor/document.ts
|
|
345
|
+
var MIN_CANVAS_SIDE = 1;
|
|
346
|
+
var MAX_CANVAS_SIDE = 16384;
|
|
347
|
+
var DEFAULT_DPI = 300;
|
|
348
|
+
var nextId = 1;
|
|
349
|
+
function newPaintLayerId() {
|
|
350
|
+
return `pl${Date.now().toString(36)}${(nextId++).toString(36)}`;
|
|
351
|
+
}
|
|
352
|
+
function createPaintLayer(name, id = newPaintLayerId()) {
|
|
353
|
+
return { type: "pixel", id, name, visible: true, opacity: 1, blend: "normal", surface: emptySurface(4), offset: { x: 0, y: 0 } };
|
|
354
|
+
}
|
|
355
|
+
var PaintDocumentError = class extends Error {
|
|
356
|
+
constructor(message) {
|
|
357
|
+
super(message);
|
|
358
|
+
this.name = "PaintDocumentError";
|
|
1268
359
|
}
|
|
1269
360
|
};
|
|
361
|
+
function normalizeCanvas(raw) {
|
|
362
|
+
const side = (v, what) => {
|
|
363
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new PaintDocumentError(`${what} must be a number of pixels (${MIN_CANVAS_SIDE}..${MAX_CANVAS_SIDE}).`);
|
|
364
|
+
const n = Math.round(v);
|
|
365
|
+
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.`);
|
|
366
|
+
return n;
|
|
367
|
+
};
|
|
368
|
+
const dpi = raw.dpi === void 0 ? DEFAULT_DPI : raw.dpi;
|
|
369
|
+
if (typeof dpi !== "number" || !Number.isFinite(dpi) || dpi < 1 || dpi > 1e4) throw new PaintDocumentError(`dpi ${dpi} is out of range; use 1..10000.`);
|
|
370
|
+
return { width: side(raw.width, "width"), height: side(raw.height, "height"), dpi: Math.round(dpi * 100) / 100 };
|
|
371
|
+
}
|
|
372
|
+
function createPaintDocument(options = {}) {
|
|
373
|
+
const canvas = normalizeCanvas({ width: options.width ?? 2048, height: options.height ?? 1536, dpi: options.dpi });
|
|
374
|
+
const layer = createPaintLayer("Layer 1");
|
|
375
|
+
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
376
|
+
}
|
|
1270
377
|
|
|
1271
378
|
// src/paint-editor/thick.ts
|
|
1272
379
|
var DEFAULT_LIGHTING = { azimuth: 315, elevation: 40, relief: 0.7 };
|
|
@@ -3937,696 +3044,6 @@ function downloadBytes(bytes, fileName, mimeType) {
|
|
|
3937
3044
|
setTimeout(() => g.URL.revokeObjectURL(url), 1e3);
|
|
3938
3045
|
}
|
|
3939
3046
|
|
|
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
3047
|
// src/paint-editor/thumbnails.ts
|
|
4631
3048
|
var thumbWorld = null;
|
|
4632
3049
|
function paintWetTestStroke(brush, paper, color, width, height) {
|
|
@@ -4922,9 +3339,9 @@ function perspectiveGuideLines(g, size, count = 12) {
|
|
|
4922
3339
|
}
|
|
4923
3340
|
|
|
4924
3341
|
// src/paint-editor/color.ts
|
|
4925
|
-
var
|
|
3342
|
+
var HEX = /^#?([0-9a-f]{6})$/i;
|
|
4926
3343
|
function parseHexColor(value, what = "color") {
|
|
4927
|
-
const m = typeof value === "string" ?
|
|
3344
|
+
const m = typeof value === "string" ? HEX.exec(value.trim()) : null;
|
|
4928
3345
|
if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
4929
3346
|
const n = parseInt(m[1], 16);
|
|
4930
3347
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
@@ -5279,6 +3696,8 @@ function samplePath(d, options = {}) {
|
|
|
5279
3696
|
if (!(spacing >= 0.25 && spacing <= 100)) throw new Error(`spacing ${spacing} is out of range; use 0.25..100 document pixels.`);
|
|
5280
3697
|
const profile = options.pressure;
|
|
5281
3698
|
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.");
|
|
3699
|
+
const speed = options.speed;
|
|
3700
|
+
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
3701
|
let contours;
|
|
5283
3702
|
try {
|
|
5284
3703
|
contours = pathToContours(d);
|
|
@@ -5306,7 +3725,10 @@ function samplePath(d, options = {}) {
|
|
|
5306
3725
|
const b = line[Math.min(j + 1, line.length - 1)];
|
|
5307
3726
|
const x = Math.round((a[0] + (b[0] - a[0]) * f) * 1e3) / 1e3;
|
|
5308
3727
|
const y = Math.round((a[1] + (b[1] - a[1]) * f) * 1e3) / 1e3;
|
|
5309
|
-
|
|
3728
|
+
const p = { x, y };
|
|
3729
|
+
if (profile) p.pressure = Math.round(profileAt(profile, k / n) * 1e3) / 1e3;
|
|
3730
|
+
if (speed !== void 0) p.t = Math.round(at / speed * 1e3) / 1e3;
|
|
3731
|
+
points.push(p);
|
|
5310
3732
|
}
|
|
5311
3733
|
out.push(points);
|
|
5312
3734
|
}
|
|
@@ -5904,10 +4326,10 @@ async function libraryCall(call) {
|
|
|
5904
4326
|
throw e instanceof BrushLibraryError ? new PaintEditorError(e.message) : e;
|
|
5905
4327
|
}
|
|
5906
4328
|
}
|
|
5907
|
-
var
|
|
4329
|
+
var HEX2 = /^#[0-9a-f]{6}$/i;
|
|
5908
4330
|
var MAX_ADD_STICKERS = 500;
|
|
5909
4331
|
function parseColor(value, what = "color") {
|
|
5910
|
-
if (typeof value !== "string" || !
|
|
4332
|
+
if (typeof value !== "string" || !HEX2.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
5911
4333
|
return value.trim().toLowerCase();
|
|
5912
4334
|
}
|
|
5913
4335
|
function rgbaOf(hex2) {
|
|
@@ -6141,36 +4563,82 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6141
4563
|
}
|
|
6142
4564
|
/**
|
|
6143
4565
|
* Runs `fn` as one undo step named `label`: every change it makes (strokes, fills, new layers) is
|
|
6144
|
-
* undone together.
|
|
6145
|
-
*
|
|
4566
|
+
* undone together. When `fn` returns a promise, the group holds until it settles; until then other
|
|
4567
|
+
* changes (the user's strokes, undo) are refused, except those `fn` makes through `within`, so
|
|
4568
|
+
* nothing else lands in (or is lost with) the group's step. Nested groups join the outer one. If
|
|
4569
|
+
* `fn` throws or rejects, its changes are put back and nothing is recorded.
|
|
6146
4570
|
*/
|
|
6147
4571
|
group(label, fn) {
|
|
6148
|
-
if (this.grouping) return fn();
|
|
4572
|
+
if (this.grouping && (!this.grouping.pending || this.grouping.inside)) return fn((step) => step());
|
|
6149
4573
|
this.requireIdle("grouping changes");
|
|
6150
4574
|
const before = this.doc;
|
|
6151
4575
|
const revision = this.docRevision;
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
4576
|
+
const strokesBefore = this.strokeCounter;
|
|
4577
|
+
const origin = this.getOrigin();
|
|
4578
|
+
const g = { label, before, changed: false, pending: false, inside: 0 };
|
|
4579
|
+
this.grouping = g;
|
|
4580
|
+
const within = (step) => {
|
|
4581
|
+
if (this.grouping !== g) return step();
|
|
4582
|
+
g.inside++;
|
|
4583
|
+
try {
|
|
4584
|
+
return step();
|
|
4585
|
+
} finally {
|
|
4586
|
+
g.inside--;
|
|
4587
|
+
}
|
|
4588
|
+
};
|
|
4589
|
+
const rollback = () => {
|
|
6157
4590
|
this.grouping = null;
|
|
4591
|
+
this.strokeCounter = strokesBefore;
|
|
4592
|
+
if (this.wetOp) {
|
|
4593
|
+
const snap = this.wetOp.before;
|
|
4594
|
+
this.wetOp = null;
|
|
4595
|
+
this.pendingWetEvents = [];
|
|
4596
|
+
this.wet.restore(snap);
|
|
4597
|
+
}
|
|
6158
4598
|
if (this.doc !== before) {
|
|
6159
4599
|
this.doc = before;
|
|
6160
4600
|
this.docRevision = revision + 1;
|
|
4601
|
+
this.wet.syncDisplays();
|
|
4602
|
+
this.emit();
|
|
4603
|
+
}
|
|
4604
|
+
};
|
|
4605
|
+
const finish = () => {
|
|
4606
|
+
this.grouping = null;
|
|
4607
|
+
if (g.changed && this.doc !== before || this.wetOp) {
|
|
4608
|
+
const entry = { label: g.label, origin, before, after: this.doc };
|
|
4609
|
+
if (this.wetOp) {
|
|
4610
|
+
entry.wet = { before: this.wetOp.before, after: this.wet.snapshot(), lost: false };
|
|
4611
|
+
this.wetOp = null;
|
|
4612
|
+
}
|
|
4613
|
+
this.undoStack.push(entry);
|
|
4614
|
+
if (this.undoStack.length > this.historyLimit) this.undoStack.shift();
|
|
4615
|
+
this.redoStack = [];
|
|
4616
|
+
this.trimWetHistory();
|
|
4617
|
+
this.notifyCommit({ kind: "execute", label: g.label, origin: entry.origin });
|
|
6161
4618
|
this.emit();
|
|
6162
4619
|
}
|
|
4620
|
+
};
|
|
4621
|
+
let result;
|
|
4622
|
+
try {
|
|
4623
|
+
result = fn(within);
|
|
4624
|
+
} catch (e) {
|
|
4625
|
+
rollback();
|
|
6163
4626
|
throw e;
|
|
6164
4627
|
}
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
4628
|
+
if (result && typeof result.then === "function") {
|
|
4629
|
+
g.pending = true;
|
|
4630
|
+
return Promise.resolve(result).then(
|
|
4631
|
+
(val) => {
|
|
4632
|
+
finish();
|
|
4633
|
+
return val;
|
|
4634
|
+
},
|
|
4635
|
+
(e) => {
|
|
4636
|
+
rollback();
|
|
4637
|
+
throw e;
|
|
4638
|
+
}
|
|
4639
|
+
);
|
|
6173
4640
|
}
|
|
4641
|
+
finish();
|
|
6174
4642
|
return result;
|
|
6175
4643
|
}
|
|
6176
4644
|
/**
|
|
@@ -6297,11 +4765,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6297
4765
|
/** Why the document cannot be replaced or stepped through right now, or `null`. */
|
|
6298
4766
|
busyReason() {
|
|
6299
4767
|
if (this.session) return "A stroke is in progress; end or cancel it first.";
|
|
4768
|
+
if (this.grouping?.pending && !this.grouping.inside) return `"${this.grouping.label}" is still being painted; wait for it to finish first.`;
|
|
6300
4769
|
if (this.wet.busy) return "Wet paint is mid-step; wait a moment (wetIdle()) first.";
|
|
6301
4770
|
return null;
|
|
6302
4771
|
}
|
|
6303
4772
|
requireIdle(what) {
|
|
6304
4773
|
if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);
|
|
4774
|
+
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
4775
|
}
|
|
6306
4776
|
commit(label, next, before = this.doc) {
|
|
6307
4777
|
if (this.grouping) {
|
|
@@ -7968,7 +6438,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7968
6438
|
} catch (e) {
|
|
7969
6439
|
throw new PaintEditorError(e instanceof Error ? e.message : String(e));
|
|
7970
6440
|
}
|
|
7971
|
-
const { path: _path, spacing: _spacing, pressure: _pressure, ...stroke } = init;
|
|
6441
|
+
const { path: _path, spacing: _spacing, pressure: _pressure, speed: _speed, ...stroke } = init;
|
|
7972
6442
|
const before = this.doc;
|
|
7973
6443
|
let doc = this.doc;
|
|
7974
6444
|
let bounds = null;
|
|
@@ -10164,6 +8634,9 @@ var PaintDirector = class {
|
|
|
10164
8634
|
case "stroke":
|
|
10165
8635
|
await this.say(id, command.note, 500);
|
|
10166
8636
|
return this.stroke(id, command.stroke);
|
|
8637
|
+
case "strokes":
|
|
8638
|
+
await this.say(id, command.note, 500);
|
|
8639
|
+
return this.strokes(id, command.strokes, command.label);
|
|
10167
8640
|
case "fill": {
|
|
10168
8641
|
await this.say(id, command.note, 500);
|
|
10169
8642
|
const { width, height } = c.getState().document.canvas;
|
|
@@ -10189,11 +8662,11 @@ var PaintDirector = class {
|
|
|
10189
8662
|
}
|
|
10190
8663
|
}
|
|
10191
8664
|
/** 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) {
|
|
8665
|
+
async stroke(id, init, within = (step) => step()) {
|
|
10193
8666
|
const c = this.controller;
|
|
10194
8667
|
const points = c.checkPoints(init.points);
|
|
10195
8668
|
const { points: _points, ...rest } = init;
|
|
10196
|
-
const session = c.beginStroke({ snapToPerspective: false, start: points[0], ...rest });
|
|
8669
|
+
const session = within(() => c.beginStroke({ snapToPerspective: false, start: points[0], ...rest }));
|
|
10197
8670
|
try {
|
|
10198
8671
|
await this.glide(id, points[0]);
|
|
10199
8672
|
this.presence.set({ id, pressed: true });
|
|
@@ -10221,7 +8694,46 @@ var PaintDirector = class {
|
|
|
10221
8694
|
throw error;
|
|
10222
8695
|
}
|
|
10223
8696
|
}
|
|
8697
|
+
/** Paints several strokes live in turn and commits them as one undo step (e.g. a path's subpaths). */
|
|
8698
|
+
async strokes(id, inits, label) {
|
|
8699
|
+
const c = this.controller;
|
|
8700
|
+
if (!Array.isArray(inits) || !inits.length) throw new PaintEditorError("strokes is empty; give at least one stroke.");
|
|
8701
|
+
const checked = inits.map((s) => ({ ...s, points: c.checkPoints(s.points) }));
|
|
8702
|
+
const first = checked[0];
|
|
8703
|
+
const base = first.brushId ? c.getBrush(first.brushId) : c.getState().brush;
|
|
8704
|
+
const stepLabel = label ?? `${base?.name ?? "Brush"} path stroke`;
|
|
8705
|
+
return c.group(stepLabel, async (within) => {
|
|
8706
|
+
let layerId = first.layerId ?? c.document.activeLayerId ?? "";
|
|
8707
|
+
let bounds = null;
|
|
8708
|
+
let dabs = 0;
|
|
8709
|
+
const strokeIds = [];
|
|
8710
|
+
let last = null;
|
|
8711
|
+
for (const s of checked) {
|
|
8712
|
+
const r = await this.stroke(id, s, within);
|
|
8713
|
+
layerId = r.layerId;
|
|
8714
|
+
bounds = unionRect2(bounds, r.bounds);
|
|
8715
|
+
dabs += r.dabs;
|
|
8716
|
+
if (r.strokeId) strokeIds.push(r.strokeId);
|
|
8717
|
+
last = r;
|
|
8718
|
+
}
|
|
8719
|
+
return {
|
|
8720
|
+
layerId,
|
|
8721
|
+
bounds,
|
|
8722
|
+
dabs,
|
|
8723
|
+
strokeIds,
|
|
8724
|
+
...last?.mixerLoad ? { mixerLoad: last.mixerLoad } : {},
|
|
8725
|
+
...last?.wet !== void 0 ? { wet: last.wet } : {}
|
|
8726
|
+
};
|
|
8727
|
+
});
|
|
8728
|
+
}
|
|
10224
8729
|
};
|
|
8730
|
+
function unionRect2(a, b) {
|
|
8731
|
+
if (!a) return b ? { ...b } : null;
|
|
8732
|
+
if (!b) return { ...a };
|
|
8733
|
+
const x = Math.min(a.x, b.x);
|
|
8734
|
+
const y = Math.min(a.y, b.y);
|
|
8735
|
+
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 };
|
|
8736
|
+
}
|
|
10225
8737
|
|
|
10226
8738
|
// src/paint-editor/playback.ts
|
|
10227
8739
|
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 +8765,7 @@ function planPaintToolPlayback(tool, _args) {
|
|
|
10253
8765
|
if (control) steps.push({ type: "target", ref: control }, { type: "press" }, { type: "release" });
|
|
10254
8766
|
return steps.length ? steps : null;
|
|
10255
8767
|
}
|
|
10256
|
-
var DIRECTED = /* @__PURE__ */ new Set(["paint_stroke", "fill_layer", "replay_strokes"]);
|
|
8768
|
+
var DIRECTED = /* @__PURE__ */ new Set(["paint_stroke", "stroke_path", "fill_layer", "replay_strokes"]);
|
|
10257
8769
|
function withPaintPlayback(provider, options) {
|
|
10258
8770
|
const plan = (tool) => {
|
|
10259
8771
|
if (tool.plan || tool.playback !== void 0) return tool;
|
|
@@ -11288,7 +9800,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11288
9800
|
const add = (t) => tools.push(t);
|
|
11289
9801
|
const act = async (command, direct) => {
|
|
11290
9802
|
if (!options.director) return direct();
|
|
11291
|
-
const
|
|
9803
|
+
const cmd = typeof command === "function" ? command() : command;
|
|
9804
|
+
const [r] = await options.director.perform(options.actor ?? { id: "agent", name: "Agent" }, [cmd]);
|
|
11292
9805
|
if (!r || !r.ok) throw new Error(r && !r.ok ? r.message : "The director did not run the command.");
|
|
11293
9806
|
return r.result;
|
|
11294
9807
|
};
|
|
@@ -11394,7 +9907,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11394
9907
|
annotations: { title: "Get the paper", ...READ6 },
|
|
11395
9908
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
11396
9909
|
});
|
|
11397
|
-
const
|
|
9910
|
+
const tuning = {
|
|
11398
9911
|
type: "object",
|
|
11399
9912
|
properties: {
|
|
11400
9913
|
contrast: { type: "number", minimum: 0, maximum: 3, description: "Height contrast around the middle (default 1)." },
|
|
@@ -11417,8 +9930,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11417
9930
|
pattern: { type: "string", enum: [...GRAIN_PATTERNS], description: "Grain pattern." },
|
|
11418
9931
|
tiling: { type: "string", enum: ["repeat", "mirror"], description: "How the grain tile repeats (mirror hides seams)." },
|
|
11419
9932
|
shading: { type: "number", minimum: 0, maximum: 1, description: "How much the grain shades the paper colour." },
|
|
11420
|
-
dry: { ...
|
|
11421
|
-
wet: { ...
|
|
9933
|
+
dry: { ...tuning, description: "Height map tuning for dry media." },
|
|
9934
|
+
wet: { ...tuning, description: "Height map tuning for wet media (the grain water and pigment follow)." },
|
|
11422
9935
|
absorption: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how fast the paper soaks up water (high: washes stop spreading sooner)." },
|
|
11423
9936
|
permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how freely water flows over the fibres (low: more ragged, slower spreading)." },
|
|
11424
9937
|
roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain pins wet edges and catches granulating pigment." },
|
|
@@ -11737,13 +10250,14 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11737
10250
|
add({
|
|
11738
10251
|
name: "stroke_path",
|
|
11739
10252
|
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).",
|
|
10253
|
+
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
10254
|
inputSchema: {
|
|
11742
10255
|
type: "object",
|
|
11743
10256
|
properties: {
|
|
11744
10257
|
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
10258
|
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
10259
|
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." },
|
|
10260
|
+
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
10261
|
layerId: LAYER_ID4,
|
|
11748
10262
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
11749
10263
|
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 +10279,30 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11765
10279
|
if (refs.length) await c.prepareStickers(refs);
|
|
11766
10280
|
}
|
|
11767
10281
|
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
|
|
10282
|
+
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 };
|
|
10283
|
+
const r = await act(
|
|
10284
|
+
() => {
|
|
10285
|
+
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".');
|
|
10286
|
+
let subpaths;
|
|
10287
|
+
try {
|
|
10288
|
+
subpaths = samplePath(path, init);
|
|
10289
|
+
} catch (e) {
|
|
10290
|
+
throw new PaintEditorError(e instanceof Error ? e.message : String(e));
|
|
10291
|
+
}
|
|
10292
|
+
const { path: _path, spacing: _spacing, pressure: _pressure, speed: _speed, ...stroke } = init;
|
|
10293
|
+
return {
|
|
10294
|
+
kind: "strokes",
|
|
10295
|
+
label: `${base?.name ?? "Brush"} path stroke`,
|
|
10296
|
+
strokes: subpaths.map((points, i) => ({
|
|
10297
|
+
...stroke,
|
|
10298
|
+
...stroke.seed !== void 0 ? { seed: stroke.seed + i } : {},
|
|
10299
|
+
snapToPerspective: false,
|
|
10300
|
+
points
|
|
10301
|
+
}))
|
|
10302
|
+
};
|
|
10303
|
+
},
|
|
10304
|
+
() => c.strokePath(init)
|
|
10305
|
+
);
|
|
11769
10306
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11770
10307
|
}
|
|
11771
10308
|
});
|
|
@@ -13058,7 +11595,7 @@ function createPaintChangeFeed(controller, options = {}) {
|
|
|
13058
11595
|
}
|
|
13059
11596
|
};
|
|
13060
11597
|
}
|
|
13061
|
-
var
|
|
11598
|
+
var HEX3 = /^#[0-9a-f]{6}$/i;
|
|
13062
11599
|
var DRY_ENGINES = /* @__PURE__ */ new Set(["dab", "particle", "bristle", "mixer"]);
|
|
13063
11600
|
function suggestionBrushRefusal(brush) {
|
|
13064
11601
|
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 +11635,20 @@ function createPaintProposals(options) {
|
|
|
13098
11635
|
if (op.kind === "stroke") {
|
|
13099
11636
|
if (!op.points?.length) throw new PaintEditorError(`${at}: a stroke needs points in document pixels.`);
|
|
13100
11637
|
points += op.points.length;
|
|
13101
|
-
if (op.color !== void 0 && !
|
|
11638
|
+
if (op.color !== void 0 && !HEX3.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
13102
11639
|
const brush = op.brushId ? controller.getBrush(op.brushId) : controller.getState().brush;
|
|
13103
11640
|
if (!brush) throw new PaintEditorError(`${at}: unknown brush "${op.brushId}". Call list_brush_presets for the ids.`);
|
|
13104
11641
|
const refusal = suggestionBrushRefusal(brush);
|
|
13105
11642
|
if (refusal) throw new PaintEditorError(`${at}: ${refusal}`);
|
|
13106
11643
|
if (op.size !== void 0 && !(op.size > 0)) throw new PaintEditorError(`${at}: size must be above 0 (document pixels).`);
|
|
13107
11644
|
} else if (op.kind === "wash") {
|
|
13108
|
-
if (!
|
|
11645
|
+
if (!HEX3.test(op.color ?? "")) throw new PaintEditorError(`${at}: a wash needs color as #rrggbb.`);
|
|
13109
11646
|
if (op.opacity !== void 0 && !(op.opacity > 0 && op.opacity <= 1)) throw new PaintEditorError(`${at}: opacity must be in (0, 1].`);
|
|
13110
11647
|
} else if (op.kind === "echo") {
|
|
13111
11648
|
if (!op.strokeIds?.length) throw new PaintEditorError(`${at}: an echo needs strokeIds (list_strokes).`);
|
|
13112
11649
|
const unknown = op.strokeIds.filter((id) => !known.has(id));
|
|
13113
11650
|
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 && !
|
|
11651
|
+
if (op.color !== void 0 && !HEX3.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
13115
11652
|
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
11653
|
for (const id of op.strokeIds) {
|
|
13117
11654
|
const stroke = controller.getStrokes().find((s) => s.id === id);
|