@pixerate/editor 0.108.0 → 0.110.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/animation/index.cjs +5 -5
- package/dist/animation/index.cjs.map +1 -1
- package/dist/animation/index.js +9 -9
- package/dist/animation-editor/index.cjs +59 -59
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +2 -1
- package/dist/animation-editor/index.d.ts +2 -1
- package/dist/animation-editor/index.js +29 -29
- package/dist/blending-DXl06NSx.d.ts +486 -0
- package/dist/blending-aOE6Y3UE.d.cts +486 -0
- package/dist/canvas/index.cjs +27 -27
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.js +11 -11
- package/dist/{chunk-2FGRTYJN.cjs → chunk-477UVMNR.cjs} +3 -3
- package/dist/{chunk-2FGRTYJN.cjs.map → chunk-477UVMNR.cjs.map} +1 -1
- package/dist/chunk-7VOHOPLW.cjs +1136 -0
- package/dist/chunk-7VOHOPLW.cjs.map +1 -0
- package/dist/{chunk-HUQ7EHVI.js → chunk-BLWCAQCW.js} +723 -1975
- package/dist/chunk-BLWCAQCW.js.map +1 -0
- package/dist/{chunk-SOROZ2BF.js → chunk-DMRSJBJF.js} +1939 -318
- package/dist/chunk-DMRSJBJF.js.map +1 -0
- package/dist/chunk-FTBZPI77.cjs +1 -0
- package/dist/chunk-FTBZPI77.cjs.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-JLYX4V57.cjs +2001 -0
- package/dist/chunk-JLYX4V57.cjs.map +1 -0
- package/dist/chunk-KB2YBCVS.js +2001 -0
- package/dist/chunk-KB2YBCVS.js.map +1 -0
- package/dist/{chunk-G4BIYE6R.js → chunk-LQEEDHOP.js} +40 -2
- package/dist/chunk-LQEEDHOP.js.map +1 -0
- package/dist/{chunk-FRD3FUXI.cjs → chunk-MGL4U5WA.cjs} +2144 -523
- package/dist/chunk-MGL4U5WA.cjs.map +1 -0
- package/dist/{chunk-T3QZ6S47.cjs → chunk-POGBUYWE.cjs} +131 -93
- package/dist/chunk-POGBUYWE.cjs.map +1 -0
- package/dist/chunk-VKK5VS2T.js +1 -0
- package/dist/{chunk-UTEVPQTS.cjs → chunk-W3GXSUSQ.cjs} +746 -1998
- package/dist/chunk-W3GXSUSQ.cjs.map +1 -0
- package/dist/{chunk-SZ6KMZNM.cjs → chunk-XIOMMGPD.cjs} +6 -2
- package/dist/chunk-XIOMMGPD.cjs.map +1 -0
- package/dist/{chunk-JK6M6V7G.js → chunk-Z3YWVUWT.js} +4 -4
- package/dist/{chunk-3PHA3UKI.js → chunk-Z4BC6M74.js} +523 -121
- package/dist/chunk-Z4BC6M74.js.map +1 -0
- package/dist/{export-a9bnF7Cl.d.ts → export-CO2ryu3I.d.ts} +419 -18
- package/dist/{export-JGWQEPJn.d.cts → export-DRiLzP_j.d.cts} +419 -18
- package/dist/image-editor/index.cjs +1665 -1113
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +78 -14
- package/dist/image-editor/index.d.ts +78 -14
- package/dist/image-editor/index.js +843 -291
- 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 +1056 -287
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +304 -22
- package/dist/paint-editor/index.d.ts +304 -22
- package/dist/paint-editor/index.js +852 -83
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +7 -3
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +3 -129
- package/dist/raster/index.d.ts +3 -129
- package/dist/raster/index.js +6 -2
- package/dist/{compositor-D92URMlm.d.cts → render-BEriFuqK.d.cts} +136 -2
- package/dist/{compositor-Kt_M4ruo.d.ts → render-tjzKaik-.d.ts} +136 -2
- 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/dist/{svg-SseJTIrA.d.ts → svg-D_cfkhlB.d.cts} +3 -629
- package/dist/{svg-ClVIG4T7.d.cts → svg-wIaKLrU0.d.ts} +3 -629
- package/dist/types-DtL_qLFz.d.ts +630 -0
- package/dist/types-DyQq-WPY.d.cts +630 -0
- package/dist/vector/index.cjs +563 -563
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +4 -2
- package/dist/vector/index.d.ts +4 -2
- package/dist/vector/index.js +69 -69
- package/dist/video-editor/index.cjs +3 -3
- package/dist/video-editor/index.js +2 -2
- package/package.json +1 -1
- 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-DWGZUSES.cjs +0 -694
- package/dist/chunk-DWGZUSES.cjs.map +0 -1
- package/dist/chunk-FRD3FUXI.cjs.map +0 -1
- package/dist/chunk-G4BIYE6R.js.map +0 -1
- package/dist/chunk-HUFQD4CB.cjs +0 -734
- package/dist/chunk-HUFQD4CB.cjs.map +0 -1
- package/dist/chunk-HUQ7EHVI.js.map +0 -1
- package/dist/chunk-KKREERP2.js +0 -694
- package/dist/chunk-KKREERP2.js.map +0 -1
- package/dist/chunk-SOROZ2BF.js.map +0 -1
- package/dist/chunk-SZ6KMZNM.cjs.map +0 -1
- package/dist/chunk-T3QZ6S47.cjs.map +0 -1
- package/dist/chunk-UTEVPQTS.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-XYBJKUAU.js.map → chunk-VKK5VS2T.js.map} +0 -0
- /package/dist/{chunk-JK6M6V7G.js.map → chunk-Z3YWVUWT.js.map} +0 -0
|
@@ -4,9 +4,10 @@ import {
|
|
|
4
4
|
resolveExternalAssets,
|
|
5
5
|
serializeLayers,
|
|
6
6
|
serializeLayersExternal
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-DMRSJBJF.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,20 +59,26 @@ 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,
|
|
60
75
|
realPresenceClock,
|
|
61
76
|
tweenPresenceCursor
|
|
62
77
|
} from "../chunk-B6DSIEXA.js";
|
|
63
|
-
import "../chunk-
|
|
78
|
+
import "../chunk-BLWCAQCW.js";
|
|
79
|
+
import "../chunk-YCSFS3GK.js";
|
|
80
|
+
import "../chunk-ZRJMJIQU.js";
|
|
81
|
+
import "../chunk-TGD52FEZ.js";
|
|
64
82
|
|
|
65
83
|
// src/paint-editor/noise.ts
|
|
66
84
|
function mix32(h) {
|
|
@@ -435,8 +453,11 @@ function createPaintDocument(options = {}) {
|
|
|
435
453
|
}
|
|
436
454
|
|
|
437
455
|
// src/paint-editor/brushes.ts
|
|
438
|
-
var PAINT_BRUSH_ENGINES = ["dab", "eraser", "mixer"];
|
|
439
|
-
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 };
|
|
440
461
|
var BRUSH_ROTATIONS = ["fixed", "stroke", "barrel"];
|
|
441
462
|
var DEFAULT_DYNAMICS = {
|
|
442
463
|
minSize: 0,
|
|
@@ -469,21 +490,62 @@ function normalizeDynamics(raw = {}, base = DEFAULT_DYNAMICS) {
|
|
|
469
490
|
};
|
|
470
491
|
}
|
|
471
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
|
+
}
|
|
472
519
|
function normalizePaintBrush(raw = {}, base) {
|
|
473
520
|
const b = base ?? BASE_BRUSH;
|
|
474
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 };
|
|
475
534
|
return {
|
|
476
535
|
id,
|
|
477
536
|
name: typeof raw.name === "string" && raw.name.trim() ? raw.name.trim().slice(0, 80) : b.name,
|
|
478
537
|
category: PAINT_BRUSH_CATEGORIES.includes(raw.category) ? raw.category : b.category,
|
|
479
|
-
engine
|
|
538
|
+
engine,
|
|
480
539
|
tip: normalizeBrush({ ...b.tip, ...raw.tip ?? {} }),
|
|
481
540
|
grain: unit2(raw.grain, b.grain),
|
|
482
541
|
pressureSmoothing: range(raw.pressureSmoothing, 0, 0.95, b.pressureSmoothing),
|
|
483
542
|
rotation: BRUSH_ROTATIONS.includes(raw.rotation) ? raw.rotation : b.rotation,
|
|
484
543
|
dynamics: normalizeDynamics(raw.dynamics ?? {}, b.dynamics),
|
|
485
|
-
mixer
|
|
544
|
+
mixer,
|
|
486
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),
|
|
487
549
|
...raw.builtin ? { builtin: true } : {}
|
|
488
550
|
};
|
|
489
551
|
}
|
|
@@ -498,7 +560,10 @@ var BASE_BRUSH = {
|
|
|
498
560
|
rotation: "fixed",
|
|
499
561
|
dynamics: { ...DEFAULT_DYNAMICS },
|
|
500
562
|
mixer: { ...DEFAULT_MIXER },
|
|
501
|
-
loadsColor: true
|
|
563
|
+
loadsColor: true,
|
|
564
|
+
behavior: { ...DEFAULT_BEHAVIOR, multicolor: { ...DEFAULT_MULTI_COLOR, colors: [] } },
|
|
565
|
+
blender: { ...DEFAULT_BLENDER },
|
|
566
|
+
smudge: { ...DEFAULT_SMUDGE }
|
|
502
567
|
};
|
|
503
568
|
var builtin = (init) => ({ ...normalizePaintBrush(init), builtin: true });
|
|
504
569
|
var BUILTIN_BRUSHES = [
|
|
@@ -612,19 +677,54 @@ var BUILTIN_BRUSHES = [
|
|
|
612
677
|
id: "blender",
|
|
613
678
|
name: "Blender",
|
|
614
679
|
category: "blender",
|
|
615
|
-
engine: "
|
|
680
|
+
engine: "blender",
|
|
616
681
|
loadsColor: false,
|
|
617
|
-
tip: { size: 34, hardness: 0.4, spacing: 0.
|
|
618
|
-
|
|
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" }
|
|
619
684
|
}),
|
|
620
685
|
builtin({
|
|
621
686
|
id: "smudge",
|
|
622
687
|
name: "Smudge",
|
|
623
688
|
category: "smudge",
|
|
624
|
-
engine: "
|
|
689
|
+
engine: "smudge",
|
|
625
690
|
loadsColor: false,
|
|
626
|
-
tip: { size: 26, hardness: 0.6, spacing: 0.06, opacity: 1, flow:
|
|
627
|
-
|
|
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 }
|
|
628
728
|
}),
|
|
629
729
|
builtin({
|
|
630
730
|
id: "mixer-brush",
|
|
@@ -804,7 +904,9 @@ var PenDabWalker = class {
|
|
|
804
904
|
opacity: Math.min(1, Math.max(0, opacity)),
|
|
805
905
|
angle,
|
|
806
906
|
roundness: Math.min(1, Math.max(0.01, tip.roundness * (1 - dyn.tiltRoundness * s.lean * 0.8))),
|
|
807
|
-
pressure: s.pressure
|
|
907
|
+
pressure: s.pressure,
|
|
908
|
+
direction: this.dir,
|
|
909
|
+
index: i
|
|
808
910
|
};
|
|
809
911
|
}
|
|
810
912
|
push(p, out) {
|
|
@@ -935,6 +1037,248 @@ function toStrokePoint(p) {
|
|
|
935
1037
|
return { x: p.x, y: p.y, pressure: pressureOf(p), ...typeof p.t === "number" ? { t: p.t } : {} };
|
|
936
1038
|
}
|
|
937
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
|
+
|
|
938
1282
|
// src/paint-editor/stroke.ts
|
|
939
1283
|
function defaultTile(surface) {
|
|
940
1284
|
const data = new Uint8ClampedArray(TILE * TILE * surface.channels);
|
|
@@ -973,10 +1317,18 @@ var PaintStrokeSession = class {
|
|
|
973
1317
|
selection: null,
|
|
974
1318
|
offset: init.offset
|
|
975
1319
|
};
|
|
976
|
-
if (brush.engine === "mixer") {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
+
});
|
|
980
1332
|
} else {
|
|
981
1333
|
this.dry = new DryStroke(brush, { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies });
|
|
982
1334
|
}
|
|
@@ -991,15 +1343,14 @@ var PaintStrokeSession = class {
|
|
|
991
1343
|
touched = /* @__PURE__ */ new Set();
|
|
992
1344
|
fresh = /* @__PURE__ */ new Set();
|
|
993
1345
|
dry = null;
|
|
994
|
-
|
|
995
|
-
mixerSmoother = null;
|
|
1346
|
+
wet = null;
|
|
996
1347
|
apply;
|
|
997
1348
|
copies;
|
|
998
1349
|
bounds = null;
|
|
999
1350
|
lastRaw = null;
|
|
1000
1351
|
closed = false;
|
|
1001
1352
|
result = null;
|
|
1002
|
-
|
|
1353
|
+
tileTarget() {
|
|
1003
1354
|
return {
|
|
1004
1355
|
offset: this.init.offset,
|
|
1005
1356
|
lockTransparency: this.init.lockTransparency,
|
|
@@ -1027,15 +1378,7 @@ var PaintStrokeSession = class {
|
|
|
1027
1378
|
if (filter) points = points.map(filter);
|
|
1028
1379
|
for (const p of points) this.points.push(p);
|
|
1029
1380
|
this.lastRaw = points[points.length - 1];
|
|
1030
|
-
|
|
1031
|
-
else {
|
|
1032
|
-
const rects = [];
|
|
1033
|
-
for (const raw of points) {
|
|
1034
|
-
const p = toStrokePoint(this.mixerSmoother.push(raw));
|
|
1035
|
-
this.copies.forEach((c, i) => rects.push(...this.mixers[i].push([toStrokePoint(c.map(p))])));
|
|
1036
|
-
}
|
|
1037
|
-
this.update(rects);
|
|
1038
|
-
}
|
|
1381
|
+
this.update(this.dry ? this.dry.push(points) : this.wet.push(points));
|
|
1039
1382
|
}
|
|
1040
1383
|
update(rects) {
|
|
1041
1384
|
if (!rects.length) return;
|
|
@@ -1068,13 +1411,7 @@ var PaintStrokeSession = class {
|
|
|
1068
1411
|
/** Ends the stroke (one undo step) and returns what changed. */
|
|
1069
1412
|
end() {
|
|
1070
1413
|
if (this.closed) throw new Error("This stroke has already ended.");
|
|
1071
|
-
|
|
1072
|
-
else {
|
|
1073
|
-
const rects = [];
|
|
1074
|
-
const last = this.lastRaw ? toStrokePoint(this.lastRaw) : void 0;
|
|
1075
|
-
this.copies.forEach((c, i) => rects.push(...this.mixers[i].finish(last ? toStrokePoint(c.map(last)) : void 0)));
|
|
1076
|
-
this.update(rects);
|
|
1077
|
-
}
|
|
1414
|
+
this.update(this.dry ? this.dry.finish() : this.wet.finish());
|
|
1078
1415
|
this.closed = true;
|
|
1079
1416
|
const ch = this.base.channels;
|
|
1080
1417
|
const tiles = new Map(this.base.tiles);
|
|
@@ -1087,14 +1424,10 @@ var PaintStrokeSession = class {
|
|
|
1087
1424
|
if (!sameTile(prev, tile)) changed = true;
|
|
1088
1425
|
}
|
|
1089
1426
|
const surface = changed ? new TiledSurface(ch, tiles, this.base.defaultValue) : null;
|
|
1090
|
-
const dabs = this.dry ? this.dry.dabCount : this.
|
|
1427
|
+
const dabs = this.dry ? this.dry.dabCount : this.wet.dabCount;
|
|
1091
1428
|
this.result = { layerId: this.layerId, bounds: changed ? this.bounds : null, dabs, strokeId: null };
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
let load = this.mixers[0].load;
|
|
1095
|
-
if (brush.mixer.cleanEachStroke) load = cleanMixer(load);
|
|
1096
|
-
this.result.mixerLoad = load;
|
|
1097
|
-
}
|
|
1429
|
+
const carried = this.wet?.load;
|
|
1430
|
+
if (carried) this.result.mixerLoad = this.init.brush.behavior.dirty ? carried : cleanMixer(carried);
|
|
1098
1431
|
this.host.commit(this, surface, false);
|
|
1099
1432
|
return this.result;
|
|
1100
1433
|
}
|
|
@@ -1110,6 +1443,11 @@ function mixerLoadForStroke(brush, current, color) {
|
|
|
1110
1443
|
if (brush.mixer.loadEachStroke || !current.reservoir) return loadMixer(current, color);
|
|
1111
1444
|
return current;
|
|
1112
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
|
+
}
|
|
1113
1451
|
|
|
1114
1452
|
// src/paint-editor/strokes.ts
|
|
1115
1453
|
var IDENTITY_AFFINE = [1, 0, 0, 1, 0, 0];
|
|
@@ -1155,6 +1493,17 @@ function readPoint(raw) {
|
|
|
1155
1493
|
function readAffine(raw) {
|
|
1156
1494
|
return Array.isArray(raw) && raw.length === 6 && raw.every(finite) ? raw : null;
|
|
1157
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
|
+
}
|
|
1158
1507
|
function normalizeBrushStroke(raw) {
|
|
1159
1508
|
if (!raw || typeof raw !== "object") return null;
|
|
1160
1509
|
const r = raw;
|
|
@@ -1171,6 +1520,8 @@ function normalizeBrushStroke(raw) {
|
|
|
1171
1520
|
};
|
|
1172
1521
|
const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m) => m !== null) : [];
|
|
1173
1522
|
if (copies.length) out.copies = copies;
|
|
1523
|
+
const load = readLoad(r.load);
|
|
1524
|
+
if (load) out.load = load;
|
|
1174
1525
|
return out;
|
|
1175
1526
|
}
|
|
1176
1527
|
function strokeBounds(points) {
|
|
@@ -1322,7 +1673,7 @@ function downsampleRgba(data, width, height, tw, th) {
|
|
|
1322
1673
|
// src/paint-editor/serialize.ts
|
|
1323
1674
|
var PAINT_FORMAT = "pixerate-paint";
|
|
1324
1675
|
var PAINT_SCHEMA_VERSION = 1;
|
|
1325
|
-
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"]);
|
|
1326
1677
|
function documentExtensions(raw) {
|
|
1327
1678
|
const out = {};
|
|
1328
1679
|
for (const [k, v] of Object.entries(raw)) if (!KNOWN.has(k)) out[k] = v;
|
|
@@ -1342,6 +1693,7 @@ function header(state, options) {
|
|
|
1342
1693
|
if (options.strokes !== false && state.strokes.length) out.strokes = JSON.parse(JSON.stringify(state.strokes));
|
|
1343
1694
|
if (options.guides !== void 0) out.guides = JSON.parse(JSON.stringify(options.guides));
|
|
1344
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));
|
|
1345
1697
|
return out;
|
|
1346
1698
|
}
|
|
1347
1699
|
function serializePaintDocument(state, options = {}) {
|
|
@@ -1389,7 +1741,7 @@ function deserializePaintDocument(raw) {
|
|
|
1389
1741
|
const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);
|
|
1390
1742
|
const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
|
|
1391
1743
|
const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : flattenLayers(layers).filter((l) => l.type === "pixel").pop()?.id ?? null;
|
|
1392
|
-
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 };
|
|
1393
1745
|
}
|
|
1394
1746
|
async function deserializePaintDocumentAsync(raw, store) {
|
|
1395
1747
|
if (!raw || typeof raw !== "object") throw new Error("Not a paint document: expected a JSON object.");
|
|
@@ -1574,7 +1926,7 @@ function paintTestStroke(surface, brush, paper, color, width, height) {
|
|
|
1574
1926
|
colorHex: color,
|
|
1575
1927
|
affineCopies: [],
|
|
1576
1928
|
copies: [IDENTITY_COPY],
|
|
1577
|
-
mixerLoad:
|
|
1929
|
+
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
|
|
1578
1930
|
}
|
|
1579
1931
|
);
|
|
1580
1932
|
session.push(thumbnailStroke(width, height));
|
|
@@ -1590,7 +1942,7 @@ function renderBrushThumbnail(brush, options) {
|
|
|
1590
1942
|
let surface = emptySurface(4);
|
|
1591
1943
|
if (b.engine === "eraser") {
|
|
1592
1944
|
surface = fillRect(surface, { x: 0, y: 0, width, height }, rgba);
|
|
1593
|
-
} 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) {
|
|
1594
1946
|
const half = Math.ceil(height / 2);
|
|
1595
1947
|
surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);
|
|
1596
1948
|
surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
|
|
@@ -1783,9 +2135,9 @@ function perspectiveGuideLines(g, size, count = 12) {
|
|
|
1783
2135
|
}
|
|
1784
2136
|
|
|
1785
2137
|
// src/paint-editor/color.ts
|
|
1786
|
-
var
|
|
2138
|
+
var HEX3 = /^#?([0-9a-f]{6})$/i;
|
|
1787
2139
|
function parseHexColor(value, what = "color") {
|
|
1788
|
-
const m = typeof value === "string" ?
|
|
2140
|
+
const m = typeof value === "string" ? HEX3.exec(value.trim()) : null;
|
|
1789
2141
|
if (!m) throw new Error(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
1790
2142
|
const n = parseInt(m[1], 16);
|
|
1791
2143
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
@@ -1913,10 +2265,18 @@ var PAD_KNIFE = normalizePaintBrush({
|
|
|
1913
2265
|
id: "pad-knife",
|
|
1914
2266
|
name: "Palette knife",
|
|
1915
2267
|
category: "smudge",
|
|
1916
|
-
engine: "
|
|
2268
|
+
engine: "smudge",
|
|
1917
2269
|
loadsColor: false,
|
|
1918
|
-
tip: { size: 34, hardness: 0.9, spacing: 0.08, roundness: 0.35, angle: 90, opacity: 1, flow:
|
|
1919
|
-
|
|
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 }
|
|
1920
2280
|
});
|
|
1921
2281
|
var MixingPad = class {
|
|
1922
2282
|
width = MIXING_PAD_WIDTH;
|
|
@@ -1934,7 +2294,11 @@ var MixingPad = class {
|
|
|
1934
2294
|
}
|
|
1935
2295
|
let brush = options.tool === "knife" ? PAD_KNIFE : PAD_PAINT;
|
|
1936
2296
|
if (options.size !== void 0) brush = normalizePaintBrush({ tip: { ...brush.tip, size: options.size } }, brush);
|
|
1937
|
-
|
|
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);
|
|
1938
2302
|
let result = null;
|
|
1939
2303
|
const session = new PaintStrokeSession(
|
|
1940
2304
|
{ update: () => {
|
|
@@ -1950,10 +2314,10 @@ var MixingPad = class {
|
|
|
1950
2314
|
grain: null,
|
|
1951
2315
|
paperDepth: 0,
|
|
1952
2316
|
seed: 1,
|
|
1953
|
-
colorHex:
|
|
2317
|
+
colorHex: color,
|
|
1954
2318
|
affineCopies: [],
|
|
1955
2319
|
copies: [IDENTITY_COPY],
|
|
1956
|
-
mixerLoad:
|
|
2320
|
+
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255])
|
|
1957
2321
|
}
|
|
1958
2322
|
);
|
|
1959
2323
|
session.push(points);
|
|
@@ -1992,6 +2356,82 @@ var MixingPad = class {
|
|
|
1992
2356
|
this.surface = emptySurface(4);
|
|
1993
2357
|
this.version++;
|
|
1994
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
|
+
}
|
|
1995
2435
|
/** The pad over white, `width × height` RGBA. */
|
|
1996
2436
|
pixels() {
|
|
1997
2437
|
const px = readRegion(this.surface, { x: 0, y: 0, width: this.width, height: this.height });
|
|
@@ -2013,9 +2453,9 @@ var PaintEditorError = class extends Error {
|
|
|
2013
2453
|
this.name = "PaintEditorError";
|
|
2014
2454
|
}
|
|
2015
2455
|
};
|
|
2016
|
-
var
|
|
2456
|
+
var HEX4 = /^#[0-9a-f]{6}$/i;
|
|
2017
2457
|
function parseColor(value, what = "color") {
|
|
2018
|
-
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".`);
|
|
2019
2459
|
return value.trim().toLowerCase();
|
|
2020
2460
|
}
|
|
2021
2461
|
function rgbaOf(hex) {
|
|
@@ -2041,6 +2481,8 @@ var PaintEditorController = class {
|
|
|
2041
2481
|
historyLimit;
|
|
2042
2482
|
brush;
|
|
2043
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;
|
|
2044
2486
|
color;
|
|
2045
2487
|
secondaryColor = "#ffffff";
|
|
2046
2488
|
mixerLoad = EMPTY_MIXER_LOAD;
|
|
@@ -2059,6 +2501,8 @@ var PaintEditorController = class {
|
|
|
2059
2501
|
guides;
|
|
2060
2502
|
swatches = DEFAULT_SWATCHES.map((s) => ({ ...s }));
|
|
2061
2503
|
pad = new MixingPad();
|
|
2504
|
+
savedPads = [];
|
|
2505
|
+
nextPadId = 1;
|
|
2062
2506
|
tracingVersion = 1;
|
|
2063
2507
|
tracingCache = null;
|
|
2064
2508
|
imagePixels = /* @__PURE__ */ new Map();
|
|
@@ -2096,7 +2540,7 @@ var PaintEditorController = class {
|
|
|
2096
2540
|
this.cached = {
|
|
2097
2541
|
document: this.doc,
|
|
2098
2542
|
brush: this.brush,
|
|
2099
|
-
brushes: [...BUILTIN_BRUSHES, ...this.userBrushes],
|
|
2543
|
+
brushes: this.library ??= [...BUILTIN_BRUSHES, ...this.userBrushes],
|
|
2100
2544
|
color: this.color,
|
|
2101
2545
|
secondaryColor: this.secondaryColor,
|
|
2102
2546
|
mixerLoad: this.mixerLoad,
|
|
@@ -2109,6 +2553,7 @@ var PaintEditorController = class {
|
|
|
2109
2553
|
guides: this.guides,
|
|
2110
2554
|
swatches: this.swatches,
|
|
2111
2555
|
mixingPadVersion: this.pad.version,
|
|
2556
|
+
savedMixingPads: this.savedPads.map(({ id, name }) => ({ id, name })),
|
|
2112
2557
|
stroking: Boolean(this.session),
|
|
2113
2558
|
version: this.version
|
|
2114
2559
|
};
|
|
@@ -2478,6 +2923,7 @@ var PaintEditorController = class {
|
|
|
2478
2923
|
const i = this.userBrushes.findIndex((b) => b.id === options.id);
|
|
2479
2924
|
if (i >= 0) this.userBrushes[i] = saved;
|
|
2480
2925
|
else this.userBrushes.push(saved);
|
|
2926
|
+
this.library = null;
|
|
2481
2927
|
this.emit([]);
|
|
2482
2928
|
return saved;
|
|
2483
2929
|
}
|
|
@@ -2486,6 +2932,7 @@ var PaintEditorController = class {
|
|
|
2486
2932
|
const i = this.userBrushes.findIndex((b) => b.id === id);
|
|
2487
2933
|
if (i < 0) throw new PaintEditorError(`Unknown brush "${id}". Call list_brushes for the ids.`);
|
|
2488
2934
|
this.userBrushes.splice(i, 1);
|
|
2935
|
+
this.library = null;
|
|
2489
2936
|
this.emit([]);
|
|
2490
2937
|
}
|
|
2491
2938
|
/** The user's saved brushes. */
|
|
@@ -2526,7 +2973,10 @@ var PaintEditorController = class {
|
|
|
2526
2973
|
else this.userBrushes.push(b);
|
|
2527
2974
|
added.push(b.id);
|
|
2528
2975
|
}
|
|
2529
|
-
if (added.length)
|
|
2976
|
+
if (added.length) {
|
|
2977
|
+
this.library = null;
|
|
2978
|
+
this.emit([]);
|
|
2979
|
+
}
|
|
2530
2980
|
return { added, skipped, problems };
|
|
2531
2981
|
}
|
|
2532
2982
|
/** A thumbnail of a brush: a test stroke on the current paper. */
|
|
@@ -2600,6 +3050,14 @@ var PaintEditorController = class {
|
|
|
2600
3050
|
if (init.brush) brush = normalizePaintBrush({ ...init.brush, id: brush.id }, brush);
|
|
2601
3051
|
return brush;
|
|
2602
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
|
+
}
|
|
2603
3061
|
/** Extra copies every new stroke paints (the painting guides add mirror and kaleidoscope copies). */
|
|
2604
3062
|
strokeCopies() {
|
|
2605
3063
|
return symmetryCopies(this.guides.symmetry);
|
|
@@ -2640,7 +3098,8 @@ var PaintEditorController = class {
|
|
|
2640
3098
|
affineCopies: copies,
|
|
2641
3099
|
copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
|
|
2642
3100
|
inputFilter: snapper ? (p) => snapper.snap(p) : void 0,
|
|
2643
|
-
mixerLoad:
|
|
3101
|
+
mixerLoad: init.load ?? brushLoadForStroke(brush, this.mixerLoad, [rgba[0] / 255, rgba[1] / 255, rgba[2] / 255]),
|
|
3102
|
+
secondary: rgbaOf(this.secondaryColor)
|
|
2644
3103
|
});
|
|
2645
3104
|
return session;
|
|
2646
3105
|
}
|
|
@@ -2655,6 +3114,7 @@ var PaintEditorController = class {
|
|
|
2655
3114
|
seed: init.seed
|
|
2656
3115
|
};
|
|
2657
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));
|
|
2658
3118
|
return stroke;
|
|
2659
3119
|
}
|
|
2660
3120
|
/**
|
|
@@ -2673,7 +3133,7 @@ var PaintEditorController = class {
|
|
|
2673
3133
|
this.live = null;
|
|
2674
3134
|
this.liveLayers = null;
|
|
2675
3135
|
const brush = s.init.brush;
|
|
2676
|
-
if (!cancelled && s.result?.mixerLoad
|
|
3136
|
+
if (!cancelled && s.result?.mixerLoad) this.keepLoad(brush, s.result.mixerLoad);
|
|
2677
3137
|
if (cancelled || !surface) {
|
|
2678
3138
|
this.emit();
|
|
2679
3139
|
return;
|
|
@@ -2717,7 +3177,7 @@ var PaintEditorController = class {
|
|
|
2717
3177
|
this.requireIdle("painting");
|
|
2718
3178
|
const points = this.checkPoints(init.points);
|
|
2719
3179
|
const { doc, result, brush } = this.runStroke(this.doc, { ...init, start: init.start ?? points[0] }, points);
|
|
2720
|
-
if (result.mixerLoad
|
|
3180
|
+
if (result.mixerLoad) this.keepLoad(brush, result.mixerLoad);
|
|
2721
3181
|
if (doc !== this.doc) this.commit(`${brush.name} stroke`, doc);
|
|
2722
3182
|
else this.emit([]);
|
|
2723
3183
|
return result;
|
|
@@ -2777,7 +3237,12 @@ var PaintEditorController = class {
|
|
|
2777
3237
|
}
|
|
2778
3238
|
const layerId = options.layerId ?? s.layerId;
|
|
2779
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.`);
|
|
2780
|
-
|
|
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);
|
|
2781
3246
|
doc = run.doc;
|
|
2782
3247
|
bounds = unionRect(bounds, run.result.bounds);
|
|
2783
3248
|
if (run.result.strokeId) ids.push(run.result.strokeId);
|
|
@@ -2856,14 +3321,15 @@ var PaintEditorController = class {
|
|
|
2856
3321
|
// --- Save, load and export -------------------------------------------------------
|
|
2857
3322
|
/** The document as JSON with inline PNG assets. */
|
|
2858
3323
|
toJSON(options = {}) {
|
|
2859
|
-
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() });
|
|
2860
3325
|
}
|
|
2861
3326
|
/** The document as JSON with each PNG stored in `store`. */
|
|
2862
3327
|
toJSONExternal(store, options = {}) {
|
|
2863
|
-
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() });
|
|
2864
3329
|
}
|
|
2865
3330
|
adopt(loaded) {
|
|
2866
3331
|
this.extensions = loaded.extensions;
|
|
3332
|
+
this.readMixingPad(loaded.mixingPad, loaded.problems);
|
|
2867
3333
|
this.readGuides(loaded.guides, loaded.state, loaded.problems);
|
|
2868
3334
|
if (Array.isArray(loaded.swatches)) {
|
|
2869
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() });
|
|
@@ -3218,6 +3684,99 @@ var PaintEditorController = class {
|
|
|
3218
3684
|
this.pad.clear();
|
|
3219
3685
|
this.emit([]);
|
|
3220
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
|
+
}
|
|
3221
3780
|
/** A layer's tile surface (live while stroking), for thumbnails. */
|
|
3222
3781
|
layerSurface(id) {
|
|
3223
3782
|
const l = findLayer(this.currentLayers(), id)?.layer;
|
|
@@ -3675,7 +4234,7 @@ function createPaintGuideTools(c) {
|
|
|
3675
4234
|
{
|
|
3676
4235
|
name: "mixing_pad_stroke",
|
|
3677
4236
|
title: "Paint on the mixing pad",
|
|
3678
|
-
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.",
|
|
3679
4238
|
inputSchema: {
|
|
3680
4239
|
type: "object",
|
|
3681
4240
|
properties: {
|
|
@@ -3730,6 +4289,73 @@ function createPaintGuideTools(c) {
|
|
|
3730
4289
|
return { cleared: true };
|
|
3731
4290
|
}
|
|
3732
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
|
+
},
|
|
3733
4359
|
{
|
|
3734
4360
|
name: "render_mixing_pad",
|
|
3735
4361
|
title: "Render the mixing pad",
|
|
@@ -3799,28 +4425,79 @@ var DYNAMICS = {
|
|
|
3799
4425
|
};
|
|
3800
4426
|
var MIXER = {
|
|
3801
4427
|
type: "object",
|
|
3802
|
-
description: "Mixer engine settings (Mixer Brush
|
|
4428
|
+
description: "Mixer engine settings (the Mixer Brush). Missing fields keep their current values.",
|
|
3803
4429
|
properties: {
|
|
3804
|
-
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." },
|
|
3805
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." },
|
|
3806
|
-
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." },
|
|
3807
4433
|
loadEachStroke: { type: "boolean", description: "Refill the reservoir with the colour before each stroke." },
|
|
3808
4434
|
cleanEachStroke: { type: "boolean", description: "Empty the pickup well after each stroke." },
|
|
3809
4435
|
mixing: { type: "string", enum: [...MIXING_MODELS], description: "rgb: RGB averages. pigment: mixes like paint (Kubelka-Munk)." }
|
|
3810
4436
|
},
|
|
3811
4437
|
additionalProperties: false
|
|
3812
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
|
+
};
|
|
3813
4483
|
var BRUSH_FIELDS = {
|
|
3814
4484
|
name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." },
|
|
3815
4485
|
category: { type: "string", enum: [...PAINT_BRUSH_CATEGORIES], description: "Library category." },
|
|
3816
|
-
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
|
+
},
|
|
3817
4491
|
tip: TIP,
|
|
3818
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." },
|
|
3819
4493
|
pressureSmoothing: { type: "number", minimum: 0, maximum: 0.95, description: "Pressure smoothing, separate from the tip position smoothing." },
|
|
3820
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)." },
|
|
3821
4495
|
dynamics: DYNAMICS,
|
|
3822
4496
|
mixer: MIXER,
|
|
3823
|
-
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
|
|
3824
4501
|
};
|
|
3825
4502
|
var POINTS = {
|
|
3826
4503
|
type: "array",
|
|
@@ -3849,7 +4526,20 @@ function loadResult(load) {
|
|
|
3849
4526
|
return { reservoir: hex(load.reservoir), amount: Math.round(load.amount * 1e3) / 1e3, pickup: hex(load.pickup) };
|
|
3850
4527
|
}
|
|
3851
4528
|
function brushSummary(b) {
|
|
3852
|
-
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
|
+
};
|
|
3853
4543
|
}
|
|
3854
4544
|
function createPaintEditorToolProvider(controller, options = {}) {
|
|
3855
4545
|
const c = controller;
|
|
@@ -4138,7 +4828,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4138
4828
|
add({
|
|
4139
4829
|
name: "list_brushes",
|
|
4140
4830
|
title: "List brushes",
|
|
4141
|
-
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.",
|
|
4142
4832
|
inputSchema: EMPTY2,
|
|
4143
4833
|
annotations: { title: "List brushes", ...READ2 },
|
|
4144
4834
|
handler: () => {
|
|
@@ -4149,7 +4839,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4149
4839
|
add({
|
|
4150
4840
|
name: "get_brush",
|
|
4151
4841
|
title: "Get a brush",
|
|
4152
|
-
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.",
|
|
4153
4843
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brushes (default the active brush)." } }, additionalProperties: false },
|
|
4154
4844
|
annotations: { title: "Get a brush", ...READ2 },
|
|
4155
4845
|
handler: ({ id }) => {
|
|
@@ -4284,7 +4974,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4284
4974
|
add({
|
|
4285
4975
|
name: "get_mixer_brush",
|
|
4286
4976
|
title: "Get the Mixer Brush load",
|
|
4287
|
-
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.",
|
|
4288
4978
|
inputSchema: EMPTY2,
|
|
4289
4979
|
annotations: { title: "Get the Mixer Brush load", ...READ2 },
|
|
4290
4980
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
@@ -4300,7 +4990,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4300
4990
|
add({
|
|
4301
4991
|
name: "clean_mixer_brush",
|
|
4302
4992
|
title: "Clean the Mixer Brush",
|
|
4303
|
-
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.",
|
|
4304
4994
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
4305
4995
|
annotations: { title: "Clean the Mixer Brush", ...SET2 },
|
|
4306
4996
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
@@ -4308,7 +4998,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
4308
4998
|
add({
|
|
4309
4999
|
name: "paint_stroke",
|
|
4310
5000
|
title: "Paint a brush stroke",
|
|
4311
|
-
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.",
|
|
4312
5002
|
inputSchema: {
|
|
4313
5003
|
type: "object",
|
|
4314
5004
|
properties: {
|
|
@@ -4815,6 +5505,67 @@ var BLEND_MENU_GROUPS = [
|
|
|
4815
5505
|
{ label: "Standard", modes: BLEND_MODES.filter((m) => !m.painterly && m.id !== "pigment") },
|
|
4816
5506
|
{ label: "Paint", modes: BLEND_MODES.filter((m) => m.painterly || m.id === "pigment") }
|
|
4817
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
|
+
}
|
|
4818
5569
|
export {
|
|
4819
5570
|
BLEND_MENU_GROUPS,
|
|
4820
5571
|
BLEND_MODES,
|
|
@@ -4822,9 +5573,11 @@ export {
|
|
|
4822
5573
|
BRUSH_ROTATIONS,
|
|
4823
5574
|
BRUSH_SLIDERS,
|
|
4824
5575
|
BUILTIN_BRUSHES,
|
|
5576
|
+
DEFAULT_BEHAVIOR,
|
|
4825
5577
|
DEFAULT_BRUSH_ID,
|
|
4826
5578
|
DEFAULT_DPI,
|
|
4827
5579
|
DEFAULT_DYNAMICS,
|
|
5580
|
+
DEFAULT_MULTI_COLOR,
|
|
4828
5581
|
DEFAULT_PAPER,
|
|
4829
5582
|
DEFAULT_SWATCHES,
|
|
4830
5583
|
DEFAULT_VIEW,
|
|
@@ -4843,7 +5596,9 @@ export {
|
|
|
4843
5596
|
MIN_ZOOM,
|
|
4844
5597
|
MIXING_PAD_HEIGHT,
|
|
4845
5598
|
MIXING_PAD_WIDTH,
|
|
5599
|
+
MULTI_COLOR_MODES,
|
|
4846
5600
|
MixingPad,
|
|
5601
|
+
PAD_SAMPLE_SIZES,
|
|
4847
5602
|
PAINTERLY_BLEND_MODES,
|
|
4848
5603
|
PAINT_BRUSH_CATEGORIES,
|
|
4849
5604
|
PAINT_BRUSH_ENGINES,
|
|
@@ -4855,6 +5610,7 @@ export {
|
|
|
4855
5610
|
PAPER_NODE_ID,
|
|
4856
5611
|
PAPER_SLIDERS,
|
|
4857
5612
|
PaintCanvasPresenter,
|
|
5613
|
+
PaintDabStroke,
|
|
4858
5614
|
PaintDirector,
|
|
4859
5615
|
PaintDocumentError,
|
|
4860
5616
|
PaintEditorController,
|
|
@@ -4872,6 +5628,9 @@ export {
|
|
|
4872
5628
|
TileableNoise,
|
|
4873
5629
|
affineCopy,
|
|
4874
5630
|
applyAffine,
|
|
5631
|
+
brushEngineControls,
|
|
5632
|
+
brushLoadColor,
|
|
5633
|
+
brushLoadForStroke,
|
|
4875
5634
|
brushToJSON,
|
|
4876
5635
|
clonePaper,
|
|
4877
5636
|
colorHarmony,
|
|
@@ -4903,26 +5662,35 @@ export {
|
|
|
4903
5662
|
isBlendMode,
|
|
4904
5663
|
isPainterlyBlendMode,
|
|
4905
5664
|
isPalmTouch,
|
|
5665
|
+
isWetCapable,
|
|
4906
5666
|
mapPenPoint,
|
|
4907
5667
|
mix32,
|
|
4908
5668
|
mixerLoadForStroke,
|
|
4909
5669
|
mixerTip,
|
|
5670
|
+
multiColorForDab,
|
|
5671
|
+
multiColorGradient,
|
|
5672
|
+
multiColorPalette,
|
|
5673
|
+
multiColorSliders,
|
|
4910
5674
|
newPaintLayerId,
|
|
5675
|
+
normalizeBehavior,
|
|
4911
5676
|
normalizeBrushStroke,
|
|
4912
5677
|
normalizeCanvas,
|
|
4913
5678
|
normalizeDynamics,
|
|
5679
|
+
normalizeMultiColor,
|
|
4914
5680
|
normalizePaintBrush,
|
|
4915
5681
|
normalizePaper,
|
|
4916
5682
|
normalizePerspective,
|
|
4917
5683
|
normalizeRotation,
|
|
4918
5684
|
normalizeSymmetry,
|
|
4919
5685
|
normalizeView,
|
|
5686
|
+
padSampleRadius,
|
|
4920
5687
|
paperGrain,
|
|
4921
5688
|
paperKey,
|
|
4922
5689
|
paperNode,
|
|
4923
5690
|
paperPreset,
|
|
4924
5691
|
parseBrushLibrary,
|
|
4925
5692
|
parseHexColor,
|
|
5693
|
+
patchBrushValue,
|
|
4926
5694
|
patchPath,
|
|
4927
5695
|
penOrientation,
|
|
4928
5696
|
perspectiveDirections,
|
|
@@ -4953,6 +5721,7 @@ export {
|
|
|
4953
5721
|
thumbnailStroke,
|
|
4954
5722
|
tiltToOrientation,
|
|
4955
5723
|
toStrokePoint,
|
|
5724
|
+
usesPaintExchange,
|
|
4956
5725
|
variationAffine,
|
|
4957
5726
|
varyStroke,
|
|
4958
5727
|
viewCssTransform,
|