@pixerate/editor 0.266.2 → 0.268.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.cjs +2 -2
- package/dist/agent/index.js +1 -1
- package/dist/animation-editor/controller.d.cts +8 -0
- package/dist/animation-editor/controller.d.ts +8 -0
- package/dist/animation-editor/effect-tools.d.cts +1 -1
- package/dist/animation-editor/effect-tools.d.ts +1 -1
- package/dist/animation-editor/index.cjs +175 -108
- package/dist/animation-editor/index.js +94 -27
- package/dist/brushes/index.cjs +201 -75
- package/dist/brushes/index.d.cts +4 -3
- package/dist/brushes/index.d.ts +4 -3
- package/dist/brushes/index.js +138 -12
- package/dist/brushes/library.d.cts +27 -1
- package/dist/brushes/library.d.ts +27 -1
- package/dist/brushes/profiles.d.cts +10 -1
- package/dist/brushes/profiles.d.ts +10 -1
- package/dist/brushes/schema.d.cts +4 -0
- package/dist/brushes/schema.d.ts +4 -0
- package/dist/brushes/sources.d.cts +114 -0
- package/dist/brushes/sources.d.ts +114 -0
- package/dist/brushes/types.d.cts +37 -5
- package/dist/brushes/types.d.ts +37 -5
- package/dist/brushes/ui.d.cts +40 -4
- package/dist/brushes/ui.d.ts +40 -4
- package/dist/{brushstrokeRender-KD453OBX.js → brushstrokeRender-A727TTNQ.js} +7 -7
- package/dist/{brushstrokeRender-FSUWXUZY.cjs → brushstrokeRender-WR2N5HHY.cjs} +23 -23
- package/dist/canvas/brushTypes.d.cts +12 -0
- package/dist/canvas/brushTypes.d.ts +12 -0
- package/dist/canvas/brushstroke.d.cts +1 -1
- package/dist/canvas/brushstroke.d.ts +1 -1
- package/dist/canvas/drawController.d.cts +19 -4
- package/dist/canvas/drawController.d.ts +19 -4
- package/dist/canvas/index.cjs +144 -105
- package/dist/canvas/index.js +46 -7
- package/dist/{chunk-TOID45I2.cjs → chunk-2WOGXEDX.cjs} +1 -1
- package/dist/{chunk-XORJ7UZO.cjs → chunk-3VHH6V67.cjs} +13 -12
- package/dist/{chunk-3C33YTNN.js → chunk-3XDSNIGF.js} +73 -6
- package/dist/chunk-62MFUHSI.cjs +802 -0
- package/dist/{chunk-R44Y3EFS.js → chunk-74ODPZLX.js} +17 -345
- package/dist/{chunk-GCVFEPJG.js → chunk-ACTHSQOI.js} +182 -0
- package/dist/chunk-ADMRNWR7.js +340 -0
- package/dist/{chunk-MVUQSK5J.cjs → chunk-B3G7BKUV.cjs} +111 -439
- package/dist/{chunk-DXYHQ2VN.js → chunk-BAZMCFVL.js} +1 -1
- package/dist/{chunk-VRK322ZC.cjs → chunk-BPJSI4NT.cjs} +2 -2
- package/dist/{chunk-RFJZGTHA.cjs → chunk-EU2ZZA6L.cjs} +400 -69
- package/dist/{chunk-XC6DPRYI.js → chunk-GSTSZPSR.js} +154 -949
- package/dist/{chunk-EZOJBP4D.js → chunk-HFY5LG5J.js} +1 -1
- package/dist/chunk-HVO6XCI4.js +802 -0
- package/dist/{chunk-YHDHFJUC.cjs → chunk-J5VQYK7T.cjs} +217 -1012
- package/dist/{chunk-XRXFVRGY.cjs → chunk-JBB3GDBB.cjs} +2483 -208
- package/dist/{chunk-ACFSFUQH.js → chunk-JL6YH5WD.js} +4 -0
- package/dist/{chunk-CIJBSUM5.js → chunk-KZI3YWKW.js} +353 -22
- package/dist/{chunk-4ODRXQRW.js → chunk-LETF75CA.js} +1 -1
- package/dist/{chunk-FQPUF2W2.cjs → chunk-NIBFKXMD.cjs} +217 -35
- package/dist/{chunk-AQZQWXEU.cjs → chunk-NPWWSCWT.cjs} +35 -35
- package/dist/{chunk-DOZ6F2K2.js → chunk-NSETJI74.js} +3 -2
- package/dist/{chunk-PE6YJMQT.cjs → chunk-PFHJS4HE.cjs} +20 -19
- package/dist/{chunk-7KNW3WXZ.cjs → chunk-QEVLRLUN.cjs} +63 -0
- package/dist/{chunk-LK4SXL3V.js → chunk-QNY7BDFP.js} +1 -1
- package/dist/{chunk-OTN6VB5S.cjs → chunk-RDFHWAHL.cjs} +5 -1
- package/dist/{chunk-PXDIAYB6.cjs → chunk-STLTOISW.cjs} +34 -214
- package/dist/{chunk-T2LDTEXK.js → chunk-UVTO3D4E.js} +2333 -58
- package/dist/{chunk-6JL2D5SG.js → chunk-VE6UKYQ2.js} +3 -2
- package/dist/{chunk-TXFWZ5ZJ.js → chunk-W6JDUXP3.js} +4 -184
- package/dist/{chunk-3SBLVGOM.cjs → chunk-YOVXQHA3.cjs} +23 -23
- package/dist/{chunk-WLUVZKVD.cjs → chunk-YXP6X3SY.cjs} +103 -36
- package/dist/chunk-YZWM6ZIP.cjs +340 -0
- package/dist/{chunk-472INMWH.js → chunk-ZA7L5YJB.js} +63 -0
- package/dist/commands/builtins.d.cts +12 -1
- package/dist/commands/builtins.d.ts +12 -1
- package/dist/commands/index.cjs +2 -2
- package/dist/commands/index.js +1 -1
- package/dist/image-editor/controller.d.cts +1 -1
- package/dist/image-editor/controller.d.ts +1 -1
- package/dist/image-editor/index.cjs +868 -864
- package/dist/image-editor/index.js +123 -119
- package/dist/paint/index.cjs +8 -6
- package/dist/paint/index.js +56 -54
- package/dist/paint-editor/brush-library-tools.d.cts +12 -2
- package/dist/paint-editor/brush-library-tools.d.ts +12 -2
- package/dist/paint-editor/brush-model.d.cts +29 -1
- package/dist/paint-editor/brush-model.d.ts +29 -1
- package/dist/paint-editor/brush-source-art.d.cts +32 -0
- package/dist/paint-editor/brush-source-art.d.ts +32 -0
- package/dist/paint-editor/controller.d.cts +11 -4
- package/dist/paint-editor/controller.d.ts +11 -4
- package/dist/paint-editor/dab-stroke.d.cts +2 -0
- package/dist/paint-editor/dab-stroke.d.ts +2 -0
- package/dist/paint-editor/engine.d.cts +11 -2
- package/dist/paint-editor/engine.d.ts +11 -2
- package/dist/paint-editor/index.cjs +967 -2335
- package/dist/paint-editor/index.d.cts +3 -1
- package/dist/paint-editor/index.d.ts +3 -1
- package/dist/paint-editor/index.js +394 -1762
- package/dist/paint-editor/path-samples.d.cts +2 -0
- package/dist/paint-editor/path-samples.d.ts +2 -0
- package/dist/paint-editor/presence.d.cts +20 -1
- package/dist/paint-editor/presence.d.ts +20 -1
- package/dist/paint-editor/stamp-tip.d.cts +8 -5
- package/dist/paint-editor/stamp-tip.d.ts +8 -5
- package/dist/paint-editor/thumbnails.d.cts +6 -0
- package/dist/paint-editor/thumbnails.d.ts +6 -0
- package/dist/paint-editor/tip-grain.d.cts +34 -0
- package/dist/paint-editor/tip-grain.d.ts +34 -0
- package/dist/raster/index.cjs +2 -2
- package/dist/raster/index.js +9 -9
- package/dist/stickers/helpers.d.cts +2 -0
- package/dist/stickers/helpers.d.ts +2 -0
- package/dist/stickers/index.cjs +41 -35
- package/dist/stickers/index.js +22 -16
- package/dist/stickers/types.d.cts +4 -2
- package/dist/stickers/types.d.ts +4 -2
- package/dist/vector/backdrop.d.cts +13 -0
- package/dist/vector/backdrop.d.ts +13 -0
- package/dist/vector/brushstroke.d.cts +54 -7
- package/dist/vector/brushstroke.d.ts +54 -7
- package/dist/vector/brushstrokeMedia.d.cts +96 -0
- package/dist/vector/brushstrokeMedia.d.ts +96 -0
- package/dist/vector/controller.d.cts +17 -3
- package/dist/vector/controller.d.ts +17 -3
- package/dist/vector/index.cjs +1850 -1390
- package/dist/vector/index.d.cts +2 -0
- package/dist/vector/index.d.ts +2 -0
- package/dist/vector/index.js +520 -60
- package/dist/vector/render.d.cts +6 -0
- package/dist/vector/render.d.ts +6 -0
- package/dist/vector/types.d.cts +14 -0
- package/dist/vector/types.d.ts +14 -0
- package/package.json +1 -1
- package/skills/pixerate-animation-editor/SKILL.md +1 -0
- package/skills/pixerate-brushes/SKILL.md +37 -10
- package/skills/pixerate-brushes-design/SKILL.md +15 -5
- package/skills/pixerate-brushes-design/evals/port-to-vector.json +8 -8
- package/skills/pixerate-canvas/SKILL.md +11 -1
- package/skills/pixerate-image-editor/SKILL.md +19 -64
- package/skills/pixerate-image-editor/references/color-management.md +2 -1
- package/skills/pixerate-image-editor/references/free-transform.md +12 -0
- package/skills/pixerate-image-editor/references/layer-effects.md +10 -0
- package/skills/pixerate-image-editor/references/painting.md +17 -0
- package/skills/pixerate-image-editor/references/photoshop-files.md +1 -1
- package/skills/pixerate-image-editor/references/shapes-and-type.md +14 -0
- package/skills/pixerate-image-editor/references/smart-objects.md +10 -0
- package/skills/pixerate-paint-editor/SKILL.md +14 -4
- package/skills/pixerate-paint-editor/references/brush-facets.md +3 -1
- package/skills/pixerate-paint-editor/references/wet-media.md +9 -0
- package/skills/pixerate-paint-sketch-to-finish/SKILL.md +1 -0
- package/skills/pixerate-vector/SKILL.md +16 -6
- package/skills/pixerate-vector/references/best-practices.md +1 -0
- package/skills/pixerate-vector/references/commands.md +19 -0
|
@@ -4,10 +4,15 @@ import {
|
|
|
4
4
|
VECTOR_PROFILE,
|
|
5
5
|
checkBrush,
|
|
6
6
|
closest,
|
|
7
|
+
isBrushSourceRef,
|
|
7
8
|
listPresets,
|
|
8
9
|
normalizeBrushSpec,
|
|
9
10
|
resolveBrush
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-KZI3YWKW.js";
|
|
12
|
+
import {
|
|
13
|
+
editorStampOf,
|
|
14
|
+
stampCoverage
|
|
15
|
+
} from "./chunk-JL6YH5WD.js";
|
|
11
16
|
import {
|
|
12
17
|
parseProvenance
|
|
13
18
|
} from "./chunk-M6DNZNVY.js";
|
|
@@ -52,12 +57,71 @@ import {
|
|
|
52
57
|
import {
|
|
53
58
|
brushColourHex
|
|
54
59
|
} from "./chunk-DVPQFMH6.js";
|
|
60
|
+
import {
|
|
61
|
+
DEFAULT_FLOW,
|
|
62
|
+
DEFAULT_WET_PARAMS,
|
|
63
|
+
FLAT,
|
|
64
|
+
PIGMENT_UNIT,
|
|
65
|
+
WET_TILE,
|
|
66
|
+
WetMedium,
|
|
67
|
+
WetWorld,
|
|
68
|
+
createWetGpuEngine,
|
|
69
|
+
findWebGpu,
|
|
70
|
+
flowField,
|
|
71
|
+
moveWetSnapshot,
|
|
72
|
+
normalizeFlow,
|
|
73
|
+
normalizeTilt,
|
|
74
|
+
normalizeWetParams,
|
|
75
|
+
pigmentOf
|
|
76
|
+
} from "./chunk-GSTSZPSR.js";
|
|
55
77
|
import {
|
|
56
78
|
BrushstrokeCache,
|
|
57
|
-
GRAIN_PATTERNS,
|
|
58
79
|
brushstrokeDataUrl,
|
|
80
|
+
brushstrokeSamples,
|
|
59
81
|
isBrushstrokeBrush
|
|
60
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-W6JDUXP3.js";
|
|
83
|
+
import {
|
|
84
|
+
CoverageMap,
|
|
85
|
+
EMPTY_MIXER_LOAD,
|
|
86
|
+
GRAIN_PATTERNS,
|
|
87
|
+
PaintExchange,
|
|
88
|
+
PaintGradientSampler,
|
|
89
|
+
PaperGrain,
|
|
90
|
+
SmudgeTip,
|
|
91
|
+
TileMedium,
|
|
92
|
+
blenderDab,
|
|
93
|
+
createBlenderScratch,
|
|
94
|
+
dabRect,
|
|
95
|
+
exchangeRates,
|
|
96
|
+
grainCatch,
|
|
97
|
+
loadMixer,
|
|
98
|
+
mixerRates,
|
|
99
|
+
parsePaintSource,
|
|
100
|
+
rasterizeDab,
|
|
101
|
+
readPaintSource,
|
|
102
|
+
resolvePaintGradient,
|
|
103
|
+
scalePaintGradient
|
|
104
|
+
} from "./chunk-ACTHSQOI.js";
|
|
105
|
+
import {
|
|
106
|
+
rand01,
|
|
107
|
+
randSigned
|
|
108
|
+
} from "./chunk-4D6KP5BV.js";
|
|
109
|
+
import {
|
|
110
|
+
BRUSHSTROKE_MAX_SCALE,
|
|
111
|
+
BRUSHSTROKE_MIN_SCALE
|
|
112
|
+
} from "./chunk-ODBT55Q7.js";
|
|
113
|
+
import {
|
|
114
|
+
mixPigment8
|
|
115
|
+
} from "./chunk-IRSTHLQQ.js";
|
|
116
|
+
import {
|
|
117
|
+
TILE,
|
|
118
|
+
TiledSurface,
|
|
119
|
+
emptySurface,
|
|
120
|
+
readRegion,
|
|
121
|
+
tileKey,
|
|
122
|
+
translateSurface,
|
|
123
|
+
writeRegion
|
|
124
|
+
} from "./chunk-HVVR5PZW.js";
|
|
61
125
|
import {
|
|
62
126
|
oklabToRgb,
|
|
63
127
|
rgbToOklab
|
|
@@ -2320,7 +2384,7 @@ function vectorStrokeParts(path, style) {
|
|
|
2320
2384
|
const cap = capExtent(style, width);
|
|
2321
2385
|
const tipMode = style.arrowAlign === "tip";
|
|
2322
2386
|
const trims = contours.map(() => [0, 0]);
|
|
2323
|
-
const
|
|
2387
|
+
const place3 = (end, contour, ci) => {
|
|
2324
2388
|
const kind = end === "start" ? style.startArrow : style.endArrow;
|
|
2325
2389
|
if (!kind || contour.closed) return;
|
|
2326
2390
|
const hw = arrowheadWeight(style, end);
|
|
@@ -2341,8 +2405,8 @@ function vectorStrokeParts(path, style) {
|
|
|
2341
2405
|
}
|
|
2342
2406
|
heads.push({ end, kind, tip, dir, inset, path: arrowheadPath(kind, tip, dir, hw) });
|
|
2343
2407
|
};
|
|
2344
|
-
|
|
2345
|
-
|
|
2408
|
+
place3("start", first, 0);
|
|
2409
|
+
place3("end", last, contours.length - 1);
|
|
2346
2410
|
if (!tipMode || !heads.length) return { align, line: path, drawWidth, heads };
|
|
2347
2411
|
const trimmed = contours.map((c, i) => {
|
|
2348
2412
|
const [s0, s1] = trims[i];
|
|
@@ -2534,17 +2598,1946 @@ function vectorExtrudeFaces(path, params) {
|
|
|
2534
2598
|
return order.map((i) => faces[i].face);
|
|
2535
2599
|
}
|
|
2536
2600
|
|
|
2601
|
+
// src/paint-editor/effect-paint.ts
|
|
2602
|
+
var EFFECT_STRIP_HEIGHT = 256;
|
|
2603
|
+
var EFFECT_MAP_REPEATS = ["repeat", "mirror", "clamp"];
|
|
2604
|
+
function effectPaintSize(p, canvas) {
|
|
2605
|
+
return p.map ? { width: p.map.length, height: EFFECT_STRIP_HEIGHT } : { width: canvas.width, height: canvas.height };
|
|
2606
|
+
}
|
|
2607
|
+
function readEffectStrokeMap(raw) {
|
|
2608
|
+
if (raw === void 0 || raw === null || raw.by === "canvas") return void 0;
|
|
2609
|
+
const r = raw;
|
|
2610
|
+
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).`);
|
|
2611
|
+
const length2 = r.length === void 0 ? 300 : Number(r.length);
|
|
2612
|
+
if (!(Number.isFinite(length2) && length2 >= 16 && length2 <= 4096)) throw new Error(`map.length ${JSON.stringify(r.length)} is out of range; use 16..4096 px of stroke per repeat.`);
|
|
2613
|
+
const repeat = r.repeat === void 0 ? "repeat" : r.repeat;
|
|
2614
|
+
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(", ")}.`);
|
|
2615
|
+
return { by: "stroke", length: Math.round(length2), repeat };
|
|
2616
|
+
}
|
|
2617
|
+
function stripIndex(img, map, dab, x, y) {
|
|
2618
|
+
const ca = Math.cos(dab.direction);
|
|
2619
|
+
const sa = Math.sin(dab.direction);
|
|
2620
|
+
const dx = x - dab.x;
|
|
2621
|
+
const dy = y - dab.y;
|
|
2622
|
+
let u = (dab.travel + dx * ca - dy * sa) / map.length;
|
|
2623
|
+
if (map.repeat === "clamp") u = Math.min(1, Math.max(0, u));
|
|
2624
|
+
else if (map.repeat === "mirror") {
|
|
2625
|
+
const m = (u % 2 + 2) % 2;
|
|
2626
|
+
u = m > 1 ? 2 - m : m;
|
|
2627
|
+
} else u = (u % 1 + 1) % 1;
|
|
2628
|
+
const v2 = Math.min(1, Math.max(0, ((dx * sa + dy * ca) / Math.max(0.5, dab.radius) + 1) / 2));
|
|
2629
|
+
const px = Math.min(img.width - 1, Math.floor(u * img.width));
|
|
2630
|
+
const py = Math.min(img.height - 1, Math.floor(v2 * img.height));
|
|
2631
|
+
return (py * img.width + px) * 4;
|
|
2632
|
+
}
|
|
2633
|
+
function effectPaintKey(p) {
|
|
2634
|
+
return JSON.stringify({ type: "effect", effect: p.effect, provider: p.provider, values: p.values, time: p.time, ...p.map ? { map: p.map } : {} });
|
|
2635
|
+
}
|
|
2636
|
+
function isPaintEffectPaint(raw) {
|
|
2637
|
+
return Boolean(raw) && typeof raw === "object" && raw.type === "effect";
|
|
2638
|
+
}
|
|
2639
|
+
function readMapLeniently(raw) {
|
|
2640
|
+
try {
|
|
2641
|
+
return readEffectStrokeMap(raw);
|
|
2642
|
+
} catch {
|
|
2643
|
+
return void 0;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
function readPaintEffectPaint(raw) {
|
|
2647
|
+
if (!isPaintEffectPaint(raw)) return null;
|
|
2648
|
+
const r = raw;
|
|
2649
|
+
if (typeof r.effect !== "string" || !r.effect || !r.values || typeof r.values !== "object" || Array.isArray(r.values)) return null;
|
|
2650
|
+
return {
|
|
2651
|
+
type: "effect",
|
|
2652
|
+
effect: r.effect,
|
|
2653
|
+
provider: typeof r.provider === "string" ? r.provider : "",
|
|
2654
|
+
values: JSON.parse(JSON.stringify(r.values)),
|
|
2655
|
+
time: typeof r.time === "number" && Number.isFinite(r.time) && r.time > 0 ? r.time : 0,
|
|
2656
|
+
...readMapLeniently(r.map) ? { map: readMapLeniently(r.map) } : {},
|
|
2657
|
+
...r.live === true ? { live: true } : {}
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
function sampleEffectField(img, rect, vals, field, dab) {
|
|
2661
|
+
const { width, height, data } = img;
|
|
2662
|
+
if (img.map && dab) {
|
|
2663
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
2664
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
2665
|
+
const i = stripIndex(img, img.map, dab, rect.x + xx + 0.5, rect.y + yy + 0.5);
|
|
2666
|
+
const f = k * 3;
|
|
2667
|
+
const a = data[i + 3];
|
|
2668
|
+
if (a < 255) vals[k] *= a / 255;
|
|
2669
|
+
field[f] = data[i] / 255;
|
|
2670
|
+
field[f + 1] = data[i + 1] / 255;
|
|
2671
|
+
field[f + 2] = data[i + 2] / 255;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
2677
|
+
const y = rect.y + yy;
|
|
2678
|
+
const inY = y >= 0 && y < height;
|
|
2679
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
2680
|
+
const x = rect.x + xx;
|
|
2681
|
+
const f = k * 3;
|
|
2682
|
+
if (!inY || x < 0 || x >= width) {
|
|
2683
|
+
vals[k] = 0;
|
|
2684
|
+
field[f] = field[f + 1] = field[f + 2] = 0;
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
const i = (y * width + x) * 4;
|
|
2688
|
+
const a = data[i + 3];
|
|
2689
|
+
if (a < 255) vals[k] *= a / 255;
|
|
2690
|
+
field[f] = data[i] / 255;
|
|
2691
|
+
field[f + 1] = data[i + 1] / 255;
|
|
2692
|
+
field[f + 2] = data[i + 2] / 255;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
function sampleEffectAt(img, x, y, out, dab) {
|
|
2697
|
+
if (img.map && dab) {
|
|
2698
|
+
const i2 = stripIndex(img, img.map, dab, x, y);
|
|
2699
|
+
out[0] = img.data[i2] / 255;
|
|
2700
|
+
out[1] = img.data[i2 + 1] / 255;
|
|
2701
|
+
out[2] = img.data[i2 + 2] / 255;
|
|
2702
|
+
return img.data[i2 + 3] / 255;
|
|
2703
|
+
}
|
|
2704
|
+
const px = Math.floor(x);
|
|
2705
|
+
const py = Math.floor(y);
|
|
2706
|
+
if (px < 0 || py < 0 || px >= img.width || py >= img.height) return 0;
|
|
2707
|
+
const i = (py * img.width + px) * 4;
|
|
2708
|
+
out[0] = img.data[i] / 255;
|
|
2709
|
+
out[1] = img.data[i + 1] / 255;
|
|
2710
|
+
out[2] = img.data[i + 2] / 255;
|
|
2711
|
+
return img.data[i + 3] / 255;
|
|
2712
|
+
}
|
|
2713
|
+
function effectDabField(img, rect, cov, centre, mixed, field, steps = 0, dab = null) {
|
|
2714
|
+
sampleEffectField(img, rect, cov, field, dab);
|
|
2715
|
+
const d0 = mixed[0] - centre[0];
|
|
2716
|
+
const d1 = mixed[1] - centre[1];
|
|
2717
|
+
const d2 = mixed[2] - centre[2];
|
|
2718
|
+
const n = rect.width * rect.height;
|
|
2719
|
+
for (let k = 0; k < n; k++) {
|
|
2720
|
+
if (steps) cov[k] = Math.round(cov[k] * steps) / steps;
|
|
2721
|
+
const f = k * 3;
|
|
2722
|
+
field[f] = Math.min(1, Math.max(0, field[f] + d0));
|
|
2723
|
+
field[f + 1] = Math.min(1, Math.max(0, field[f + 1] + d1));
|
|
2724
|
+
field[f + 2] = Math.min(1, Math.max(0, field[f + 2] + d2));
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
// src/paint-editor/stamp-tip.ts
|
|
2729
|
+
var samplers = /* @__PURE__ */ new WeakMap();
|
|
2730
|
+
function stampSampler(ref, art, hardness = 1) {
|
|
2731
|
+
const sampler = baseSampler(ref, art);
|
|
2732
|
+
if (!sampler || hardness >= 1 || !isBrushSourceRef(ref)) return sampler;
|
|
2733
|
+
const h = Math.max(0, hardness);
|
|
2734
|
+
return (u, v2, seed) => {
|
|
2735
|
+
const r = Math.hypot(u, v2) / Math.SQRT2;
|
|
2736
|
+
if (r <= h) return sampler(u, v2, seed);
|
|
2737
|
+
const t = Math.min(1, (r - h) / (1 - h));
|
|
2738
|
+
return sampler(u, v2, seed) * (1 - t * t * (3 - 2 * t));
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
function baseSampler(ref, art) {
|
|
2742
|
+
let kind = null;
|
|
2743
|
+
try {
|
|
2744
|
+
kind = editorStampOf(ref.includes(":") ? ref : `editor:${ref}`);
|
|
2745
|
+
} catch {
|
|
2746
|
+
return null;
|
|
2747
|
+
}
|
|
2748
|
+
if (kind) {
|
|
2749
|
+
const k = kind;
|
|
2750
|
+
return (u, v2, seed) => stampCoverage(k, u, v2, seed);
|
|
2751
|
+
}
|
|
2752
|
+
const a = art?.(ref);
|
|
2753
|
+
if (!a || !(a.width > 0 && a.height > 0)) return null;
|
|
2754
|
+
const hit = samplers.get(a);
|
|
2755
|
+
if (hit) return hit;
|
|
2756
|
+
const { width: w, height: h, data } = a;
|
|
2757
|
+
const scale3 = Math.max(w, h) / 2;
|
|
2758
|
+
const alpha = (x, y) => x < 0 || y < 0 || x >= w || y >= h ? 0 : data[(y * w + x) * 4 + 3] / 255;
|
|
2759
|
+
const sampler = (u, v2) => {
|
|
2760
|
+
const x = u * scale3 + w / 2 - 0.5;
|
|
2761
|
+
const y = v2 * scale3 + h / 2 - 0.5;
|
|
2762
|
+
const x0 = Math.floor(x);
|
|
2763
|
+
const y0 = Math.floor(y);
|
|
2764
|
+
const fx = x - x0;
|
|
2765
|
+
const fy = y - y0;
|
|
2766
|
+
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;
|
|
2767
|
+
};
|
|
2768
|
+
samplers.set(a, sampler);
|
|
2769
|
+
return sampler;
|
|
2770
|
+
}
|
|
2771
|
+
function stampDabRect(d) {
|
|
2772
|
+
const r = Math.max(0.5, d.radius) * Math.SQRT2 + 1.5;
|
|
2773
|
+
const x0 = Math.floor(d.x - r);
|
|
2774
|
+
const y0 = Math.floor(d.y - r);
|
|
2775
|
+
return { x: x0, y: y0, width: Math.ceil(d.x + r) - x0, height: Math.ceil(d.y + r) - y0 };
|
|
2776
|
+
}
|
|
2777
|
+
function rasterizeStampDab(d, sample, seed, rect, out) {
|
|
2778
|
+
const w = rect.width;
|
|
2779
|
+
const h = rect.height;
|
|
2780
|
+
out.fill(0, 0, w * h);
|
|
2781
|
+
const r = Math.max(0.5, d.radius);
|
|
2782
|
+
const ro = Math.min(1, Math.max(0.05, d.roundness));
|
|
2783
|
+
const sn = Math.sin(d.angle);
|
|
2784
|
+
const cs = Math.cos(d.angle);
|
|
2785
|
+
const ss = r < 6 ? 3 : 1;
|
|
2786
|
+
const fu = d.flip && d.flip & 1 ? -1 : 1;
|
|
2787
|
+
const fv = d.flip && d.flip & 2 ? -1 : 1;
|
|
2788
|
+
for (let yy = 0; yy < h; yy++) {
|
|
2789
|
+
for (let xx = 0; xx < w; xx++) {
|
|
2790
|
+
let sum = 0;
|
|
2791
|
+
for (let sy = 0; sy < ss; sy++) {
|
|
2792
|
+
for (let sx = 0; sx < ss; sx++) {
|
|
2793
|
+
const dx = rect.x + xx + (sx + 0.5) / ss - d.x;
|
|
2794
|
+
const dy = rect.y + yy + (sy + 0.5) / ss - d.y;
|
|
2795
|
+
const u = (dx * cs - dy * sn) / r * fu;
|
|
2796
|
+
const v2 = (dx * sn + dy * cs) / (r * ro) * fv;
|
|
2797
|
+
if (u > -1.05 && u < 1.05 && v2 > -1.05 && v2 < 1.05) sum += sample(u, v2, seed);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
const val = sum / (ss * ss);
|
|
2801
|
+
if (val > 0) out[yy * w + xx] = Math.min(1, val) * d.alpha;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
function brushStickerRefs(brush) {
|
|
2806
|
+
const refs = brush.engine === "sticker" ? [...brush.sticker.stamps] : brush.engine === "particle" ? [...brush.particle.dotTextures] : brush.engine === "dab" && brush.stamp ? [brush.stamp] : [];
|
|
2807
|
+
return [...new Set(refs.filter((r) => r.includes(":") && !r.startsWith("editor:") && !isBrushSourceRef(r)))];
|
|
2808
|
+
}
|
|
2809
|
+
function sprayDroplets(d, spray, seed) {
|
|
2810
|
+
const count = 1 + Math.round(spray.density * 31);
|
|
2811
|
+
const key = d.index * 131 + Math.round(d.travel * 8);
|
|
2812
|
+
const out = [];
|
|
2813
|
+
for (let i = 0; i < count; i++) {
|
|
2814
|
+
const n = key * 64 + i;
|
|
2815
|
+
const a = rand01(seed, n, 61) * Math.PI * 2;
|
|
2816
|
+
const rr = Math.sqrt(rand01(seed, n, 67)) * d.radius;
|
|
2817
|
+
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 });
|
|
2818
|
+
}
|
|
2819
|
+
return out;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// src/paint-editor/paper.ts
|
|
2823
|
+
var PAPER_FAMILIES = ["watercolor", "drawing", "pastel", "canvas", "panel", "smooth"];
|
|
2824
|
+
var unit2 = (v2, d) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(1, Math.max(0, v2)) : d;
|
|
2825
|
+
var clampNum = (v2, lo, hi, d) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(hi, Math.max(lo, v2)) : d;
|
|
2826
|
+
var HEX = /^#[0-9a-f]{6}$/i;
|
|
2827
|
+
var tuning = (contrast = 1, invert2 = false) => ({ contrast, invert: invert2 });
|
|
2828
|
+
function preset(id, name, family, pattern, color2, o) {
|
|
2829
|
+
return {
|
|
2830
|
+
preset: id,
|
|
2831
|
+
name,
|
|
2832
|
+
family,
|
|
2833
|
+
pattern,
|
|
2834
|
+
seed: o.seed ?? 1,
|
|
2835
|
+
color: color2,
|
|
2836
|
+
scale: o.scale ?? 1,
|
|
2837
|
+
tiling: "mirror",
|
|
2838
|
+
depth: o.depth,
|
|
2839
|
+
shading: o.shading ?? 0.5,
|
|
2840
|
+
dry: tuning(o.contrast ?? 1),
|
|
2841
|
+
wet: tuning(1),
|
|
2842
|
+
absorption: o.absorption,
|
|
2843
|
+
permeability: o.permeability,
|
|
2844
|
+
roughness: o.roughness,
|
|
2845
|
+
...o.backlight ? { backlight: o.backlight, backlightColor: o.backlightColor ?? "#fff1d6" } : {}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
var PAPER_LIBRARY = [
|
|
2849
|
+
preset("cold-pressed", "Cold pressed watercolour", "watercolor", "cold-pressed", "#f7f3ea", { depth: 0.5, absorption: 0.7, permeability: 0.5, roughness: 0.6 }),
|
|
2850
|
+
preset("hot-pressed", "Hot pressed watercolour", "watercolor", "hot-pressed", "#f8f6ef", { depth: 0.25, absorption: 0.55, permeability: 0.4, roughness: 0.25 }),
|
|
2851
|
+
preset("rough-watercolor", "Rough watercolour", "watercolor", "rough", "#f5f0e4", { depth: 0.75, absorption: 0.8, permeability: 0.6, roughness: 0.85 }),
|
|
2852
|
+
preset("cartridge", "Cartridge", "drawing", "vellum", "#fbfaf6", { depth: 0.3, absorption: 0.4, permeability: 0.3, roughness: 0.35 }),
|
|
2853
|
+
preset("laid", "Laid paper", "drawing", "laid", "#f3ecdc", { depth: 0.45, absorption: 0.45, permeability: 0.35, roughness: 0.45 }),
|
|
2854
|
+
preset("ingres", "Charcoal paper", "drawing", "ingres", "#ede6d6", { depth: 0.6, absorption: 0.4, permeability: 0.3, roughness: 0.65 }),
|
|
2855
|
+
preset("sanded-pastel", "Sanded pastel paper", "pastel", "sanded", "#d8cfc0", { depth: 0.7, absorption: 0.3, permeability: 0.2, roughness: 0.9, shading: 0.35 }),
|
|
2856
|
+
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 }),
|
|
2857
|
+
preset("cotton-canvas", "Cotton canvas", "canvas", "canvas", "#efe9dc", { depth: 0.55, absorption: 0.35, permeability: 0.25, roughness: 0.6 }),
|
|
2858
|
+
preset("fine-linen", "Fine linen", "canvas", "linen", "#e9e2d2", { depth: 0.45, absorption: 0.3, permeability: 0.25, roughness: 0.5 }),
|
|
2859
|
+
preset("gesso", "Gesso panel", "panel", "gesso", "#f6f4ef", { depth: 0.35, absorption: 0.2, permeability: 0.15, roughness: 0.4 }),
|
|
2860
|
+
preset("smooth", "Smooth bristol", "smooth", "smooth", "#ffffff", { depth: 0, absorption: 0.3, permeability: 0.2, roughness: 0.05, shading: 0 }),
|
|
2861
|
+
// Backlit: smooth paper on a lightbox, for glazes that glow like stained glass.
|
|
2862
|
+
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" })
|
|
2863
|
+
];
|
|
2864
|
+
var DEFAULT_PAPER = PAPER_LIBRARY[0];
|
|
2865
|
+
function paperPreset(id) {
|
|
2866
|
+
const p = PAPER_LIBRARY.find((x) => x.preset === id);
|
|
2867
|
+
return p ? clonePaper(p) : void 0;
|
|
2868
|
+
}
|
|
2869
|
+
function clonePaper(p) {
|
|
2870
|
+
return { ...p, dry: { ...p.dry }, wet: { ...p.wet }, ...p.offset ? { offset: { ...p.offset } } : {} };
|
|
2871
|
+
}
|
|
2872
|
+
function readOffset(raw, d) {
|
|
2873
|
+
const o = raw && typeof raw === "object" ? raw : null;
|
|
2874
|
+
const x = o && typeof o.x === "number" && Number.isFinite(o.x) ? Math.round(o.x) : d?.x ?? 0;
|
|
2875
|
+
const y = o && typeof o.y === "number" && Number.isFinite(o.y) ? Math.round(o.y) : d?.y ?? 0;
|
|
2876
|
+
return x || y ? { offset: { x, y } } : {};
|
|
2877
|
+
}
|
|
2878
|
+
function readTuning(raw, d) {
|
|
2879
|
+
const r = raw && typeof raw === "object" ? raw : {};
|
|
2880
|
+
return { contrast: clampNum(r.contrast, 0, 3, d.contrast), invert: typeof r.invert === "boolean" ? r.invert : d.invert };
|
|
2881
|
+
}
|
|
2882
|
+
function normalizePaper(raw = {}, base) {
|
|
2883
|
+
const r = raw;
|
|
2884
|
+
const b = base ?? (typeof r.preset === "string" ? paperPreset(r.preset) : void 0) ?? clonePaper(DEFAULT_PAPER);
|
|
2885
|
+
return {
|
|
2886
|
+
preset: typeof r.preset === "string" && r.preset ? r.preset : b.preset,
|
|
2887
|
+
name: typeof r.name === "string" && r.name.trim() ? r.name.trim() : b.name,
|
|
2888
|
+
family: PAPER_FAMILIES.includes(r.family) ? r.family : b.family,
|
|
2889
|
+
pattern: GRAIN_PATTERNS.includes(r.pattern) ? r.pattern : b.pattern,
|
|
2890
|
+
seed: typeof r.seed === "number" && Number.isFinite(r.seed) ? Math.round(Math.abs(r.seed)) % 2147483647 : b.seed,
|
|
2891
|
+
color: typeof r.color === "string" && HEX.test(r.color) ? r.color.toLowerCase() : b.color,
|
|
2892
|
+
scale: clampNum(r.scale, 0.25, 8, b.scale),
|
|
2893
|
+
tiling: r.tiling === "repeat" || r.tiling === "mirror" ? r.tiling : b.tiling,
|
|
2894
|
+
depth: unit2(r.depth, b.depth),
|
|
2895
|
+
shading: unit2(r.shading, b.shading),
|
|
2896
|
+
dry: readTuning(r.dry, b.dry),
|
|
2897
|
+
wet: readTuning(r.wet, b.wet),
|
|
2898
|
+
absorption: unit2(r.absorption, b.absorption),
|
|
2899
|
+
permeability: unit2(r.permeability, b.permeability),
|
|
2900
|
+
roughness: unit2(r.roughness, b.roughness),
|
|
2901
|
+
...unit2(r.metallic, b.metallic ?? 0) > 0 ? { metallic: unit2(r.metallic, b.metallic ?? 0) } : {},
|
|
2902
|
+
...unit2(r.backlight, b.backlight ?? 0) > 0 ? { backlight: unit2(r.backlight, b.backlight ?? 0) } : {},
|
|
2903
|
+
...typeof r.backlightColor === "string" && HEX.test(r.backlightColor) ? { backlightColor: r.backlightColor.toLowerCase() } : b.backlightColor ? { backlightColor: b.backlightColor } : {},
|
|
2904
|
+
...readOffset(r.offset, b.offset)
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
function hexToRgb8(hex) {
|
|
2908
|
+
const n = parseInt(hex.slice(1, 7), 16);
|
|
2909
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
2910
|
+
}
|
|
2911
|
+
function renderPaper(paper, rect, grain = new PaperGrain(paper), light) {
|
|
2912
|
+
const [r, g, b] = hexToRgb8(paper.color);
|
|
2913
|
+
const out = new Uint8ClampedArray(rect.width * rect.height * 4);
|
|
2914
|
+
const shade = grain.flat ? 0 : paper.shading * 0.16;
|
|
2915
|
+
const metal = light ? paper.metallic ?? 0 : 0;
|
|
2916
|
+
let hx = 0;
|
|
2917
|
+
let hy = 0;
|
|
2918
|
+
let hz = 1;
|
|
2919
|
+
if (metal > 0 && light) {
|
|
2920
|
+
const az = light.azimuth * Math.PI / 180;
|
|
2921
|
+
const el = light.elevation * Math.PI / 180;
|
|
2922
|
+
const lx = Math.cos(el) * Math.sin(az);
|
|
2923
|
+
const ly = -Math.cos(el) * Math.cos(az);
|
|
2924
|
+
const lz = Math.sin(el) + 1;
|
|
2925
|
+
const n = Math.hypot(lx, ly, lz);
|
|
2926
|
+
hx = lx / n;
|
|
2927
|
+
hy = ly / n;
|
|
2928
|
+
hz = lz / n;
|
|
2929
|
+
}
|
|
2930
|
+
for (let y = 0; y < rect.height; y++) {
|
|
2931
|
+
for (let x = 0; x < rect.width; x++) {
|
|
2932
|
+
const i = (y * rect.width + x) * 4;
|
|
2933
|
+
const px = rect.x + x + 0.5;
|
|
2934
|
+
const py = rect.y + y + 0.5;
|
|
2935
|
+
const k = shade ? 1 - shade * (1 - grain.at(px, py)) : 1;
|
|
2936
|
+
let spec = 0;
|
|
2937
|
+
if (metal > 0) {
|
|
2938
|
+
const gx = (grain.at(px + 1, py) - grain.at(px - 1, py)) * 4;
|
|
2939
|
+
const gy = (grain.at(px, py + 1) - grain.at(px, py - 1)) * 4;
|
|
2940
|
+
const nl = Math.hypot(gx, gy, 1);
|
|
2941
|
+
const nh = Math.max(0, (-gx * hx - gy * hy + hz) / nl);
|
|
2942
|
+
spec = metal * Math.pow(nh, 24) * 0.9;
|
|
2943
|
+
}
|
|
2944
|
+
out[i] = Math.min(255, r * k * (1 - 0.25 * metal) + spec * (r * 0.6 + 102));
|
|
2945
|
+
out[i + 1] = Math.min(255, g * k * (1 - 0.25 * metal) + spec * (g * 0.6 + 102));
|
|
2946
|
+
out[i + 2] = Math.min(255, b * k * (1 - 0.25 * metal) + spec * (b * 0.6 + 102));
|
|
2947
|
+
out[i + 3] = 255;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
return out;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
// src/paint-editor/tip-grain.ts
|
|
2954
|
+
var ImageGrain = class {
|
|
2955
|
+
constructor(w, h, alpha, contrast, invert2, scale3) {
|
|
2956
|
+
this.w = w;
|
|
2957
|
+
this.h = h;
|
|
2958
|
+
this.scale = scale3;
|
|
2959
|
+
this.heights = new Float32Array(w * h);
|
|
2960
|
+
for (let i = 0; i < w * h; i++) {
|
|
2961
|
+
const v2 = Math.min(1, Math.max(0, (alpha[i * 4 + 3] / 255 - 0.5) * contrast + 0.5));
|
|
2962
|
+
this.heights[i] = invert2 ? 1 - v2 : v2;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
w;
|
|
2966
|
+
h;
|
|
2967
|
+
scale;
|
|
2968
|
+
heights;
|
|
2969
|
+
at(x, y) {
|
|
2970
|
+
const { w, h } = this;
|
|
2971
|
+
const u = x / this.scale - 0.5;
|
|
2972
|
+
const v2 = y / this.scale - 0.5;
|
|
2973
|
+
const x0 = Math.floor(u);
|
|
2974
|
+
const y0 = Math.floor(v2);
|
|
2975
|
+
const fx = u - x0;
|
|
2976
|
+
const fy = v2 - y0;
|
|
2977
|
+
const xa = (x0 % w + w) % w;
|
|
2978
|
+
const ya = (y0 % h + h) % h;
|
|
2979
|
+
const xb = (xa + 1) % w;
|
|
2980
|
+
const yb = (ya + 1) % h;
|
|
2981
|
+
const hs = this.heights;
|
|
2982
|
+
return (hs[ya * w + xa] * (1 - fx) + hs[ya * w + xb] * fx) * (1 - fy) + (hs[yb * w + xa] * (1 - fx) + hs[yb * w + xb] * fx) * fy;
|
|
2983
|
+
}
|
|
2984
|
+
};
|
|
2985
|
+
var TipGrain = class _TipGrain {
|
|
2986
|
+
constructor(field, settings) {
|
|
2987
|
+
this.field = field;
|
|
2988
|
+
this.settings = settings;
|
|
2989
|
+
}
|
|
2990
|
+
field;
|
|
2991
|
+
settings;
|
|
2992
|
+
/**
|
|
2993
|
+
* The grain for `settings`, or `null` when there is none to apply: no depth, or an image source
|
|
2994
|
+
* whose art is not loaded (the dabs then paint without it).
|
|
2995
|
+
*/
|
|
2996
|
+
static create(settings, art) {
|
|
2997
|
+
if (!settings || settings.depth <= 0) return null;
|
|
2998
|
+
if (GRAIN_PATTERNS.includes(settings.source)) {
|
|
2999
|
+
if (settings.source === "smooth") return null;
|
|
3000
|
+
const tuning2 = { contrast: settings.contrast, invert: settings.invert };
|
|
3001
|
+
const paper = new PaperGrain({ pattern: settings.source, seed: 0, scale: settings.scale, tiling: "repeat", dry: tuning2, wet: tuning2 });
|
|
3002
|
+
return new _TipGrain(paper, settings);
|
|
3003
|
+
}
|
|
3004
|
+
const a = art?.(settings.source);
|
|
3005
|
+
if (!a || !(a.width > 0 && a.height > 0)) return null;
|
|
3006
|
+
return new _TipGrain(new ImageGrain(a.width, a.height, a.data, settings.contrast, settings.invert, settings.scale), settings);
|
|
3007
|
+
}
|
|
3008
|
+
/** Multiplies the dab's coverage `vals` (over `rect`) by the grain. */
|
|
3009
|
+
apply(vals, rect, d) {
|
|
3010
|
+
const depth = this.settings.depth;
|
|
3011
|
+
const moving = this.settings.mode === "moving";
|
|
3012
|
+
const cs = Math.cos(d.angle);
|
|
3013
|
+
const sn = Math.sin(d.angle);
|
|
3014
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3015
|
+
const py = rect.y + yy + 0.5;
|
|
3016
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3017
|
+
if (!(vals[k] > 0)) continue;
|
|
3018
|
+
const px = rect.x + xx + 0.5;
|
|
3019
|
+
let gx = px;
|
|
3020
|
+
let gy = py;
|
|
3021
|
+
if (moving) {
|
|
3022
|
+
const dx = px - d.x;
|
|
3023
|
+
const dy = py - d.y;
|
|
3024
|
+
gx = dx * cs - dy * sn + d.travel;
|
|
3025
|
+
gy = dx * sn + dy * cs;
|
|
3026
|
+
}
|
|
3027
|
+
vals[k] *= 1 - depth * (1 - this.field.at(gx, gy));
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
};
|
|
3032
|
+
|
|
3033
|
+
// src/paint-editor/engine.ts
|
|
3034
|
+
function penOrientation(p) {
|
|
3035
|
+
if (typeof p.altitude === "number" && Number.isFinite(p.altitude)) {
|
|
3036
|
+
const altitude = Math.min(Math.PI / 2, Math.max(0, p.altitude));
|
|
3037
|
+
const azimuth = typeof p.azimuth === "number" && Number.isFinite(p.azimuth) ? p.azimuth : 0;
|
|
3038
|
+
return { altitude, azimuth };
|
|
3039
|
+
}
|
|
3040
|
+
const hasX = typeof p.tiltX === "number" && Number.isFinite(p.tiltX);
|
|
3041
|
+
const hasY = typeof p.tiltY === "number" && Number.isFinite(p.tiltY);
|
|
3042
|
+
if (!hasX && !hasY) return null;
|
|
3043
|
+
return tiltToOrientation(hasX ? p.tiltX : 0, hasY ? p.tiltY : 0);
|
|
3044
|
+
}
|
|
3045
|
+
function tiltToOrientation(tiltX, tiltY) {
|
|
3046
|
+
const clampT = (v2) => Math.min(89.999, Math.max(-89.999, v2));
|
|
3047
|
+
const tx = Math.tan(clampT(tiltX) * Math.PI / 180);
|
|
3048
|
+
const ty = Math.tan(clampT(tiltY) * Math.PI / 180);
|
|
3049
|
+
const r = Math.hypot(tx, ty);
|
|
3050
|
+
if (r < 1e-9) return { altitude: Math.PI / 2, azimuth: 0 };
|
|
3051
|
+
let azimuth = Math.atan2(ty, tx);
|
|
3052
|
+
if (azimuth < 0) azimuth += 2 * Math.PI;
|
|
3053
|
+
return { altitude: Math.atan(1 / r), azimuth };
|
|
3054
|
+
}
|
|
3055
|
+
var pressureOf = (p) => typeof p.pressure === "number" && Number.isFinite(p.pressure) ? Math.min(1, Math.max(0, p.pressure)) : 1;
|
|
3056
|
+
var PenSmoother = class {
|
|
3057
|
+
pos = null;
|
|
3058
|
+
pressure = null;
|
|
3059
|
+
a;
|
|
3060
|
+
ap;
|
|
3061
|
+
constructor(position, pressure) {
|
|
3062
|
+
this.a = 1 - Math.min(0.95, Math.max(0, position));
|
|
3063
|
+
this.ap = 1 - Math.min(0.95, Math.max(0, pressure));
|
|
3064
|
+
}
|
|
3065
|
+
push(p) {
|
|
3066
|
+
const pr = pressureOf(p);
|
|
3067
|
+
if (!this.pos) {
|
|
3068
|
+
this.pos = { x: p.x, y: p.y };
|
|
3069
|
+
this.pressure = pr;
|
|
3070
|
+
return p;
|
|
3071
|
+
}
|
|
3072
|
+
this.pos = { x: this.pos.x + (p.x - this.pos.x) * this.a, y: this.pos.y + (p.y - this.pos.y) * this.a };
|
|
3073
|
+
this.pressure = this.pressure + (pr - this.pressure) * this.ap;
|
|
3074
|
+
return { ...p, x: this.pos.x, y: this.pos.y, pressure: this.pressure };
|
|
3075
|
+
}
|
|
3076
|
+
/** Where the smoothed position is now (or `null` before the first sample). */
|
|
3077
|
+
get position() {
|
|
3078
|
+
return this.pos;
|
|
3079
|
+
}
|
|
3080
|
+
};
|
|
3081
|
+
var IDENTITY_COPY = { map: (p) => p, linear: [1, 0, 0, 1] };
|
|
3082
|
+
var FULL_SPEED = 2.5;
|
|
3083
|
+
var PenDabWalker = class {
|
|
3084
|
+
constructor(brush, seed = 0, linear = [1, 0, 0, 1]) {
|
|
3085
|
+
this.brush = brush;
|
|
3086
|
+
this.seed = seed;
|
|
3087
|
+
this.linear = linear;
|
|
3088
|
+
}
|
|
3089
|
+
brush;
|
|
3090
|
+
seed;
|
|
3091
|
+
linear;
|
|
3092
|
+
last = null;
|
|
3093
|
+
nextAt = 0;
|
|
3094
|
+
pendingFirst = null;
|
|
3095
|
+
index = 0;
|
|
3096
|
+
dir = 0;
|
|
3097
|
+
lastT = null;
|
|
3098
|
+
firstT = null;
|
|
3099
|
+
speed = null;
|
|
3100
|
+
travelled = 0;
|
|
3101
|
+
sample(p) {
|
|
3102
|
+
const o = penOrientation(p);
|
|
3103
|
+
let speed = null;
|
|
3104
|
+
let time = null;
|
|
3105
|
+
if (this.last) this.travelled += Math.hypot(p.x - this.last.x, p.y - this.last.y);
|
|
3106
|
+
if (typeof p.t === "number" && Number.isFinite(p.t)) {
|
|
3107
|
+
if (this.firstT === null) this.firstT = p.t;
|
|
3108
|
+
time = p.t - this.firstT;
|
|
3109
|
+
if (this.last && this.lastT !== null && p.t > this.lastT) {
|
|
3110
|
+
const v2 = Math.hypot(p.x - this.last.x, p.y - this.last.y) / (p.t - this.lastT);
|
|
3111
|
+
this.speed = this.speed === null ? v2 : this.speed + (v2 - this.speed) * 0.35;
|
|
3112
|
+
}
|
|
3113
|
+
this.lastT = p.t;
|
|
3114
|
+
speed = this.speed ?? 0;
|
|
3115
|
+
}
|
|
3116
|
+
return {
|
|
3117
|
+
x: p.x,
|
|
3118
|
+
y: p.y,
|
|
3119
|
+
pressure: pressureOf(p),
|
|
3120
|
+
lean: o ? 1 - o.altitude / (Math.PI / 2) : 0,
|
|
3121
|
+
azimuth: o ? o.azimuth : 0,
|
|
3122
|
+
twist: typeof p.twist === "number" && Number.isFinite(p.twist) ? p.twist : null,
|
|
3123
|
+
speed,
|
|
3124
|
+
travel: this.travelled,
|
|
3125
|
+
time
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
diameter(s) {
|
|
3129
|
+
const tip = this.brush.tip;
|
|
3130
|
+
const dyn = this.brush.dynamics;
|
|
3131
|
+
let d = tip.size;
|
|
3132
|
+
if (tip.pressureSize) d *= dyn.minSize + (1 - dyn.minSize) * s.pressure;
|
|
3133
|
+
d *= 1 + dyn.tiltSize * s.lean * 1.5;
|
|
3134
|
+
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));
|
|
3135
|
+
return d;
|
|
3136
|
+
}
|
|
3137
|
+
stepLen(s) {
|
|
3138
|
+
return Math.max(0.5, Math.max(1, this.diameter(s)) * Math.max(0.01, this.brush.tip.spacing));
|
|
3139
|
+
}
|
|
3140
|
+
/** A rasterizer angle (CCW, y up) mapped through this copy's linear part. */
|
|
3141
|
+
mapAngle(theta) {
|
|
3142
|
+
const [a, b, c, d] = this.linear;
|
|
3143
|
+
if (a === 1 && b === 0 && c === 0 && d === 1) return theta;
|
|
3144
|
+
const vx = Math.cos(theta);
|
|
3145
|
+
const vy = -Math.sin(theta);
|
|
3146
|
+
return Math.atan2(-(c * vx + d * vy), a * vx + b * vy);
|
|
3147
|
+
}
|
|
3148
|
+
dab(s) {
|
|
3149
|
+
const { tip, dynamics: dyn, rotation } = this.brush;
|
|
3150
|
+
const i = this.index++;
|
|
3151
|
+
const seed = this.seed;
|
|
3152
|
+
let diameter = this.diameter(s);
|
|
3153
|
+
if (dyn.sizeJitter) diameter *= Math.max(0.05, 1 + dyn.sizeJitter * randSigned(seed, i, 1 /* Size */));
|
|
3154
|
+
const base = tip.angle * Math.PI / 180;
|
|
3155
|
+
let angle;
|
|
3156
|
+
if (rotation === "stroke") angle = this.dir + base;
|
|
3157
|
+
else if (rotation === "barrel" && s.twist !== null) angle = this.mapAngle(base - s.twist * Math.PI / 180);
|
|
3158
|
+
else if (dyn.tiltRoundness > 0 && s.lean > 0.02) angle = this.mapAngle(base - s.azimuth);
|
|
3159
|
+
else angle = this.mapAngle(base);
|
|
3160
|
+
if (dyn.angleJitter) angle += dyn.angleJitter * Math.PI * randSigned(seed, i, 2 /* Angle */);
|
|
3161
|
+
let opacity = tip.pressureOpacity ? s.pressure : 1;
|
|
3162
|
+
opacity *= 1 - dyn.tiltOpacity * s.lean * 0.8;
|
|
3163
|
+
if (s.speed !== null && dyn.velocityOpacity) opacity *= Math.max(0, 1 + dyn.velocityOpacity * (Math.min(1, s.speed / FULL_SPEED) - 0.5));
|
|
3164
|
+
if (dyn.opacityJitter) opacity *= 1 - dyn.opacityJitter * (randSigned(seed, i, 3 /* Opacity */) * 0.5 + 0.5);
|
|
3165
|
+
const flow = tip.flow * (1 - dyn.pressureFlow + dyn.pressureFlow * s.pressure);
|
|
3166
|
+
let { x, y } = s;
|
|
3167
|
+
if (dyn.scatter) {
|
|
3168
|
+
const off = dyn.scatter * diameter * randSigned(seed, i, 4 /* Scatter */);
|
|
3169
|
+
x += Math.sin(this.dir) * off;
|
|
3170
|
+
y += Math.cos(this.dir) * off;
|
|
3171
|
+
}
|
|
3172
|
+
let flip = 0;
|
|
3173
|
+
if (dyn.flip && dyn.flip !== "none") {
|
|
3174
|
+
if (dyn.flip !== "y" && rand01(seed, i, 5 /* FlipX */) < 0.5) flip |= 1;
|
|
3175
|
+
if (dyn.flip !== "x" && rand01(seed, i, 6 /* FlipY */) < 0.5) flip |= 2;
|
|
3176
|
+
}
|
|
3177
|
+
return {
|
|
3178
|
+
...flip ? { flip } : {},
|
|
3179
|
+
x,
|
|
3180
|
+
y,
|
|
3181
|
+
radius: Math.max(0.5, diameter / 2),
|
|
3182
|
+
alpha: Math.min(1, Math.max(0, flow)),
|
|
3183
|
+
opacity: Math.min(1, Math.max(0, opacity)),
|
|
3184
|
+
angle,
|
|
3185
|
+
roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),
|
|
3186
|
+
pressure: s.pressure,
|
|
3187
|
+
direction: this.dir,
|
|
3188
|
+
index: i,
|
|
3189
|
+
travel: s.travel,
|
|
3190
|
+
time: s.time,
|
|
3191
|
+
lean: s.lean,
|
|
3192
|
+
speed: s.speed
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
push(p, out) {
|
|
3196
|
+
const a = this.last;
|
|
3197
|
+
const s = this.sample(p);
|
|
3198
|
+
if (!a) {
|
|
3199
|
+
this.last = s;
|
|
3200
|
+
this.pendingFirst = s;
|
|
3201
|
+
this.nextAt = this.stepLen(s);
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const dx = s.x - a.x;
|
|
3205
|
+
const dy = s.y - a.y;
|
|
3206
|
+
const len2 = Math.hypot(dx, dy);
|
|
3207
|
+
if (len2 > 1e-9) this.dir = Math.atan2(-dy, dx);
|
|
3208
|
+
if (this.pendingFirst) {
|
|
3209
|
+
this.lay(this.pendingFirst, out);
|
|
3210
|
+
this.pendingFirst = null;
|
|
3211
|
+
}
|
|
3212
|
+
if (len2 > 1e-9) {
|
|
3213
|
+
while (this.nextAt <= len2 + 1e-9) {
|
|
3214
|
+
const f = Math.min(1, this.nextAt / len2);
|
|
3215
|
+
const lerp4 = (u, v2) => u + (v2 - u) * f;
|
|
3216
|
+
const q = {
|
|
3217
|
+
x: a.x + dx * f,
|
|
3218
|
+
y: a.y + dy * f,
|
|
3219
|
+
pressure: lerp4(a.pressure, s.pressure),
|
|
3220
|
+
lean: lerp4(a.lean, s.lean),
|
|
3221
|
+
azimuth: lerpAngle(a.azimuth, s.azimuth, f),
|
|
3222
|
+
twist: a.twist !== null && s.twist !== null ? lerpDegrees(a.twist, s.twist, f) : s.twist,
|
|
3223
|
+
speed: a.speed !== null && s.speed !== null ? lerp4(a.speed, s.speed) : s.speed,
|
|
3224
|
+
travel: a.travel + this.nextAt,
|
|
3225
|
+
time: a.time !== null && s.time !== null ? lerp4(a.time, s.time) : s.time
|
|
3226
|
+
};
|
|
3227
|
+
this.lay(q, out);
|
|
3228
|
+
this.nextAt += this.stepLen(q);
|
|
3229
|
+
}
|
|
3230
|
+
this.nextAt -= len2;
|
|
3231
|
+
}
|
|
3232
|
+
this.last = s;
|
|
3233
|
+
}
|
|
3234
|
+
finish(out) {
|
|
3235
|
+
if (this.pendingFirst) {
|
|
3236
|
+
this.lay(this.pendingFirst, out);
|
|
3237
|
+
this.pendingFirst = null;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
/** The dabs of one spacing step: `dynamics.count` of them, each with its own index and so its own jitter. */
|
|
3241
|
+
lay(s, out) {
|
|
3242
|
+
const n = this.brush.dynamics.count ?? 1;
|
|
3243
|
+
for (let k = 0; k < n; k++) out.push(this.dab(s));
|
|
3244
|
+
}
|
|
3245
|
+
};
|
|
3246
|
+
function lerpAngle(a, b, t) {
|
|
3247
|
+
let d = b - a;
|
|
3248
|
+
while (d > Math.PI) d -= 2 * Math.PI;
|
|
3249
|
+
while (d < -Math.PI) d += 2 * Math.PI;
|
|
3250
|
+
return a + d * t;
|
|
3251
|
+
}
|
|
3252
|
+
function lerpDegrees(a, b, t) {
|
|
3253
|
+
let d = b - a;
|
|
3254
|
+
while (d > 180) d -= 360;
|
|
3255
|
+
while (d < -180) d += 360;
|
|
3256
|
+
return a + d * t;
|
|
3257
|
+
}
|
|
3258
|
+
var DryStroke = class {
|
|
3259
|
+
constructor(brush, options) {
|
|
3260
|
+
this.brush = brush;
|
|
3261
|
+
this.options = options;
|
|
3262
|
+
this.stamp = brush.stamp ? stampSampler(brush.stamp, options.stampArt, brush.tip.hardness) : null;
|
|
3263
|
+
this.tipGrain = TipGrain.create(brush.tipGrain, options.stampArt);
|
|
3264
|
+
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
3265
|
+
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
3266
|
+
this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear));
|
|
3267
|
+
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
3268
|
+
}
|
|
3269
|
+
brush;
|
|
3270
|
+
options;
|
|
3271
|
+
coverage = new CoverageMap();
|
|
3272
|
+
dabCount = 0;
|
|
3273
|
+
smoother;
|
|
3274
|
+
walkers;
|
|
3275
|
+
copies;
|
|
3276
|
+
depth;
|
|
3277
|
+
buf = new Float32Array(0);
|
|
3278
|
+
field = new Float32Array(0);
|
|
3279
|
+
lastRaw = null;
|
|
3280
|
+
lastFed = null;
|
|
3281
|
+
/** The stamp tip's shape, or `null` for the round computed tip. */
|
|
3282
|
+
stamp;
|
|
3283
|
+
/** The tip's own grain, or `null`. */
|
|
3284
|
+
tipGrain;
|
|
3285
|
+
/** Feeds raw pen samples; returns the document rectangles the new dabs touched. */
|
|
3286
|
+
push(points) {
|
|
3287
|
+
const dabs = [];
|
|
3288
|
+
for (const raw of points) {
|
|
3289
|
+
this.lastRaw = raw;
|
|
3290
|
+
const p = this.smoother.push(raw);
|
|
3291
|
+
this.lastFed = p;
|
|
3292
|
+
this.copies.forEach((c, i) => this.walkers[i].push(c.map(p), dabs));
|
|
3293
|
+
}
|
|
3294
|
+
return this.paint(dabs);
|
|
3295
|
+
}
|
|
3296
|
+
/** Ends the stroke: the smoothed path catches up with the last raw sample; a single tap paints one dab. */
|
|
3297
|
+
finish() {
|
|
3298
|
+
const dabs = [];
|
|
3299
|
+
const raw = this.lastRaw;
|
|
3300
|
+
const fed = this.lastFed;
|
|
3301
|
+
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));
|
|
3302
|
+
for (const w of this.walkers) w.finish(dabs);
|
|
3303
|
+
return this.paint(dabs);
|
|
3304
|
+
}
|
|
3305
|
+
paint(dabs) {
|
|
3306
|
+
const rects = [];
|
|
3307
|
+
const spray = this.brush.spray;
|
|
3308
|
+
const seed = this.options.seed ?? 0;
|
|
3309
|
+
for (const d of dabs) {
|
|
3310
|
+
if (!spray) {
|
|
3311
|
+
this.mark(d, d, rects);
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
for (const drop of sprayDroplets(d, spray, seed)) this.mark(drop, d, rects);
|
|
3315
|
+
}
|
|
3316
|
+
return rects;
|
|
3317
|
+
}
|
|
3318
|
+
/** Lays one mark (a dab, or a spray droplet of dab `d`): the round tip or the stamp, the tip grain, the paper grain, effect paint. */
|
|
3319
|
+
mark(m, d, rects) {
|
|
3320
|
+
const hardness = this.brush.tip.hardness;
|
|
3321
|
+
const grain = this.options.grain;
|
|
3322
|
+
const rect = this.stamp ? stampDabRect(m) : dabRect(m, hardness);
|
|
3323
|
+
const n = rect.width * rect.height;
|
|
3324
|
+
if (this.buf.length < n) this.buf = new Float32Array(n);
|
|
3325
|
+
const vals = this.buf.subarray(0, n);
|
|
3326
|
+
if (this.stamp) rasterizeStampDab(m, this.stamp, d.index, rect, vals);
|
|
3327
|
+
else rasterizeDab(m, hardness, rect, vals);
|
|
3328
|
+
this.tipGrain?.apply(vals, rect, { x: m.x, y: m.y, angle: m.angle, travel: d.travel });
|
|
3329
|
+
if (this.depth > 0 && grain) {
|
|
3330
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3331
|
+
const y = rect.y + yy + 0.5;
|
|
3332
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3333
|
+
if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
let field = null;
|
|
3338
|
+
if (this.options.effect) {
|
|
3339
|
+
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
3340
|
+
field = this.field.subarray(0, n * 3);
|
|
3341
|
+
sampleEffectField(this.options.effect, rect, vals, field, d);
|
|
3342
|
+
}
|
|
3343
|
+
this.coverage.accumulate(rect, vals, m.opacity, null, field);
|
|
3344
|
+
rects.push(rect);
|
|
3345
|
+
this.dabCount++;
|
|
3346
|
+
}
|
|
3347
|
+
};
|
|
3348
|
+
function mixerTip(brush) {
|
|
3349
|
+
return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };
|
|
3350
|
+
}
|
|
3351
|
+
function toStrokePoint(p) {
|
|
3352
|
+
return { x: p.x, y: p.y, pressure: pressureOf(p), ...typeof p.t === "number" ? { t: p.t } : {} };
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
// src/paint-editor/dab-stroke.ts
|
|
3356
|
+
var GRADIENT_STEPS = 32;
|
|
3357
|
+
function multiColorPalette(mc, color2, secondary) {
|
|
3358
|
+
if (mc.colors.length >= 2) {
|
|
3359
|
+
return mc.colors.map((h) => {
|
|
3360
|
+
const n = parseInt(h.slice(1), 16);
|
|
3361
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
3364
|
+
return [[...color2], [...secondary]];
|
|
3365
|
+
}
|
|
3366
|
+
function soften(f, softness) {
|
|
3367
|
+
if (softness >= 1) return f;
|
|
3368
|
+
const w = Math.max(1e-3, softness);
|
|
3369
|
+
return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));
|
|
3370
|
+
}
|
|
3371
|
+
function mixRgb(a, b, t, mixing) {
|
|
3372
|
+
if (t <= 0) return [...a];
|
|
3373
|
+
if (t >= 1) return [...b];
|
|
3374
|
+
if (mixing === "pigment") {
|
|
3375
|
+
const o = [0, 0, 0];
|
|
3376
|
+
const q = (v2) => Math.round(Math.min(1, Math.max(0, v2)) * 255);
|
|
3377
|
+
mixPigment8(q(a[0]), q(a[1]), q(a[2]), q(b[0]), q(b[1]), q(b[2]), t, o);
|
|
3378
|
+
return [o[0], o[1], o[2]];
|
|
3379
|
+
}
|
|
3380
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
3381
|
+
}
|
|
3382
|
+
function multiColorGradient(palette, softness, mixing) {
|
|
3383
|
+
const n = palette.length;
|
|
3384
|
+
const out = [];
|
|
3385
|
+
for (let j = 0; j < GRADIENT_STEPS; j++) {
|
|
3386
|
+
const f = j / (GRADIENT_STEPS - 1) * (n - 1);
|
|
3387
|
+
const i = Math.min(n - 2, Math.floor(f));
|
|
3388
|
+
out.push(mixRgb(palette[i], palette[i + 1], soften(f - i, softness), mixing));
|
|
3389
|
+
}
|
|
3390
|
+
return out;
|
|
3391
|
+
}
|
|
3392
|
+
function multiColorForDab(mc, palette, index, seed, mixing) {
|
|
3393
|
+
const n = palette.length;
|
|
3394
|
+
if (mc.order === "random") {
|
|
3395
|
+
const run = Math.floor(index / mc.length);
|
|
3396
|
+
return [...palette[Math.min(n - 1, Math.floor(rand01(seed, run, 11) * n))]];
|
|
3397
|
+
}
|
|
3398
|
+
const f = index / mc.length % n;
|
|
3399
|
+
const i = Math.floor(f);
|
|
3400
|
+
return mixRgb(palette[i], palette[(i + 1) % n], soften(f - i, mc.softness), mixing);
|
|
3401
|
+
}
|
|
3402
|
+
var PaintDabStroke = class {
|
|
3403
|
+
constructor(brush, options) {
|
|
3404
|
+
this.brush = brush;
|
|
3405
|
+
this.options = options;
|
|
3406
|
+
this.stamp = brush.engine === "dab" && brush.stamp ? stampSampler(brush.stamp, options.stampArt, brush.tip.hardness) : null;
|
|
3407
|
+
this.tipGrain = TipGrain.create(brush.tipGrain, options.stampArt);
|
|
3408
|
+
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
3409
|
+
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
3410
|
+
const exchanging = brush.engine === "mixer" || brush.engine === "dab";
|
|
3411
|
+
const rates = brush.engine === "mixer" ? mixerRates(brush.mixer) : exchangeRates(brush.behavior.pickup, brush.behavior.mix, 1, brush.behavior.mixing);
|
|
3412
|
+
this.mixing = brush.engine === "mixer" ? brush.mixer.mixing : brush.behavior.mixing;
|
|
3413
|
+
this.states = this.copies.map((c, i) => ({
|
|
3414
|
+
walker: new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear),
|
|
3415
|
+
exchange: exchanging ? new PaintExchange(rates, options.load) : null,
|
|
3416
|
+
smudge: brush.engine === "smudge" ? new SmudgeTip(brush.smudge, brush.tip.size * (1 + brush.dynamics.tiltSize * 1.5)) : null
|
|
3417
|
+
}));
|
|
3418
|
+
this.medium = new TileMedium(options.target, brush.engine === "dab" ? { blend: brush.tip.blend } : {});
|
|
3419
|
+
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
3420
|
+
this.coverage = brush.engine === "dab" ? new CoverageMap() : null;
|
|
3421
|
+
this.sampler = exchanging && options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
|
|
3422
|
+
const table = this.sampler?.acrossTip ? this.sampler.tipTable(GRADIENT_STEPS) : null;
|
|
3423
|
+
this.gradient = table ? table.map((e) => e.rgb) : null;
|
|
3424
|
+
this.gradientAlpha = table && table.some((e) => e.alpha < 1) ? table.map((e) => e.alpha) : null;
|
|
3425
|
+
this.scratch = brush.engine === "blender" ? createBlenderScratch() : null;
|
|
3426
|
+
}
|
|
3427
|
+
brush;
|
|
3428
|
+
options;
|
|
3429
|
+
dabCount = 0;
|
|
3430
|
+
smoother;
|
|
3431
|
+
copies;
|
|
3432
|
+
states;
|
|
3433
|
+
medium;
|
|
3434
|
+
depth;
|
|
3435
|
+
coverage;
|
|
3436
|
+
sampler;
|
|
3437
|
+
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
3438
|
+
gradient;
|
|
3439
|
+
/** Opacity across the tip (`across-tip` gradients with opacity stops), or `null`. */
|
|
3440
|
+
gradientAlpha;
|
|
3441
|
+
alphaCov = new Float32Array(0);
|
|
3442
|
+
fieldLut = [];
|
|
3443
|
+
scratch;
|
|
3444
|
+
mixing;
|
|
3445
|
+
tip = new Float32Array(0);
|
|
3446
|
+
cov = new Float32Array(0);
|
|
3447
|
+
field = new Float32Array(0);
|
|
3448
|
+
paint = [0, 0, 0];
|
|
3449
|
+
lastRaw = null;
|
|
3450
|
+
lastFed = null;
|
|
3451
|
+
/** The stamp tip's shape (dab brushes), or `null` for the round computed tip. */
|
|
3452
|
+
stamp;
|
|
3453
|
+
/** The tip's own grain, or `null`. */
|
|
3454
|
+
tipGrain;
|
|
3455
|
+
/** What copy 0's brush carries after the dabs so far (mixer and wet-capable dab brushes). */
|
|
3456
|
+
get load() {
|
|
3457
|
+
return this.states[0].exchange?.load ?? null;
|
|
3458
|
+
}
|
|
3459
|
+
push(points) {
|
|
3460
|
+
const rects = [];
|
|
3461
|
+
for (const raw of points) {
|
|
3462
|
+
this.lastRaw = raw;
|
|
3463
|
+
const p = this.smoother.push(raw);
|
|
3464
|
+
this.lastFed = p;
|
|
3465
|
+
this.copies.forEach((c, i) => {
|
|
3466
|
+
const dabs = [];
|
|
3467
|
+
this.states[i].walker.push(c.map(p), dabs);
|
|
3468
|
+
this.paintDabs(i, dabs, rects);
|
|
3469
|
+
});
|
|
3470
|
+
}
|
|
3471
|
+
return rects;
|
|
3472
|
+
}
|
|
3473
|
+
/** Ends the stroke: the smoothed path catches up with the last raw sample; a tap paints one dab. */
|
|
3474
|
+
finish() {
|
|
3475
|
+
const rects = [];
|
|
3476
|
+
const raw = this.lastRaw;
|
|
3477
|
+
const fed = this.lastFed;
|
|
3478
|
+
this.copies.forEach((c, i) => {
|
|
3479
|
+
const dabs = [];
|
|
3480
|
+
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
3481
|
+
this.states[i].walker.finish(dabs);
|
|
3482
|
+
this.paintDabs(i, dabs, rects);
|
|
3483
|
+
});
|
|
3484
|
+
return rects;
|
|
3485
|
+
}
|
|
3486
|
+
paintDabs(copy, dabs, rects) {
|
|
3487
|
+
const spray = this.brush.engine === "dab" ? this.brush.spray : null;
|
|
3488
|
+
for (const d of spray ? dabs.flatMap((dab) => sprayDroplets(dab, spray, (this.options.seed ?? 0) + copy * 7919)) : dabs) {
|
|
3489
|
+
const rect = this.stamp ? stampDabRect(d) : dabRect(d, this.brush.tip.hardness);
|
|
3490
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
3491
|
+
this.dab(copy, d, rect);
|
|
3492
|
+
rects.push(rect);
|
|
3493
|
+
this.dabCount++;
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
/** The tip's pure coverage (`tip`) and, with grain, what the paper catches of it (`cov`). */
|
|
3497
|
+
rasterize(d, rect) {
|
|
3498
|
+
const n = rect.width * rect.height;
|
|
3499
|
+
if (this.tip.length < n) {
|
|
3500
|
+
this.tip = new Float32Array(n);
|
|
3501
|
+
this.cov = new Float32Array(n);
|
|
3502
|
+
}
|
|
3503
|
+
const tip = this.tip.subarray(0, n);
|
|
3504
|
+
tip.fill(0);
|
|
3505
|
+
if (this.stamp) rasterizeStampDab({ ...d, alpha: 1 }, this.stamp, d.index, rect, tip);
|
|
3506
|
+
else rasterizeDab({ ...d, alpha: 1 }, this.brush.tip.hardness, rect, tip);
|
|
3507
|
+
this.tipGrain?.apply(tip, rect, d);
|
|
3508
|
+
if (!(this.depth > 0 && this.options.grain)) return { tip, cov: tip };
|
|
3509
|
+
const cov = this.cov.subarray(0, n);
|
|
3510
|
+
const grain = this.options.grain;
|
|
3511
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3512
|
+
const y = rect.y + yy + 0.5;
|
|
3513
|
+
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;
|
|
3514
|
+
}
|
|
3515
|
+
return { tip, cov };
|
|
3516
|
+
}
|
|
3517
|
+
dab(copy, d, rect) {
|
|
3518
|
+
const brush = this.brush;
|
|
3519
|
+
const { tip, cov } = this.rasterize(d, rect);
|
|
3520
|
+
const st = this.states[copy];
|
|
3521
|
+
if (brush.engine === "blender") {
|
|
3522
|
+
blenderDab(this.medium, rect, cov, d.opacity * d.alpha * brush.tip.opacity, d.radius, brush.blender, this.scratch);
|
|
3523
|
+
return;
|
|
3524
|
+
}
|
|
3525
|
+
if (brush.engine === "smudge") {
|
|
3526
|
+
st.smudge.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: d.opacity * d.alpha * brush.tip.opacity }, rect, cov);
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
const ex = st.exchange;
|
|
3530
|
+
ex.pickUp(this.medium.sample(rect, tip));
|
|
3531
|
+
let flow = 1;
|
|
3532
|
+
if (this.options.effect) {
|
|
3533
|
+
const rgb = [0, 0, 0];
|
|
3534
|
+
flow = sampleEffectAt(this.options.effect, d.x, d.y, rgb, d);
|
|
3535
|
+
ex.setReservoirColor(rgb[0], rgb[1], rgb[2]);
|
|
3536
|
+
} else if (this.sampler && !this.gradient) {
|
|
3537
|
+
const g = this.sampler.dab(d, this.sample);
|
|
3538
|
+
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
3539
|
+
flow = g.alpha;
|
|
3540
|
+
}
|
|
3541
|
+
let field = null;
|
|
3542
|
+
let total;
|
|
3543
|
+
let laid = cov;
|
|
3544
|
+
if (this.gradient) {
|
|
3545
|
+
total = this.tipField(ex, d, rect);
|
|
3546
|
+
field = this.field;
|
|
3547
|
+
if (this.gradientAlpha) laid = this.tipAlpha(d, rect, cov);
|
|
3548
|
+
} else total = ex.paint(this.paint);
|
|
3549
|
+
if (total > 0 && flow > 0) {
|
|
3550
|
+
if (this.coverage) this.depositWithCeiling(d, rect, laid, field, ex, flow);
|
|
3551
|
+
else {
|
|
3552
|
+
const dep = Math.min(1, total) * d.alpha * brush.tip.opacity * d.opacity * flow;
|
|
3553
|
+
if (dep > 0) this.medium.deposit(rect, laid, dep, this.paint, ex.paintPigment, field);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
ex.drain();
|
|
3557
|
+
}
|
|
3558
|
+
/** Fills `this.field` with the tip gradient (mixed with the carried paint) per pixel; returns the paint weight. */
|
|
3559
|
+
tipField(ex, d, rect) {
|
|
3560
|
+
const g = this.gradient;
|
|
3561
|
+
const lut = this.fieldLut;
|
|
3562
|
+
let total = 0;
|
|
3563
|
+
for (let j = 0; j < g.length; j++) {
|
|
3564
|
+
ex.setReservoirColor(g[j][0], g[j][1], g[j][2]);
|
|
3565
|
+
const out = lut[j] ?? [0, 0, 0];
|
|
3566
|
+
total = ex.paint(out);
|
|
3567
|
+
lut[j] = out;
|
|
3568
|
+
}
|
|
3569
|
+
const n = rect.width * rect.height;
|
|
3570
|
+
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
3571
|
+
const f = this.field;
|
|
3572
|
+
const nx = Math.sin(d.direction);
|
|
3573
|
+
const ny = Math.cos(d.direction);
|
|
3574
|
+
const r = Math.max(0.5, d.radius);
|
|
3575
|
+
const last = g.length - 1;
|
|
3576
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3577
|
+
const py = rect.y + yy + 0.5 - d.y;
|
|
3578
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3579
|
+
const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;
|
|
3580
|
+
const j = Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)));
|
|
3581
|
+
f[k * 3] = lut[j][0];
|
|
3582
|
+
f[k * 3 + 1] = lut[j][1];
|
|
3583
|
+
f[k * 3 + 2] = lut[j][2];
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
return total;
|
|
3587
|
+
}
|
|
3588
|
+
/** `cov` scaled by the gradient's opacity across the tip. */
|
|
3589
|
+
tipAlpha(d, rect, cov) {
|
|
3590
|
+
const a = this.gradientAlpha;
|
|
3591
|
+
const n = rect.width * rect.height;
|
|
3592
|
+
if (this.alphaCov.length < n) this.alphaCov = new Float32Array(n);
|
|
3593
|
+
const out = this.alphaCov.subarray(0, n);
|
|
3594
|
+
const nx = Math.sin(d.direction);
|
|
3595
|
+
const ny = Math.cos(d.direction);
|
|
3596
|
+
const r = Math.max(0.5, d.radius);
|
|
3597
|
+
const last = a.length - 1;
|
|
3598
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3599
|
+
const py = rect.y + yy + 0.5 - d.y;
|
|
3600
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3601
|
+
const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;
|
|
3602
|
+
out[k] = cov[k] * a[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return out;
|
|
3606
|
+
}
|
|
3607
|
+
/** A dab brush's deposit: only what this dab adds to the stroke's coverage, under its opacity ceiling. */
|
|
3608
|
+
depositWithCeiling(d, rect, cov, field, ex, flow = 1) {
|
|
3609
|
+
const map = this.coverage;
|
|
3610
|
+
const n = rect.width * rect.height;
|
|
3611
|
+
const before = new Float32Array(n);
|
|
3612
|
+
const vals = new Float32Array(n);
|
|
3613
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3614
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3615
|
+
if (cov[k] <= 0) continue;
|
|
3616
|
+
before[k] = map.get(rect.x + xx, rect.y + yy);
|
|
3617
|
+
vals[k] = cov[k] * d.alpha * flow;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
map.accumulate(rect, vals, d.opacity);
|
|
3621
|
+
const o = this.brush.tip.opacity;
|
|
3622
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3623
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3624
|
+
if (cov[k] <= 0) {
|
|
3625
|
+
vals[k] = 0;
|
|
3626
|
+
continue;
|
|
3627
|
+
}
|
|
3628
|
+
const c0 = before[k] * o;
|
|
3629
|
+
const c1 = map.get(rect.x + xx, rect.y + yy) * o;
|
|
3630
|
+
vals[k] = c1 > c0 && c0 < 1 ? (c1 - c0) / (1 - c0) : 0;
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
this.medium.deposit(rect, vals, 1, this.paint, ex.paintPigment, field);
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3636
|
+
|
|
3637
|
+
// src/paint-editor/wet.ts
|
|
3638
|
+
var STROKE_WATER = 0.9;
|
|
3639
|
+
var STROKE_MASS = PIGMENT_UNIT * 2.5;
|
|
3640
|
+
var STROKE_LIGHT = 1.5;
|
|
3641
|
+
var COVERAGE_STEPS = 4096;
|
|
3642
|
+
function union(a, b) {
|
|
3643
|
+
if (!a) return { ...b };
|
|
3644
|
+
const x = Math.min(a.x, b.x);
|
|
3645
|
+
const y = Math.min(a.y, b.y);
|
|
3646
|
+
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 };
|
|
3647
|
+
}
|
|
3648
|
+
function wetPaperInput(paper) {
|
|
3649
|
+
const grain = new PaperGrain(paper, "wet");
|
|
3650
|
+
const flat = grain.flat;
|
|
3651
|
+
return {
|
|
3652
|
+
absorption: paper.absorption,
|
|
3653
|
+
permeability: paper.permeability,
|
|
3654
|
+
roughness: flat ? 0 : paper.roughness,
|
|
3655
|
+
color: hexToRgb8(paper.color),
|
|
3656
|
+
heightAt: flat ? () => 0.5 : (x, y) => grain.at(x, y)
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
var WetStrokeDriver = class {
|
|
3660
|
+
constructor(init) {
|
|
3661
|
+
this.init = init;
|
|
3662
|
+
const brush = init.brush;
|
|
3663
|
+
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
3664
|
+
const copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
3665
|
+
const wet = brush.wet;
|
|
3666
|
+
const spacing = Math.max(0.02, brush.tip.spacing);
|
|
3667
|
+
const perDab = Math.min(1, spacing * 1.2);
|
|
3668
|
+
const thin = wet.thinner;
|
|
3669
|
+
this.medium = new WetMedium(init.world, init.laneId, {
|
|
3670
|
+
water: STROKE_WATER * Math.min(1, wet.water * (1 + thin)) * perDab,
|
|
3671
|
+
mass: STROKE_MASS * wet.pigment * (1 - 0.6 * thin) * perDab,
|
|
3672
|
+
body: wet.body * (1 - thin),
|
|
3673
|
+
light: 0
|
|
3674
|
+
});
|
|
3675
|
+
this.lightBase = wet.tool === "paint" ? STROKE_LIGHT * brush.light.amount * (1 - 0.6 * thin) * perDab : 0;
|
|
3676
|
+
const rates = exchangeRates(brush.behavior.pickup, brush.behavior.mix, wet.load, brush.behavior.mixing);
|
|
3677
|
+
this.states = copies.map((c, i) => ({
|
|
3678
|
+
walker: new PenDabWalker(brush, init.seed + i * 7919 | 0, c.linear),
|
|
3679
|
+
exchange: wet.tool === "paint" ? new PaintExchange(rates, init.load) : null,
|
|
3680
|
+
last: null
|
|
3681
|
+
}));
|
|
3682
|
+
this.copies = copies;
|
|
3683
|
+
this.depth = init.grain && !init.grain.flat ? Math.min(1, Math.max(0, init.paperDepth * brush.grain)) : 0;
|
|
3684
|
+
const g = init.paint ?? null;
|
|
3685
|
+
this.sampler = g ? new PaintGradientSampler(g.map.by === "across-tip" ? { ...g, map: { by: "distance", length: 300, repeat: "mirror" } } : g, init.seed) : null;
|
|
3686
|
+
}
|
|
3687
|
+
init;
|
|
3688
|
+
dabCount = 0;
|
|
3689
|
+
bounds = null;
|
|
3690
|
+
/** The world's clock at which each raw point was applied (recorded with the stroke). */
|
|
3691
|
+
steps = [];
|
|
3692
|
+
smoother;
|
|
3693
|
+
states;
|
|
3694
|
+
medium;
|
|
3695
|
+
queue = [];
|
|
3696
|
+
finishing = false;
|
|
3697
|
+
finished = false;
|
|
3698
|
+
lastRaw = null;
|
|
3699
|
+
lastFed = null;
|
|
3700
|
+
tip = new Float32Array(0);
|
|
3701
|
+
cov = new Float32Array(0);
|
|
3702
|
+
paint = [0, 0, 0];
|
|
3703
|
+
centre = [0, 0, 0];
|
|
3704
|
+
depth;
|
|
3705
|
+
lightBase;
|
|
3706
|
+
sampler;
|
|
3707
|
+
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
3708
|
+
copies;
|
|
3709
|
+
/** What copy 0's brush carries after the dabs so far. */
|
|
3710
|
+
get load() {
|
|
3711
|
+
return this.states[0].exchange?.load ?? null;
|
|
3712
|
+
}
|
|
3713
|
+
/** Whether every point (and the end) has been applied. */
|
|
3714
|
+
get done() {
|
|
3715
|
+
return this.finished;
|
|
3716
|
+
}
|
|
3717
|
+
/** Adds raw points; applies them now unless a batch is running. Returns where pixels changed. */
|
|
3718
|
+
push(points) {
|
|
3719
|
+
for (const p of points) this.queue.push(p);
|
|
3720
|
+
return this.flush();
|
|
3721
|
+
}
|
|
3722
|
+
/** Asks for the end; it happens now unless a batch is running (then at the next `flush`). */
|
|
3723
|
+
finish() {
|
|
3724
|
+
this.finishing = true;
|
|
3725
|
+
return this.flush();
|
|
3726
|
+
}
|
|
3727
|
+
/** Applies waiting points (and the end, when asked) if the world is between batches. */
|
|
3728
|
+
flush() {
|
|
3729
|
+
const rects = [];
|
|
3730
|
+
if (this.init.world.busy || this.finished) return rects;
|
|
3731
|
+
while (this.queue.length) {
|
|
3732
|
+
const raw = this.queue.shift();
|
|
3733
|
+
this.steps.push(this.init.world.clock);
|
|
3734
|
+
this.lastRaw = raw;
|
|
3735
|
+
const p = this.smoother.push(raw);
|
|
3736
|
+
this.lastFed = p;
|
|
3737
|
+
this.copies.forEach((c, i) => {
|
|
3738
|
+
const dabs = [];
|
|
3739
|
+
this.states[i].walker.push(c.map(p), dabs);
|
|
3740
|
+
this.applyDabs(i, dabs, rects);
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
if (this.finishing) {
|
|
3744
|
+
const raw = this.lastRaw;
|
|
3745
|
+
const fed = this.lastFed;
|
|
3746
|
+
this.copies.forEach((c, i) => {
|
|
3747
|
+
const dabs = [];
|
|
3748
|
+
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
3749
|
+
this.states[i].walker.finish(dabs);
|
|
3750
|
+
this.applyDabs(i, dabs, rects);
|
|
3751
|
+
});
|
|
3752
|
+
this.finished = true;
|
|
3753
|
+
}
|
|
3754
|
+
for (const r of rects) this.bounds = union(this.bounds, r);
|
|
3755
|
+
return rects;
|
|
3756
|
+
}
|
|
3757
|
+
applyDabs(copy, dabs, rects) {
|
|
3758
|
+
for (const d of dabs) {
|
|
3759
|
+
const rect = dabRect(d, this.init.brush.tip.hardness);
|
|
3760
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
3761
|
+
this.dab(copy, d, rect);
|
|
3762
|
+
rects.push(rect);
|
|
3763
|
+
this.dabCount++;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
/** The tip's coverage over `rect`, with the grain, quantised. */
|
|
3767
|
+
coverage(d, rect) {
|
|
3768
|
+
const n = rect.width * rect.height;
|
|
3769
|
+
if (this.tip.length < n) {
|
|
3770
|
+
this.tip = new Float32Array(n);
|
|
3771
|
+
this.cov = new Float32Array(n);
|
|
3772
|
+
}
|
|
3773
|
+
const tip = this.tip.subarray(0, n);
|
|
3774
|
+
const cov = this.cov.subarray(0, n);
|
|
3775
|
+
tip.fill(0);
|
|
3776
|
+
rasterizeDab({ ...d, alpha: 1 }, this.init.brush.tip.hardness, rect, tip);
|
|
3777
|
+
const grain = this.depth > 0 ? this.init.grain : null;
|
|
3778
|
+
const mask = this.init.mask;
|
|
3779
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
3780
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
3781
|
+
if (mask && tip[k] > 0) {
|
|
3782
|
+
const mx = rect.x + xx;
|
|
3783
|
+
const my = rect.y + yy;
|
|
3784
|
+
tip[k] *= mx >= 0 && my >= 0 && mx < mask.width && my < mask.height ? mask.data[my * mask.width + mx] / 255 : 0;
|
|
3785
|
+
}
|
|
3786
|
+
let v2 = tip[k];
|
|
3787
|
+
if (v2 > 0 && grain) v2 *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, d.pressure);
|
|
3788
|
+
tip[k] = Math.round(tip[k] * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
3789
|
+
cov[k] = Math.round(v2 * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
return { tip, cov };
|
|
3793
|
+
}
|
|
3794
|
+
dab(copy, d, rect) {
|
|
3795
|
+
const { world, laneId, brush } = this.init;
|
|
3796
|
+
const wet = brush.wet;
|
|
3797
|
+
const st = this.states[copy];
|
|
3798
|
+
const { tip, cov } = this.coverage(d, rect);
|
|
3799
|
+
const k = Math.round(Math.min(1, d.alpha * d.opacity * brush.tip.opacity) * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
3800
|
+
if (!(k > 0)) return;
|
|
3801
|
+
switch (wet.tool) {
|
|
3802
|
+
case "paint": {
|
|
3803
|
+
const ex = st.exchange;
|
|
3804
|
+
this.medium.options.light = this.lightBase * (brush.light.pressure ? d.pressure : 1);
|
|
3805
|
+
let flow = 1;
|
|
3806
|
+
const effect = this.init.effect;
|
|
3807
|
+
if (effect) {
|
|
3808
|
+
sampleEffectAt(effect, d.x, d.y, this.centre, d);
|
|
3809
|
+
ex.setReservoirColor(this.centre[0], this.centre[1], this.centre[2]);
|
|
3810
|
+
} else if (this.sampler) {
|
|
3811
|
+
const g = this.sampler.dab(d, this.sample);
|
|
3812
|
+
ex.setReservoirColor(g.rgb[0], g.rgb[1], g.rgb[2]);
|
|
3813
|
+
flow = g.alpha;
|
|
3814
|
+
}
|
|
3815
|
+
ex.pickUp(this.medium.sample(rect, tip));
|
|
3816
|
+
const total = ex.paint(this.paint);
|
|
3817
|
+
if (total > 0 && flow > 0 && effect) {
|
|
3818
|
+
const ec = cov.slice();
|
|
3819
|
+
const field = new Float32Array(ec.length * 3);
|
|
3820
|
+
effectDabField(effect, rect, ec, this.centre, this.paint, field, COVERAGE_STEPS, d);
|
|
3821
|
+
this.medium.deposit(rect, ec, Math.min(1, total) * k, this.paint, null, field);
|
|
3822
|
+
} else if (total > 0 && flow > 0) this.medium.deposit(rect, cov, Math.min(1, total) * k * flow, this.paint, null);
|
|
3823
|
+
else world.paint(laneId, rect, cov, this.medium.options.water * k, null);
|
|
3824
|
+
ex.drain();
|
|
3825
|
+
if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);
|
|
3826
|
+
break;
|
|
3827
|
+
}
|
|
3828
|
+
case "water": {
|
|
3829
|
+
world.paint(laneId, rect, cov, this.medium.options.water * k * (wet.water > 0 ? 1 : 0), null);
|
|
3830
|
+
if (wet.rewet > 0) world.rewet(laneId, rect, cov, wet.rewet * k * 0.15, this.init.paperColor);
|
|
3831
|
+
break;
|
|
3832
|
+
}
|
|
3833
|
+
case "dry":
|
|
3834
|
+
world.remove(laneId, rect, cov, wet.strength * 0.35 * k, 0, 0, wet.strength * 0.35 * k);
|
|
3835
|
+
break;
|
|
3836
|
+
case "blow": {
|
|
3837
|
+
const s = wet.strength * 0.08 * k;
|
|
3838
|
+
world.push(laneId, rect, cov, Math.cos(d.direction) * s, -Math.sin(d.direction) * s);
|
|
3839
|
+
break;
|
|
3840
|
+
}
|
|
3841
|
+
case "blend":
|
|
3842
|
+
world.blend(laneId, rect, cov, wet.strength * k);
|
|
3843
|
+
break;
|
|
3844
|
+
case "smudge": {
|
|
3845
|
+
if (st.last) world.drag(laneId, rect, cov, d.x - st.last.x, d.y - st.last.y, wet.strength * k);
|
|
3846
|
+
break;
|
|
3847
|
+
}
|
|
3848
|
+
case "blot":
|
|
3849
|
+
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);
|
|
3850
|
+
break;
|
|
3851
|
+
}
|
|
3852
|
+
st.last = d;
|
|
3853
|
+
}
|
|
3854
|
+
};
|
|
3855
|
+
var WET_MEDIUM_PARAMS = {
|
|
3856
|
+
watercolor: {},
|
|
3857
|
+
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 },
|
|
3858
|
+
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 }
|
|
3859
|
+
};
|
|
3860
|
+
var WET_GPU_MODES = ["auto", "off"];
|
|
3861
|
+
var GPU_LEAD = 16;
|
|
3862
|
+
var DISPLAY_TILES_PER_TICK = 24;
|
|
3863
|
+
var PaintWetLayers = class {
|
|
3864
|
+
constructor(host, options = {}) {
|
|
3865
|
+
this.host = host;
|
|
3866
|
+
this.options = options;
|
|
3867
|
+
this.speed = Math.max(1, Math.min(2e3, options.speed ?? 120));
|
|
3868
|
+
this.undoMemory = Math.max(0, options.undoMemory ?? 512 * 1024 * 1024);
|
|
3869
|
+
this.gpuMode = options.gpu === "off" ? "off" : "auto";
|
|
3870
|
+
this.gpuMinTiles = Math.max(1, Math.floor(options.gpuMinTiles ?? 16));
|
|
3871
|
+
}
|
|
3872
|
+
host;
|
|
3873
|
+
options;
|
|
3874
|
+
world = null;
|
|
3875
|
+
tilt = { ...FLAT };
|
|
3876
|
+
settings = /* @__PURE__ */ new Map();
|
|
3877
|
+
paused = false;
|
|
3878
|
+
/** Wet strokes dry as soon as they end (one undo step with the stroke). */
|
|
3879
|
+
instantDry = false;
|
|
3880
|
+
speed;
|
|
3881
|
+
overlay = false;
|
|
3882
|
+
undoMemory;
|
|
3883
|
+
/** The live wet stroke, if any. */
|
|
3884
|
+
driver = null;
|
|
3885
|
+
lastTick = null;
|
|
3886
|
+
ticking = null;
|
|
3887
|
+
stepsOwed = 0;
|
|
3888
|
+
displaying = null;
|
|
3889
|
+
leadAt = 0;
|
|
3890
|
+
gpuMode;
|
|
3891
|
+
gpuMinTiles;
|
|
3892
|
+
gpu = null;
|
|
3893
|
+
gpuSupport = null;
|
|
3894
|
+
gpuStarting = null;
|
|
3895
|
+
// --- Engine B ------------------------------------------------------------------
|
|
3896
|
+
/** Starts the GPU engine for the world (once; it may turn out unavailable). */
|
|
3897
|
+
startGpu() {
|
|
3898
|
+
if (this.gpuMode === "off" || this.gpu || this.gpuStarting || this.gpuSupport) return;
|
|
3899
|
+
const provider = this.options.gpuProvider === void 0 ? findWebGpu() : this.options.gpuProvider;
|
|
3900
|
+
this.gpuStarting = createWetGpuEngine(provider).then(({ engine, support }) => {
|
|
3901
|
+
this.gpuSupport = support;
|
|
3902
|
+
if (!engine) return;
|
|
3903
|
+
if (this.gpuMode === "off" || !this.world) {
|
|
3904
|
+
engine.destroy();
|
|
3905
|
+
this.gpuSupport = null;
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
this.gpu = engine;
|
|
3909
|
+
this.world.gpuMinTiles = this.gpuMinTiles;
|
|
3910
|
+
this.world.attachGpu(engine);
|
|
3911
|
+
}).finally(() => {
|
|
3912
|
+
this.gpuStarting = null;
|
|
3913
|
+
this.host.changed();
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
/** Resolves once the GPU engine has started or turned out unavailable. */
|
|
3917
|
+
async gpuReady() {
|
|
3918
|
+
if (this.gpuStarting) await this.gpuStarting;
|
|
3919
|
+
return this.engineInfo();
|
|
3920
|
+
}
|
|
3921
|
+
/** `auto`: use the GPU for large wet areas when there is one; `off`: the CPU only. */
|
|
3922
|
+
setGpu(mode, minTiles) {
|
|
3923
|
+
if (minTiles !== void 0) {
|
|
3924
|
+
this.gpuMinTiles = Math.max(1, Math.floor(minTiles));
|
|
3925
|
+
if (this.world) this.world.gpuMinTiles = this.gpuMinTiles;
|
|
3926
|
+
}
|
|
3927
|
+
if (mode === this.gpuMode) return this.engineInfo();
|
|
3928
|
+
this.gpuMode = mode;
|
|
3929
|
+
if (mode === "off") {
|
|
3930
|
+
this.world?.attachGpu(null);
|
|
3931
|
+
this.gpu?.destroy();
|
|
3932
|
+
this.gpu = null;
|
|
3933
|
+
this.gpuSupport = null;
|
|
3934
|
+
} else if (this.world) this.startGpu();
|
|
3935
|
+
return this.engineInfo();
|
|
3936
|
+
}
|
|
3937
|
+
engineInfo() {
|
|
3938
|
+
const w = this.world;
|
|
3939
|
+
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() };
|
|
3940
|
+
if (this.gpuMode === "off") return { ...base, state: "off", reason: "Forced to the CPU." };
|
|
3941
|
+
if (this.gpuStarting) return { ...base, state: "starting", reason: null };
|
|
3942
|
+
if (w?.gpuError) return { ...base, state: "failed", reason: w.gpuError };
|
|
3943
|
+
if (this.gpuSupport && !this.gpuSupport.supported) return { ...base, state: "unavailable", reason: this.gpuSupport.reason };
|
|
3944
|
+
if (!this.gpu) return { ...base, state: "not-started", reason: "Starts with the first wet stroke." };
|
|
3945
|
+
if (w?.gpuActive) return { ...base, state: "active", reason: null };
|
|
3946
|
+
return { ...base, state: "standby", reason: `The GPU takes over at ${this.gpuMinTiles} wet tiles (256 px).` };
|
|
3947
|
+
}
|
|
3948
|
+
/** The world (made on first use, at the canvas size). */
|
|
3949
|
+
ensureWorld() {
|
|
3950
|
+
const { width, height } = this.host.canvas();
|
|
3951
|
+
if (this.world && (this.world.width !== width || this.world.height !== height)) {
|
|
3952
|
+
if (this.world.laneIds().length) throw new Error("The wet world is the wrong size; dry the paint before resizing the canvas.");
|
|
3953
|
+
this.dropWorld();
|
|
3954
|
+
}
|
|
3955
|
+
if (!this.world) this.world = this.makeWorld(width, height, this.host.paper());
|
|
3956
|
+
return this.world;
|
|
3957
|
+
}
|
|
3958
|
+
makeWorld(width, height, paper) {
|
|
3959
|
+
const world = new WetWorld(width, height, wetPaperInput(paper), { kernel: this.options.kernel, threads: this.options.threads });
|
|
3960
|
+
world.setTilt(this.tilt);
|
|
3961
|
+
world.gpuMinTiles = this.gpuMinTiles;
|
|
3962
|
+
if (this.gpu) world.attachGpu(this.gpu);
|
|
3963
|
+
else this.startGpu();
|
|
3964
|
+
return world;
|
|
3965
|
+
}
|
|
3966
|
+
/**
|
|
3967
|
+
* Whether `extend` can keep the wet paint when the canvas grows by `left` / `top` on those
|
|
3968
|
+
* sides: the shift has to be whole wet tiles (256 px), and no wet stroke may be live.
|
|
3969
|
+
*/
|
|
3970
|
+
canExtend(left2, top) {
|
|
3971
|
+
return left2 % WET_TILE === 0 && top % WET_TILE === 0 && !this.driver;
|
|
3972
|
+
}
|
|
3973
|
+
/**
|
|
3974
|
+
* Keeps the wet paint while the canvas grows to `width`×`height` by `left` / `top` on those
|
|
3975
|
+
* sides (`canExtend` must hold): the world is made again at the new size with every wet layer
|
|
3976
|
+
* moved by (left, top), still wet, its base and flow map with it; `paper` is the new
|
|
3977
|
+
* document's paper (its grain moved with the paint). Wet settings' flow centres move too. The
|
|
3978
|
+
* caller shows the layers again (`syncDisplays`) once the document is the new size.
|
|
3979
|
+
*/
|
|
3980
|
+
extend(width, height, left2, top, paper) {
|
|
3981
|
+
if (!this.canExtend(left2, top)) throw new Error("Wet paint only moves by whole 256 px tiles, between strokes.");
|
|
3982
|
+
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 + left2, y: s.flow.center.y + top } } });
|
|
3983
|
+
const old = this.world;
|
|
3984
|
+
const snap = old?.laneIds().length ? old.snapshot() : null;
|
|
3985
|
+
this.dropWorld();
|
|
3986
|
+
this.lastTick = null;
|
|
3987
|
+
this.stepsOwed = 0;
|
|
3988
|
+
if (!old || !snap) return;
|
|
3989
|
+
const moved = moveWetSnapshot(snap, old, { width, height }, left2, top, (l) => {
|
|
3990
|
+
const flow = this.settings.get(l.id)?.flow;
|
|
3991
|
+
return {
|
|
3992
|
+
base: translateSurface(l.base, left2, top),
|
|
3993
|
+
...l.light ? { light: translateSurface(l.light, left2, top) } : {},
|
|
3994
|
+
flow: l.flow && flow && flow.kind !== "none" ? flowField(flow, width, height) : l.flow
|
|
3995
|
+
};
|
|
3996
|
+
});
|
|
3997
|
+
this.world = this.makeWorld(width, height, paper);
|
|
3998
|
+
this.world.restore(moved);
|
|
3999
|
+
}
|
|
4000
|
+
/** Whether any layer (or `layerId`) has wet paint. */
|
|
4001
|
+
isWet(layerId) {
|
|
4002
|
+
const w = this.world;
|
|
4003
|
+
if (!w) return false;
|
|
4004
|
+
if (layerId !== void 0) return w.hasLane(layerId) && w.isActive(layerId);
|
|
4005
|
+
return w.laneIds().some((id) => w.isActive(id));
|
|
4006
|
+
}
|
|
4007
|
+
/** Layers holding a wet layer (wet or drying). */
|
|
4008
|
+
wetLayerIds() {
|
|
4009
|
+
return this.world?.laneIds() ?? [];
|
|
4010
|
+
}
|
|
4011
|
+
/** Whether a batch is running (edits wait). */
|
|
4012
|
+
get busy() {
|
|
4013
|
+
return Boolean(this.world?.busy);
|
|
4014
|
+
}
|
|
4015
|
+
/** Resolves when no batch is running. */
|
|
4016
|
+
async idle() {
|
|
4017
|
+
while (this.ticking) await this.ticking;
|
|
4018
|
+
}
|
|
4019
|
+
settingsOf(layerId, medium = "watercolor") {
|
|
4020
|
+
let s = this.settings.get(layerId);
|
|
4021
|
+
if (!s) {
|
|
4022
|
+
s = { params: normalizeWetParams(WET_MEDIUM_PARAMS[medium], DEFAULT_WET_PARAMS), flow: { ...DEFAULT_FLOW } };
|
|
4023
|
+
this.settings.set(layerId, s);
|
|
4024
|
+
}
|
|
4025
|
+
return s;
|
|
4026
|
+
}
|
|
4027
|
+
/** Puts a layer in the world (its current pixels become the base under the wet paint). */
|
|
4028
|
+
ensureLane(layerId, medium) {
|
|
4029
|
+
const w = this.ensureWorld();
|
|
4030
|
+
if (!w.hasLane(layerId)) {
|
|
4031
|
+
const base = this.host.layerSurface(layerId) ?? new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
|
|
4032
|
+
const s = this.settingsOf(layerId, medium);
|
|
4033
|
+
w.ensureLane(layerId, { params: s.params, base, light: this.host.layerLight(layerId) });
|
|
4034
|
+
const { width, height } = this.host.canvas();
|
|
4035
|
+
if (s.flow.kind !== "none") w.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);
|
|
4036
|
+
}
|
|
4037
|
+
return w;
|
|
4038
|
+
}
|
|
4039
|
+
setParams(layerId, update) {
|
|
4040
|
+
const s = this.settingsOf(layerId);
|
|
4041
|
+
s.params = normalizeWetParams(update, s.params);
|
|
4042
|
+
if (this.world?.hasLane(layerId)) this.world.setParams(layerId, s.params);
|
|
4043
|
+
return { ...s.params };
|
|
4044
|
+
}
|
|
4045
|
+
setFlow(layerId, update) {
|
|
4046
|
+
const s = this.settingsOf(layerId);
|
|
4047
|
+
s.flow = normalizeFlow(update, s.flow);
|
|
4048
|
+
if (this.world?.hasLane(layerId)) {
|
|
4049
|
+
const { width, height } = this.host.canvas();
|
|
4050
|
+
this.world.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);
|
|
4051
|
+
}
|
|
4052
|
+
return { ...s.flow };
|
|
4053
|
+
}
|
|
4054
|
+
setTilt(update) {
|
|
4055
|
+
this.tilt = normalizeTilt(update, this.tilt);
|
|
4056
|
+
this.world?.setTilt(this.tilt);
|
|
4057
|
+
return { ...this.tilt };
|
|
4058
|
+
}
|
|
4059
|
+
/** The paper changed. */
|
|
4060
|
+
paperChanged() {
|
|
4061
|
+
this.world?.setPaper(wetPaperInput(this.host.paper()));
|
|
4062
|
+
}
|
|
4063
|
+
/** A layer as tools describe it. */
|
|
4064
|
+
info(layerId) {
|
|
4065
|
+
const s = this.settingsOf(layerId);
|
|
4066
|
+
const st = this.world?.hasLane(layerId) ? this.world.stats(layerId) : null;
|
|
4067
|
+
return {
|
|
4068
|
+
layerId,
|
|
4069
|
+
wet: Boolean(st && st.wetCells > 0),
|
|
4070
|
+
wetCells: st?.wetCells ?? 0,
|
|
4071
|
+
wetBounds: st?.wetBounds ?? null,
|
|
4072
|
+
activeTiles: st?.activeTiles ?? 0,
|
|
4073
|
+
params: { ...s.params },
|
|
4074
|
+
flow: { ...s.flow }
|
|
4075
|
+
};
|
|
4076
|
+
}
|
|
4077
|
+
backend() {
|
|
4078
|
+
return this.world?.backend() ?? null;
|
|
4079
|
+
}
|
|
4080
|
+
/** Shows the current wet pixels (at most `maxTiles` reshaded per layer). */
|
|
4081
|
+
syncDisplays(maxTiles = Infinity) {
|
|
4082
|
+
const w = this.world;
|
|
4083
|
+
if (!w) return [];
|
|
4084
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
4085
|
+
const rects = [];
|
|
4086
|
+
for (const id of w.laneIds()) {
|
|
4087
|
+
const d = w.display(id, maxTiles);
|
|
4088
|
+
if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
|
|
4089
|
+
rects.push(...d.changed);
|
|
4090
|
+
}
|
|
4091
|
+
this.syncLights();
|
|
4092
|
+
if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
|
|
4093
|
+
return rects;
|
|
4094
|
+
}
|
|
4095
|
+
/** Like `syncDisplays`, shading on the GPU when Engine B is active (all stale tiles at once). */
|
|
4096
|
+
async syncDisplaysGpu(maxCpuTiles, lead = false) {
|
|
4097
|
+
const w = this.world;
|
|
4098
|
+
if (!w) return [];
|
|
4099
|
+
const surfaces = /* @__PURE__ */ new Map();
|
|
4100
|
+
const rects = [];
|
|
4101
|
+
for (const id of w.laneIds()) {
|
|
4102
|
+
const d = await w.displayGpu(id, maxCpuTiles, lead);
|
|
4103
|
+
if (!w.hasLane(id)) continue;
|
|
4104
|
+
if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
|
|
4105
|
+
rects.push(...d.changed);
|
|
4106
|
+
}
|
|
4107
|
+
this.syncLights();
|
|
4108
|
+
if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
|
|
4109
|
+
return rects;
|
|
4110
|
+
}
|
|
4111
|
+
/** Shows each wet layer's live light paint (the light under it with the wet light shaded in). */
|
|
4112
|
+
syncLights() {
|
|
4113
|
+
const w = this.world;
|
|
4114
|
+
if (!w) return;
|
|
4115
|
+
const lights = /* @__PURE__ */ new Map();
|
|
4116
|
+
for (const id of w.laneIds()) {
|
|
4117
|
+
const l = w.lightDisplay(id);
|
|
4118
|
+
if (l && this.host.layerLight(id) !== l) lights.set(id, l);
|
|
4119
|
+
}
|
|
4120
|
+
if (lights.size) this.host.updateLights(lights);
|
|
4121
|
+
}
|
|
4122
|
+
/** Drops the wet layers whose paint has all dried (they become ordinary pixel layers). */
|
|
4123
|
+
retireDry() {
|
|
4124
|
+
const w = this.world;
|
|
4125
|
+
if (!w) return;
|
|
4126
|
+
for (const id of w.laneIds()) {
|
|
4127
|
+
if (w.isActive(id) || this.driver?.init.laneId === id) continue;
|
|
4128
|
+
const base = w.getBase(id);
|
|
4129
|
+
const light = w.isLit(id) ? w.getLight(id) : null;
|
|
4130
|
+
w.removeLane(id);
|
|
4131
|
+
if (light && this.host.layerLight(id) !== light) this.host.updateLights(/* @__PURE__ */ new Map([[id, light]]));
|
|
4132
|
+
this.host.updateSurfaces(/* @__PURE__ */ new Map([[id, base]]), []);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Advances time by the wall clock (`now` in ms, e.g. `performance.now()`): `speed` steps per
|
|
4137
|
+
* second, at most `maxSteps` per call. Call it every frame; it does nothing while paused or dry.
|
|
4138
|
+
* Resolves when the steps are done and the display is up to date.
|
|
4139
|
+
*/
|
|
4140
|
+
tick(now, maxSteps = 8) {
|
|
4141
|
+
if (this.ticking) {
|
|
4142
|
+
this.leadFrame(now);
|
|
4143
|
+
return this.ticking;
|
|
4144
|
+
}
|
|
4145
|
+
if (this.displaying) return this.displaying;
|
|
4146
|
+
const last = this.lastTick;
|
|
4147
|
+
this.lastTick = now;
|
|
4148
|
+
const w = this.world;
|
|
4149
|
+
if (!w || this.paused || last === null) {
|
|
4150
|
+
this.flushDriver();
|
|
4151
|
+
return Promise.resolve();
|
|
4152
|
+
}
|
|
4153
|
+
if (!w.isActive()) {
|
|
4154
|
+
this.flushDriver();
|
|
4155
|
+
this.retireDry();
|
|
4156
|
+
return Promise.resolve();
|
|
4157
|
+
}
|
|
4158
|
+
const lead = w.gpuActive && !this.driver;
|
|
4159
|
+
w.gpuLeadMode = lead;
|
|
4160
|
+
const most = lead ? Math.max(maxSteps, GPU_LEAD) : maxSteps;
|
|
4161
|
+
this.stepsOwed = Math.min(most * 4, this.stepsOwed + (now - last) / 1e3 * this.speed);
|
|
4162
|
+
const n = Math.min(most, Math.floor(this.stepsOwed));
|
|
4163
|
+
this.leadAt = now;
|
|
4164
|
+
if (n < 1) {
|
|
4165
|
+
this.flushDriver();
|
|
4166
|
+
return Promise.resolve();
|
|
4167
|
+
}
|
|
4168
|
+
this.stepsOwed -= n;
|
|
4169
|
+
const run = (async () => {
|
|
4170
|
+
try {
|
|
4171
|
+
await w.step(n);
|
|
4172
|
+
} finally {
|
|
4173
|
+
this.ticking = null;
|
|
4174
|
+
}
|
|
4175
|
+
this.flushDriver();
|
|
4176
|
+
if (w.gpuActive) {
|
|
4177
|
+
const shading = this.syncDisplaysGpu(DISPLAY_TILES_PER_TICK).then(() => void 0);
|
|
4178
|
+
this.displaying = shading;
|
|
4179
|
+
try {
|
|
4180
|
+
await shading;
|
|
4181
|
+
} finally {
|
|
4182
|
+
this.displaying = null;
|
|
4183
|
+
}
|
|
4184
|
+
} else this.syncDisplays(DISPLAY_TILES_PER_TICK);
|
|
4185
|
+
this.retireDry();
|
|
4186
|
+
this.host.changed();
|
|
4187
|
+
})();
|
|
4188
|
+
this.ticking = run;
|
|
4189
|
+
return run;
|
|
4190
|
+
}
|
|
4191
|
+
/** Lead mode: steps the GPU on by the time since the last frame (within the running batch) and shows it. */
|
|
4192
|
+
leadFrame(now) {
|
|
4193
|
+
const w = this.world;
|
|
4194
|
+
if (!w || this.displaying || this.driver || !w.gpuLeading) return;
|
|
4195
|
+
const k = Math.max(1, Math.round((now - this.leadAt) / 1e3 * this.speed));
|
|
4196
|
+
if (!w.gpuLead(k)) return;
|
|
4197
|
+
this.leadAt = now;
|
|
4198
|
+
const shading = this.syncDisplaysGpu(0, true).then(() => this.host.changed());
|
|
4199
|
+
this.displaying = shading;
|
|
4200
|
+
void shading.finally(() => {
|
|
4201
|
+
if (this.displaying === shading) this.displaying = null;
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
flushDriver() {
|
|
4205
|
+
const d = this.driver;
|
|
4206
|
+
if (!d || !this.world) return;
|
|
4207
|
+
const rects = d.flush();
|
|
4208
|
+
if (rects.length) this.syncDisplays(DISPLAY_TILES_PER_TICK);
|
|
4209
|
+
if (d.done) this.onDriverDone?.(d);
|
|
4210
|
+
}
|
|
4211
|
+
/** Called when a live stroke that ended mid-batch has finished applying. */
|
|
4212
|
+
onDriverDone = null;
|
|
4213
|
+
/** Steps `steps` times now (agents, tests). Returns the steps run. */
|
|
4214
|
+
async advance(steps) {
|
|
4215
|
+
await this.idle();
|
|
4216
|
+
const w = this.world;
|
|
4217
|
+
if (!w) return 0;
|
|
4218
|
+
await w.ready;
|
|
4219
|
+
await this.idle();
|
|
4220
|
+
const start = w.clock;
|
|
4221
|
+
const n = Math.max(0, Math.floor(steps));
|
|
4222
|
+
const job = (async () => {
|
|
4223
|
+
try {
|
|
4224
|
+
await w.step(n);
|
|
4225
|
+
} finally {
|
|
4226
|
+
this.ticking = null;
|
|
4227
|
+
}
|
|
4228
|
+
})();
|
|
4229
|
+
this.ticking = job;
|
|
4230
|
+
await job;
|
|
4231
|
+
if (w.gpuActive) await this.syncDisplaysGpu(Infinity);
|
|
4232
|
+
else this.syncDisplays();
|
|
4233
|
+
this.retireDry();
|
|
4234
|
+
return w.clock - start;
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Dries at once (everything settles where it is), on one layer or all, optionally only in a
|
|
4238
|
+
* rectangle. Returns the layers and rectangles that dried.
|
|
4239
|
+
*/
|
|
4240
|
+
dry(layerId, rect) {
|
|
4241
|
+
const w = this.world;
|
|
4242
|
+
if (!w) return [];
|
|
4243
|
+
const ids = layerId !== void 0 ? w.hasLane(layerId) ? [layerId] : [] : w.laneIds();
|
|
4244
|
+
const out = [];
|
|
4245
|
+
for (const id of ids) out.push({ layerId: id, bounds: w.dry(id, rect) });
|
|
4246
|
+
this.syncDisplays();
|
|
4247
|
+
this.retireDry();
|
|
4248
|
+
return out;
|
|
4249
|
+
}
|
|
4250
|
+
/** The layer's light paint with all its wet paint dried (`null` without a wet layer or light), without changing anything. */
|
|
4251
|
+
bakedLight(layerId) {
|
|
4252
|
+
const w = this.world;
|
|
4253
|
+
if (!w?.hasLane(layerId) || !w.isLit(layerId)) return null;
|
|
4254
|
+
return w.bakedLight(layerId);
|
|
4255
|
+
}
|
|
4256
|
+
/** The layer's pixels with all its wet paint dried (deterministic), without changing anything. */
|
|
4257
|
+
bakedSurface(layerId) {
|
|
4258
|
+
const w = this.world;
|
|
4259
|
+
if (!w?.hasLane(layerId)) return null;
|
|
4260
|
+
return w.bakedSurface(layerId);
|
|
4261
|
+
}
|
|
4262
|
+
/** A snapshot of the world for undo (`null` when there is none). */
|
|
4263
|
+
snapshot() {
|
|
4264
|
+
const w = this.world;
|
|
4265
|
+
if (!w || !w.laneIds().length) return null;
|
|
4266
|
+
return w.snapshot();
|
|
4267
|
+
}
|
|
4268
|
+
/** Puts a snapshot back (`null`: no wet layers). Returns the layer surfaces it implies. */
|
|
4269
|
+
restore(snap) {
|
|
4270
|
+
if (!snap) {
|
|
4271
|
+
if (this.world) for (const id of this.world.laneIds()) this.world.removeLane(id);
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
const { width, height } = this.host.canvas();
|
|
4275
|
+
if (this.world && (this.world.width !== width || this.world.height !== height)) this.dropWorld();
|
|
4276
|
+
this.ensureWorld().restore(snap);
|
|
4277
|
+
this.syncDisplays();
|
|
4278
|
+
}
|
|
4279
|
+
dropWorld() {
|
|
4280
|
+
this.world?.destroy();
|
|
4281
|
+
this.world = null;
|
|
4282
|
+
this.driver = null;
|
|
4283
|
+
}
|
|
4284
|
+
/** Forgets everything wet (a new canvas, a resize, opening a document). */
|
|
4285
|
+
reset() {
|
|
4286
|
+
this.world?.destroy();
|
|
4287
|
+
this.world = null;
|
|
4288
|
+
this.driver = null;
|
|
4289
|
+
this.settings.clear();
|
|
4290
|
+
this.lastTick = null;
|
|
4291
|
+
this.stepsOwed = 0;
|
|
4292
|
+
}
|
|
4293
|
+
/** Water overlay tiles (RGBA) over a document rectangle, for the view. */
|
|
4294
|
+
waterOverlay(rect) {
|
|
4295
|
+
const w = this.world;
|
|
4296
|
+
if (!w) return [];
|
|
4297
|
+
const out = [];
|
|
4298
|
+
const tx0 = Math.max(0, Math.floor(rect.x / TILE));
|
|
4299
|
+
const ty0 = Math.max(0, Math.floor(rect.y / TILE));
|
|
4300
|
+
const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);
|
|
4301
|
+
const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);
|
|
4302
|
+
for (let ty = ty0; ty <= ty1; ty++)
|
|
4303
|
+
for (let tx = tx0; tx <= tx1; tx++) {
|
|
4304
|
+
let merged = null;
|
|
4305
|
+
for (const id of w.laneIds()) {
|
|
4306
|
+
const t = w.waterOverlay(id, tx, ty);
|
|
4307
|
+
if (!t) continue;
|
|
4308
|
+
if (!merged) merged = t;
|
|
4309
|
+
else for (let i = 3; i < t.length; i += 4) merged[i] = Math.max(merged[i], t[i]);
|
|
4310
|
+
}
|
|
4311
|
+
if (merged) out.push({ tx, ty, data: merged });
|
|
4312
|
+
}
|
|
4313
|
+
return out;
|
|
4314
|
+
}
|
|
4315
|
+
destroy() {
|
|
4316
|
+
this.world?.destroy();
|
|
4317
|
+
this.world = null;
|
|
4318
|
+
this.gpu?.destroy();
|
|
4319
|
+
this.gpu = null;
|
|
4320
|
+
}
|
|
4321
|
+
};
|
|
4322
|
+
function wetPigmentOf(hex, body) {
|
|
4323
|
+
const [r, g, b] = hexToRgb8(hex);
|
|
4324
|
+
return Array.from(pigmentOf([r / 255, g / 255, b / 255], body));
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
// src/vector/brushstrokeMedia.ts
|
|
4328
|
+
var VECTOR_WET_STEPS = 120;
|
|
4329
|
+
function vectorMediaEngine(brush) {
|
|
4330
|
+
if (brush.engine === "wet") return brush.wet.tool === "paint" ? "wet" : null;
|
|
4331
|
+
return brush.engine === "mixer" || brush.engine === "smudge" || brush.engine === "blender" ? brush.engine : null;
|
|
4332
|
+
}
|
|
4333
|
+
var usesVectorBackdrop = (engine) => engine === "mixer" || engine === "smudge" || engine === "blender";
|
|
4334
|
+
var finite2 = (v2) => typeof v2 === "number" && Number.isFinite(v2);
|
|
4335
|
+
function place(input, brush, options, pad = 0) {
|
|
4336
|
+
const samples = brushstrokeSamples(input);
|
|
4337
|
+
if (!samples.length) return null;
|
|
4338
|
+
for (const s of samples) if (!finite2(s.x) || !finite2(s.y)) throw new Error("Brushstroke samples need finite x and y.");
|
|
4339
|
+
let scale3 = finite2(options.scale) && options.scale > 0 ? Math.min(BRUSHSTROKE_MAX_SCALE, Math.max(BRUSHSTROKE_MIN_SCALE, options.scale)) : 1;
|
|
4340
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
4341
|
+
for (const s of samples) {
|
|
4342
|
+
x0 = Math.min(x0, s.x);
|
|
4343
|
+
y0 = Math.min(y0, s.y);
|
|
4344
|
+
x1 = Math.max(x1, s.x);
|
|
4345
|
+
y1 = Math.max(y1, s.y);
|
|
4346
|
+
}
|
|
4347
|
+
const reach = brush.tip.size * (1 + 1.5 * brush.dynamics.tiltSize) / 2 + 2 + pad;
|
|
4348
|
+
const maxPixels = finite2(options.maxPixels) && options.maxPixels > 0 ? options.maxPixels : 4096 * 4096;
|
|
4349
|
+
const area = (x1 - x0 + 2 * reach) * (y1 - y0 + 2 * reach);
|
|
4350
|
+
if (area * scale3 * scale3 > maxPixels) scale3 = Math.max(BRUSHSTROKE_MIN_SCALE, Math.sqrt(maxPixels / area));
|
|
4351
|
+
const rect = { x: Math.floor((x0 - reach) * scale3), y: Math.floor((y0 - reach) * scale3), width: 0, height: 0 };
|
|
4352
|
+
rect.width = Math.max(1, Math.ceil((x1 + reach) * scale3) - rect.x);
|
|
4353
|
+
rect.height = Math.max(1, Math.ceil((y1 + reach) * scale3) - rect.y);
|
|
4354
|
+
const points = samples.map((s) => ({ ...s, x: s.x * scale3 - rect.x, y: s.y * scale3 - rect.y }));
|
|
4355
|
+
if (options.closed && points.length > 2) points.push({ ...points[0] });
|
|
4356
|
+
const sized = { ...brush, pressureSmoothing: 0, tip: { ...brush.tip, size: brush.tip.size * scale3, smoothing: 0 } };
|
|
4357
|
+
const [r, g, b, a] = options.color;
|
|
4358
|
+
const hex = "#" + [r, g, b].map((v2) => Math.round(Math.min(255, Math.max(0, v2))).toString(16).padStart(2, "0")).join("");
|
|
4359
|
+
const source = options.paint ?? null;
|
|
4360
|
+
const paint = source?.type === "gradient" ? resolvePaintGradient(scalePaintGradient(source, scale3), { paint: hex, secondary: "#ffffff" }) : null;
|
|
4361
|
+
const color2 = source?.type === "color" ? hexRgb(source.color) : [r / 255, g / 255, b / 255];
|
|
4362
|
+
return { points, brush: sized, rect, scale: scale3, color: color2, alpha: Math.min(1, Math.max(0, a / 255)), paint };
|
|
4363
|
+
}
|
|
4364
|
+
function hexRgb(hex) {
|
|
4365
|
+
const n = parseInt(hex.slice(1), 16);
|
|
4366
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
4367
|
+
}
|
|
4368
|
+
var EMPTY = { image: { width: 0, height: 0, data: new Uint8ClampedArray(0) }, bounds: null, scale: 1, dabs: 0 };
|
|
4369
|
+
function cropped(data, placed, dabs, keep) {
|
|
4370
|
+
const { width: w, height: h } = placed.rect;
|
|
4371
|
+
let cx0 = w, cy0 = h, cx1 = -1, cy1 = -1;
|
|
4372
|
+
for (let y = 0; y < h; y++) {
|
|
4373
|
+
for (let x = 0; x < w; x++) {
|
|
4374
|
+
const i = (y * w + x) * 4;
|
|
4375
|
+
if (keep ? !keep(i) : data[i + 3] === 0) continue;
|
|
4376
|
+
if (x < cx0) cx0 = x;
|
|
4377
|
+
if (x > cx1) cx1 = x;
|
|
4378
|
+
if (y < cy0) cy0 = y;
|
|
4379
|
+
if (y > cy1) cy1 = y;
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
if (cx1 < 0) return { ...EMPTY, scale: placed.scale, dabs };
|
|
4383
|
+
const cw = cx1 - cx0 + 1;
|
|
4384
|
+
const ch = cy1 - cy0 + 1;
|
|
4385
|
+
const out = new Uint8ClampedArray(cw * ch * 4);
|
|
4386
|
+
for (let y = 0; y < ch; y++) {
|
|
4387
|
+
for (let x = 0; x < cw; x++) {
|
|
4388
|
+
const i = ((y + cy0) * w + x + cx0) * 4;
|
|
4389
|
+
if (keep && !keep(i)) continue;
|
|
4390
|
+
const o = (y * cw + x) * 4;
|
|
4391
|
+
out[o] = data[i];
|
|
4392
|
+
out[o + 1] = data[i + 1];
|
|
4393
|
+
out[o + 2] = data[i + 2];
|
|
4394
|
+
out[o + 3] = placed.alpha < 1 ? Math.round(data[i + 3] * placed.alpha) : data[i + 3];
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
const s = placed.scale;
|
|
4398
|
+
return { image: { width: cw, height: ch, data: out }, bounds: { x: (placed.rect.x + cx0) / s, y: (placed.rect.y + cy0) / s, width: cw / s, height: ch / s }, scale: s, dabs };
|
|
4399
|
+
}
|
|
4400
|
+
var WetBrushstrokeJob = class {
|
|
4401
|
+
constructor(input, brush, options, steps = VECTOR_WET_STEPS) {
|
|
4402
|
+
this.steps = steps;
|
|
4403
|
+
if (vectorMediaEngine(brush) !== "wet") throw new Error(`'${brush.name}' is not a wet paint brush.`);
|
|
4404
|
+
this.placed = place(input, brush, options, brush.tip.size * 0.25);
|
|
4405
|
+
if (!this.placed) {
|
|
4406
|
+
this.world = null;
|
|
4407
|
+
this.dabs = 0;
|
|
4408
|
+
this.final = EMPTY;
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
const { rect, points } = this.placed;
|
|
4412
|
+
this.world = new WetWorld(rect.width, rect.height, wetPaperInput(DEFAULT_PAPER), { kernel: "typescript", threads: 0 });
|
|
4413
|
+
const medium = this.placed.brush.wet.medium ?? "watercolor";
|
|
4414
|
+
this.world.ensureLane("s", { params: WET_MEDIUM_PARAMS[medium] ?? {} });
|
|
4415
|
+
const color2 = this.placed.color;
|
|
4416
|
+
const driver = new WetStrokeDriver({
|
|
4417
|
+
world: this.world,
|
|
4418
|
+
laneId: "s",
|
|
4419
|
+
brush: this.placed.brush,
|
|
4420
|
+
color: color2,
|
|
4421
|
+
load: { reservoir: [...color2], amount: 1, pickup: null },
|
|
4422
|
+
grain: null,
|
|
4423
|
+
paperDepth: 0,
|
|
4424
|
+
paperColor: [255, 255, 255],
|
|
4425
|
+
seed: options.seed ?? 0,
|
|
4426
|
+
copies: [],
|
|
4427
|
+
paint: this.placed.paint
|
|
4428
|
+
});
|
|
4429
|
+
driver.push(points);
|
|
4430
|
+
driver.finish();
|
|
4431
|
+
this.dabs = driver.dabCount;
|
|
4432
|
+
}
|
|
4433
|
+
steps;
|
|
4434
|
+
world;
|
|
4435
|
+
placed;
|
|
4436
|
+
dabs;
|
|
4437
|
+
done = 0;
|
|
4438
|
+
final = null;
|
|
4439
|
+
/** Steps run so far. */
|
|
4440
|
+
get progress() {
|
|
4441
|
+
return this.done;
|
|
4442
|
+
}
|
|
4443
|
+
get finished() {
|
|
4444
|
+
return this.final !== null;
|
|
4445
|
+
}
|
|
4446
|
+
/** Runs steps until `budgetMs` has passed (at least one) or all have run. Returns whether it is finished. */
|
|
4447
|
+
run(budgetMs = Infinity) {
|
|
4448
|
+
if (this.final || !this.world) return true;
|
|
4449
|
+
const start = Date.now();
|
|
4450
|
+
while (this.done < this.steps) {
|
|
4451
|
+
this.world.stepSync(1);
|
|
4452
|
+
this.done++;
|
|
4453
|
+
if (Date.now() - start >= budgetMs) break;
|
|
4454
|
+
}
|
|
4455
|
+
if (this.done >= this.steps) {
|
|
4456
|
+
this.final = this.bake();
|
|
4457
|
+
this.destroy();
|
|
4458
|
+
}
|
|
4459
|
+
return this.final !== null;
|
|
4460
|
+
}
|
|
4461
|
+
/** The paint dried where it is now (step 0: as laid). */
|
|
4462
|
+
image() {
|
|
4463
|
+
return this.final ?? this.bake();
|
|
4464
|
+
}
|
|
4465
|
+
/** The settled image: runs the remaining steps now. */
|
|
4466
|
+
settled() {
|
|
4467
|
+
this.run();
|
|
4468
|
+
return this.final;
|
|
4469
|
+
}
|
|
4470
|
+
destroy() {
|
|
4471
|
+
this.world?.destroy();
|
|
4472
|
+
}
|
|
4473
|
+
bake() {
|
|
4474
|
+
if (!this.world || !this.placed) return EMPTY;
|
|
4475
|
+
const { width, height } = this.placed.rect;
|
|
4476
|
+
const data = readRegion(this.world.bakedSurface("s"), { x: 0, y: 0, width, height });
|
|
4477
|
+
return cropped(data, this.placed, this.dabs);
|
|
4478
|
+
}
|
|
4479
|
+
};
|
|
4480
|
+
function renderWetBrushstroke(input, brush, options) {
|
|
4481
|
+
return new WetBrushstrokeJob(input, brush, options).settled();
|
|
4482
|
+
}
|
|
4483
|
+
function vectorBackdropRect(input, brush, options) {
|
|
4484
|
+
const placed = place(input, brush, options);
|
|
4485
|
+
return placed ? { rect: placed.rect, scale: placed.scale } : null;
|
|
4486
|
+
}
|
|
4487
|
+
function renderBackdropBrushstroke(input, brush, options) {
|
|
4488
|
+
const engine = vectorMediaEngine(brush);
|
|
4489
|
+
if (!usesVectorBackdrop(engine)) throw new Error(`'${brush.name}' is not a Mixer Brush, smudge or blender.`);
|
|
4490
|
+
const placed = place(input, brush, options);
|
|
4491
|
+
if (!placed) return EMPTY;
|
|
4492
|
+
const { rect } = placed;
|
|
4493
|
+
const area = { x: 0, y: 0, width: rect.width, height: rect.height };
|
|
4494
|
+
const before = options.backdrop ? options.backdrop(rect, placed.scale) : new Uint8ClampedArray(rect.width * rect.height * 4);
|
|
4495
|
+
if (before.length !== rect.width * rect.height * 4) throw new Error(`The backdrop must be ${rect.width}\xD7${rect.height} RGBA pixels.`);
|
|
4496
|
+
const cached = options.cache?.(before);
|
|
4497
|
+
const hit = cached?.get();
|
|
4498
|
+
if (hit) return hit;
|
|
4499
|
+
const base = writeRegion(emptySurface(4), area, before);
|
|
4500
|
+
const live = /* @__PURE__ */ new Map();
|
|
4501
|
+
const target = {
|
|
4502
|
+
offset: { x: 0, y: 0 },
|
|
4503
|
+
lockTransparency: false,
|
|
4504
|
+
selection: null,
|
|
4505
|
+
readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? base.tiles.get(tileKey(tx, ty))?.data ?? null,
|
|
4506
|
+
writeTile: (tx, ty) => {
|
|
4507
|
+
const key = tileKey(tx, ty);
|
|
4508
|
+
let t = live.get(key);
|
|
4509
|
+
if (!t) live.set(key, t = base.tiles.get(key) ? new Uint8ClampedArray(base.tiles.get(key).data) : new Uint8ClampedArray(TILE * TILE * 4));
|
|
4510
|
+
return t;
|
|
4511
|
+
}
|
|
4512
|
+
};
|
|
4513
|
+
const color2 = placed.color;
|
|
4514
|
+
const load = placed.brush.engine === "mixer" && placed.brush.loadsColor ? loadMixer(EMPTY_MIXER_LOAD, color2) : EMPTY_MIXER_LOAD;
|
|
4515
|
+
const stroke = new PaintDabStroke(placed.brush, { target, color: color2, secondary: [1, 1, 1], load, grain: null, paperDepth: 0, seed: options.seed ?? 0, paint: placed.paint });
|
|
4516
|
+
stroke.push(placed.points);
|
|
4517
|
+
stroke.finish();
|
|
4518
|
+
const after = new Uint8ClampedArray(rect.width * rect.height * 4);
|
|
4519
|
+
const tiles = new Map(base.tiles);
|
|
4520
|
+
for (const [key, data] of live) tiles.set(key, { data, uniform: null });
|
|
4521
|
+
readRegion(new TiledSurface(4, tiles), area, after);
|
|
4522
|
+
const result = cropped(after, placed, stroke.dabCount, (i) => after[i] !== before[i] || after[i + 1] !== before[i + 1] || after[i + 2] !== before[i + 2] || after[i + 3] !== before[i + 3]);
|
|
4523
|
+
cached?.set(result);
|
|
4524
|
+
return result;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
2537
4527
|
// src/vector/brushstroke.ts
|
|
2538
4528
|
var VECTOR_BRUSHSTROKE_SCALE = 2;
|
|
2539
4529
|
var PRESETS = listPresets(VECTOR_PROFILE).map((spec) => BUILTIN_BRUSHES.find((b) => b.id === spec.id));
|
|
2540
4530
|
function admitted(spec) {
|
|
2541
4531
|
try {
|
|
2542
4532
|
const brush = normalizeBrushSpec(spec);
|
|
2543
|
-
return checkBrush(brush, VECTOR_PROFILE).ok &&
|
|
4533
|
+
return checkBrush(brush, VECTOR_PROFILE).ok && isVectorBrushstrokeEngine(resolveBrush(brush)) ? brush : null;
|
|
2544
4534
|
} catch {
|
|
2545
4535
|
return null;
|
|
2546
4536
|
}
|
|
2547
4537
|
}
|
|
4538
|
+
function isVectorBrushstrokeEngine(brush) {
|
|
4539
|
+
return brush.engine === "dab" && isBrushstrokeBrush(brush) || vectorMediaEngine(brush) !== null;
|
|
4540
|
+
}
|
|
2548
4541
|
function vectorBrushstrokeBrushes(user = []) {
|
|
2549
4542
|
const out = [...PRESETS];
|
|
2550
4543
|
for (const raw of user) {
|
|
@@ -2567,10 +4560,10 @@ function vectorBrushstrokeBrushError(id, user = []) {
|
|
|
2567
4560
|
if (vectorBrushstrokeBrush(id)) return null;
|
|
2568
4561
|
const saved = user.find((b) => b.id === id);
|
|
2569
4562
|
const ids = vectorBrushstrokeBrushes(user).map((b) => b.id);
|
|
2570
|
-
const reasons = (b) => checkBrush(b, VECTOR_PROFILE).issues.filter((i) => i.severity === "error").map((i) => `${i.reason} ${i.fix}`).join(" ") || "
|
|
4563
|
+
const reasons = (b) => checkBrush(b, VECTOR_PROFILE).issues.filter((i) => i.severity === "error").map((i) => `${i.reason} ${i.fix}`).join(" ") || "its engine does not paint brushstrokes.";
|
|
2571
4564
|
if (saved) return admitted(saved) ? null : `Brush "${id}" (${saved.name}) can't paint a brushstroke: ${reasons(saved)} Use one of: ${ids.join(", ")}.`;
|
|
2572
|
-
const
|
|
2573
|
-
if (
|
|
4565
|
+
const preset2 = BRUSH_PRESETS.find((b) => b.id === id);
|
|
4566
|
+
if (preset2) return `Brush "${id}" (${preset2.name}) can't paint a brushstroke: ${reasons(preset2)} Use one of: ${ids.join(", ")}.`;
|
|
2574
4567
|
return `Unknown brush "${id}". ${closest(id, ids)}Use one of: ${ids.join(", ")} (vector.listBrushes lists them).`;
|
|
2575
4568
|
}
|
|
2576
4569
|
function vectorBrushstrokeDefaultColour(id, user = [], scheme = "light") {
|
|
@@ -2594,6 +4587,8 @@ function sanitizeVectorBrushstroke(value, user = []) {
|
|
|
2594
4587
|
}
|
|
2595
4588
|
const seed = sanitizeSeed(r.seed);
|
|
2596
4589
|
if (seed !== void 0) out.seed = seed;
|
|
4590
|
+
const paint = readVectorBrushstrokePaint(r.paint);
|
|
4591
|
+
if (paint) out.paint = paint;
|
|
2597
4592
|
if (r.paper === null) out.paper = null;
|
|
2598
4593
|
else {
|
|
2599
4594
|
const paper = sanitizeVectorBrushstrokePaper(r.paper);
|
|
@@ -2601,9 +4596,24 @@ function sanitizeVectorBrushstroke(value, user = []) {
|
|
|
2601
4596
|
}
|
|
2602
4597
|
return out;
|
|
2603
4598
|
}
|
|
4599
|
+
var TIMED_MAPPINGS = ["velocity", "time"];
|
|
4600
|
+
function vectorBrushstrokePaintError(paint) {
|
|
4601
|
+
let source;
|
|
4602
|
+
try {
|
|
4603
|
+
source = parsePaintSource(paint, "paint");
|
|
4604
|
+
} catch (e) {
|
|
4605
|
+
return e.message;
|
|
4606
|
+
}
|
|
4607
|
+
if (source.type === "gradient" && TIMED_MAPPINGS.includes(source.map.by)) return `paint.map.by "${source.map.by}" needs timed samples, which vector paths do not have; use distance, pressure, tilt, direction, across-tip or random.`;
|
|
4608
|
+
return null;
|
|
4609
|
+
}
|
|
4610
|
+
function readVectorBrushstrokePaint(value) {
|
|
4611
|
+
if (value === void 0 || value === null || vectorBrushstrokePaintError(value)) return null;
|
|
4612
|
+
return readPaintSource(value);
|
|
4613
|
+
}
|
|
2604
4614
|
var VECTOR_BRUSHSTROKE_MAX_SEED = 2147483647;
|
|
2605
|
-
var
|
|
2606
|
-
var sanitizeSeed = (v2) =>
|
|
4615
|
+
var finite3 = (v2) => typeof v2 === "number" && Number.isFinite(v2);
|
|
4616
|
+
var sanitizeSeed = (v2) => finite3(v2) ? Math.min(VECTOR_BRUSHSTROKE_MAX_SEED, Math.max(0, Math.round(v2))) : void 0;
|
|
2607
4617
|
function sanitizeVectorBrushstrokePaper(value) {
|
|
2608
4618
|
if (!value || typeof value !== "object") return null;
|
|
2609
4619
|
const r = value;
|
|
@@ -2611,8 +4621,8 @@ function sanitizeVectorBrushstrokePaper(value) {
|
|
|
2611
4621
|
if (typeof r.pattern === "string" && GRAIN_PATTERNS.includes(r.pattern)) out.pattern = r.pattern;
|
|
2612
4622
|
const seed = sanitizeSeed(r.seed);
|
|
2613
4623
|
if (seed !== void 0) out.seed = seed;
|
|
2614
|
-
if (
|
|
2615
|
-
if (
|
|
4624
|
+
if (finite3(r.scale) && r.scale > 0) out.scale = Math.min(8, Math.max(0.25, r.scale));
|
|
4625
|
+
if (finite3(r.depth)) out.depth = Math.min(1, Math.max(0, r.depth));
|
|
2616
4626
|
return Object.keys(out).length ? out : null;
|
|
2617
4627
|
}
|
|
2618
4628
|
function vectorBrushstrokeCharacterError(brush) {
|
|
@@ -2625,8 +4635,8 @@ function vectorBrushstrokeCharacterError(brush) {
|
|
|
2625
4635
|
if (extra.length) return `paper has unknown field ${extra.map((k) => `"${k}"`).join(", ")}: use pattern, seed, scale, depth.`;
|
|
2626
4636
|
if (r.pattern !== void 0 && !GRAIN_PATTERNS.includes(r.pattern)) return `Unknown paper pattern "${String(r.pattern)}": use one of ${GRAIN_PATTERNS.join(", ")}.`;
|
|
2627
4637
|
if (r.seed !== void 0 && !(Number.isInteger(r.seed) && r.seed >= 0 && r.seed <= VECTOR_BRUSHSTROKE_MAX_SEED)) return `paper.seed must be an integer from 0 to ${VECTOR_BRUSHSTROKE_MAX_SEED}.`;
|
|
2628
|
-
if (r.scale !== void 0 && !(
|
|
2629
|
-
if (r.depth !== void 0 && !(
|
|
4638
|
+
if (r.scale !== void 0 && !(finite3(r.scale) && r.scale >= 0.25 && r.scale <= 8)) return "paper.scale must be from 0.25 to 8 (1 is the pattern's natural size).";
|
|
4639
|
+
if (r.depth !== void 0 && !(finite3(r.depth) && r.depth >= 0 && r.depth <= 1)) return "paper.depth must be from 0 to 1.";
|
|
2630
4640
|
return null;
|
|
2631
4641
|
}
|
|
2632
4642
|
function randomVectorBrushstrokeSeed() {
|
|
@@ -2639,29 +4649,240 @@ function vectorBrushstrokeReach(layer) {
|
|
|
2639
4649
|
if (!isVectorBrushstrokeLayer(layer)) return 0;
|
|
2640
4650
|
return (layer.brush.size ?? vectorBrushstrokeBrushOf(layer.brush).tip.size) / 2;
|
|
2641
4651
|
}
|
|
4652
|
+
var backdropSource = null;
|
|
4653
|
+
function setVectorBackdropSource(source) {
|
|
4654
|
+
backdropSource = source;
|
|
4655
|
+
}
|
|
4656
|
+
var hex22 = (v2) => Math.round(Math.min(1, Math.max(0, v2)) * 255).toString(16).padStart(2, "0");
|
|
4657
|
+
function gradientPaint(g, map) {
|
|
4658
|
+
const stops = sortedVectorGradientStops(g);
|
|
4659
|
+
const exact2 = stops.length >= 2 && stops.length <= 16 && !g.smoothness && (g.interpolation ?? "srgb") === "srgb" && stops.every((s) => parseVectorColor(s.color));
|
|
4660
|
+
if (exact2) {
|
|
4661
|
+
return {
|
|
4662
|
+
type: "gradient",
|
|
4663
|
+
space: "srgb",
|
|
4664
|
+
map,
|
|
4665
|
+
stops: stops.map((s) => {
|
|
4666
|
+
const c = parseVectorColor(s.color);
|
|
4667
|
+
const opacity = Math.min(1, Math.max(0, (s.opacity ?? 1) * c[3]));
|
|
4668
|
+
const midpoint = s.midpoint !== void 0 && s.midpoint !== 0.5 ? Math.min(0.95, Math.max(0.05, s.midpoint)) : void 0;
|
|
4669
|
+
return { offset: Math.min(1, Math.max(0, s.offset)), color: `#${hex22(c[0])}${hex22(c[1])}${hex22(c[2])}`, ...opacity < 1 ? { opacity } : {}, ...midpoint !== void 0 ? { midpoint } : {} };
|
|
4670
|
+
})
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
const ramp = vectorGradientRamp(g);
|
|
4674
|
+
const n = 16;
|
|
4675
|
+
return {
|
|
4676
|
+
type: "gradient",
|
|
4677
|
+
space: "srgb",
|
|
4678
|
+
map,
|
|
4679
|
+
stops: Array.from({ length: n }, (_, i) => {
|
|
4680
|
+
const t = i / (n - 1);
|
|
4681
|
+
const c = ramp ? ramp.sample(t) : [0, 0, 0, 1];
|
|
4682
|
+
return { offset: t, color: `#${hex22(c[0])}${hex22(c[1])}${hex22(c[2])}`, ...c[3] < 1 ? { opacity: Math.max(0, c[3]) } : {} };
|
|
4683
|
+
})
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
4686
|
+
var REPEAT = { pad: "clamp", repeat: "repeat", reflect: "mirror" };
|
|
4687
|
+
function strokePaintOf(node, layer, brush, length2) {
|
|
4688
|
+
const engine = vectorMediaEngine(brush);
|
|
4689
|
+
if (engine === "smudge" || engine === "blender") return { color: [0, 0, 0, 255], paint: null, within: null };
|
|
4690
|
+
const solid = vectorPaintSolid(layer.paint) ?? "#000000";
|
|
4691
|
+
const c = parseVectorColor(solid) ?? [0, 0, 0, 1];
|
|
4692
|
+
const color2 = [c[0] * 255, c[1] * 255, c[2] * 255, c[3] * 255].map(Math.round);
|
|
4693
|
+
if (layer.brush.paint) return { color: color2, paint: layer.brush.paint, within: null };
|
|
4694
|
+
const g = layer.paint;
|
|
4695
|
+
if (isVectorGradient(g) && g.stops?.length) {
|
|
4696
|
+
const mode = g.strokeMode ?? "within";
|
|
4697
|
+
if (mode === "along") return { color: [color2[0], color2[1], color2[2], 255], paint: gradientPaint(g, { by: "distance", length: Math.max(1, length2), repeat: REPEAT[g.spread ?? "pad"], unit: "px" }), within: null };
|
|
4698
|
+
if (mode === "across") return { color: [color2[0], color2[1], color2[2], 255], paint: gradientPaint(g, { by: "across-tip" }), within: null };
|
|
4699
|
+
if (engine === "mixer") return { color: [color2[0], color2[1], color2[2], 255], paint: gradientPaint(g, { by: "distance", length: Math.max(1, length2), repeat: REPEAT[g.spread ?? "pad"] }), within: null };
|
|
4700
|
+
const b = bezierPathBounds(subpathsToBezier(node.subpaths));
|
|
4701
|
+
return { color: [255, 255, 255, 255], paint: null, within: { gradient: g, bounds: b ? { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY } : null } };
|
|
4702
|
+
}
|
|
4703
|
+
if (brush.paint && !TIMED_MAPPINGS.includes(brush.paint.map.by)) return { color: color2, paint: brush.paint, within: null };
|
|
4704
|
+
return { color: color2, paint: null, within: null };
|
|
4705
|
+
}
|
|
4706
|
+
function recoloured(stroke, within) {
|
|
4707
|
+
if (!stroke.bounds) return stroke;
|
|
4708
|
+
const ramp = vectorGradientRamp(within.gradient);
|
|
4709
|
+
if (!ramp) return stroke;
|
|
4710
|
+
const lut = Array.from({ length: 256 }, (_, i) => ramp.sample(i / 255));
|
|
4711
|
+
const { width, height, data } = stroke.image;
|
|
4712
|
+
const out = new Uint8ClampedArray(data);
|
|
4713
|
+
const { x: bx, y: by } = stroke.bounds;
|
|
4714
|
+
const s = stroke.scale;
|
|
4715
|
+
for (let y = 0; y < height; y++) {
|
|
4716
|
+
for (let x = 0; x < width; x++) {
|
|
4717
|
+
const i = (y * width + x) * 4;
|
|
4718
|
+
if (!out[i + 3]) continue;
|
|
4719
|
+
const t = vectorGradientTAt(within.gradient, within.bounds, { x: bx + (x + 0.5) / s, y: by + (y + 0.5) / s });
|
|
4720
|
+
const col = lut[Math.round(Math.min(1, Math.max(0, t)) * 255)];
|
|
4721
|
+
out[i] = Math.round(out[i] * col[0]);
|
|
4722
|
+
out[i + 1] = Math.round(out[i + 1] * col[1]);
|
|
4723
|
+
out[i + 2] = Math.round(out[i + 2] * col[2]);
|
|
4724
|
+
out[i + 3] = Math.round(out[i + 3] * col[3]);
|
|
4725
|
+
}
|
|
4726
|
+
}
|
|
4727
|
+
return { ...stroke, image: { width, height, data: out } };
|
|
4728
|
+
}
|
|
4729
|
+
function hash(s, h = 2166136261) {
|
|
4730
|
+
for (let i = 0; i < s.length; i++) {
|
|
4731
|
+
h ^= s.charCodeAt(i);
|
|
4732
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
4733
|
+
}
|
|
4734
|
+
return h;
|
|
4735
|
+
}
|
|
4736
|
+
function hashBytes(b, h = 2166136261) {
|
|
4737
|
+
for (let i = 0; i < b.length; i++) {
|
|
4738
|
+
h ^= b[i];
|
|
4739
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
4740
|
+
}
|
|
4741
|
+
return h;
|
|
4742
|
+
}
|
|
4743
|
+
function mediaKey(input, brush, options) {
|
|
4744
|
+
const path = JSON.stringify(input);
|
|
4745
|
+
return `${hash(path).toString(16)}:${path.length}:${hash(JSON.stringify([brush, options])).toString(16)}`;
|
|
4746
|
+
}
|
|
4747
|
+
var Lru = class {
|
|
4748
|
+
constructor(capacity) {
|
|
4749
|
+
this.capacity = capacity;
|
|
4750
|
+
}
|
|
4751
|
+
capacity;
|
|
4752
|
+
map = /* @__PURE__ */ new Map();
|
|
4753
|
+
get(key) {
|
|
4754
|
+
const v2 = this.map.get(key);
|
|
4755
|
+
if (v2 !== void 0) {
|
|
4756
|
+
this.map.delete(key);
|
|
4757
|
+
this.map.set(key, v2);
|
|
4758
|
+
}
|
|
4759
|
+
return v2;
|
|
4760
|
+
}
|
|
4761
|
+
has(key) {
|
|
4762
|
+
return this.map.has(key);
|
|
4763
|
+
}
|
|
4764
|
+
set(key, value) {
|
|
4765
|
+
this.map.delete(key);
|
|
4766
|
+
this.map.set(key, value);
|
|
4767
|
+
while (this.map.size > this.capacity) this.map.delete(this.map.keys().next().value);
|
|
4768
|
+
}
|
|
4769
|
+
};
|
|
4770
|
+
var WET_SLICE_MS = 12;
|
|
4771
|
+
var wetSettled = new Lru(128);
|
|
4772
|
+
var wetInstant = /* @__PURE__ */ new Map();
|
|
4773
|
+
var wetJobs = /* @__PURE__ */ new Map();
|
|
4774
|
+
var settleListeners = /* @__PURE__ */ new Set();
|
|
4775
|
+
var wetTimer = null;
|
|
4776
|
+
var wetGeneration = 0;
|
|
4777
|
+
function wetFinished(key, job) {
|
|
4778
|
+
wetGeneration++;
|
|
4779
|
+
wetSettled.set(key, job.image());
|
|
4780
|
+
wetJobs.delete(key);
|
|
4781
|
+
wetInstant.delete(key);
|
|
4782
|
+
}
|
|
4783
|
+
function notifySettled() {
|
|
4784
|
+
for (const fn of [...settleListeners]) fn();
|
|
4785
|
+
}
|
|
4786
|
+
function scheduleWet() {
|
|
4787
|
+
if (wetTimer || !wetJobs.size) return;
|
|
4788
|
+
wetTimer = setTimeout(() => {
|
|
4789
|
+
wetTimer = null;
|
|
4790
|
+
const first = wetJobs.entries().next().value;
|
|
4791
|
+
if (first && first[1].run(WET_SLICE_MS)) {
|
|
4792
|
+
wetFinished(first[0], first[1]);
|
|
4793
|
+
notifySettled();
|
|
4794
|
+
}
|
|
4795
|
+
scheduleWet();
|
|
4796
|
+
}, 0);
|
|
4797
|
+
}
|
|
4798
|
+
function onVectorBrushstrokesSettled(listener) {
|
|
4799
|
+
settleListeners.add(listener);
|
|
4800
|
+
return () => settleListeners.delete(listener);
|
|
4801
|
+
}
|
|
4802
|
+
function vectorBrushstrokesSettling() {
|
|
4803
|
+
return wetJobs.size;
|
|
4804
|
+
}
|
|
4805
|
+
function settleVectorBrushstrokes() {
|
|
4806
|
+
const n = wetJobs.size;
|
|
4807
|
+
if (!n) return 0;
|
|
4808
|
+
for (const [key, job] of [...wetJobs]) {
|
|
4809
|
+
job.run();
|
|
4810
|
+
wetFinished(key, job);
|
|
4811
|
+
}
|
|
4812
|
+
notifySettled();
|
|
4813
|
+
return n;
|
|
4814
|
+
}
|
|
4815
|
+
function wetImage(input, brush, options, settle, pending) {
|
|
4816
|
+
const key = mediaKey(input, brush, options);
|
|
4817
|
+
const done = wetSettled.get(key);
|
|
4818
|
+
if (done) return done;
|
|
4819
|
+
let job = wetJobs.get(key);
|
|
4820
|
+
if (settle === "now") {
|
|
4821
|
+
job ??= new WetBrushstrokeJob(input, brush, options);
|
|
4822
|
+
job.run();
|
|
4823
|
+
wetFinished(key, job);
|
|
4824
|
+
return wetSettled.get(key);
|
|
4825
|
+
}
|
|
4826
|
+
if (!job) {
|
|
4827
|
+
job = new WetBrushstrokeJob(input, brush, options);
|
|
4828
|
+
wetJobs.set(key, job);
|
|
4829
|
+
wetInstant.set(key, job.image());
|
|
4830
|
+
scheduleWet();
|
|
4831
|
+
}
|
|
4832
|
+
pending.push(key);
|
|
4833
|
+
return wetInstant.get(key) ?? job.image();
|
|
4834
|
+
}
|
|
2642
4835
|
var cache = new BrushstrokeCache(256);
|
|
4836
|
+
var backdropResults = new Lru(64);
|
|
2643
4837
|
var urls = /* @__PURE__ */ new WeakMap();
|
|
2644
4838
|
var layerCache = /* @__PURE__ */ new WeakMap();
|
|
2645
|
-
|
|
4839
|
+
var sameDeps = (a, b) => a === b || !!a && !!b && a.length === b.length && a.every((v2, i) => v2 === b[i]);
|
|
4840
|
+
function vectorBrushstrokeImages(node, layer, context = {}) {
|
|
2646
4841
|
if (!isVectorBrushstrokeLayer(layer)) return [];
|
|
4842
|
+
const preset2 = vectorBrushstrokeBrushOf(layer.brush);
|
|
4843
|
+
const engine = vectorMediaEngine(preset2);
|
|
4844
|
+
const settle = context.settle ?? "async";
|
|
4845
|
+
const document2 = context.document ?? null;
|
|
4846
|
+
const index = (node.strokes ?? []).indexOf(layer);
|
|
4847
|
+
const source = usesVectorBackdrop(engine) && backdropSource ? backdropSource(document2, node, Math.max(0, index), settle) : null;
|
|
4848
|
+
const deps = source ? [...source.deps, wetGeneration] : null;
|
|
2647
4849
|
let byLayer = layerCache.get(node);
|
|
2648
4850
|
const hit = byLayer?.get(layer);
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
const brush = layer.brush.size ? { ...
|
|
2652
|
-
const solid = vectorPaintSolid(layer.paint) ?? "#000000";
|
|
2653
|
-
const c = parseVectorColor(solid) ?? [0, 0, 0, 1];
|
|
2654
|
-
const color2 = [c[0] * 255, c[1] * 255, c[2] * 255, c[3] * 255].map(Math.round);
|
|
4851
|
+
const stale = hit && (hit.pending.length && hit.pending.some((k) => wetSettled.has(k)) || settle === "now" && !hit.settled);
|
|
4852
|
+
if (hit && sameDeps(hit.deps, deps) && !stale) return hit.images;
|
|
4853
|
+
const brush = layer.brush.size ? { ...preset2, tip: { ...preset2.tip, size: layer.brush.size } } : preset2;
|
|
2655
4854
|
const { seed, paper } = layer.brush;
|
|
2656
4855
|
const character = seed !== void 0 ? { character: { seed, origin: { x: 0, y: 0 }, unit: 1, ...paper !== void 0 ? { paper } : {} } } : {};
|
|
2657
4856
|
const out = [];
|
|
4857
|
+
const pending = [];
|
|
2658
4858
|
for (const sp of node.subpaths) {
|
|
2659
4859
|
if (sp.anchors.length < 2) continue;
|
|
2660
4860
|
for (const contour of pathToContours(subpathsToBezier([sp]))) {
|
|
2661
4861
|
const ring = flattenChain(closeContour(contour, contour.closed), 0.25);
|
|
2662
4862
|
if (contour.closed && ring.length > 2) ring.push(ring[0]);
|
|
2663
4863
|
if (ring.length < 2) continue;
|
|
2664
|
-
|
|
4864
|
+
let length2 = 0;
|
|
4865
|
+
for (let i = 1; i < ring.length; i++) length2 += Math.hypot(ring[i][0] - ring[i - 1][0], ring[i][1] - ring[i - 1][1]);
|
|
4866
|
+
const input = { points: ring.map(([x, y]) => ({ x, y })), ...layer.brush.pressure ? { pressure: layer.brush.pressure } : {} };
|
|
4867
|
+
const sp2 = strokePaintOf(node, layer, brush, length2);
|
|
4868
|
+
const options = { scale: VECTOR_BRUSHSTROKE_SCALE, color: sp2.color, paint: sp2.paint, seed: seed ?? 0, maxPixels: 2048 * 2048 };
|
|
4869
|
+
let stroke;
|
|
4870
|
+
if (engine === "wet") stroke = wetImage(input, brush, { ...options, maxPixels: 1024 * 1024 }, settle, pending);
|
|
4871
|
+
else if (engine) {
|
|
4872
|
+
const key = mediaKey(input, brush, options);
|
|
4873
|
+
stroke = renderBackdropBrushstroke(input, brush, {
|
|
4874
|
+
...options,
|
|
4875
|
+
backdrop: source?.backdrop ?? null,
|
|
4876
|
+
cache: (bytes) => {
|
|
4877
|
+
const k = `${key}:${hashBytes(bytes).toString(16)}`;
|
|
4878
|
+
return { get: () => backdropResults.get(k), set: (img) => backdropResults.set(k, img) };
|
|
4879
|
+
}
|
|
4880
|
+
});
|
|
4881
|
+
} else {
|
|
4882
|
+
const { paint, ...rest } = options;
|
|
4883
|
+
stroke = cache.render(input, brush, { ...rest, ...paint ? { paint } : {}, ...character });
|
|
4884
|
+
}
|
|
4885
|
+
if (sp2.within) stroke = recoloured(stroke, sp2.within);
|
|
2665
4886
|
if (!stroke.bounds) continue;
|
|
2666
4887
|
let href = urls.get(stroke);
|
|
2667
4888
|
if (!href) urls.set(stroke, href = brushstrokeDataUrl(stroke));
|
|
@@ -2669,7 +4890,7 @@ function vectorBrushstrokeImages(node, layer) {
|
|
|
2669
4890
|
}
|
|
2670
4891
|
}
|
|
2671
4892
|
if (!byLayer) layerCache.set(node, byLayer = /* @__PURE__ */ new Map());
|
|
2672
|
-
byLayer.set(layer, out);
|
|
4893
|
+
byLayer.set(layer, { images: out, deps, pending, settled: settle === "now" || !pending.length && !deps });
|
|
2673
4894
|
return out;
|
|
2674
4895
|
}
|
|
2675
4896
|
|
|
@@ -2964,13 +5185,13 @@ function subsOf(path) {
|
|
|
2964
5185
|
return pathToContours(path).map((c) => ({ segs: closeContour(c).filter((s) => !degenerate(s)).map(raise), closed: c.closed })).filter((s) => s.segs.length > 0);
|
|
2965
5186
|
}
|
|
2966
5187
|
var subsToPath = (subs) => contoursToPath(subs.map((s) => ({ segments: s.segs, closed: s.closed })));
|
|
2967
|
-
var
|
|
5188
|
+
var unit3 = (x, y) => {
|
|
2968
5189
|
const l = Math.hypot(x, y);
|
|
2969
5190
|
return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
|
|
2970
5191
|
};
|
|
2971
5192
|
function startTangent(s) {
|
|
2972
5193
|
for (let i = 1; i < s.length; i++) {
|
|
2973
|
-
const u =
|
|
5194
|
+
const u = unit3(s[i][0] - s[0][0], s[i][1] - s[0][1]);
|
|
2974
5195
|
if (u) return u;
|
|
2975
5196
|
}
|
|
2976
5197
|
return [1, 0];
|
|
@@ -2978,7 +5199,7 @@ function startTangent(s) {
|
|
|
2978
5199
|
function endTangent(s) {
|
|
2979
5200
|
const end = s[s.length - 1];
|
|
2980
5201
|
for (let i = s.length - 2; i >= 0; i--) {
|
|
2981
|
-
const u =
|
|
5202
|
+
const u = unit3(end[0] - s[i][0], end[1] - s[i][1]);
|
|
2982
5203
|
if (u) return u;
|
|
2983
5204
|
}
|
|
2984
5205
|
return [1, 0];
|
|
@@ -2987,7 +5208,7 @@ function tangentAt(s, t) {
|
|
|
2987
5208
|
if (t <= 1e-9) return startTangent(s);
|
|
2988
5209
|
if (t >= 1 - 1e-9) return endTangent(s);
|
|
2989
5210
|
const d = segmentDerivative(s, t);
|
|
2990
|
-
return
|
|
5211
|
+
return unit3(d[0], d[1]) ?? startTangent(s);
|
|
2991
5212
|
}
|
|
2992
5213
|
var perp2 = (v2) => [-v2[1], v2[0]];
|
|
2993
5214
|
function effectPathLength(path) {
|
|
@@ -3211,7 +5432,7 @@ function resample(sub5, m) {
|
|
|
3211
5432
|
let tan = tout;
|
|
3212
5433
|
if (prev !== null) {
|
|
3213
5434
|
const tin = endTangent(segs[prev]);
|
|
3214
|
-
tan =
|
|
5435
|
+
tan = unit3(tin[0] + tout[0], tin[1] + tout[1]) ?? tout;
|
|
3215
5436
|
}
|
|
3216
5437
|
out.push({ p: segs[j][0], tan, lenPrev: prev === null ? 0 : lens[prev] / m, lenNext: piece });
|
|
3217
5438
|
} else {
|
|
@@ -3269,17 +5490,17 @@ function splitmix(x) {
|
|
|
3269
5490
|
z = (z ^ z >> 27n) * 0x94d049bb133111ebn & MASK64;
|
|
3270
5491
|
return z ^ z >> 31n;
|
|
3271
5492
|
}
|
|
3272
|
-
function
|
|
5493
|
+
function hash3(seed, idx, t) {
|
|
3273
5494
|
const h = splitmix(splitmix(splitmix(seed) ^ idx) ^ BigInt.asUintN(64, BigInt(t)));
|
|
3274
5495
|
return Number(h >> 11n) / 2 ** 53 * 2 - 1;
|
|
3275
5496
|
}
|
|
3276
5497
|
function noise(seed, idx, t) {
|
|
3277
5498
|
const i = Math.floor(t);
|
|
3278
5499
|
const f = t - i;
|
|
3279
|
-
const p0 =
|
|
3280
|
-
const p1 =
|
|
3281
|
-
const p2 =
|
|
3282
|
-
const p3 =
|
|
5500
|
+
const p0 = hash3(seed, idx, i - 1);
|
|
5501
|
+
const p1 = hash3(seed, idx, i);
|
|
5502
|
+
const p2 = hash3(seed, idx, i + 1);
|
|
5503
|
+
const p3 = hash3(seed, idx, i + 2);
|
|
3283
5504
|
const v2 = 0.5 * (2 * p1 + (-p0 + p2) * f + (2 * p0 - 5 * p1 + 4 * p2 - p3) * f * f + (-p0 + 3 * p1 - 3 * p2 + p3) * f * f * f);
|
|
3284
5505
|
return Math.min(1, Math.max(-1, v2));
|
|
3285
5506
|
}
|
|
@@ -3544,11 +5765,11 @@ function vectorFrameChildMatrix(resizing, box, from, to) {
|
|
|
3544
5765
|
const sy = box.height > 1e-9 ? (y1 - y0) / box.height : 1;
|
|
3545
5766
|
return [sx, 0, 0, sy, x0 - sx * box.x, y0 - sy * box.y];
|
|
3546
5767
|
}
|
|
3547
|
-
var
|
|
5768
|
+
var finite4 = (v2) => typeof v2 === "number" && Number.isFinite(v2);
|
|
3548
5769
|
function sanitizeVectorFrame(value) {
|
|
3549
5770
|
if (!value || typeof value !== "object") return null;
|
|
3550
5771
|
const r = value;
|
|
3551
|
-
if (!
|
|
5772
|
+
if (!finite4(r.x) || !finite4(r.y) || !finite4(r.width) || !finite4(r.height) || r.width < 0 || r.height < 0) return null;
|
|
3552
5773
|
const out = { x: r.x, y: r.y, width: r.width, height: r.height };
|
|
3553
5774
|
if (typeof r.fill === "string" || r.fill && typeof r.fill === "object") out.fill = r.fill;
|
|
3554
5775
|
if (r.clip === false) out.clip = false;
|
|
@@ -4196,7 +6417,7 @@ function vectorMaskSummary(node) {
|
|
|
4196
6417
|
enabled: node.children[0].visible !== false
|
|
4197
6418
|
};
|
|
4198
6419
|
}
|
|
4199
|
-
var
|
|
6420
|
+
var place2 = (path, m) => isIdentityMatrix(m) ? path : transformBezierPath(path, m);
|
|
4200
6421
|
function vectorMaskRegionShapes(node, type, parent = IDENTITY_MATRIX, self = true) {
|
|
4201
6422
|
if (!self && node.visible === false) return [];
|
|
4202
6423
|
if (isVectorFrame(node)) node = vectorFrameDrawn(node);
|
|
@@ -4214,17 +6435,17 @@ function vectorMaskRegionShapes(node, type, parent = IDENTITY_MATRIX, self = tru
|
|
|
4214
6435
|
function regionOfPath(path, style, type, m) {
|
|
4215
6436
|
if (!path.length) return [];
|
|
4216
6437
|
const fillRule = style.fillRule ?? "nonzero";
|
|
4217
|
-
if (type === "clip") return [{ path:
|
|
6438
|
+
if (type === "clip") return [{ path: place2(path, m), fillRule }];
|
|
4218
6439
|
const out = [];
|
|
4219
6440
|
let filled = false;
|
|
4220
6441
|
for (const l of vectorPaintLayerNodes(style)) {
|
|
4221
6442
|
if (!l.drawn) continue;
|
|
4222
6443
|
if (l.kind === "fill") {
|
|
4223
|
-
if (!filled) out.push({ path:
|
|
6444
|
+
if (!filled) out.push({ path: place2(path, m), fillRule });
|
|
4224
6445
|
filled = true;
|
|
4225
6446
|
} else if ((l.node.strokeWidth ?? 1) > 0) {
|
|
4226
6447
|
const area = vectorStrokeOutlinePath(path, l.node);
|
|
4227
|
-
if (area.length) out.push({ path:
|
|
6448
|
+
if (area.length) out.push({ path: place2(area, m), fillRule: "nonzero" });
|
|
4228
6449
|
}
|
|
4229
6450
|
}
|
|
4230
6451
|
return out;
|
|
@@ -6208,8 +8429,8 @@ function sanitizeVectorLens(value) {
|
|
|
6208
8429
|
if (spec.color !== null) out.color = typeof r.color === "string" && parseVectorColor(r.color) ? r.color : spec.color;
|
|
6209
8430
|
return out;
|
|
6210
8431
|
}
|
|
6211
|
-
var
|
|
6212
|
-
var formatRgba = (c) => c[3] >= 1 ? `#${
|
|
8432
|
+
var hex23 = (v2) => Math.round(Math.min(1, Math.max(0, v2)) * 255).toString(16).padStart(2, "0");
|
|
8433
|
+
var formatRgba = (c) => c[3] >= 1 ? `#${hex23(c[0])}${hex23(c[1])}${hex23(c[2])}` : `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(c[3] * 1e3) / 1e3})`;
|
|
6213
8434
|
var luma = (c) => 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
|
6214
8435
|
var mix = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, a[3]];
|
|
6215
8436
|
var HEAT = [
|
|
@@ -6631,15 +8852,15 @@ var BERN_INV = [
|
|
|
6631
8852
|
[0, 0, 0, 1]
|
|
6632
8853
|
];
|
|
6633
8854
|
var pct = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? Math.min(1, Math.max(-1, v2 / 100)) : 0;
|
|
6634
|
-
function warpPresetMesh(
|
|
6635
|
-
if (!WARP_STYLES.includes(
|
|
8855
|
+
function warpPresetMesh(preset2, bounds) {
|
|
8856
|
+
if (!WARP_STYLES.includes(preset2.style)) return null;
|
|
6636
8857
|
const { x, y, width: w, height: h } = bounds;
|
|
6637
8858
|
if (!(w > 0 && h > 0)) return null;
|
|
6638
|
-
const b = pct(
|
|
6639
|
-
const hd = pct(
|
|
6640
|
-
const vd = pct(
|
|
8859
|
+
const b = pct(preset2.bend);
|
|
8860
|
+
const hd = pct(preset2.horizontal);
|
|
8861
|
+
const vd = pct(preset2.vertical);
|
|
6641
8862
|
if (b === 0 && hd === 0 && vd === 0) return null;
|
|
6642
|
-
const grid = warpPresetGrid(
|
|
8863
|
+
const grid = warpPresetGrid(preset2.style, b, hd, vd, preset2.orientation === "vertical", h / w);
|
|
6643
8864
|
return { us: [0, 1], vs: [0, 1], points: grid.flat().map((p) => ({ x: x + p[0] * w, y: y + p[1] * h })) };
|
|
6644
8865
|
}
|
|
6645
8866
|
var warpMeshColumns = (m) => 3 * (m.us.length - 1) + 1;
|
|
@@ -7308,8 +9529,8 @@ var VECTOR_EFFECT_PARAM_SPECS = {
|
|
|
7308
9529
|
var cloneMesh = (m) => ({ us: m.us.slice(), vs: m.vs.slice(), points: m.points.map((p) => ({ x: p.x, y: p.y })) });
|
|
7309
9530
|
function vectorEnvelopeMesh(params, bounds) {
|
|
7310
9531
|
if (params.style === "mesh") return params.mesh ?? identityWarpMesh(bounds, params.columns, params.rows);
|
|
7311
|
-
const
|
|
7312
|
-
return
|
|
9532
|
+
const preset2 = warpPresetMesh({ style: params.style, bend: params.bend, horizontal: params.horizontal, vertical: params.vertical, orientation: params.orientation }, bounds);
|
|
9533
|
+
return preset2 ? resizeWarpMesh(preset2, params.columns, params.rows) : identityWarpMesh(bounds, params.columns, params.rows);
|
|
7313
9534
|
}
|
|
7314
9535
|
function vectorEnvelopeMap(params, bounds) {
|
|
7315
9536
|
if (!bounds || !(bounds.width > 0 && bounds.height > 0)) return null;
|
|
@@ -8295,7 +10516,7 @@ var add4 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
|
8295
10516
|
var sub4 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
8296
10517
|
var scale2 = (a, k) => ({ x: a.x * k, y: a.y * k });
|
|
8297
10518
|
var left = (d) => ({ x: d.y, y: -d.x });
|
|
8298
|
-
var
|
|
10519
|
+
var unit4 = (v2) => {
|
|
8299
10520
|
const l = Math.hypot(v2.x, v2.y);
|
|
8300
10521
|
return l > 1e-12 ? { x: v2.x / l, y: v2.y / l } : { x: 1, y: 0 };
|
|
8301
10522
|
};
|
|
@@ -8352,7 +10573,7 @@ function vectorStrokeGradientDefs(g, id, node, options = {}) {
|
|
|
8352
10573
|
for (let i = 0; i < segCount; i++) {
|
|
8353
10574
|
const a = pts[i];
|
|
8354
10575
|
const b = pts[(i + 1) % n];
|
|
8355
|
-
dirs.push(
|
|
10576
|
+
dirs.push(unit4(sub4(b, a)));
|
|
8356
10577
|
lengths.push(lengths[i] + Math.hypot(b.x - a.x, b.y - a.y));
|
|
8357
10578
|
}
|
|
8358
10579
|
const total = lengths[segCount];
|
|
@@ -8710,7 +10931,7 @@ function vectorDocumentToSvg(doc, options = {}) {
|
|
|
8710
10931
|
for (const l of layers) {
|
|
8711
10932
|
if (l.kind === "stroke" && isVectorBrushstrokeLayer(l.layer)) {
|
|
8712
10933
|
lines.push(`${indent(depth + 1)}<g data-vector-part="brushstroke" data-vector-layer="${l.index}"${layerAttrs(l.layer)}>`);
|
|
8713
|
-
for (const im of vectorBrushstrokeImages(node, l.layer)) lines.push(`${indent(depth + 2)}<image href="${im.href}" x="${num3(im.x)}" y="${num3(im.y)}" width="${num3(im.width)}" height="${num3(im.height)}" preserveAspectRatio="none"/>`);
|
|
10934
|
+
for (const im of vectorBrushstrokeImages(node, l.layer, { document: doc, settle: "now" })) lines.push(`${indent(depth + 2)}<image href="${im.href}" x="${num3(im.x)}" y="${num3(im.y)}" width="${num3(im.width)}" height="${num3(im.height)}" preserveAspectRatio="none"/>`);
|
|
8714
10935
|
lines.push(`${indent(depth + 1)}</g>`);
|
|
8715
10936
|
continue;
|
|
8716
10937
|
}
|
|
@@ -9666,9 +11887,9 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
9666
11887
|
const symbolId = symbolFor(target);
|
|
9667
11888
|
if (!symbolId) return null;
|
|
9668
11889
|
const own = json2(el.attrs["data-vector-instance"]);
|
|
9669
|
-
const
|
|
11890
|
+
const place3 = multiplyMatrices(translateMatrix(length(el.attrs.x), length(el.attrs.y)), m);
|
|
9670
11891
|
const node = { id: idFor(el), type: "instance", ...meta, symbolId, ...own?.scaleStrokes === false ? { scaleStrokes: false } : {}, ...own?.overrides ? { overrides: own.overrides } : {} };
|
|
9671
|
-
return isIdentityMatrix(
|
|
11892
|
+
return isIdentityMatrix(place3) ? node : { ...node, transform: place3 };
|
|
9672
11893
|
};
|
|
9673
11894
|
const ownInstance = (g, m, meta) => {
|
|
9674
11895
|
const raw = json2(g.attrs["data-vector-instance"]);
|
|
@@ -9677,7 +11898,7 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
9677
11898
|
const symbolId = sym2 ? symbolFor(sym2) : raw.symbolId;
|
|
9678
11899
|
if (!symbolId) return null;
|
|
9679
11900
|
const t = Array.isArray(raw.transform) && raw.transform.length === 6 && raw.transform.every(Number.isFinite) ? raw.transform : IDENTITY_MATRIX;
|
|
9680
|
-
const
|
|
11901
|
+
const place3 = multiplyMatrices(t, m);
|
|
9681
11902
|
const drawn = g.children.find((c) => c.name === "g");
|
|
9682
11903
|
const effects = sanitizeVectorEffects(raw.effects);
|
|
9683
11904
|
const node = {
|
|
@@ -9700,7 +11921,7 @@ function parseVectorSvg(svg, options = {}) {
|
|
|
9700
11921
|
if (drawn.attrs.display === "none") node.visible = false;
|
|
9701
11922
|
usedIds.add(node.id);
|
|
9702
11923
|
}
|
|
9703
|
-
return isIdentityMatrix(
|
|
11924
|
+
return isIdentityMatrix(place3) ? node : { ...node, transform: place3 };
|
|
9704
11925
|
};
|
|
9705
11926
|
const visit2 = (el, style, matrix) => {
|
|
9706
11927
|
const out = [];
|
|
@@ -10843,19 +13064,73 @@ export {
|
|
|
10843
13064
|
shadeVectorColor,
|
|
10844
13065
|
vectorPathRings,
|
|
10845
13066
|
vectorExtrudeFaces,
|
|
13067
|
+
EFFECT_STRIP_HEIGHT,
|
|
13068
|
+
EFFECT_MAP_REPEATS,
|
|
13069
|
+
effectPaintSize,
|
|
13070
|
+
readEffectStrokeMap,
|
|
13071
|
+
effectPaintKey,
|
|
13072
|
+
isPaintEffectPaint,
|
|
13073
|
+
readPaintEffectPaint,
|
|
13074
|
+
sampleEffectField,
|
|
13075
|
+
sampleEffectAt,
|
|
13076
|
+
effectDabField,
|
|
13077
|
+
stampSampler,
|
|
13078
|
+
brushStickerRefs,
|
|
13079
|
+
PAPER_FAMILIES,
|
|
13080
|
+
PAPER_LIBRARY,
|
|
13081
|
+
DEFAULT_PAPER,
|
|
13082
|
+
paperPreset,
|
|
13083
|
+
clonePaper,
|
|
13084
|
+
normalizePaper,
|
|
13085
|
+
hexToRgb8,
|
|
13086
|
+
renderPaper,
|
|
13087
|
+
TipGrain,
|
|
13088
|
+
penOrientation,
|
|
13089
|
+
tiltToOrientation,
|
|
13090
|
+
PenSmoother,
|
|
13091
|
+
IDENTITY_COPY,
|
|
13092
|
+
PenDabWalker,
|
|
13093
|
+
DryStroke,
|
|
13094
|
+
mixerTip,
|
|
13095
|
+
toStrokePoint,
|
|
13096
|
+
multiColorPalette,
|
|
13097
|
+
multiColorGradient,
|
|
13098
|
+
multiColorForDab,
|
|
13099
|
+
PaintDabStroke,
|
|
13100
|
+
wetPaperInput,
|
|
13101
|
+
WetStrokeDriver,
|
|
13102
|
+
WET_MEDIUM_PARAMS,
|
|
13103
|
+
WET_GPU_MODES,
|
|
13104
|
+
GPU_LEAD,
|
|
13105
|
+
PaintWetLayers,
|
|
13106
|
+
wetPigmentOf,
|
|
13107
|
+
VECTOR_WET_STEPS,
|
|
13108
|
+
vectorMediaEngine,
|
|
13109
|
+
usesVectorBackdrop,
|
|
13110
|
+
WetBrushstrokeJob,
|
|
13111
|
+
renderWetBrushstroke,
|
|
13112
|
+
vectorBackdropRect,
|
|
13113
|
+
renderBackdropBrushstroke,
|
|
10846
13114
|
VECTOR_BRUSHSTROKE_SCALE,
|
|
13115
|
+
isVectorBrushstrokeEngine,
|
|
10847
13116
|
vectorBrushstrokeBrushes,
|
|
10848
13117
|
vectorBrushstrokeBrush,
|
|
10849
13118
|
vectorBrushstrokeBrushOf,
|
|
10850
13119
|
vectorBrushstrokeBrushError,
|
|
10851
13120
|
vectorBrushstrokeDefaultColour,
|
|
10852
13121
|
sanitizeVectorBrushstroke,
|
|
13122
|
+
vectorBrushstrokePaintError,
|
|
13123
|
+
readVectorBrushstrokePaint,
|
|
10853
13124
|
VECTOR_BRUSHSTROKE_MAX_SEED,
|
|
10854
13125
|
sanitizeVectorBrushstrokePaper,
|
|
10855
13126
|
vectorBrushstrokeCharacterError,
|
|
10856
13127
|
randomVectorBrushstrokeSeed,
|
|
10857
13128
|
isVectorBrushstrokeLayer,
|
|
10858
13129
|
vectorBrushstrokeReach,
|
|
13130
|
+
setVectorBackdropSource,
|
|
13131
|
+
onVectorBrushstrokesSettled,
|
|
13132
|
+
vectorBrushstrokesSettling,
|
|
13133
|
+
settleVectorBrushstrokes,
|
|
10859
13134
|
vectorBrushstrokeImages,
|
|
10860
13135
|
VECTOR_STROKE_SETTING_KEYS,
|
|
10861
13136
|
VECTOR_BLEND_MODE_LABELS,
|