@pixerate/editor 0.109.0 → 0.111.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/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/blending-DXl06NSx.d.ts +486 -0
- package/dist/blending-aOE6Y3UE.d.cts +486 -0
- package/dist/chunk-7VOHOPLW.cjs +1136 -0
- package/dist/chunk-7VOHOPLW.cjs.map +1 -0
- package/dist/chunk-FTBZPI77.cjs +1 -0
- package/dist/chunk-FTBZPI77.cjs.map +1 -0
- package/dist/{chunk-RE77QYUF.js → chunk-HHZ6ZJTW.js} +880 -86
- package/dist/chunk-HHZ6ZJTW.js.map +1 -0
- package/dist/{chunk-WMBIJCHA.js → chunk-HNS7UHCM.js} +5 -1
- package/dist/{chunk-WMBIJCHA.js.map → chunk-HNS7UHCM.js.map} +1 -1
- package/dist/{chunk-CFMWALBM.js → chunk-LQEEDHOP.js} +2 -2
- package/dist/{chunk-D37BX2ZA.cjs → chunk-PHUMLSCW.cjs} +1141 -347
- package/dist/chunk-PHUMLSCW.cjs.map +1 -0
- package/dist/{chunk-AZTZRLDI.cjs → chunk-POGBUYWE.cjs} +92 -92
- package/dist/{chunk-AZTZRLDI.cjs.map → chunk-POGBUYWE.cjs.map} +1 -1
- package/dist/chunk-VKK5VS2T.js +1 -0
- package/dist/{chunk-SZ6KMZNM.cjs → chunk-XIOMMGPD.cjs} +6 -2
- package/dist/chunk-XIOMMGPD.cjs.map +1 -0
- package/dist/{chunk-3PHA3UKI.js → chunk-Z4BC6M74.js} +523 -121
- package/dist/chunk-Z4BC6M74.js.map +1 -0
- package/dist/{export-BLPLaE6Z.d.ts → export-DBNVwb6h.d.ts} +521 -12
- package/dist/{export-DSUc4fQD.d.cts → export-DoDhCszr.d.cts} +521 -12
- package/dist/image-editor/index.cjs +1630 -900
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +104 -16
- package/dist/image-editor/index.d.ts +104 -16
- package/dist/image-editor/index.js +776 -46
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint/index.cjs +28 -4
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +3 -89
- package/dist/paint/index.d.ts +3 -89
- package/dist/paint/index.js +27 -3
- package/dist/paint-editor/index.cjs +1052 -286
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +302 -23
- package/dist/paint-editor/index.d.ts +302 -23
- package/dist/paint-editor/index.js +848 -82
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +5 -3
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +2 -2
- package/dist/raster/index.d.ts +2 -2
- package/dist/raster/index.js +4 -2
- package/dist/{render-C-fO_mes.d.cts → render-BEriFuqK.d.cts} +1 -1
- package/dist/{render-OLIMk_v0.d.ts → render-tjzKaik-.d.ts} +1 -1
- package/dist/{surface-CkKOHGPm.d.cts → surface-C8RRg_Bh.d.cts} +109 -1
- package/dist/{surface-CkKOHGPm.d.ts → surface-C8RRg_Bh.d.ts} +109 -1
- package/package.json +1 -1
- package/skills/pixerate-image-editor/SKILL.md +27 -30
- package/skills/pixerate-image-editor/references/shapes-and-type.md +60 -0
- package/skills/pixerate-paint-editor/SKILL.md +49 -13
- package/dist/chunk-3PHA3UKI.js.map +0 -1
- package/dist/chunk-6BU5WU4R.cjs +0 -1
- package/dist/chunk-6BU5WU4R.cjs.map +0 -1
- package/dist/chunk-D37BX2ZA.cjs.map +0 -1
- package/dist/chunk-HUFQD4CB.cjs +0 -734
- package/dist/chunk-HUFQD4CB.cjs.map +0 -1
- package/dist/chunk-RE77QYUF.js.map +0 -1
- package/dist/chunk-SZ6KMZNM.cjs.map +0 -1
- package/dist/chunk-XYBJKUAU.js +0 -1
- package/dist/mixer-5ziHU8OS.d.cts +0 -282
- package/dist/mixer-DNqiYXYQ.d.ts +0 -282
- /package/dist/{chunk-CFMWALBM.js.map → chunk-LQEEDHOP.js.map} +0 -0
- /package/dist/{chunk-XYBJKUAU.js.map → chunk-VKK5VS2T.js.map} +0 -0
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
resolveExternalAssets,
|
|
5
5
|
serializeLayers,
|
|
6
6
|
serializeLayersExternal
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-HHZ6ZJTW.js";
|
|
8
8
|
import {
|
|
9
9
|
RasterCompositor,
|
|
10
|
+
base64ToBytes,
|
|
10
11
|
bytesToBase64,
|
|
11
12
|
decodePng,
|
|
12
13
|
encodePng,
|
|
@@ -14,28 +15,39 @@ import {
|
|
|
14
15
|
flattenLayers,
|
|
15
16
|
insertLayer,
|
|
16
17
|
isLayerLocked,
|
|
18
|
+
isPng,
|
|
17
19
|
mapLayer,
|
|
18
20
|
parseDataUrl,
|
|
19
21
|
pngDataUrl
|
|
20
|
-
} from "../chunk-
|
|
22
|
+
} from "../chunk-LQEEDHOP.js";
|
|
21
23
|
import "../chunk-GX332W2C.js";
|
|
22
24
|
import {
|
|
23
25
|
CoverageMap,
|
|
26
|
+
DEFAULT_BLENDER,
|
|
24
27
|
DEFAULT_BRUSH,
|
|
25
28
|
DEFAULT_MIXER,
|
|
29
|
+
DEFAULT_SMUDGE,
|
|
26
30
|
EMPTY_MIXER_LOAD,
|
|
27
31
|
MAX_BRUSH_SIZE,
|
|
28
32
|
MIXING_MODELS,
|
|
29
|
-
|
|
33
|
+
PaintExchange,
|
|
34
|
+
SmudgeTip,
|
|
35
|
+
TileMedium,
|
|
30
36
|
applyStrokeToTile,
|
|
37
|
+
blenderDab,
|
|
31
38
|
cleanMixer,
|
|
39
|
+
createBlenderScratch,
|
|
32
40
|
createPigmentTools,
|
|
33
41
|
dabRect,
|
|
42
|
+
exchangeRates,
|
|
34
43
|
loadMixer,
|
|
44
|
+
mixerRates,
|
|
45
|
+
normalizeBlender,
|
|
35
46
|
normalizeBrush,
|
|
36
47
|
normalizeMixer,
|
|
48
|
+
normalizeSmudge,
|
|
37
49
|
rasterizeDab
|
|
38
|
-
} from "../chunk-
|
|
50
|
+
} from "../chunk-Z4BC6M74.js";
|
|
39
51
|
import {
|
|
40
52
|
BLEND_MODES,
|
|
41
53
|
PAINTERLY_BLEND_MODES,
|
|
@@ -47,13 +59,16 @@ import {
|
|
|
47
59
|
finishTile,
|
|
48
60
|
isBlendMode,
|
|
49
61
|
isPainterlyBlendMode,
|
|
62
|
+
mixPigment8,
|
|
63
|
+
mixPigments,
|
|
50
64
|
readPixel,
|
|
51
65
|
readRegion,
|
|
52
66
|
standardBlendMode,
|
|
53
67
|
surfaceFromPixels,
|
|
54
68
|
tileKey,
|
|
55
|
-
tilesInRect
|
|
56
|
-
|
|
69
|
+
tilesInRect,
|
|
70
|
+
writeRegion
|
|
71
|
+
} from "../chunk-HNS7UHCM.js";
|
|
57
72
|
import {
|
|
58
73
|
PerformanceQueue,
|
|
59
74
|
PresenceStore,
|
|
@@ -438,8 +453,11 @@ function createPaintDocument(options = {}) {
|
|
|
438
453
|
}
|
|
439
454
|
|
|
440
455
|
// src/paint-editor/brushes.ts
|
|
441
|
-
var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer"];
|
|
442
|
-
var PAINT_BRUSH_CATEGORIES = ["pencil", "pastel", "charcoal", "airbrush", "marker", "ink", "eraser", "blender", "smudge", "mixer", "custom"];
|
|
456
|
+
var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer", "blender", "smudge"];
|
|
457
|
+
var PAINT_BRUSH_CATEGORIES = ["pencil", "pastel", "charcoal", "airbrush", "marker", "ink", "paint", "eraser", "blender", "smudge", "mixer", "custom"];
|
|
458
|
+
var MULTI_COLOR_MODES = ["off", "tip", "dab"];
|
|
459
|
+
var DEFAULT_MULTI_COLOR = { mode: "off", colors: [], order: "sequence", length: 12, softness: 1 };
|
|
460
|
+
var DEFAULT_BEHAVIOR = { pickup: 0, mix: 0.5, dirty: false, mixing: "pigment", multicolor: DEFAULT_MULTI_COLOR };
|
|
443
461
|
var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
|
|
444
462
|
var DEFAULT_DYNAMICS = {
|
|
445
463
|
minSize: 0,
|
|
@@ -472,21 +490,62 @@ function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
|
|
|
472
490
|
};
|
|
473
491
|
}
|
|
474
492
|
var BRUSH_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
493
|
+
var HEX2 = /^#[0-9a-f]{6}$/i;
|
|
494
|
+
function normalizeMultiColor(raw = {}, base = DEFAULT_MULTI_COLOR) {
|
|
495
|
+
const colors = Array.isArray(raw.colors) ? raw.colors.filter((c) => typeof c === "string" && HEX2.test(c.trim())).map((c) => c.trim().toLowerCase()).slice(0, 8) : base.colors;
|
|
496
|
+
return {
|
|
497
|
+
mode: MULTI_COLOR_MODES.includes(raw.mode) ? raw.mode : base.mode,
|
|
498
|
+
colors: [...colors],
|
|
499
|
+
order: raw.order === "random" || raw.order === "sequence" ? raw.order : base.order,
|
|
500
|
+
length: typeof raw.length === "number" && Number.isFinite(raw.length) ? Math.min(500, Math.max(1, Math.round(raw.length))) : base.length,
|
|
501
|
+
softness: unit2(raw.softness, base.softness)
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function normalizeBehavior(raw = {}, base = DEFAULT_BEHAVIOR) {
|
|
505
|
+
return {
|
|
506
|
+
pickup: unit2(raw.pickup, base.pickup),
|
|
507
|
+
mix: unit2(raw.mix, base.mix),
|
|
508
|
+
dirty: typeof raw.dirty === "boolean" ? raw.dirty : base.dirty,
|
|
509
|
+
mixing: raw.mixing === "rgb" || raw.mixing === "pigment" ? raw.mixing : base.mixing,
|
|
510
|
+
multicolor: normalizeMultiColor(raw.multicolor ?? {}, base.multicolor)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function isWetCapable(brush) {
|
|
514
|
+
return brush.engine === "dab" || brush.engine === "mixer";
|
|
515
|
+
}
|
|
516
|
+
function usesPaintExchange(brush) {
|
|
517
|
+
return brush.engine === "dab" && (brush.behavior.pickup > 0 || brush.behavior.multicolor.mode !== "off");
|
|
518
|
+
}
|
|
475
519
|
function normalizePaintBrush(raw = {}, base) {
|
|
476
520
|
const b = base ?? BASE_BRUSH;
|
|
477
521
|
const id = typeof raw.id === "string" && BRUSH_ID.test(raw.id) ? raw.id : b.id;
|
|
522
|
+
const engine = PAINT_BRUSH_ENGINES.includes(raw.engine) ? raw.engine : b.engine;
|
|
523
|
+
const rb = raw.behavior ?? {};
|
|
524
|
+
const mixerIn = { ...b.mixer, ...raw.mixer ?? {} };
|
|
525
|
+
if (engine === "mixer") {
|
|
526
|
+
if (rb.pickup !== void 0) mixerIn.wet = rb.pickup;
|
|
527
|
+
if (rb.mix !== void 0) mixerIn.mix = rb.mix;
|
|
528
|
+
if (rb.dirty !== void 0) mixerIn.cleanEachStroke = !rb.dirty;
|
|
529
|
+
if (rb.mixing !== void 0) mixerIn.mixing = rb.mixing;
|
|
530
|
+
}
|
|
531
|
+
const mixer = normalizeMixer(mixerIn);
|
|
532
|
+
let behavior = normalizeBehavior(rb, b.behavior);
|
|
533
|
+
if (engine === "mixer") behavior = { ...behavior, pickup: mixer.wet, mix: mixer.mix, dirty: !mixer.cleanEachStroke, mixing: mixer.mixing };
|
|
478
534
|
return {
|
|
479
535
|
id,
|
|
480
536
|
name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
|
|
481
537
|
category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
|
|
482
|
-
engine
|
|
538
|
+
engine,
|
|
483
539
|
tip: normalizeBrush({ ...b.tip, ...raw.tip ?? {} }),
|
|
484
540
|
grain: unit2(raw.grain, b.grain),
|
|
485
541
|
pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
|
|
486
542
|
rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
|
|
487
543
|
dynamics: normalizeDynamics(raw.dynamics ?? {}, b.dynamics),
|
|
488
|
-
mixer
|
|
544
|
+
mixer,
|
|
489
545
|
loadsColor: typeof raw.loadsColor === "boolean" ? raw.loadsColor : b.loadsColor,
|
|
546
|
+
behavior,
|
|
547
|
+
blender: normalizeBlender(raw.blender ?? {}, b.blender),
|
|
548
|
+
smudge: normalizeSmudge(raw.smudge ?? {}, b.smudge),
|
|
490
549
|
...raw.builtin ? { builtin: true } : {}
|
|
491
550
|
};
|
|
492
551
|
}
|
|
@@ -501,7 +560,10 @@ var BASE_BRUSH = {
|
|
|
501
560
|
rotation: "fixed",
|
|
502
561
|
dynamics: { ...DEFAULT_DYNAMICS },
|
|
503
562
|
mixer: { ...DEFAULT_MIXER },
|
|
504
|
-
loadsColor: true
|
|
563
|
+
loadsColor: true,
|
|
564
|
+
behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
|
|
565
|
+
blender: { ...DEFAULT_BLENDER },
|
|
566
|
+
smudge: { ...DEFAULT_SMUDGE }
|
|
505
567
|
};
|
|
506
568
|
var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
|
|
507
569
|
var BUILTIN_BRUSHES = [
|
|
@@ -615,19 +677,54 @@ var BUILTIN_BRUSHES = [
|
|
|
615
677
|
id: "blender",
|
|
616
678
|
name: "Blender",
|
|
617
679
|
category: "blender",
|
|
618
|
-
engine: "
|
|
680
|
+
engine: "blender",
|
|
619
681
|
loadsColor: false,
|
|
620
|
-
tip: { size: 34, hardness: 0.4, spacing: 0.
|
|
621
|
-
|
|
682
|
+
tip: { size: 34, hardness: 0.4, spacing: 0.1, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
|
|
683
|
+
blender: { strength: 0.5, distance: 0.35, mixing: "pigment" }
|
|
622
684
|
}),
|
|
623
685
|
builtin({
|
|
624
686
|
id: "smudge",
|
|
625
687
|
name: "Smudge",
|
|
626
688
|
category: "smudge",
|
|
627
|
-
engine: "
|
|
689
|
+
engine: "smudge",
|
|
628
690
|
loadsColor: false,
|
|
629
|
-
tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow:
|
|
630
|
-
|
|
691
|
+
tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: false, pressureOpacity: true },
|
|
692
|
+
smudge: { strength: 0.75, length: 0.85, mixing: "rgb" }
|
|
693
|
+
}),
|
|
694
|
+
builtin({
|
|
695
|
+
id: "wet-smudge",
|
|
696
|
+
name: "Pigment smudge",
|
|
697
|
+
category: "smudge",
|
|
698
|
+
engine: "smudge",
|
|
699
|
+
loadsColor: false,
|
|
700
|
+
tip: { size: 30, hardness: 0.5, spacing: 0.06, opacity: 1, flow: 1, smoothing: 0.2, pressureSize: true, pressureOpacity: true },
|
|
701
|
+
smudge: { strength: 0.7, length: 0.9, mixing: "pigment" },
|
|
702
|
+
dynamics: { minSize: 0.6 }
|
|
703
|
+
}),
|
|
704
|
+
builtin({
|
|
705
|
+
id: "dirty-round",
|
|
706
|
+
name: "Dirty round",
|
|
707
|
+
category: "paint",
|
|
708
|
+
tip: { size: 22, hardness: 0.65, spacing: 0.06, opacity: 1, flow: 0.7, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
709
|
+
behavior: { pickup: 0.55, mix: 0.6, dirty: true, mixing: "pigment" },
|
|
710
|
+
dynamics: { minSize: 0.5 }
|
|
711
|
+
}),
|
|
712
|
+
builtin({
|
|
713
|
+
id: "multicolor-flat",
|
|
714
|
+
name: "Multi-colour flat",
|
|
715
|
+
category: "paint",
|
|
716
|
+
rotation: "stroke",
|
|
717
|
+
tip: { size: 30, hardness: 0.75, spacing: 0.05, angle: 90, roundness: 0.3, opacity: 1, flow: 0.85, smoothing: 0.2, pressureSize: true, pressureOpacity: false },
|
|
718
|
+
behavior: { pickup: 0.15, mix: 0.4, mixing: "pigment", multicolor: { mode: "tip", softness: 0.7 } },
|
|
719
|
+
dynamics: { minSize: 0.6 }
|
|
720
|
+
}),
|
|
721
|
+
builtin({
|
|
722
|
+
id: "palette-dabber",
|
|
723
|
+
name: "Palette dabber",
|
|
724
|
+
category: "paint",
|
|
725
|
+
tip: { size: 18, hardness: 0.7, spacing: 0.35, opacity: 1, flow: 0.9, smoothing: 0.1, pressureSize: true, pressureOpacity: false },
|
|
726
|
+
behavior: { multicolor: { mode: "dab", order: "sequence", length: 6, softness: 0.4 } },
|
|
727
|
+
dynamics: { minSize: 0.5, sizeJitter: 0.15, scatter: 0.3 }
|
|
631
728
|
}),
|
|
632
729
|
builtin({
|
|
633
730
|
id: "mixer-brush",
|
|
@@ -807,7 +904,9 @@ var PenDabWalker = class {
|
|
|
807
904
|
opacity: Math.min(1, Math.max(0, opacity)),
|
|
808
905
|
angle,
|
|
809
906
|
roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),
|
|
810
|
-
pressure: s.pressure
|
|
907
|
+
pressure: s.pressure,
|
|
908
|
+
direction: this.dir,
|
|
909
|
+
index: i
|
|
811
910
|
};
|
|
812
911
|
}
|
|
813
912
|
push(p, out) {
|
|
@@ -938,6 +1037,248 @@ function toStrokePoint(p) {
|
|
|
938
1037
|
return { x: p.x, y: p.y, pressure: pressureOf(p), ...typeof p.t === "number" ? { t: p.t } : {} };
|
|
939
1038
|
}
|
|
940
1039
|
|
|
1040
|
+
// src/paint-editor/dab-stroke.ts
|
|
1041
|
+
var GRADIENT_STEPS = 32;
|
|
1042
|
+
function multiColorPalette(mc, color, secondary) {
|
|
1043
|
+
if (mc.colors.length >= 2) {
|
|
1044
|
+
return mc.colors.map((h) => {
|
|
1045
|
+
const n = parseInt(h.slice(1), 16);
|
|
1046
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
return [[...color], [...secondary]];
|
|
1050
|
+
}
|
|
1051
|
+
function soften(f, softness) {
|
|
1052
|
+
if (softness >= 1) return f;
|
|
1053
|
+
const w = Math.max(1e-3, softness);
|
|
1054
|
+
return Math.min(1, Math.max(0, (f - 0.5) / w + 0.5));
|
|
1055
|
+
}
|
|
1056
|
+
function mixRgb(a, b, t, mixing) {
|
|
1057
|
+
if (t <= 0) return [...a];
|
|
1058
|
+
if (t >= 1) return [...b];
|
|
1059
|
+
if (mixing === "pigment") {
|
|
1060
|
+
const o = [0, 0, 0];
|
|
1061
|
+
const q = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255);
|
|
1062
|
+
mixPigment8(q(a[0]), q(a[1]), q(a[2]), q(b[0]), q(b[1]), q(b[2]), t, o);
|
|
1063
|
+
return [o[0], o[1], o[2]];
|
|
1064
|
+
}
|
|
1065
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
1066
|
+
}
|
|
1067
|
+
function multiColorGradient(palette, softness, mixing) {
|
|
1068
|
+
const n = palette.length;
|
|
1069
|
+
const out = [];
|
|
1070
|
+
for (let j = 0; j < GRADIENT_STEPS; j++) {
|
|
1071
|
+
const f = j / (GRADIENT_STEPS - 1) * (n - 1);
|
|
1072
|
+
const i = Math.min(n - 2, Math.floor(f));
|
|
1073
|
+
out.push(mixRgb(palette[i], palette[i + 1], soften(f - i, softness), mixing));
|
|
1074
|
+
}
|
|
1075
|
+
return out;
|
|
1076
|
+
}
|
|
1077
|
+
function multiColorForDab(mc, palette, index, seed, mixing) {
|
|
1078
|
+
const n = palette.length;
|
|
1079
|
+
if (mc.order === "random") {
|
|
1080
|
+
const run = Math.floor(index / mc.length);
|
|
1081
|
+
return [...palette[Math.min(n - 1, Math.floor(rand01(seed, run, 11) * n))]];
|
|
1082
|
+
}
|
|
1083
|
+
const f = index / mc.length % n;
|
|
1084
|
+
const i = Math.floor(f);
|
|
1085
|
+
return mixRgb(palette[i], palette[(i + 1) % n], soften(f - i, mc.softness), mixing);
|
|
1086
|
+
}
|
|
1087
|
+
var PaintDabStroke = class {
|
|
1088
|
+
constructor(brush, options) {
|
|
1089
|
+
this.brush = brush;
|
|
1090
|
+
this.options = options;
|
|
1091
|
+
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
1092
|
+
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
1093
|
+
const exchanging = brush.engine === "mixer" || brush.engine === "dab";
|
|
1094
|
+
const rates = brush.engine === "mixer" ? mixerRates(brush.mixer) : exchangeRates(brush.behavior.pickup, brush.behavior.mix, 1, brush.behavior.mixing);
|
|
1095
|
+
this.mixing = brush.engine === "mixer" ? brush.mixer.mixing : brush.behavior.mixing;
|
|
1096
|
+
this.states = this.copies.map((c, i) => ({
|
|
1097
|
+
walker: new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear),
|
|
1098
|
+
exchange: exchanging ? new PaintExchange(rates, options.load) : null,
|
|
1099
|
+
smudge: brush.engine === "smudge" ? new SmudgeTip(brush.smudge, brush.tip.size * (1 + brush.dynamics.tiltSize * 1.5)) : null
|
|
1100
|
+
}));
|
|
1101
|
+
this.medium = new TileMedium(options.target, brush.engine === "dab" ? { blend: brush.tip.blend } : {});
|
|
1102
|
+
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
1103
|
+
this.coverage = brush.engine === "dab" ? new CoverageMap() : null;
|
|
1104
|
+
const mc = brush.behavior.multicolor;
|
|
1105
|
+
this.palette = exchanging && mc.mode !== "off" ? multiColorPalette(mc, options.color, options.secondary) : null;
|
|
1106
|
+
this.gradient = this.palette && mc.mode === "tip" ? multiColorGradient(this.palette, mc.softness, this.mixing) : null;
|
|
1107
|
+
this.scratch = brush.engine === "blender" ? createBlenderScratch() : null;
|
|
1108
|
+
}
|
|
1109
|
+
brush;
|
|
1110
|
+
options;
|
|
1111
|
+
dabCount = 0;
|
|
1112
|
+
smoother;
|
|
1113
|
+
copies;
|
|
1114
|
+
states;
|
|
1115
|
+
medium;
|
|
1116
|
+
depth;
|
|
1117
|
+
coverage;
|
|
1118
|
+
palette;
|
|
1119
|
+
gradient;
|
|
1120
|
+
fieldLut = [];
|
|
1121
|
+
scratch;
|
|
1122
|
+
mixing;
|
|
1123
|
+
tip = new Float32Array(0);
|
|
1124
|
+
cov = new Float32Array(0);
|
|
1125
|
+
field = new Float32Array(0);
|
|
1126
|
+
paint = [0, 0, 0];
|
|
1127
|
+
lastRaw = null;
|
|
1128
|
+
lastFed = null;
|
|
1129
|
+
/** What copy 0's brush carries after the dabs so far (mixer and wet-capable dab brushes). */
|
|
1130
|
+
get load() {
|
|
1131
|
+
return this.states[0].exchange?.load ?? null;
|
|
1132
|
+
}
|
|
1133
|
+
push(points) {
|
|
1134
|
+
const rects = [];
|
|
1135
|
+
for (const raw of points) {
|
|
1136
|
+
this.lastRaw = raw;
|
|
1137
|
+
const p = this.smoother.push(raw);
|
|
1138
|
+
this.lastFed = p;
|
|
1139
|
+
this.copies.forEach((c, i) => {
|
|
1140
|
+
const dabs = [];
|
|
1141
|
+
this.states[i].walker.push(c.map(p), dabs);
|
|
1142
|
+
this.paintDabs(i, dabs, rects);
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
return rects;
|
|
1146
|
+
}
|
|
1147
|
+
/** Ends the stroke: the smoothed path catches up with the last raw sample; a tap paints one dab. */
|
|
1148
|
+
finish() {
|
|
1149
|
+
const rects = [];
|
|
1150
|
+
const raw = this.lastRaw;
|
|
1151
|
+
const fed = this.lastFed;
|
|
1152
|
+
this.copies.forEach((c, i) => {
|
|
1153
|
+
const dabs = [];
|
|
1154
|
+
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
1155
|
+
this.states[i].walker.finish(dabs);
|
|
1156
|
+
this.paintDabs(i, dabs, rects);
|
|
1157
|
+
});
|
|
1158
|
+
return rects;
|
|
1159
|
+
}
|
|
1160
|
+
paintDabs(copy, dabs, rects) {
|
|
1161
|
+
for (const d of dabs) {
|
|
1162
|
+
const rect = dabRect(d, this.brush.tip.hardness);
|
|
1163
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1164
|
+
this.dab(copy, d, rect);
|
|
1165
|
+
rects.push(rect);
|
|
1166
|
+
this.dabCount++;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
/** The tip's pure coverage (`tip`) and, with grain, what the paper catches of it (`cov`). */
|
|
1170
|
+
rasterize(d, rect) {
|
|
1171
|
+
const n = rect.width * rect.height;
|
|
1172
|
+
if (this.tip.length < n) {
|
|
1173
|
+
this.tip = new Float32Array(n);
|
|
1174
|
+
this.cov = new Float32Array(n);
|
|
1175
|
+
}
|
|
1176
|
+
const tip = this.tip.subarray(0, n);
|
|
1177
|
+
tip.fill(0);
|
|
1178
|
+
rasterizeDab({ ...d, alpha: 1 }, this.brush.tip.hardness, rect, tip);
|
|
1179
|
+
if (!(this.depth > 0 && this.options.grain)) return { tip, cov: tip };
|
|
1180
|
+
const cov = this.cov.subarray(0, n);
|
|
1181
|
+
const grain = this.options.grain;
|
|
1182
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1183
|
+
const y = rect.y + yy + 0.5;
|
|
1184
|
+
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;
|
|
1185
|
+
}
|
|
1186
|
+
return { tip, cov };
|
|
1187
|
+
}
|
|
1188
|
+
dab(copy, d, rect) {
|
|
1189
|
+
const brush = this.brush;
|
|
1190
|
+
const { tip, cov } = this.rasterize(d, rect);
|
|
1191
|
+
const st = this.states[copy];
|
|
1192
|
+
if (brush.engine === "blender") {
|
|
1193
|
+
blenderDab(this.medium, rect, cov, d.opacity * d.alpha * brush.tip.opacity, d.radius, brush.blender, this.scratch);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (brush.engine === "smudge") {
|
|
1197
|
+
st.smudge.dab(this.medium, { x: d.x, y: d.y, radius: d.radius, opacity: d.opacity * d.alpha * brush.tip.opacity }, rect, cov);
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const ex = st.exchange;
|
|
1201
|
+
ex.pickUp(this.medium.sample(rect, tip));
|
|
1202
|
+
const mc = brush.behavior.multicolor;
|
|
1203
|
+
if (this.palette && mc.mode === "dab") {
|
|
1204
|
+
const c = multiColorForDab(mc, this.palette, d.index, this.options.seed ?? 0, this.mixing);
|
|
1205
|
+
ex.setReservoirColor(c[0], c[1], c[2]);
|
|
1206
|
+
}
|
|
1207
|
+
let field = null;
|
|
1208
|
+
let total;
|
|
1209
|
+
if (this.gradient) {
|
|
1210
|
+
total = this.tipField(ex, d, rect);
|
|
1211
|
+
field = this.field;
|
|
1212
|
+
} else total = ex.paint(this.paint);
|
|
1213
|
+
if (total > 0) {
|
|
1214
|
+
if (this.coverage) this.depositWithCeiling(d, rect, cov, field, ex);
|
|
1215
|
+
else {
|
|
1216
|
+
const dep = Math.min(1, total) * d.alpha * brush.tip.opacity * d.opacity;
|
|
1217
|
+
if (dep > 0) this.medium.deposit(rect, cov, dep, this.paint, ex.paintPigment, field);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
ex.drain();
|
|
1221
|
+
}
|
|
1222
|
+
/** Fills `this.field` with the tip gradient (mixed with the carried paint) per pixel; returns the paint weight. */
|
|
1223
|
+
tipField(ex, d, rect) {
|
|
1224
|
+
const g = this.gradient;
|
|
1225
|
+
const lut = this.fieldLut;
|
|
1226
|
+
let total = 0;
|
|
1227
|
+
for (let j = 0; j < g.length; j++) {
|
|
1228
|
+
ex.setReservoirColor(g[j][0], g[j][1], g[j][2]);
|
|
1229
|
+
const out = lut[j] ?? [0, 0, 0];
|
|
1230
|
+
total = ex.paint(out);
|
|
1231
|
+
lut[j] = out;
|
|
1232
|
+
}
|
|
1233
|
+
const n = rect.width * rect.height;
|
|
1234
|
+
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
1235
|
+
const f = this.field;
|
|
1236
|
+
const nx = Math.sin(d.direction);
|
|
1237
|
+
const ny = Math.cos(d.direction);
|
|
1238
|
+
const r = Math.max(0.5, d.radius);
|
|
1239
|
+
const last = g.length - 1;
|
|
1240
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1241
|
+
const py = rect.y + yy + 0.5 - d.y;
|
|
1242
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1243
|
+
const u = ((rect.x + xx + 0.5 - d.x) * nx + py * ny) / r;
|
|
1244
|
+
const j = Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)));
|
|
1245
|
+
f[k * 3] = lut[j][0];
|
|
1246
|
+
f[k * 3 + 1] = lut[j][1];
|
|
1247
|
+
f[k * 3 + 2] = lut[j][2];
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return total;
|
|
1251
|
+
}
|
|
1252
|
+
/** A dab brush's deposit: only what this dab adds to the stroke's coverage, under its opacity ceiling. */
|
|
1253
|
+
depositWithCeiling(d, rect, cov, field, ex) {
|
|
1254
|
+
const map = this.coverage;
|
|
1255
|
+
const n = rect.width * rect.height;
|
|
1256
|
+
const before = new Float32Array(n);
|
|
1257
|
+
const vals = new Float32Array(n);
|
|
1258
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1259
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1260
|
+
if (cov[k] <= 0) continue;
|
|
1261
|
+
before[k] = map.get(rect.x + xx, rect.y + yy);
|
|
1262
|
+
vals[k] = cov[k] * d.alpha;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
map.accumulate(rect, vals, d.opacity);
|
|
1266
|
+
const o = this.brush.tip.opacity;
|
|
1267
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
1268
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
1269
|
+
if (cov[k] <= 0) {
|
|
1270
|
+
vals[k] = 0;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const c0 = before[k] * o;
|
|
1274
|
+
const c1 = map.get(rect.x + xx, rect.y + yy) * o;
|
|
1275
|
+
vals[k] = c1 > c0 && c0 < 1 ? (c1 - c0) / (1 - c0) : 0;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
this.medium.deposit(rect, vals, 1, this.paint, ex.paintPigment, field);
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
|
|
941
1282
|
// src/paint-editor/stroke.ts
|
|
942
1283
|
function defaultTile(surface) {
|
|
943
1284
|
const data = new Uint8ClampedArray(TILE * TILE * surface.channels);
|
|
@@ -976,10 +1317,18 @@ var PaintStrokeSession = class {
|
|
|
976
1317
|
selection: null,
|
|
977
1318
|
offset: init.offset
|
|
978
1319
|
};
|
|
979
|
-
if (brush.engine === "mixer") {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1320
|
+
if (brush.engine === "mixer" || brush.engine === "blender" || brush.engine === "smudge" || usesPaintExchange(brush)) {
|
|
1321
|
+
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
1322
|
+
this.wet = new PaintDabStroke(brush, {
|
|
1323
|
+
target: this.tileTarget(),
|
|
1324
|
+
color: rgb(init.color),
|
|
1325
|
+
secondary: rgb(init.secondary ?? init.background),
|
|
1326
|
+
load: init.mixerLoad,
|
|
1327
|
+
grain: init.grain,
|
|
1328
|
+
paperDepth: init.paperDepth,
|
|
1329
|
+
seed: init.seed,
|
|
1330
|
+
copies: this.copies
|
|
1331
|
+
});
|
|
983
1332
|
} else {
|
|
984
1333
|
this.dry = new DryStroke(brush, { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies });
|
|
985
1334
|
}
|
|
@@ -994,15 +1343,14 @@ var PaintStrokeSession = class {
|
|
|
994
1343
|
touched = /* @__PURE__ */ new Set();
|
|
995
1344
|
fresh = /* @__PURE__ */ new Set();
|
|
996
1345
|
dry = null;
|
|
997
|
-
|
|
998
|
-
mixerSmoother = null;
|
|
1346
|
+
wet = null;
|
|
999
1347
|
apply;
|
|
1000
1348
|
copies;
|
|
1001
1349
|
bounds = null;
|
|
1002
1350
|
lastRaw = null;
|
|
1003
1351
|
closed = false;
|
|
1004
1352
|
result = null;
|
|
1005
|
-
|
|
1353
|
+
tileTarget() {
|
|
1006
1354
|
return {
|
|
1007
1355
|
offset: this.init.offset,
|
|
1008
1356
|
lockTransparency: this.init.lockTransparency,
|
|
@@ -1030,15 +1378,7 @@ var PaintStrokeSession = class {
|
|
|
1030
1378
|
if (filter) points = points.map(filter);
|
|
1031
1379
|
for (const p of points) this.points.push(p);
|
|
1032
1380
|
this.lastRaw = points[points.length - 1];
|
|
1033
|
-
|
|
1034
|
-
else {
|
|
1035
|
-
const rects = [];
|
|
1036
|
-
for (const raw of points) {
|
|
1037
|
-
const p = toStrokePoint(this.mixerSmoother.push(raw));
|
|
1038
|
-
this.copies.forEach((c, i) => rects.push(...this.mixers[i].push([toStrokePoint(c.map(p))])));
|
|
1039
|
-
}
|
|
1040
|
-
this.update(rects);
|
|
1041
|
-
}
|
|
1381
|
+
this.update(this.dry ? this.dry.push(points) : this.wet.push(points));
|
|
1042
1382
|
}
|
|
1043
1383
|
update(rects) {
|
|
1044
1384
|
if (!rects.length) return;
|
|
@@ -1071,13 +1411,7 @@ var PaintStrokeSession = class {
|
|
|
1071
1411
|
/** Ends the stroke (one undo step) and returns what changed. */
|
|
1072
1412
|
end() {
|
|
1073
1413
|
if (this.closed) throw new Error("This stroke has already ended.");
|
|
1074
|
-
|
|
1075
|
-
else {
|
|
1076
|
-
const rects = [];
|
|
1077
|
-
const last = this.lastRaw ? toStrokePoint(this.lastRaw) : void 0;
|
|
1078
|
-
this.copies.forEach((c, i) => rects.push(...this.mixers[i].finish(last ? toStrokePoint(c.map(last)) : void 0)));
|
|
1079
|
-
this.update(rects);
|
|
1080
|
-
}
|
|
1414
|
+
this.update(this.dry ? this.dry.finish() : this.wet.finish());
|
|
1081
1415
|
this.closed = true;
|
|
1082
1416
|
const ch = this.base.channels;
|
|
1083
1417
|
const tiles = new Map(this.base.tiles);
|
|
@@ -1090,14 +1424,10 @@ var PaintStrokeSession = class {
|
|
|
1090
1424
|
if (!sameTile(prev, tile)) changed = true;
|
|
1091
1425
|
}
|
|
1092
1426
|
const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
|
|
1093
|
-
const dabs = this.dry ? this.dry.dabCount : this.
|
|
1427
|
+
const dabs = this.dry ? this.dry.dabCount : this.wet.dabCount;
|
|
1094
1428
|
this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
let load = this.mixers[0].load;
|
|
1098
|
-
if (brush.mixer.cleanEachStroke) load = cleanMixer(load);
|
|
1099
|
-
this.result.mixerLoad = load;
|
|
1100
|
-
}
|
|
1429
|
+
const carried = this.wet?.load;
|
|
1430
|
+
if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);
|
|
1101
1431
|
this.host.commit(this, surface, false);
|
|
1102
1432
|
return this.result;
|
|
1103
1433
|
}
|
|
@@ -1113,6 +1443,11 @@ function mixerLoadForStroke(brush, current, color) {
|
|
|
1113
1443
|
if (brush.mixer.loadEachStroke || !current.reservoir) return loadMixer(current, color);
|
|
1114
1444
|
return current;
|
|
1115
1445
|
}
|
|
1446
|
+
function brushLoadForStroke(brush, current, color) {
|
|
1447
|
+
if (brush.engine === "mixer") return mixerLoadForStroke(brush, current, color);
|
|
1448
|
+
if (usesPaintExchange(brush)) return { reservoir: [color[0], color[1], color[2]], amount: 1, pickup: brush.behavior.dirty ? current.pickup : null };
|
|
1449
|
+
return EMPTY_MIXER_LOAD;
|
|
1450
|
+
}
|
|
1116
1451
|
|
|
1117
1452
|
// src/paint-editor/strokes.ts
|
|
1118
1453
|
var IDENTITY_AFFINE = [1, 0, 0, 1, 0, 0];
|
|
@@ -1158,6 +1493,17 @@ function readPoint(raw) {
|
|
|
1158
1493
|
function readAffine(raw) {
|
|
1159
1494
|
return Array.isArray(raw) && raw.length === 6 && raw.every(finite) ? raw : null;
|
|
1160
1495
|
}
|
|
1496
|
+
function readRgb(raw) {
|
|
1497
|
+
return Array.isArray(raw) && raw.length === 3 && raw.every(finite) ? raw.map((v) => Math.min(1, Math.max(0, v))) : null;
|
|
1498
|
+
}
|
|
1499
|
+
function readLoad(raw) {
|
|
1500
|
+
if (!raw || typeof raw !== "object") return null;
|
|
1501
|
+
const r = raw;
|
|
1502
|
+
const reservoir = readRgb(r.reservoir);
|
|
1503
|
+
const pickup = readRgb(r.pickup);
|
|
1504
|
+
if (!reservoir && !pickup) return null;
|
|
1505
|
+
return { reservoir, amount: reservoir && finite(r.amount) ? Math.min(1, Math.max(0, r.amount)) : 0, pickup };
|
|
1506
|
+
}
|
|
1161
1507
|
function normalizeBrushStroke(raw) {
|
|
1162
1508
|
if (!raw || typeof raw !== "object") return null;
|
|
1163
1509
|
const r = raw;
|
|
@@ -1174,6 +1520,8 @@ function normalizeBrushStroke(raw) {
|
|
|
1174
1520
|
};
|
|
1175
1521
|
const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m) => m !== null) : [];
|
|
1176
1522
|
if (copies.length) out.copies = copies;
|
|
1523
|
+
const load = readLoad(r.load);
|
|
1524
|
+
if (load) out.load = load;
|
|
1177
1525
|
return out;
|
|
1178
1526
|
}
|
|
1179
1527
|
function strokeBounds(points) {
|
|
@@ -1325,7 +1673,7 @@ function downsampleRgba(data, width, height, tw, th) {
|
|
|
1325
1673
|
// src/paint-editor/serialize.ts
|
|
1326
1674
|
var PAINT_FORMAT = "pixerate-paint";
|
|
1327
1675
|
var PAINT_SCHEMA_VERSION = 1;
|
|
1328
|
-
var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches"]);
|
|
1676
|
+
var KNOWN = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "guides", "swatches", "mixingPad"]);
|
|
1329
1677
|
function documentExtensions(raw) {
|
|
1330
1678
|
const out = {};
|
|
1331
1679
|
for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;
|
|
@@ -1345,6 +1693,7 @@ function header(state, options) {
|
|
|
1345
1693
|
if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));
|
|
1346
1694
|
if (options.guides !== void 0) out.guides = JSON.parse(JSON.stringify(options.guides));
|
|
1347
1695
|
if (options.swatches !== void 0) out.swatches = JSON.parse(JSON.stringify(options.swatches));
|
|
1696
|
+
if (options.mixingPad !== void 0) out.mixingPad = JSON.parse(JSON.stringify(options.mixingPad));
|
|
1348
1697
|
return out;
|
|
1349
1698
|
}
|
|
1350
1699
|
function serializePaintDocument(state, options = {}) {
|
|
@@ -1392,7 +1741,7 @@ function deserializePaintDocument(raw) {
|
|
|
1392
1741
|
const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);
|
|
1393
1742
|
const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
|
|
1394
1743
|
const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : flattenLayers(layers).filter((l) => l.type === "pixel").pop()?.id ?? null;
|
|
1395
|
-
return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, problems };
|
|
1744
|
+
return { state: { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) }, extensions: documentExtensions(r), guides: r.guides, swatches: r.swatches, mixingPad: r.mixingPad, problems };
|
|
1396
1745
|
}
|
|
1397
1746
|
async function deserializePaintDocumentAsync(raw, store) {
|
|
1398
1747
|
if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
|
|
@@ -1577,7 +1926,7 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
1577
1926
|
colorHex: color,
|
|
1578
1927
|
affineCopies: [],
|
|
1579
1928
|
copies: [IDENTITY_COPY],
|
|
1580
|
-
mixerLoad:
|
|
1929
|
+
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
|
|
1581
1930
|
}
|
|
1582
1931
|
);
|
|
1583
1932
|
session.push(thumbnailStroke(width, height));
|
|
@@ -1593,7 +1942,7 @@ function renderBrushThumbnail(brush, options) {
|
|
|
1593
1942
|
let surface = emptySurface(4);
|
|
1594
1943
|
if (b.engine === "eraser") {
|
|
1595
1944
|
surface = fillRect(surface, { x: 0, y: 0, width, height }, rgba);
|
|
1596
|
-
} else if (b.engine === "mixer" && !b.loadsColor) {
|
|
1945
|
+
} else if (b.engine === "blender" || b.engine === "smudge" || b.engine === "mixer" && !b.loadsColor || b.behavior.pickup > 0) {
|
|
1597
1946
|
const half = Math.ceil(height / 2);
|
|
1598
1947
|
surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);
|
|
1599
1948
|
surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
|
|
@@ -1786,9 +2135,9 @@ function perspectiveGuideLines(g, size, count = 12) {
|
|
|
1786
2135
|
}
|
|
1787
2136
|
|
|
1788
2137
|
// src/paint-editor/color.ts
|
|
1789
|
-
var
|
|
2138
|
+
var HEX3 = /^#?([0-9a-f]{6})$/i;
|
|
1790
2139
|
function parseHexColor(value, what = "color") {
|
|
1791
|
-
const m = typeof value === "string" ?
|
|
2140
|
+
const m = typeof value === "string" ? HEX3.exec(value.trim()) : null;
|
|
1792
2141
|
if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
1793
2142
|
const n = parseInt(m[1], 16);
|
|
1794
2143
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
@@ -1916,10 +2265,18 @@ var PAD_KNIFE = normalizePaintBrush({
|
|
|
1916
2265
|
id: "pad-knife",
|
|
1917
2266
|
name: "Palette knife",
|
|
1918
2267
|
category: "smudge",
|
|
1919
|
-
engine: "
|
|
2268
|
+
engine: "smudge",
|
|
1920
2269
|
loadsColor: false,
|
|
1921
|
-
tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow:
|
|
1922
|
-
|
|
2270
|
+
tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow: 1, pressureSize: false, pressureOpacity: false },
|
|
2271
|
+
// A knife drags streaks of paint and folds them into each other, mixing them as pigment.
|
|
2272
|
+
smudge: { strength: 0.6, length: 0.8, mixing: "pigment" }
|
|
2273
|
+
});
|
|
2274
|
+
var PAD_BLOB = normalizePaintBrush({
|
|
2275
|
+
id: "pad-blob",
|
|
2276
|
+
name: "Pad blob",
|
|
2277
|
+
category: "custom",
|
|
2278
|
+
engine: "dab",
|
|
2279
|
+
tip: { size: 20, hardness: 1, spacing: 0.1, opacity: 1, flow: 1, pressureSize: false, pressureOpacity: false }
|
|
1923
2280
|
});
|
|
1924
2281
|
var MixingPad = class {
|
|
1925
2282
|
width = MIXING_PAD_WIDTH;
|
|
@@ -1937,7 +2294,11 @@ var MixingPad = class {
|
|
|
1937
2294
|
}
|
|
1938
2295
|
let brush = options.tool === "knife" ? PAD_KNIFE : PAD_PAINT;
|
|
1939
2296
|
if (options.size !== void 0) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);
|
|
1940
|
-
|
|
2297
|
+
return this.run(brush, points, options.color);
|
|
2298
|
+
}
|
|
2299
|
+
/** Paints `points` with `brush` in `color` on the pad. */
|
|
2300
|
+
run(brush, points, color) {
|
|
2301
|
+
const [r, g, b] = hexToRgb8(color);
|
|
1941
2302
|
let result = null;
|
|
1942
2303
|
const session = new PaintStrokeSession(
|
|
1943
2304
|
{ update: () => {
|
|
@@ -1953,10 +2314,10 @@ var MixingPad = class {
|
|
|
1953
2314
|
grain: null,
|
|
1954
2315
|
paperDepth: 0,
|
|
1955
2316
|
seed: 1,
|
|
1956
|
-
colorHex:
|
|
2317
|
+
colorHex: color,
|
|
1957
2318
|
affineCopies: [],
|
|
1958
2319
|
copies: [IDENTITY_COPY],
|
|
1959
|
-
mixerLoad:
|
|
2320
|
+
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
|
|
1960
2321
|
}
|
|
1961
2322
|
);
|
|
1962
2323
|
session.push(points);
|
|
@@ -1995,6 +2356,82 @@ var MixingPad = class {
|
|
|
1995
2356
|
this.surface = emptySurface(4);
|
|
1996
2357
|
this.version++;
|
|
1997
2358
|
}
|
|
2359
|
+
/**
|
|
2360
|
+
* Mixes paints the way a painter does on a palette: blobs whose areas are in proportion to
|
|
2361
|
+
* their parts, side by side around (x, y), spiral knife passes that fold them together, and then the centre worked until it is fully mixed: a soft-edged core
|
|
2362
|
+
* of the Kubelka-Munk mix of the parts (`mixPigments`), with partly mixed streaks around it.
|
|
2363
|
+
* Returns that colour (picked back from the centre) and the pile's bounds.
|
|
2364
|
+
*
|
|
2365
|
+
* The core is laid, not smeared, because smearing alone drifts: the knife samples in RGB and a
|
|
2366
|
+
* mixed colour's scattering is re-estimated from its luminance, so repeated small mixes favour
|
|
2367
|
+
* the lighter paint (see GOTCHAS).
|
|
2368
|
+
*/
|
|
2369
|
+
mix(paints, options = {}) {
|
|
2370
|
+
if (!paints.length || paints.length > 8) throw new Error(`Give 1 to 8 paints to mix; got ${paints.length}.`);
|
|
2371
|
+
const total = paints.reduce((n, p) => n + p.parts, 0);
|
|
2372
|
+
paints.forEach((p, i) => {
|
|
2373
|
+
if (!(Number.isFinite(p.parts) && p.parts >= 0)) throw new Error(`paints[${i}].parts is ${p.parts}; use a number \u2265 0.`);
|
|
2374
|
+
});
|
|
2375
|
+
if (!(total > 0)) throw new Error("Every paint has 0 parts; give at least one paint parts above 0.");
|
|
2376
|
+
const R = options.radius ?? 40;
|
|
2377
|
+
if (!(R >= 12 && R <= 100)) throw new Error(`radius ${R} is out of range; use 12..100 pad px.`);
|
|
2378
|
+
const passes = options.passes ?? 3;
|
|
2379
|
+
if (!(Number.isInteger(passes) && passes >= 1 && passes <= 20)) throw new Error(`passes ${passes} is out of range; use a whole number 1..20.`);
|
|
2380
|
+
const cx = options.x ?? this.width / 2;
|
|
2381
|
+
const cy = options.y ?? this.height / 2;
|
|
2382
|
+
if (!(cx - R >= 0 && cy - R >= 0 && cx + R <= this.width && cy + R <= this.height)) {
|
|
2383
|
+
throw new Error(`A pile of radius ${R} at (${cx}, ${cy}) does not fit on the ${this.width}\xD7${this.height} pad; move it at least ${R} px from every edge or use a smaller radius.`);
|
|
2384
|
+
}
|
|
2385
|
+
const bounds = { x: Math.floor(cx - R), y: Math.floor(cy - R), width: Math.ceil(2 * R), height: Math.ceil(2 * R) };
|
|
2386
|
+
if (options.clear !== false) this.surface = writeRegion(this.surface, bounds, new Uint8ClampedArray(bounds.width * bounds.height * 4));
|
|
2387
|
+
const used = paints.filter((p) => p.parts > 0);
|
|
2388
|
+
const area = Math.PI * (0.7 * R) ** 2;
|
|
2389
|
+
used.forEach((p, i) => {
|
|
2390
|
+
const r = Math.sqrt(area * (p.parts / total) / Math.PI);
|
|
2391
|
+
const a = i / used.length * Math.PI * 2;
|
|
2392
|
+
const off = used.length === 1 ? 0 : 0.45 * R;
|
|
2393
|
+
const x = Math.min(cx + R - r, Math.max(cx - R + r, cx + Math.cos(a) * off));
|
|
2394
|
+
const y = Math.min(cy + R - r, Math.max(cy - R + r, cy + Math.sin(a) * off));
|
|
2395
|
+
const blob = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: Math.max(2, 2 * r) } }, PAD_BLOB);
|
|
2396
|
+
this.run(blob, [{ x, y }], p.color.toLowerCase());
|
|
2397
|
+
});
|
|
2398
|
+
const knife = normalizePaintBrush({ tip: { ...PAD_KNIFE.tip, size: Math.max(12, R * 0.5) } }, PAD_KNIFE);
|
|
2399
|
+
for (let k = 0; k < passes; k++) {
|
|
2400
|
+
const stir = [];
|
|
2401
|
+
const turns = 1.6;
|
|
2402
|
+
for (let t = 0; t <= 48; t++) {
|
|
2403
|
+
const a = t / 48 * Math.PI * 2 * turns + k * 1.3;
|
|
2404
|
+
const rr = R * (0.85 - 0.6 * (t / 48));
|
|
2405
|
+
stir.push({ x: cx + Math.cos(a) * rr, y: cy + Math.sin(a) * rr });
|
|
2406
|
+
}
|
|
2407
|
+
this.run(knife, stir, "#000000");
|
|
2408
|
+
}
|
|
2409
|
+
const mixed = mixPigments(
|
|
2410
|
+
paints.map((p) => hexToRgb8(p.color).map((v) => v / 255)),
|
|
2411
|
+
paints.map((p) => p.parts)
|
|
2412
|
+
);
|
|
2413
|
+
const core = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: R * 1.1, hardness: 0.55 } }, PAD_BLOB);
|
|
2414
|
+
const hex = "#" + mixed.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("");
|
|
2415
|
+
this.run(core, [{ x: cx, y: cy }], hex);
|
|
2416
|
+
return { color: this.pick(cx, cy, 2), bounds };
|
|
2417
|
+
}
|
|
2418
|
+
/** The pad's pixels as a PNG data URL (with alpha), for saving. */
|
|
2419
|
+
toPng() {
|
|
2420
|
+
return pngDataUrl(this.width, this.height, readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height }));
|
|
2421
|
+
}
|
|
2422
|
+
/** Replaces the pad with a PNG (data URL or base64) of the pad's size, as `toPng` writes. */
|
|
2423
|
+
fromPng(png) {
|
|
2424
|
+
const bytes = png.startsWith("data:") ? parseDataUrl(png)?.bytes : base64ToBytes(png);
|
|
2425
|
+
if (!bytes || !isPng(bytes)) throw new Error("The pad image is not a PNG data URL or base64 PNG.");
|
|
2426
|
+
const img = decodePng(bytes);
|
|
2427
|
+
if (img.width !== this.width || img.height !== this.height) throw new Error(`The pad image is ${img.width}\xD7${img.height}; a mixing pad is ${this.width}\xD7${this.height}.`);
|
|
2428
|
+
this.surface = surfaceFromPixels({ width: img.width, height: img.height, data: img.data }, 4);
|
|
2429
|
+
this.version++;
|
|
2430
|
+
}
|
|
2431
|
+
/** Whether nothing is on the pad. */
|
|
2432
|
+
get empty() {
|
|
2433
|
+
return this.surface.tiles.size === 0;
|
|
2434
|
+
}
|
|
1998
2435
|
/** The pad over white, `width × height` RGBA. */
|
|
1999
2436
|
pixels() {
|
|
2000
2437
|
const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
|
|
@@ -2016,9 +2453,9 @@ var PaintEditorError = class extends Error {
|
|
|
2016
2453
|
this.name = "PaintEditorError";
|
|
2017
2454
|
}
|
|
2018
2455
|
};
|
|
2019
|
-
var
|
|
2456
|
+
var HEX4 = /^#[0-9a-f]{6}$/i;
|
|
2020
2457
|
function parseColor(value, what = "color") {
|
|
2021
|
-
if (typeof value !== "string" || !
|
|
2458
|
+
if (typeof value !== "string" || !HEX4.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
2022
2459
|
return value.trim().toLowerCase();
|
|
2023
2460
|
}
|
|
2024
2461
|
function rgbaOf(hex) {
|
|
@@ -2044,6 +2481,8 @@ var PaintEditorController = class {
|
|
|
2044
2481
|
historyLimit;
|
|
2045
2482
|
brush;
|
|
2046
2483
|
userBrushes;
|
|
2484
|
+
/** The library list in the state (the same array until the user brushes change, so thumbnails are not redrawn). */
|
|
2485
|
+
library = null;
|
|
2047
2486
|
color;
|
|
2048
2487
|
secondaryColor = "#ffffff";
|
|
2049
2488
|
mixerLoad = EMPTY_MIXER_LOAD;
|
|
@@ -2062,6 +2501,8 @@ var PaintEditorController = class {
|
|
|
2062
2501
|
guides;
|
|
2063
2502
|
swatches = DEFAULT_SWATCHES.map((s) => ({ ...s }));
|
|
2064
2503
|
pad = new MixingPad();
|
|
2504
|
+
savedPads = [];
|
|
2505
|
+
nextPadId = 1;
|
|
2065
2506
|
tracingVersion = 1;
|
|
2066
2507
|
tracingCache = null;
|
|
2067
2508
|
imagePixels = /* @__PURE__ */ new Map();
|
|
@@ -2099,7 +2540,7 @@ var PaintEditorController = class {
|
|
|
2099
2540
|
this.cached = {
|
|
2100
2541
|
document: this.doc,
|
|
2101
2542
|
brush: this.brush,
|
|
2102
|
-
brushes: [...BUILTIN_BRUSHES, ...this.userBrushes],
|
|
2543
|
+
brushes: this.library ??= [...BUILTIN_BRUSHES, ...this.userBrushes],
|
|
2103
2544
|
color: this.color,
|
|
2104
2545
|
secondaryColor: this.secondaryColor,
|
|
2105
2546
|
mixerLoad: this.mixerLoad,
|
|
@@ -2112,6 +2553,7 @@ var PaintEditorController = class {
|
|
|
2112
2553
|
guides: this.guides,
|
|
2113
2554
|
swatches: this.swatches,
|
|
2114
2555
|
mixingPadVersion: this.pad.version,
|
|
2556
|
+
savedMixingPads: this.savedPads.map(({ id, name }) => ({ id, name })),
|
|
2115
2557
|
stroking: Boolean(this.session),
|
|
2116
2558
|
version: this.version
|
|
2117
2559
|
};
|
|
@@ -2481,6 +2923,7 @@ var PaintEditorController = class {
|
|
|
2481
2923
|
const i = this.userBrushes.findIndex((b) => b.id === options.id);
|
|
2482
2924
|
if (i >= 0) this.userBrushes[i] = saved;
|
|
2483
2925
|
else this.userBrushes.push(saved);
|
|
2926
|
+
this.library = null;
|
|
2484
2927
|
this.emit([]);
|
|
2485
2928
|
return saved;
|
|
2486
2929
|
}
|
|
@@ -2489,6 +2932,7 @@ var PaintEditorController = class {
|
|
|
2489
2932
|
const i = this.userBrushes.findIndex((b) => b.id === id);
|
|
2490
2933
|
if (i < 0) throw new PaintEditorError(`Unknown brush "${id}". Call list_brushes for the ids.`);
|
|
2491
2934
|
this.userBrushes.splice(i, 1);
|
|
2935
|
+
this.library = null;
|
|
2492
2936
|
this.emit([]);
|
|
2493
2937
|
}
|
|
2494
2938
|
/** The user's saved brushes. */
|
|
@@ -2529,7 +2973,10 @@ var PaintEditorController = class {
|
|
|
2529
2973
|
else this.userBrushes.push(b);
|
|
2530
2974
|
added.push(b.id);
|
|
2531
2975
|
}
|
|
2532
|
-
if (added.length)
|
|
2976
|
+
if (added.length) {
|
|
2977
|
+
this.library = null;
|
|
2978
|
+
this.emit([]);
|
|
2979
|
+
}
|
|
2533
2980
|
return { added, skipped, problems };
|
|
2534
2981
|
}
|
|
2535
2982
|
/** A thumbnail of a brush: a test stroke on the current paper. */
|
|
@@ -2603,6 +3050,14 @@ var PaintEditorController = class {
|
|
|
2603
3050
|
if (init.brush) brush = normalizePaintBrush({ ...init.brush, id: brush.id }, brush);
|
|
2604
3051
|
return brush;
|
|
2605
3052
|
}
|
|
3053
|
+
/**
|
|
3054
|
+
* After a stroke: a Mixer Brush that loads colour keeps its whole load; any other dirty brush
|
|
3055
|
+
* keeps only the paint it carries (the reservoir stays the Mixer Brush's).
|
|
3056
|
+
*/
|
|
3057
|
+
keepLoad(brush, load) {
|
|
3058
|
+
if (brush.engine === "mixer" && brush.loadsColor) this.mixerLoad = load;
|
|
3059
|
+
else if (brush.behavior.dirty) this.mixerLoad = { ...this.mixerLoad, pickup: load.pickup };
|
|
3060
|
+
}
|
|
2606
3061
|
/** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
|
|
2607
3062
|
strokeCopies() {
|
|
2608
3063
|
return symmetryCopies(this.guides.symmetry);
|
|
@@ -2643,7 +3098,8 @@ var PaintEditorController = class {
|
|
|
2643
3098
|
affineCopies: copies,
|
|
2644
3099
|
copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
|
|
2645
3100
|
inputFilter: snapper ? (p) => snapper.snap(p) : void 0,
|
|
2646
|
-
mixerLoad:
|
|
3101
|
+
mixerLoad: init.load ?? brushLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]),
|
|
3102
|
+
secondary: rgbaOf(this.secondaryColor)
|
|
2647
3103
|
});
|
|
2648
3104
|
return session;
|
|
2649
3105
|
}
|
|
@@ -2658,6 +3114,7 @@ var PaintEditorController = class {
|
|
|
2658
3114
|
seed: init.seed
|
|
2659
3115
|
};
|
|
2660
3116
|
if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m]);
|
|
3117
|
+
if (init.mixerLoad.reservoir || init.mixerLoad.pickup) stroke.load = JSON.parse(JSON.stringify(init.mixerLoad));
|
|
2661
3118
|
return stroke;
|
|
2662
3119
|
}
|
|
2663
3120
|
/**
|
|
@@ -2676,7 +3133,7 @@ var PaintEditorController = class {
|
|
|
2676
3133
|
this.live = null;
|
|
2677
3134
|
this.liveLayers = null;
|
|
2678
3135
|
const brush = s.init.brush;
|
|
2679
|
-
if (!cancelled && s.result?.mixerLoad
|
|
3136
|
+
if (!cancelled && s.result?.mixerLoad) this.keepLoad(brush, s.result.mixerLoad);
|
|
2680
3137
|
if (cancelled || !surface) {
|
|
2681
3138
|
this.emit();
|
|
2682
3139
|
return;
|
|
@@ -2720,7 +3177,7 @@ var PaintEditorController = class {
|
|
|
2720
3177
|
this.requireIdle("painting");
|
|
2721
3178
|
const points = this.checkPoints(init.points);
|
|
2722
3179
|
const { doc, result, brush } = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);
|
|
2723
|
-
if (result.mixerLoad
|
|
3180
|
+
if (result.mixerLoad) this.keepLoad(brush, result.mixerLoad);
|
|
2724
3181
|
if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);
|
|
2725
3182
|
else this.emit([]);
|
|
2726
3183
|
return result;
|
|
@@ -2780,7 +3237,12 @@ var PaintEditorController = class {
|
|
|
2780
3237
|
}
|
|
2781
3238
|
const layerId = options.layerId ?? s.layerId;
|
|
2782
3239
|
if (!findLayer(doc.layers, layerId)) throw new PaintEditorError(`Stroke "${s.id}" was painted on layer "${layerId}", which no longer exists; pass layerId to replay it elsewhere.`);
|
|
2783
|
-
|
|
3240
|
+
let load = s.load;
|
|
3241
|
+
if (load && options.color !== void 0 && load.reservoir) {
|
|
3242
|
+
const [r, g, b] = hexToRgb8(options.color);
|
|
3243
|
+
load = { ...load, reservoir: [r / 255, g / 255, b / 255] };
|
|
3244
|
+
}
|
|
3245
|
+
const run = this.runStroke(doc, { layerId, color: options.color ?? s.color, brush, seed: s.seed, copies: v.copies ?? [], ...load && options.brushId === void 0 ? { load } : {} }, v.points);
|
|
2784
3246
|
doc = run.doc;
|
|
2785
3247
|
bounds = unionRect(bounds, run.result.bounds);
|
|
2786
3248
|
if (run.result.strokeId) ids.push(run.result.strokeId);
|
|
@@ -2859,14 +3321,15 @@ var PaintEditorController = class {
|
|
|
2859
3321
|
// --- Save, load and export -------------------------------------------------------
|
|
2860
3322
|
/** The document as JSON with inline PNG assets. */
|
|
2861
3323
|
toJSON(options = {}) {
|
|
2862
|
-
return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
|
|
3324
|
+
return serializePaintDocument(this.doc, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches, mixingPad: this.mixingPadJSON() });
|
|
2863
3325
|
}
|
|
2864
3326
|
/** The document as JSON with each PNG stored in `store`. */
|
|
2865
3327
|
toJSONExternal(store, options = {}) {
|
|
2866
|
-
return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches });
|
|
3328
|
+
return serializePaintDocumentExternal(this.doc, store, { strokes: options.strokes, extensions: this.extensions, guides: this.guides, swatches: this.swatches, mixingPad: this.mixingPadJSON() });
|
|
2867
3329
|
}
|
|
2868
3330
|
adopt(loaded) {
|
|
2869
3331
|
this.extensions = loaded.extensions;
|
|
3332
|
+
this.readMixingPad(loaded.mixingPad, loaded.problems);
|
|
2870
3333
|
this.readGuides(loaded.guides, loaded.state, loaded.problems);
|
|
2871
3334
|
if (Array.isArray(loaded.swatches)) {
|
|
2872
3335
|
this.swatches = loaded.swatches.filter((x) => Boolean(x) && typeof x.color === "string" && /^#[0-9a-f]{6}$/i.test(x.color)).map((x) => typeof x.name === "string" ? { color: x.color.toLowerCase(), name: x.name } : { color: x.color.toLowerCase() });
|
|
@@ -3221,6 +3684,99 @@ var PaintEditorController = class {
|
|
|
3221
3684
|
this.pad.clear();
|
|
3222
3685
|
this.emit([]);
|
|
3223
3686
|
}
|
|
3687
|
+
/**
|
|
3688
|
+
* Mixes paints on the pad like a painter: blobs in proportion to their `parts`, knife passes,
|
|
3689
|
+
* and a fully mixed core of the Kubelka-Munk mix (see `MixingPad.mix`). With `setColor` the
|
|
3690
|
+
* mix becomes the paint colour. Returns the mixed colour and the pile's bounds.
|
|
3691
|
+
*/
|
|
3692
|
+
mixOnPad(paints, options = {}) {
|
|
3693
|
+
const parsed = (paints ?? []).map((p, i) => ({ color: parseColor(p?.color, `paints[${i}].color`), parts: p?.parts }));
|
|
3694
|
+
let r;
|
|
3695
|
+
try {
|
|
3696
|
+
r = this.pad.mix(parsed, options);
|
|
3697
|
+
} catch (e) {
|
|
3698
|
+
throw new PaintEditorError(e.message);
|
|
3699
|
+
}
|
|
3700
|
+
const color = r.color ?? parsed[0].color;
|
|
3701
|
+
this.emit([]);
|
|
3702
|
+
if (options.setColor) this.setColor(color);
|
|
3703
|
+
return { color, bounds: r.bounds };
|
|
3704
|
+
}
|
|
3705
|
+
/** The mixing pad as a PNG data URL (with alpha). */
|
|
3706
|
+
mixingPadPng() {
|
|
3707
|
+
return this.pad.toPng();
|
|
3708
|
+
}
|
|
3709
|
+
/** Replaces the mixing pad with a PNG (data URL or base64, 360 × 220). */
|
|
3710
|
+
setMixingPadPng(png) {
|
|
3711
|
+
try {
|
|
3712
|
+
this.pad.fromPng(png);
|
|
3713
|
+
} catch (e) {
|
|
3714
|
+
throw new PaintEditorError(e.message);
|
|
3715
|
+
}
|
|
3716
|
+
this.emit([]);
|
|
3717
|
+
}
|
|
3718
|
+
/** The mixing pads kept by name. */
|
|
3719
|
+
listMixingPads() {
|
|
3720
|
+
return this.savedPads.map(({ id, name }) => ({ id, name }));
|
|
3721
|
+
}
|
|
3722
|
+
/** Keeps the mixing pad as it is now under `name` (saved with the document). Returns its id. */
|
|
3723
|
+
saveMixingPad(name) {
|
|
3724
|
+
if (this.pad.empty) throw new PaintEditorError("The mixing pad is empty; put paint on it (mixing_pad_stroke or mix_on_pad) before saving it.");
|
|
3725
|
+
const label = (typeof name === "string" && name.trim() ? name.trim() : `Pad ${this.nextPadId}`).slice(0, 80);
|
|
3726
|
+
const pad = { id: `pad${this.nextPadId++}`, name: label, png: this.pad.toPng() };
|
|
3727
|
+
this.savedPads.push(pad);
|
|
3728
|
+
this.emit([]);
|
|
3729
|
+
return { id: pad.id, name: pad.name };
|
|
3730
|
+
}
|
|
3731
|
+
requirePad(id) {
|
|
3732
|
+
const p = this.savedPads.find((x) => x.id === id);
|
|
3733
|
+
if (!p) throw new PaintEditorError(`Unknown mixing pad "${id}". Call list_mixing_pads for the ids.`);
|
|
3734
|
+
return p;
|
|
3735
|
+
}
|
|
3736
|
+
/** Puts a kept pad back on the mixing pad (replacing what is there). */
|
|
3737
|
+
loadMixingPad(id) {
|
|
3738
|
+
const p = this.requirePad(id);
|
|
3739
|
+
this.setMixingPadPng(p.png);
|
|
3740
|
+
return { id: p.id, name: p.name };
|
|
3741
|
+
}
|
|
3742
|
+
/** Forgets a kept pad. */
|
|
3743
|
+
removeMixingPad(id) {
|
|
3744
|
+
const p = this.requirePad(id);
|
|
3745
|
+
this.savedPads = this.savedPads.filter((x) => x !== p);
|
|
3746
|
+
this.emit([]);
|
|
3747
|
+
return { id: p.id, name: p.name };
|
|
3748
|
+
}
|
|
3749
|
+
mixingPadJSON() {
|
|
3750
|
+
if (this.pad.empty && !this.savedPads.length) return void 0;
|
|
3751
|
+
return { ...this.pad.empty ? {} : { current: this.pad.toPng() }, saved: this.savedPads.map((p) => ({ ...p })) };
|
|
3752
|
+
}
|
|
3753
|
+
readMixingPad(raw, problems) {
|
|
3754
|
+
this.savedPads = [];
|
|
3755
|
+
this.nextPadId = 1;
|
|
3756
|
+
this.pad.clear();
|
|
3757
|
+
if (raw === void 0 || raw === null) return;
|
|
3758
|
+
const r = raw;
|
|
3759
|
+
if (typeof r.current === "string") {
|
|
3760
|
+
try {
|
|
3761
|
+
this.pad.fromPng(r.current);
|
|
3762
|
+
} catch (e) {
|
|
3763
|
+
problems.push(`The mixing pad could not be read (${e.message}); it starts empty.`);
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
if (Array.isArray(r.saved)) {
|
|
3767
|
+
r.saved.forEach((p, i) => {
|
|
3768
|
+
const x = p;
|
|
3769
|
+
if (!x || typeof x.png !== "string" || typeof x.name !== "string") {
|
|
3770
|
+
problems.push(`mixingPad.saved[${i}] is not a saved pad; skipped.`);
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
const id = typeof x.id === "string" && /^pad\d+$/.test(x.id) ? x.id : `pad${this.nextPadId}`;
|
|
3774
|
+
const n = Number(id.slice(3));
|
|
3775
|
+
if (n >= this.nextPadId) this.nextPadId = n + 1;
|
|
3776
|
+
this.savedPads.push({ id, name: x.name, png: x.png });
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3224
3780
|
/** A layer's tile surface (live while stroking), for thumbnails. */
|
|
3225
3781
|
layerSurface(id) {
|
|
3226
3782
|
const l = findLayer(this.currentLayers(), id)?.layer;
|
|
@@ -3678,7 +4234,7 @@ function createPaintGuideTools(c) {
|
|
|
3678
4234
|
{
|
|
3679
4235
|
name: "mixing_pad_stroke",
|
|
3680
4236
|
title: "Paint on the mixing pad",
|
|
3681
|
-
description: "Paints on the mixing pad (a 360 \xD7 220 scratch canvas, not the painting): tool paint lays down a colour with a pigment Mixer Brush; tool knife smears what is there like a palette knife, mixing it as paint. Points are pad pixels. Not an undo step. Returns the changed bounds; then pick_mixing_pad_color.",
|
|
4237
|
+
description: "Paints on the mixing pad (a 360 \xD7 220 scratch canvas, not the painting): tool paint lays down a colour with a pigment Mixer Brush; tool knife smears what is there like a palette knife, mixing it as paint. Points are pad pixels. Not an undo step. Returns the changed bounds; then pick_mixing_pad_color. For a measured mix in one call use mix_on_pad.",
|
|
3682
4238
|
inputSchema: {
|
|
3683
4239
|
type: "object",
|
|
3684
4240
|
properties: {
|
|
@@ -3733,6 +4289,73 @@ function createPaintGuideTools(c) {
|
|
|
3733
4289
|
return { cleared: true };
|
|
3734
4290
|
}
|
|
3735
4291
|
},
|
|
4292
|
+
{
|
|
4293
|
+
name: "mix_on_pad",
|
|
4294
|
+
title: "Mix paints on the pad",
|
|
4295
|
+
description: "Mixes paints on the mixing pad as a painter would: blobs whose areas follow their parts, worked with the palette knife, and a fully mixed core of the Kubelka-Munk (pigment) mix, so blue and yellow make green and white tints. One call per mix. Not an undo step (the pad is scratch space). Returns the mixed colour #rrggbb and the pile bounds in pad pixels; with setColor it becomes the paint colour. pick_mixing_pad_color reads the streaks around it.",
|
|
4296
|
+
inputSchema: {
|
|
4297
|
+
type: "object",
|
|
4298
|
+
properties: {
|
|
4299
|
+
paints: {
|
|
4300
|
+
type: "array",
|
|
4301
|
+
minItems: 1,
|
|
4302
|
+
maxItems: 8,
|
|
4303
|
+
description: "The paints, 1 to 8.",
|
|
4304
|
+
items: {
|
|
4305
|
+
type: "object",
|
|
4306
|
+
properties: {
|
|
4307
|
+
color: { ...COLOR, description: "Paint colour #rrggbb." },
|
|
4308
|
+
parts: { type: "number", minimum: 0, description: "Parts of this paint, any scale (1 and 3 for a 1:3 mix); at least one above 0." }
|
|
4309
|
+
},
|
|
4310
|
+
required: ["color", "parts"],
|
|
4311
|
+
additionalProperties: false
|
|
4312
|
+
}
|
|
4313
|
+
},
|
|
4314
|
+
x: { type: "number", minimum: 0, maximum: 360, description: "Centre of the pile, pad x (default 180)." },
|
|
4315
|
+
y: { type: "number", minimum: 0, maximum: 220, description: "Centre of the pile, pad y (default 110)." },
|
|
4316
|
+
radius: { type: "number", minimum: 12, maximum: 100, description: "Radius of the pile in pad pixels (default 40); it must fit on the pad." },
|
|
4317
|
+
passes: { type: "integer", minimum: 1, maximum: 20, description: "Palette-knife passes (default 3; more gives finer streaks around the core)." },
|
|
4318
|
+
clear: { type: "boolean", description: "Wipe the pile's area first (default true); false mixes into the paint already there." },
|
|
4319
|
+
setColor: { type: "boolean", description: "Also make the mix the paint colour (default false)." }
|
|
4320
|
+
},
|
|
4321
|
+
required: ["paints"],
|
|
4322
|
+
additionalProperties: false
|
|
4323
|
+
},
|
|
4324
|
+
annotations: { title: "Mix paints on the pad", ...WRITE },
|
|
4325
|
+
handler: ({ paints, x, y, radius, passes, clear, setColor }) => c.mixOnPad(paints, { x, y, radius, passes, clear, setColor })
|
|
4326
|
+
},
|
|
4327
|
+
{
|
|
4328
|
+
name: "save_mixing_pad",
|
|
4329
|
+
title: "Keep the mixing pad",
|
|
4330
|
+
description: "Keeps the mixing pad as it is now under a name, saved with the document, to come back to with load_mixing_pad. Refused while the pad is empty. Returns the kept pad id and name.",
|
|
4331
|
+
inputSchema: { type: "object", properties: { name: { type: "string", minLength: 1, maxLength: 80, description: 'Name (default "Pad n").' } }, additionalProperties: false },
|
|
4332
|
+
annotations: { title: "Keep the mixing pad", ...WRITE },
|
|
4333
|
+
handler: ({ name }) => c.saveMixingPad(name)
|
|
4334
|
+
},
|
|
4335
|
+
{
|
|
4336
|
+
name: "list_mixing_pads",
|
|
4337
|
+
title: "List kept mixing pads",
|
|
4338
|
+
description: "Lists the mixing pads kept with save_mixing_pad (id, name). Changes nothing.",
|
|
4339
|
+
inputSchema: EMPTY,
|
|
4340
|
+
annotations: { title: "List kept mixing pads", ...READ },
|
|
4341
|
+
handler: () => ({ pads: c.listMixingPads() })
|
|
4342
|
+
},
|
|
4343
|
+
{
|
|
4344
|
+
name: "load_mixing_pad",
|
|
4345
|
+
title: "Load a kept mixing pad",
|
|
4346
|
+
description: "Puts a kept pad (id from list_mixing_pads) back on the mixing pad, replacing what is on it. Returns the pad id and name.",
|
|
4347
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Kept pad id from list_mixing_pads." } }, required: ["id"], additionalProperties: false },
|
|
4348
|
+
annotations: { title: "Load a kept mixing pad", ...DESTRUCTIVE },
|
|
4349
|
+
handler: ({ id }) => c.loadMixingPad(id)
|
|
4350
|
+
},
|
|
4351
|
+
{
|
|
4352
|
+
name: "remove_mixing_pad",
|
|
4353
|
+
title: "Remove a kept mixing pad",
|
|
4354
|
+
description: "Forgets a kept pad (id from list_mixing_pads); the pad on screen is untouched. Returns the removed id and name.",
|
|
4355
|
+
inputSchema: { type: "object", properties: { id: { type: "string", description: "Kept pad id from list_mixing_pads." } }, required: ["id"], additionalProperties: false },
|
|
4356
|
+
annotations: { title: "Remove a kept mixing pad", ...DESTRUCTIVE },
|
|
4357
|
+
handler: ({ id }) => c.removeMixingPad(id)
|
|
4358
|
+
},
|
|
3736
4359
|
{
|
|
3737
4360
|
name: "render_mixing_pad",
|
|
3738
4361
|
title: "Render the mixing pad",
|
|
@@ -3802,28 +4425,79 @@ var DYNAMICS = {
|
|
|
3802
4425
|
};
|
|
3803
4426
|
var MIXER = {
|
|
3804
4427
|
type: "object",
|
|
3805
|
-
description: "Mixer engine settings (Mixer Brush
|
|
4428
|
+
description: "Mixer engine settings (the Mixer Brush). Missing fields keep their current values.",
|
|
3806
4429
|
properties: {
|
|
3807
|
-
wet: { type: "number", minimum: 0, maximum: 1, description: "
|
|
4430
|
+
wet: { type: "number", minimum: 0, maximum: 1, description: "Pick-up ratio: how much canvas colour each dab picks up, 0 (dry) .. 1." },
|
|
3808
4431
|
load: { type: "number", minimum: 0, maximum: 1, description: "How much paint the reservoir holds; below 1 it drains per dab, so long strokes run dry." },
|
|
3809
|
-
mix: { type: "number", minimum: 0, maximum: 1, description: "
|
|
4432
|
+
mix: { type: "number", minimum: 0, maximum: 1, description: "Mix ratio: how far each dab's paint moves from the reservoir towards the picked-up colour." },
|
|
3810
4433
|
loadEachStroke: { type: "boolean", description: "Refill the reservoir with the colour before each stroke." },
|
|
3811
4434
|
cleanEachStroke: { type: "boolean", description: "Empty the pickup well after each stroke." },
|
|
3812
4435
|
mixing: { type: "string", enum: [...MIXING_MODELS], description: "rgb: RGB averages. pigment: mixes like paint (Kubelka-Munk)." }
|
|
3813
4436
|
},
|
|
3814
4437
|
additionalProperties: false
|
|
3815
4438
|
};
|
|
4439
|
+
var MIXING = { type: "string", enum: [...MIXING_MODELS], description: "pigment: mixes like paint (Kubelka-Munk, blue and yellow make green); rgb: straight RGB averages." };
|
|
4440
|
+
var BEHAVIOR = {
|
|
4441
|
+
type: "object",
|
|
4442
|
+
description: "How the brush handles paint on the canvas (dab and mixer engines; on a mixer brush pickup, mix, dirty and mixing are its mixer wet, mix, !cleanEachStroke and mixing). Missing fields keep their current values.",
|
|
4443
|
+
properties: {
|
|
4444
|
+
pickup: { type: "number", minimum: 0, maximum: 1, description: "Pick-up ratio 0..1: how much of the paint under each dab the brush lifts and carries along (0: none, a dry brush)." },
|
|
4445
|
+
mix: { type: "number", minimum: 0, maximum: 1, description: "Mix ratio 0..1: how much of the carried paint goes into what the brush lays down, against the brush's own colour (scaled by pickup)." },
|
|
4446
|
+
dirty: { type: "boolean", description: "Dirty brush: keep the carried paint after the stroke, so the next stroke starts with the last colour it touched (needs pickup above 0). clean_mixer_brush wipes it." },
|
|
4447
|
+
mixing: MIXING,
|
|
4448
|
+
multicolor: {
|
|
4449
|
+
type: "object",
|
|
4450
|
+
description: "Multi-coloured brush. Missing fields keep their current values.",
|
|
4451
|
+
properties: {
|
|
4452
|
+
mode: { type: "string", enum: [...MULTI_COLOR_MODES], description: "off; tip: the colours lie side by side across the tip (stripes along the stroke, like a brush loaded with several paints); dab: the colour changes along the stroke." },
|
|
4453
|
+
colors: { type: "array", maxItems: 8, items: { ...COLOR2 }, description: "2..8 colours #rrggbb; fewer than 2 uses the paint colour and the secondary colour." },
|
|
4454
|
+
order: { type: "string", enum: ["sequence", "random"], description: "dab mode: sequence runs through the colours in order, blending between them; random picks one per run of dabs (seeded)." },
|
|
4455
|
+
length: { type: "integer", minimum: 1, maximum: 500, description: "dab mode: dabs per colour (default 12)." },
|
|
4456
|
+
softness: { type: "number", minimum: 0, maximum: 1, description: "How softly neighbouring colours blend: 0 hard stripes or steps, 1 a smooth gradient." }
|
|
4457
|
+
},
|
|
4458
|
+
additionalProperties: false
|
|
4459
|
+
}
|
|
4460
|
+
},
|
|
4461
|
+
additionalProperties: false
|
|
4462
|
+
};
|
|
4463
|
+
var BLENDER = {
|
|
4464
|
+
type: "object",
|
|
4465
|
+
description: "Blender engine settings. Missing fields keep their current values.",
|
|
4466
|
+
properties: {
|
|
4467
|
+
strength: { type: "number", minimum: 0, maximum: 1, description: "How far each pixel under the tip moves towards the mix of the paint near it, per dab." },
|
|
4468
|
+
distance: { type: "number", minimum: 0, maximum: 1, description: "Blend distance: how far away colours are mixed in, as a fraction of the tip diameter (0.35 default; higher blends wider and softer)." },
|
|
4469
|
+
mixing: MIXING
|
|
4470
|
+
},
|
|
4471
|
+
additionalProperties: false
|
|
4472
|
+
};
|
|
4473
|
+
var SMUDGE = {
|
|
4474
|
+
type: "object",
|
|
4475
|
+
description: "Smudge engine settings. Missing fields keep their current values.",
|
|
4476
|
+
properties: {
|
|
4477
|
+
strength: { type: "number", minimum: 0, maximum: 1, description: "How strongly the dragged paint covers what is under the tip, per dab." },
|
|
4478
|
+
length: { type: "number", minimum: 0, maximum: 1, description: "How far paint is dragged: the share of the carried paint kept from dab to dab (0.85 default; 1 drags it the whole stroke)." },
|
|
4479
|
+
mixing: { ...MIXING, description: "rgb (default): smeared colours average; pigment: they mix like paint." }
|
|
4480
|
+
},
|
|
4481
|
+
additionalProperties: false
|
|
4482
|
+
};
|
|
3816
4483
|
var BRUSH_FIELDS = {
|
|
3817
4484
|
name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." },
|
|
3818
4485
|
category: { type: "string", enum: [...PAINT_BRUSH_CATEGORIES], description: "Library category." },
|
|
3819
|
-
engine: {
|
|
4486
|
+
engine: {
|
|
4487
|
+
type: "string",
|
|
4488
|
+
enum: [...PAINT_BRUSH_ENGINES],
|
|
4489
|
+
description: "dab: paints dabs (with behavior.pickup or multicolor it paints dab by dab, lifting and mixing paint); eraser: removes paint; mixer: the Mixer Brush (a reservoir plus picked-up paint); blender: mixes the colours under and near the tip; smudge: drags paint along the stroke."
|
|
4490
|
+
},
|
|
3820
4491
|
tip: TIP,
|
|
3821
4492
|
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response: 0 ignores the grain, 1 catches only its peaks at light pressure (pencil, pastel, charcoal). Multiplied by the paper depth." },
|
|
3822
4493
|
pressureSmoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Pressure smoothing, separate from the tip position smoothing." },
|
|
3823
4494
|
rotation: { type: "string", enum: [...BRUSH_ROTATIONS], description: "What turns the tip: fixed (tip.angle), stroke (follows the direction of travel) or barrel (the pen barrel rotation, twist)." },
|
|
3824
4495
|
dynamics: DYNAMICS,
|
|
3825
4496
|
mixer: MIXER,
|
|
3826
|
-
loadsColor: { type: "boolean", description: "Mixer engine: load the colour each stroke (false for
|
|
4497
|
+
loadsColor: { type: "boolean", description: "Mixer engine: load the colour each stroke (false for a mixer that only moves paint)." },
|
|
4498
|
+
behavior: BEHAVIOR,
|
|
4499
|
+
blender: BLENDER,
|
|
4500
|
+
smudge: SMUDGE
|
|
3827
4501
|
};
|
|
3828
4502
|
var POINTS = {
|
|
3829
4503
|
type: "array",
|
|
@@ -3852,7 +4526,20 @@ function loadResult(load) {
|
|
|
3852
4526
|
return { reservoir: hex(load.reservoir), amount: Math.round(load.amount * 1e3) / 1e3, pickup: hex(load.pickup) };
|
|
3853
4527
|
}
|
|
3854
4528
|
function brushSummary(b) {
|
|
3855
|
-
return {
|
|
4529
|
+
return {
|
|
4530
|
+
id: b.id,
|
|
4531
|
+
name: b.name,
|
|
4532
|
+
category: b.category,
|
|
4533
|
+
engine: b.engine,
|
|
4534
|
+
size: b.tip.size,
|
|
4535
|
+
grain: b.grain,
|
|
4536
|
+
rotation: b.rotation,
|
|
4537
|
+
wetCapable: isWetCapable(b),
|
|
4538
|
+
pickup: b.behavior.pickup,
|
|
4539
|
+
dirty: b.behavior.dirty,
|
|
4540
|
+
multicolor: b.behavior.multicolor.mode,
|
|
4541
|
+
builtin: Boolean(b.builtin)
|
|
4542
|
+
};
|
|
3856
4543
|
}
|
|
3857
4544
|
function createPaintEditorToolProvider(controller, options = {}) {
|
|
3858
4545
|
const c = controller;
|
|
@@ -4141,7 +4828,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4141
4828
|
add({
|
|
4142
4829
|
name: "list_brushes",
|
|
4143
4830
|
title: "List brushes",
|
|
4144
|
-
description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers, blender, smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response and
|
|
4831
|
+
description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
|
|
4145
4832
|
inputSchema: EMPTY2,
|
|
4146
4833
|
annotations: { title: "List brushes", ...READ2 },
|
|
4147
4834
|
handler: () => {
|
|
@@ -4152,7 +4839,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4152
4839
|
add({
|
|
4153
4840
|
name: "get_brush",
|
|
4154
4841
|
title: "Get a brush",
|
|
4155
|
-
description: "Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer). Without id, the active brush as edited.",
|
|
4842
|
+
description: "Reads every setting of a brush (tip, grain, pressure smoothing, rotation, dynamics, mixer, behavior: pick-up / mix / dirty / multi-colour, blender, smudge). Without id, the active brush as edited.",
|
|
4156
4843
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes (default the active brush)." } }, additionalProperties: false },
|
|
4157
4844
|
annotations: { title: "Get a brush", ...READ2 },
|
|
4158
4845
|
handler: ({ id }) => {
|
|
@@ -4287,7 +4974,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4287
4974
|
add({
|
|
4288
4975
|
name: "get_mixer_brush",
|
|
4289
4976
|
title: "Get the Mixer Brush load",
|
|
4290
|
-
description: "Reads what the
|
|
4977
|
+
description: "Reads what the brush carries between strokes: reservoir #rrggbb or null (the Mixer Brush's loaded paint), amount 0..1 left, pickup #rrggbb or null (paint picked up from the canvas, kept by dirty brushes and a Mixer Brush that does not clean each stroke). Call before painting with mixer-brush when its loadEachStroke is false, or with a dirty brush.",
|
|
4291
4978
|
inputSchema: EMPTY2,
|
|
4292
4979
|
annotations: { title: "Get the Mixer Brush load", ...READ2 },
|
|
4293
4980
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
@@ -4303,7 +4990,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4303
4990
|
add({
|
|
4304
4991
|
name: "clean_mixer_brush",
|
|
4305
4992
|
title: "Clean the Mixer Brush",
|
|
4306
|
-
description: "
|
|
4993
|
+
description: "Cleans the brush: empties the picked-up paint a dirty brush or the Mixer Brush carries (and with emptyReservoir the Mixer Brush paint too). Editor state, not an undo step. Returns the load.",
|
|
4307
4994
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
4308
4995
|
annotations: { title: "Clean the Mixer Brush", ...SET2 },
|
|
4309
4996
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
@@ -4311,7 +4998,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4311
4998
|
add({
|
|
4312
4999
|
name: "paint_stroke",
|
|
4313
5000
|
title: "Paint a brush stroke",
|
|
4314
|
-
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush
|
|
5001
|
+
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
|
|
4315
5002
|
inputSchema: {
|
|
4316
5003
|
type: "object",
|
|
4317
5004
|
properties: {
|
|
@@ -4818,6 +5505,67 @@ var BLEND_MENU_GROUPS = [
|
|
|
4818
5505
|
{ label: "Standard", modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== "pigment") },
|
|
4819
5506
|
{ label: "Paint", modes: BLEND_MODES.filter((m) => m.painterly || m.id === "pigment") }
|
|
4820
5507
|
];
|
|
5508
|
+
var PICKUP_SLIDERS = [
|
|
5509
|
+
{ path: "behavior.pickup", label: "Pick-up", min: 0, max: 1, step: 0.01 },
|
|
5510
|
+
{ path: "behavior.mix", label: "Mix", min: 0, max: 1, step: 0.01 }
|
|
5511
|
+
];
|
|
5512
|
+
function brushEngineControls(brush) {
|
|
5513
|
+
switch (brush.engine) {
|
|
5514
|
+
case "blender":
|
|
5515
|
+
return {
|
|
5516
|
+
title: "Blender",
|
|
5517
|
+
sliders: [
|
|
5518
|
+
{ path: "blender.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
|
|
5519
|
+
{ path: "blender.distance", label: "Blend distance", min: 0, max: 1, step: 0.01 }
|
|
5520
|
+
],
|
|
5521
|
+
mixingPath: "blender.mixing",
|
|
5522
|
+
dirty: false,
|
|
5523
|
+
multicolor: false,
|
|
5524
|
+
load: false
|
|
5525
|
+
};
|
|
5526
|
+
case "smudge":
|
|
5527
|
+
return {
|
|
5528
|
+
title: "Smudge",
|
|
5529
|
+
sliders: [
|
|
5530
|
+
{ path: "smudge.strength", label: "Strength", min: 0, max: 1, step: 0.01 },
|
|
5531
|
+
{ path: "smudge.length", label: "Length", min: 0, max: 1, step: 0.01 }
|
|
5532
|
+
],
|
|
5533
|
+
mixingPath: "smudge.mixing",
|
|
5534
|
+
dirty: false,
|
|
5535
|
+
multicolor: false,
|
|
5536
|
+
load: false
|
|
5537
|
+
};
|
|
5538
|
+
case "mixer":
|
|
5539
|
+
return { title: "Mixer Brush", sliders: [...PICKUP_SLIDERS, { path: "mixer.load", label: "Load", min: 0, max: 1, step: 0.01 }], mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
|
|
5540
|
+
case "dab":
|
|
5541
|
+
return { title: "Paint behaviour", sliders: PICKUP_SLIDERS, mixingPath: "behavior.mixing", dirty: true, multicolor: true, load: true };
|
|
5542
|
+
default:
|
|
5543
|
+
return null;
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
function multiColorSliders(mode) {
|
|
5547
|
+
if (mode === "off") return [];
|
|
5548
|
+
const soft = { path: "behavior.multicolor.softness", label: "Colour softness", min: 0, max: 1, step: 0.01 };
|
|
5549
|
+
return mode === "dab" ? [soft, { path: "behavior.multicolor.length", label: "Dabs per colour", min: 1, max: 100, step: 1 }] : [soft];
|
|
5550
|
+
}
|
|
5551
|
+
function patchBrushValue(brush, path, value) {
|
|
5552
|
+
const set = (obj, keys) => {
|
|
5553
|
+
const [head2, ...rest2] = keys;
|
|
5554
|
+
if (!rest2.length) return { ...obj, [head2]: value };
|
|
5555
|
+
return { ...obj, [head2]: set(obj[head2] ?? {}, rest2) };
|
|
5556
|
+
};
|
|
5557
|
+
const [head, ...rest] = path.split(".");
|
|
5558
|
+
if (!rest.length) return { [head]: value };
|
|
5559
|
+
return { [head]: set({ ...brush[head] ?? {} }, rest) };
|
|
5560
|
+
}
|
|
5561
|
+
function brushLoadColor(load) {
|
|
5562
|
+
const c = load.pickup ?? load.reservoir;
|
|
5563
|
+
return c ? rgbToHexColor(Math.round(c[0] * 255), Math.round(c[1] * 255), Math.round(c[2] * 255)) : null;
|
|
5564
|
+
}
|
|
5565
|
+
var PAD_SAMPLE_SIZES = [1, 3, 5, 9, 15];
|
|
5566
|
+
function padSampleRadius(size) {
|
|
5567
|
+
return Math.max(0, Math.floor((size - 1) / 2));
|
|
5568
|
+
}
|
|
4821
5569
|
export {
|
|
4822
5570
|
BLEND_MENU_GROUPS,
|
|
4823
5571
|
BLEND_MODES,
|
|
@@ -4825,9 +5573,11 @@ export {
|
|
|
4825
5573
|
BRUSH_ROTATIONS,
|
|
4826
5574
|
BRUSH_SLIDERS,
|
|
4827
5575
|
BUILTIN_BRUSHES,
|
|
5576
|
+
DEFAULT_BEHAVIOR,
|
|
4828
5577
|
DEFAULT_BRUSH_ID,
|
|
4829
5578
|
DEFAULT_DPI,
|
|
4830
5579
|
DEFAULT_DYNAMICS,
|
|
5580
|
+
DEFAULT_MULTI_COLOR,
|
|
4831
5581
|
DEFAULT_PAPER,
|
|
4832
5582
|
DEFAULT_SWATCHES,
|
|
4833
5583
|
DEFAULT_VIEW,
|
|
@@ -4846,7 +5596,9 @@ export {
|
|
|
4846
5596
|
MIN_ZOOM,
|
|
4847
5597
|
MIXING_PAD_HEIGHT,
|
|
4848
5598
|
MIXING_PAD_WIDTH,
|
|
5599
|
+
MULTI_COLOR_MODES,
|
|
4849
5600
|
MixingPad,
|
|
5601
|
+
PAD_SAMPLE_SIZES,
|
|
4850
5602
|
PAINTERLY_BLEND_MODES,
|
|
4851
5603
|
PAINT_BRUSH_CATEGORIES,
|
|
4852
5604
|
PAINT_BRUSH_ENGINES,
|
|
@@ -4858,6 +5610,7 @@ export {
|
|
|
4858
5610
|
PAPER_NODE_ID,
|
|
4859
5611
|
PAPER_SLIDERS,
|
|
4860
5612
|
PaintCanvasPresenter,
|
|
5613
|
+
PaintDabStroke,
|
|
4861
5614
|
PaintDirector,
|
|
4862
5615
|
PaintDocumentError,
|
|
4863
5616
|
PaintEditorController,
|
|
@@ -4875,6 +5628,9 @@ export {
|
|
|
4875
5628
|
TileableNoise,
|
|
4876
5629
|
affineCopy,
|
|
4877
5630
|
applyAffine,
|
|
5631
|
+
brushEngineControls,
|
|
5632
|
+
brushLoadColor,
|
|
5633
|
+
brushLoadForStroke,
|
|
4878
5634
|
brushToJSON,
|
|
4879
5635
|
clonePaper,
|
|
4880
5636
|
colorHarmony,
|
|
@@ -4906,26 +5662,35 @@ export {
|
|
|
4906
5662
|
isBlendMode,
|
|
4907
5663
|
isPainterlyBlendMode,
|
|
4908
5664
|
isPalmTouch,
|
|
5665
|
+
isWetCapable,
|
|
4909
5666
|
mapPenPoint,
|
|
4910
5667
|
mix32,
|
|
4911
5668
|
mixerLoadForStroke,
|
|
4912
5669
|
mixerTip,
|
|
5670
|
+
multiColorForDab,
|
|
5671
|
+
multiColorGradient,
|
|
5672
|
+
multiColorPalette,
|
|
5673
|
+
multiColorSliders,
|
|
4913
5674
|
newPaintLayerId,
|
|
5675
|
+
normalizeBehavior,
|
|
4914
5676
|
normalizeBrushStroke,
|
|
4915
5677
|
normalizeCanvas,
|
|
4916
5678
|
normalizeDynamics,
|
|
5679
|
+
normalizeMultiColor,
|
|
4917
5680
|
normalizePaintBrush,
|
|
4918
5681
|
normalizePaper,
|
|
4919
5682
|
normalizePerspective,
|
|
4920
5683
|
normalizeRotation,
|
|
4921
5684
|
normalizeSymmetry,
|
|
4922
5685
|
normalizeView,
|
|
5686
|
+
padSampleRadius,
|
|
4923
5687
|
paperGrain,
|
|
4924
5688
|
paperKey,
|
|
4925
5689
|
paperNode,
|
|
4926
5690
|
paperPreset,
|
|
4927
5691
|
parseBrushLibrary,
|
|
4928
5692
|
parseHexColor,
|
|
5693
|
+
patchBrushValue,
|
|
4929
5694
|
patchPath,
|
|
4930
5695
|
penOrientation,
|
|
4931
5696
|
perspectiveDirections,
|
|
@@ -4956,6 +5721,7 @@ export {
|
|
|
4956
5721
|
thumbnailStroke,
|
|
4957
5722
|
tiltToOrientation,
|
|
4958
5723
|
toStrokePoint,
|
|
5724
|
+
usesPaintExchange,
|
|
4959
5725
|
variationAffine,
|
|
4960
5726
|
varyStroke,
|
|
4961
5727
|
viewCssTransform,
|