@pixerate/editor 0.255.0 → 0.257.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.cjs +40 -38
- package/dist/agent/index.js +9 -7
- package/dist/animation/index.cjs +4 -2
- package/dist/animation/index.js +4 -2
- package/dist/animation-editor/index.cjs +63 -61
- package/dist/animation-editor/index.js +7 -5
- package/dist/brushes/colour.d.cts +11 -0
- package/dist/brushes/colour.d.ts +11 -0
- package/dist/brushes/index.cjs +51 -30
- package/dist/brushes/index.d.cts +1 -0
- package/dist/brushes/index.d.ts +1 -0
- package/dist/brushes/index.js +25 -4
- package/dist/brushes/resolve.d.cts +2 -0
- package/dist/brushes/resolve.d.ts +2 -0
- package/dist/brushes/schema.d.cts +1 -0
- package/dist/brushes/schema.d.ts +1 -0
- package/dist/brushes/tools.d.cts +7 -0
- package/dist/brushes/tools.d.ts +7 -0
- package/dist/brushes/types.d.cts +10 -2
- package/dist/brushes/types.d.ts +10 -2
- package/dist/brushes/ui.d.cts +14 -3
- package/dist/brushes/ui.d.ts +14 -3
- package/dist/{brushstrokeRender-HCOQUSWW.js → brushstrokeRender-JI4W6F2G.js} +2 -2
- package/dist/{brushstrokeRender-ZZOXI5O5.cjs → brushstrokeRender-MQ6PSDM5.cjs} +11 -11
- package/dist/canvas/index.cjs +20 -18
- package/dist/canvas/index.js +7 -5
- package/dist/chart/index.cjs +159 -157
- package/dist/chart/index.js +9 -7
- package/dist/{chunk-7272D3K2.js → chunk-2EUW2XZX.js} +27 -276
- package/dist/{chunk-L6Y7TSQ4.js → chunk-2VJ6OVJV.js} +1 -1
- package/dist/{chunk-6ZZKRH7R.js → chunk-47V3WXDG.js} +2 -2
- package/dist/{chunk-H7ET2ISK.cjs → chunk-4IMD2TLY.cjs} +52 -52
- package/dist/{chunk-VUKUTT2W.js → chunk-4M4E3N6X.js} +1 -1
- package/dist/{chunk-TBAVCLEQ.cjs → chunk-5Q5MLXAD.cjs} +23 -23
- package/dist/{chunk-MONXNVZK.js → chunk-6I23ZCGK.js} +5 -3
- package/dist/{chunk-GOBOCZZI.js → chunk-6NQPKNUZ.js} +1 -1
- package/dist/{chunk-WYGSR3IJ.js → chunk-AORZ5M5C.js} +1 -1
- package/dist/chunk-AVTPZO4Z.cjs +142 -0
- package/dist/{chunk-WKMUBATU.js → chunk-C2WEIEUB.js} +1 -1
- package/dist/{chunk-P3K2JCJA.js → chunk-D5BXSOSU.js} +3 -3
- package/dist/chunk-E2V5DA3W.js +36 -0
- package/dist/{chunk-WZOGDKCQ.js → chunk-F7CPSRLN.js} +0 -133
- package/dist/chunk-FJBQTZS4.js +17 -0
- package/dist/{chunk-SOSBOS5T.cjs → chunk-G23BTQA5.cjs} +22 -22
- package/dist/{chunk-EUIDTZNQ.cjs → chunk-I3Y2Q3PM.cjs} +59 -308
- package/dist/{chunk-YHUMGBZD.js → chunk-ICTPLVSR.js} +22 -4
- package/dist/{chunk-LRNUZKWV.cjs → chunk-IPUTIUGQ.cjs} +1 -1
- package/dist/{chunk-6GRUVGKU.js → chunk-ITURMC2M.js} +3 -3
- package/dist/{chunk-HYWRU5VX.js → chunk-IXQZHCOF.js} +1 -1
- package/dist/chunk-J5UQHWVL.js +142 -0
- package/dist/{chunk-YBAPT7NL.js → chunk-JC2SHA4A.js} +1 -23
- package/dist/chunk-LRTR4ASZ.cjs +17 -0
- package/dist/{chunk-HUAISCUX.cjs → chunk-M2HG5DJ2.cjs} +18 -18
- package/dist/{chunk-N76AMOKE.cjs → chunk-MDLEGVEN.cjs} +42 -42
- package/dist/chunk-MEGSQUQO.cjs +36 -0
- package/dist/chunk-MLLTWAXH.cjs +276 -0
- package/dist/{chunk-RHPSE2YR.js → chunk-MQ6MGIQM.js} +2 -2
- package/dist/{chunk-PGPFWC35.cjs → chunk-MQGQTKVW.cjs} +51 -33
- package/dist/{chunk-5YWWQ36H.cjs → chunk-O3ZVZSNJ.cjs} +9 -9
- package/dist/{chunk-GCTUOTCW.cjs → chunk-P3PZ63M3.cjs} +35 -35
- package/dist/{chunk-WRZ6C4TU.cjs → chunk-P6WYDBTH.cjs} +4 -4
- package/dist/{chunk-CJ6TBL2N.cjs → chunk-RHEMMGCN.cjs} +16 -16
- package/dist/{chunk-HIMGBZAM.cjs → chunk-RSIKDWOP.cjs} +201 -38
- package/dist/{chunk-SABJMH6M.cjs → chunk-RVGP2XLF.cjs} +2 -135
- package/dist/{chunk-JVRLGV3C.js → chunk-SMTUQWGY.js} +184 -21
- package/dist/{chunk-DMTEQ6SH.js → chunk-SSFMWJDK.js} +4 -4
- package/dist/{chunk-TFF2GTGN.cjs → chunk-SXQHWXEL.cjs} +2 -24
- package/dist/{chunk-IWEVV63O.cjs → chunk-UEYLANO3.cjs} +18 -16
- package/dist/chunk-VR6OGHU5.js +276 -0
- package/dist/{chunk-AF23QK5F.js → chunk-YQ4PQTBB.js} +1 -1
- package/dist/{chunk-B7V34GIH.cjs → chunk-YSGW7VV4.cjs} +88 -88
- package/dist/{chunk-3NS5C7FB.cjs → chunk-YYBW6DF2.cjs} +76 -76
- package/dist/{chunk-UFEDHACQ.js → chunk-Z7BX4TWO.js} +2 -2
- package/dist/{chunk-RUONPPMC.cjs → chunk-ZSE25IZA.cjs} +8 -8
- package/dist/color/index.cjs +7 -3
- package/dist/color/index.js +18 -14
- package/dist/export/index.cjs +8 -6
- package/dist/export/index.js +7 -5
- package/dist/extensions/index.cjs +11 -9
- package/dist/extensions/index.js +10 -8
- package/dist/gradient/index.cjs +33 -31
- package/dist/gradient/index.js +5 -3
- package/dist/image-editor/brush-presets.d.cts +4 -1
- package/dist/image-editor/brush-presets.d.ts +4 -1
- package/dist/image-editor/controller.d.cts +10 -5
- package/dist/image-editor/controller.d.ts +10 -5
- package/dist/image-editor/index.cjs +1090 -1073
- package/dist/image-editor/index.js +37 -20
- package/dist/index.cjs +17 -15
- package/dist/index.js +11 -9
- package/dist/paint-editor/brushes.d.cts +18 -1
- package/dist/paint-editor/brushes.d.ts +18 -1
- package/dist/paint-editor/collaboration-tools.d.cts +19 -0
- package/dist/paint-editor/collaboration-tools.d.ts +19 -0
- package/dist/paint-editor/collaboration.d.cts +180 -0
- package/dist/paint-editor/collaboration.d.ts +180 -0
- package/dist/paint-editor/controller.d.cts +83 -0
- package/dist/paint-editor/controller.d.ts +83 -0
- package/dist/paint-editor/engine.d.cts +7 -0
- package/dist/paint-editor/engine.d.ts +7 -0
- package/dist/paint-editor/index.cjs +2027 -1331
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +1671 -975
- package/dist/paint-editor/mark-settings.d.cts +1 -1
- package/dist/paint-editor/mark-settings.d.ts +1 -1
- package/dist/paint-editor/procedural.d.cts +4 -2
- package/dist/paint-editor/procedural.d.ts +4 -2
- package/dist/paint-editor/stamp-tip.d.cts +45 -0
- package/dist/paint-editor/stamp-tip.d.ts +45 -0
- package/dist/paint-editor/stroke.d.cts +2 -0
- package/dist/paint-editor/stroke.d.ts +2 -0
- package/dist/stickers/index.cjs +4 -2
- package/dist/stickers/index.js +6 -4
- package/dist/typography/index.cjs +3 -2
- package/dist/typography/index.js +2 -1
- package/dist/vector/index.cjs +971 -969
- package/dist/vector/index.js +8 -6
- package/dist/video-editor/index.cjs +4 -2
- package/dist/video-editor/index.js +4 -2
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +21 -11
- package/skills/pixerate-brushes-design/SKILL.md +6 -2
- package/skills/pixerate-brushes-design/evals/canvas-chalk.json +8 -8
- package/skills/pixerate-canvas/SKILL.md +3 -2
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +35 -47
- package/skills/pixerate-paint-editor/references/painting-alongside.md +19 -0
- package/skills/pixerate-vector/SKILL.md +1 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StickerBrowser,
|
|
3
|
-
createStickerTools,
|
|
4
|
-
describeStickerRef,
|
|
5
3
|
editorStickers
|
|
6
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-F7CPSRLN.js";
|
|
7
5
|
import {
|
|
8
6
|
BRUSH_PATCH_PROPERTIES,
|
|
9
7
|
BRUSH_SLIDERS,
|
|
@@ -13,7 +11,11 @@ import {
|
|
|
13
11
|
mergeBrushPatch,
|
|
14
12
|
multiColorSliders,
|
|
15
13
|
patchBrushValue
|
|
16
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-ICTPLVSR.js";
|
|
15
|
+
import {
|
|
16
|
+
createStickerTools,
|
|
17
|
+
describeStickerRef
|
|
18
|
+
} from "../chunk-J5UQHWVL.js";
|
|
17
19
|
import {
|
|
18
20
|
toVideoFfmpeg
|
|
19
21
|
} from "../chunk-MWUZCEFT.js";
|
|
@@ -24,9 +26,12 @@ import {
|
|
|
24
26
|
resolveExternalAssets,
|
|
25
27
|
serializeLayers,
|
|
26
28
|
serializeLayersExternal
|
|
27
|
-
} from "../chunk-
|
|
28
|
-
import "../chunk-3CI7FRMK.js";
|
|
29
|
+
} from "../chunk-SSFMWJDK.js";
|
|
29
30
|
import "../chunk-ZNA3IQZF.js";
|
|
31
|
+
import {
|
|
32
|
+
brushColourHex
|
|
33
|
+
} from "../chunk-FJBQTZS4.js";
|
|
34
|
+
import "../chunk-3CI7FRMK.js";
|
|
30
35
|
import {
|
|
31
36
|
LAYER_EFFECT_KINDS,
|
|
32
37
|
LayerEffectError,
|
|
@@ -80,7 +85,7 @@ import {
|
|
|
80
85
|
realPresenceClock,
|
|
81
86
|
tweenPresenceCursor
|
|
82
87
|
} from "../chunk-IVSWRVB4.js";
|
|
83
|
-
import "../chunk-
|
|
88
|
+
import "../chunk-YQ4PQTBB.js";
|
|
84
89
|
import {
|
|
85
90
|
BRUSH_ID,
|
|
86
91
|
BRUSH_PRESETS,
|
|
@@ -95,6 +100,7 @@ import {
|
|
|
95
100
|
DEFAULT_LIGHT,
|
|
96
101
|
DEFAULT_MULTI_COLOR,
|
|
97
102
|
DEFAULT_PARTICLE,
|
|
103
|
+
DEFAULT_SPRAY,
|
|
98
104
|
DEFAULT_STICKER,
|
|
99
105
|
DEFAULT_WET_BRUSH,
|
|
100
106
|
DOT_ROTATIONS,
|
|
@@ -134,7 +140,7 @@ import {
|
|
|
134
140
|
parseBrushLibrary,
|
|
135
141
|
resolveBrush,
|
|
136
142
|
usesPaintExchange
|
|
137
|
-
} from "../chunk-
|
|
143
|
+
} from "../chunk-SMTUQWGY.js";
|
|
138
144
|
import {
|
|
139
145
|
STAMP_KINDS,
|
|
140
146
|
StickersSlot,
|
|
@@ -273,8 +279,10 @@ import {
|
|
|
273
279
|
colorHarmony,
|
|
274
280
|
hueToWheel,
|
|
275
281
|
wheelToHue
|
|
276
|
-
} from "../chunk-
|
|
277
|
-
import "../chunk-
|
|
282
|
+
} from "../chunk-2EUW2XZX.js";
|
|
283
|
+
import "../chunk-E2V5DA3W.js";
|
|
284
|
+
import "../chunk-VR6OGHU5.js";
|
|
285
|
+
import "../chunk-JC2SHA4A.js";
|
|
278
286
|
import {
|
|
279
287
|
linearToSrgb,
|
|
280
288
|
oklabToOklch,
|
|
@@ -370,8 +378,8 @@ function normalizePaper(raw = {}, base) {
|
|
|
370
378
|
...readOffset(r.offset, b.offset)
|
|
371
379
|
};
|
|
372
380
|
}
|
|
373
|
-
function hexToRgb8(
|
|
374
|
-
const n = parseInt(
|
|
381
|
+
function hexToRgb8(hex2) {
|
|
382
|
+
const n = parseInt(hex2.slice(1, 7), 16);
|
|
375
383
|
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
376
384
|
}
|
|
377
385
|
function renderPaper(paper, rect, grain = new PaperGrain(paper), light) {
|
|
@@ -577,6 +585,75 @@ function effectDabField(img, rect, cov, centre2, mixed, field, steps = 0, dab =
|
|
|
577
585
|
}
|
|
578
586
|
}
|
|
579
587
|
|
|
588
|
+
// src/paint-editor/stamp-tip.ts
|
|
589
|
+
var samplers = /* @__PURE__ */ new WeakMap();
|
|
590
|
+
function stampSampler(ref2, art) {
|
|
591
|
+
let kind = null;
|
|
592
|
+
try {
|
|
593
|
+
kind = editorStampOf(ref2.includes(":") ? ref2 : `editor:${ref2}`);
|
|
594
|
+
} catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
if (kind) {
|
|
598
|
+
const k = kind;
|
|
599
|
+
return (u, v, seed) => stampCoverage(k, u, v, seed);
|
|
600
|
+
}
|
|
601
|
+
const a = art?.(ref2);
|
|
602
|
+
if (!a || !(a.width > 0 && a.height > 0)) return null;
|
|
603
|
+
const hit = samplers.get(a);
|
|
604
|
+
if (hit) return hit;
|
|
605
|
+
const { width: w, height: h, data } = a;
|
|
606
|
+
const scale = Math.max(w, h) / 2;
|
|
607
|
+
const alpha = (x, y) => x < 0 || y < 0 || x >= w || y >= h ? 0 : data[(y * w + x) * 4 + 3] / 255;
|
|
608
|
+
const sampler = (u, v) => {
|
|
609
|
+
const x = u * scale + w / 2 - 0.5;
|
|
610
|
+
const y = v * scale + h / 2 - 0.5;
|
|
611
|
+
const x0 = Math.floor(x);
|
|
612
|
+
const y0 = Math.floor(y);
|
|
613
|
+
const fx = x - x0;
|
|
614
|
+
const fy = y - y0;
|
|
615
|
+
return (alpha(x0, y0) * (1 - fx) + alpha(x0 + 1, y0) * fx) * (1 - fy) + (alpha(x0, y0 + 1) * (1 - fx) + alpha(x0 + 1, y0 + 1) * fx) * fy;
|
|
616
|
+
};
|
|
617
|
+
samplers.set(a, sampler);
|
|
618
|
+
return sampler;
|
|
619
|
+
}
|
|
620
|
+
function stampDabRect(d) {
|
|
621
|
+
const r = Math.max(0.5, d.radius) * Math.SQRT2 + 1.5;
|
|
622
|
+
const x0 = Math.floor(d.x - r);
|
|
623
|
+
const y0 = Math.floor(d.y - r);
|
|
624
|
+
return { x: x0, y: y0, width: Math.ceil(d.x + r) - x0, height: Math.ceil(d.y + r) - y0 };
|
|
625
|
+
}
|
|
626
|
+
function rasterizeStampDab(d, sample, seed, rect, out) {
|
|
627
|
+
const w = rect.width;
|
|
628
|
+
const h = rect.height;
|
|
629
|
+
out.fill(0, 0, w * h);
|
|
630
|
+
const r = Math.max(0.5, d.radius);
|
|
631
|
+
const ro = Math.min(1, Math.max(0.05, d.roundness));
|
|
632
|
+
const sn = Math.sin(d.angle);
|
|
633
|
+
const cs = Math.cos(d.angle);
|
|
634
|
+
const ss = r < 6 ? 3 : 1;
|
|
635
|
+
for (let yy = 0; yy < h; yy++) {
|
|
636
|
+
for (let xx = 0; xx < w; xx++) {
|
|
637
|
+
let sum = 0;
|
|
638
|
+
for (let sy = 0; sy < ss; sy++) {
|
|
639
|
+
for (let sx = 0; sx < ss; sx++) {
|
|
640
|
+
const dx = rect.x + xx + (sx + 0.5) / ss - d.x;
|
|
641
|
+
const dy = rect.y + yy + (sy + 0.5) / ss - d.y;
|
|
642
|
+
const u = (dx * cs - dy * sn) / r;
|
|
643
|
+
const v = (dx * sn + dy * cs) / (r * ro);
|
|
644
|
+
if (u > -1.05 && u < 1.05 && v > -1.05 && v < 1.05) sum += sample(u, v, seed);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const val = sum / (ss * ss);
|
|
648
|
+
if (val > 0) out[yy * w + xx] = Math.min(1, val) * d.alpha;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
function brushStickerRefs(brush) {
|
|
653
|
+
const refs = brush.engine === "sticker" ? [...brush.sticker.stamps] : brush.engine === "particle" ? [...brush.particle.dotTextures] : brush.engine === "dab" && brush.stamp ? [brush.stamp] : [];
|
|
654
|
+
return [...new Set(refs.filter((r) => r.includes(":") && !r.startsWith("editor:")))];
|
|
655
|
+
}
|
|
656
|
+
|
|
580
657
|
// src/paint-editor/engine.ts
|
|
581
658
|
function penOrientation(p) {
|
|
582
659
|
if (typeof p.altitude === "number" && Number.isFinite(p.altitude)) {
|
|
@@ -795,6 +872,7 @@ var DryStroke = class {
|
|
|
795
872
|
constructor(brush, options) {
|
|
796
873
|
this.brush = brush;
|
|
797
874
|
this.options = options;
|
|
875
|
+
this.stamp = brush.stamp ? stampSampler(brush.stamp, options.stampArt) : null;
|
|
798
876
|
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
799
877
|
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
800
878
|
this.walkers = this.copies.map((c, i) => new PenDabWalker(brush, (options.seed ?? 0) + i * 7919 | 0, c.linear));
|
|
@@ -812,6 +890,8 @@ var DryStroke = class {
|
|
|
812
890
|
field = new Float32Array(0);
|
|
813
891
|
lastRaw = null;
|
|
814
892
|
lastFed = null;
|
|
893
|
+
/** The stamp tip's shape, or `null` for the round computed tip. */
|
|
894
|
+
stamp;
|
|
815
895
|
/** Feeds raw pen samples; returns the document rectangles the new dabs touched. */
|
|
816
896
|
push(points) {
|
|
817
897
|
const dabs = [];
|
|
@@ -834,34 +914,52 @@ var DryStroke = class {
|
|
|
834
914
|
}
|
|
835
915
|
paint(dabs) {
|
|
836
916
|
const rects = [];
|
|
837
|
-
const
|
|
838
|
-
const
|
|
917
|
+
const spray = this.brush.spray;
|
|
918
|
+
const seed = this.options.seed ?? 0;
|
|
839
919
|
for (const d of dabs) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const vals = this.buf.subarray(0, n);
|
|
844
|
-
rasterizeDab(d, hardness, rect, vals);
|
|
845
|
-
if (this.depth > 0 && grain) {
|
|
846
|
-
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
847
|
-
const y = rect.y + yy + 0.5;
|
|
848
|
-
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
849
|
-
if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
920
|
+
if (!spray) {
|
|
921
|
+
this.mark(d, d, rects);
|
|
922
|
+
continue;
|
|
852
923
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
924
|
+
const count = 1 + Math.round(spray.density * 31);
|
|
925
|
+
const key = d.index * 131 + Math.round(d.travel * 8);
|
|
926
|
+
for (let i = 0; i < count; i++) {
|
|
927
|
+
const a = rand01(seed, key * 64 + i, 61) * Math.PI * 2;
|
|
928
|
+
const rr = Math.sqrt(rand01(seed, key * 64 + i, 67)) * d.radius;
|
|
929
|
+
const drop = { ...d, x: d.x + Math.cos(a) * rr, y: d.y + Math.sin(a) * rr, radius: Math.max(0.5, d.radius * spray.size * (0.6 + 0.8 * rand01(seed, key * 64 + i, 71))), roundness: 1, angle: rand01(seed, key * 64 + i, 73) * Math.PI * 2 };
|
|
930
|
+
this.mark(drop, d, rects);
|
|
858
931
|
}
|
|
859
|
-
this.coverage.accumulate(rect, vals, d.opacity, null, field);
|
|
860
|
-
rects.push(rect);
|
|
861
|
-
this.dabCount++;
|
|
862
932
|
}
|
|
863
933
|
return rects;
|
|
864
934
|
}
|
|
935
|
+
/** Lays one mark (a dab, or a spray droplet of dab `d`): the round tip or the stamp, the paper grain, effect paint. */
|
|
936
|
+
mark(m, d, rects) {
|
|
937
|
+
const hardness = this.brush.tip.hardness;
|
|
938
|
+
const grain = this.options.grain;
|
|
939
|
+
const rect = this.stamp ? stampDabRect(m) : dabRect(m, hardness);
|
|
940
|
+
const n = rect.width * rect.height;
|
|
941
|
+
if (this.buf.length < n) this.buf = new Float32Array(n);
|
|
942
|
+
const vals = this.buf.subarray(0, n);
|
|
943
|
+
if (this.stamp) rasterizeStampDab(m, this.stamp, d.index, rect, vals);
|
|
944
|
+
else rasterizeDab(m, hardness, rect, vals);
|
|
945
|
+
if (this.depth > 0 && grain) {
|
|
946
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
947
|
+
const y = rect.y + yy + 0.5;
|
|
948
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
949
|
+
if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, y), this.depth, d.pressure);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
let field = null;
|
|
954
|
+
if (this.options.effect) {
|
|
955
|
+
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
956
|
+
field = this.field.subarray(0, n * 3);
|
|
957
|
+
sampleEffectField(this.options.effect, rect, vals, field, d);
|
|
958
|
+
}
|
|
959
|
+
this.coverage.accumulate(rect, vals, m.opacity, null, field);
|
|
960
|
+
rects.push(rect);
|
|
961
|
+
this.dabCount++;
|
|
962
|
+
}
|
|
865
963
|
};
|
|
866
964
|
function mixerTip(brush) {
|
|
867
965
|
return { ...brush.tip, smoothing: 0, pressureSize: brush.tip.pressureSize };
|
|
@@ -1820,984 +1918,986 @@ function lightFromJSON(raw) {
|
|
|
1820
1918
|
return surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
|
|
1821
1919
|
}
|
|
1822
1920
|
|
|
1823
|
-
// src/paint-editor/
|
|
1824
|
-
var
|
|
1825
|
-
var
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1921
|
+
// src/paint-editor/procedural.ts
|
|
1922
|
+
var STEP_PX = 1.5;
|
|
1923
|
+
var MarkStroke = class {
|
|
1924
|
+
constructor(brush, options) {
|
|
1925
|
+
this.brush = brush;
|
|
1926
|
+
this.options = options;
|
|
1927
|
+
this.sampler = options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
|
|
1928
|
+
this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;
|
|
1929
|
+
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
1930
|
+
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
1931
|
+
this.seed = options.seed ?? 0;
|
|
1932
|
+
const fine = { ...brush, tip: { ...brush.tip, spacing: Math.max(0.01, Math.min(0.25, STEP_PX / Math.max(1, brush.tip.size))) } };
|
|
1933
|
+
this.walkers = this.copies.map((c, i) => new PenDabWalker(fine, (options.seed ?? 0) + i * 7919 | 0, c.linear));
|
|
1934
|
+
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
1935
|
+
}
|
|
1936
|
+
brush;
|
|
1937
|
+
options;
|
|
1938
|
+
coverage = new CoverageMap();
|
|
1939
|
+
dabCount = 0;
|
|
1940
|
+
smoother;
|
|
1941
|
+
copies;
|
|
1942
|
+
walkers;
|
|
1943
|
+
depth;
|
|
1944
|
+
buf = new Float32Array(0);
|
|
1945
|
+
field = new Float32Array(0);
|
|
1946
|
+
lastRaw = null;
|
|
1947
|
+
lastFed = null;
|
|
1948
|
+
seed;
|
|
1949
|
+
sampler;
|
|
1950
|
+
tipTable;
|
|
1951
|
+
/** The pen dab the marks being made belong to, and its gradient colour. */
|
|
1952
|
+
pen = null;
|
|
1953
|
+
penColor = { rgb: [0, 0, 0], alpha: 1 };
|
|
1954
|
+
push(points) {
|
|
1955
|
+
const rects = [];
|
|
1956
|
+
for (const raw of points) {
|
|
1957
|
+
this.lastRaw = raw;
|
|
1958
|
+
const p = this.smoother.push(raw);
|
|
1959
|
+
this.lastFed = p;
|
|
1960
|
+
this.copies.forEach((c, i) => {
|
|
1961
|
+
const dabs = [];
|
|
1962
|
+
this.walkers[i].push(c.map(p), dabs);
|
|
1963
|
+
for (const d of dabs) this.step(i, d, rects);
|
|
1964
|
+
});
|
|
1831
1965
|
}
|
|
1966
|
+
return rects;
|
|
1832
1967
|
}
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1968
|
+
step(copy, d, rects) {
|
|
1969
|
+
this.pen = d;
|
|
1970
|
+
if (this.sampler && !this.tipTable) this.sampler.dab(d, this.penColor);
|
|
1971
|
+
this.advance(copy, d, rects);
|
|
1836
1972
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1973
|
+
/** The colour of a mark at (x, y): its pen dab's, or across the tip by its offset from the pen. */
|
|
1974
|
+
markColor(x, y) {
|
|
1975
|
+
if (!this.sampler || !this.pen) return null;
|
|
1976
|
+
if (!this.tipTable) return this.penColor;
|
|
1977
|
+
const d = this.pen;
|
|
1978
|
+
const u = ((x - d.x) * Math.sin(d.direction) + (y - d.y) * Math.cos(d.direction)) / Math.max(0.5, d.radius);
|
|
1979
|
+
const last = this.tipTable.length - 1;
|
|
1980
|
+
return this.tipTable[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
|
|
1981
|
+
}
|
|
1982
|
+
finish() {
|
|
1983
|
+
const rects = [];
|
|
1984
|
+
const raw = this.lastRaw;
|
|
1985
|
+
const fed = this.lastFed;
|
|
1986
|
+
this.copies.forEach((c, i) => {
|
|
1987
|
+
const dabs = [];
|
|
1988
|
+
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
1989
|
+
this.walkers[i].finish(dabs);
|
|
1990
|
+
for (const d of dabs) this.step(i, d, rects);
|
|
1991
|
+
});
|
|
1992
|
+
return rects;
|
|
1993
|
+
}
|
|
1994
|
+
/** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
|
|
1995
|
+
mark(x, y, r, alpha, opacity, pressure, rects) {
|
|
1996
|
+
if (!(r > 0.05) || !(alpha > 0)) return;
|
|
1997
|
+
const dab = { x, y, radius: Math.max(0.5, r), alpha: Math.min(1, alpha), opacity: Math.min(1, opacity), angle: 0, roundness: 1 };
|
|
1998
|
+
const hardness = this.brush.tip.hardness;
|
|
1999
|
+
const rect = dabRect(dab, hardness);
|
|
2000
|
+
const n = rect.width * rect.height;
|
|
2001
|
+
if (n <= 0) return;
|
|
2002
|
+
const vals = this.scratch(n);
|
|
2003
|
+
rasterizeDab(dab, hardness, rect, vals);
|
|
2004
|
+
this.lay(rect, vals, dab.opacity, pressure, x, y, rects);
|
|
1861
2005
|
}
|
|
1862
2006
|
/**
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
* the next call.
|
|
2007
|
+
* Stamps a textured mark: `kind` from the sticker stamps over a square of half-size `r`, turned
|
|
2008
|
+
* `rotation` radians clockwise, with flow `alpha` towards `opacity`. Small marks are supersampled.
|
|
1866
2009
|
*/
|
|
1867
|
-
|
|
1868
|
-
if (
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
1871
|
-
|
|
1872
|
-
const
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2010
|
+
stamp(x, y, r, kind, rotation, seed, alpha, opacity, pressure, rects) {
|
|
2011
|
+
if (!(r > 0.05) || !(alpha > 0)) return;
|
|
2012
|
+
const sample = stampSampler(kind, this.options.stampArt);
|
|
2013
|
+
if (!sample) return this.mark(x, y, r, alpha, opacity, pressure, rects);
|
|
2014
|
+
const reach = Math.max(0.5, r) * Math.SQRT2 + 1;
|
|
2015
|
+
const rect = { x: Math.floor(x - reach), y: Math.floor(y - reach), width: 0, height: 0 };
|
|
2016
|
+
rect.width = Math.ceil(x + reach) - rect.x;
|
|
2017
|
+
rect.height = Math.ceil(y + reach) - rect.y;
|
|
2018
|
+
const n = rect.width * rect.height;
|
|
2019
|
+
const vals = this.scratch(n);
|
|
2020
|
+
const ss = r < 10 ? 3 : 1;
|
|
2021
|
+
const c = Math.cos(rotation);
|
|
2022
|
+
const s = Math.sin(rotation);
|
|
2023
|
+
const inv = 1 / Math.max(0.5, r);
|
|
2024
|
+
const a = Math.min(1, alpha);
|
|
2025
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
2026
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
2027
|
+
let sum = 0;
|
|
2028
|
+
for (let sy = 0; sy < ss; sy++) {
|
|
2029
|
+
for (let sx = 0; sx < ss; sx++) {
|
|
2030
|
+
const dx = rect.x + xx + (sx + 0.5) / ss - x;
|
|
2031
|
+
const dy = rect.y + yy + (sy + 0.5) / ss - y;
|
|
2032
|
+
sum += sample((dx * c + dy * s) * inv, (dy * c - dx * s) * inv, seed);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
vals[k] = sum / (ss * ss) * a;
|
|
1892
2036
|
}
|
|
1893
|
-
|
|
2037
|
+
}
|
|
2038
|
+
this.lay(rect, vals, Math.min(1, opacity), pressure, x, y, rects);
|
|
1894
2039
|
}
|
|
1895
|
-
/**
|
|
1896
|
-
|
|
1897
|
-
this.
|
|
2040
|
+
/** A scratch buffer of `n` values (reused; valid until the next mark). */
|
|
2041
|
+
scratch(n) {
|
|
2042
|
+
if (this.buf.length < n) this.buf = new Float32Array(n);
|
|
2043
|
+
const vals = this.buf.subarray(0, n);
|
|
2044
|
+
vals.fill(0);
|
|
2045
|
+
return vals;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Lays coverage `vals` over `rect` towards `opacity`: paper grain, then effect paint or the
|
|
2049
|
+
* colour of a mark at (x, y) (gradient paint), into the stroke's coverage.
|
|
2050
|
+
*/
|
|
2051
|
+
lay(rect, vals, opacity, pressure, x, y, rects) {
|
|
2052
|
+
const n = rect.width * rect.height;
|
|
2053
|
+
if (n <= 0) return;
|
|
2054
|
+
const grain = this.options.grain;
|
|
2055
|
+
if (this.depth > 0 && grain) {
|
|
2056
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++)
|
|
2057
|
+
for (let xx = 0; xx < rect.width; xx++, k++) if (vals[k] > 0) vals[k] *= grainCatch(grain.at(rect.x + xx + 0.5, rect.y + yy + 0.5), this.depth, pressure);
|
|
2058
|
+
}
|
|
2059
|
+
if (this.options.effect) {
|
|
2060
|
+
if (this.field.length < n * 3) this.field = new Float32Array(n * 3);
|
|
2061
|
+
const field = this.field.subarray(0, n * 3);
|
|
2062
|
+
sampleEffectField(this.options.effect, rect, vals, field, this.pen);
|
|
2063
|
+
this.coverage.accumulate(rect, vals, opacity, null, field);
|
|
2064
|
+
} else {
|
|
2065
|
+
const color = this.markColor(x, y);
|
|
2066
|
+
if (color && color.alpha < 1) for (let k = 0; k < n; k++) vals[k] *= color.alpha;
|
|
2067
|
+
this.coverage.accumulate(rect, vals, opacity, color ? color.rgb : null);
|
|
2068
|
+
}
|
|
2069
|
+
rects.push(rect);
|
|
2070
|
+
this.dabCount++;
|
|
2071
|
+
}
|
|
2072
|
+
/** Marks along a segment, spaced by a fraction of the radius. */
|
|
2073
|
+
segment(x0, y0, x1, y1, r, alpha, opacity, pressure, rects) {
|
|
2074
|
+
const len = Math.hypot(x1 - x0, y1 - y0);
|
|
2075
|
+
const step = Math.max(0.35, r * 0.5);
|
|
2076
|
+
const n = Math.max(1, Math.ceil(len / step));
|
|
2077
|
+
for (let i = 1; i <= n; i++) this.mark(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, r, alpha, opacity, pressure, rects);
|
|
1898
2078
|
}
|
|
1899
2079
|
};
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
const y0 = Math.floor(fy);
|
|
1906
|
-
const tx = fx - x0;
|
|
1907
|
-
const ty = fy - y0;
|
|
1908
|
-
let r = 0;
|
|
1909
|
-
let g = 0;
|
|
1910
|
-
let b = 0;
|
|
1911
|
-
let a = 0;
|
|
1912
|
-
for (let k = 0; k < 4; k++) {
|
|
1913
|
-
const xx = x0 + (k & 1);
|
|
1914
|
-
const yy = y0 + (k >> 1);
|
|
1915
|
-
if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
|
|
1916
|
-
const wgt = (k & 1 ? tx : 1 - tx) * (k >> 1 ? ty : 1 - ty);
|
|
1917
|
-
const i = (yy * w + xx) * 4;
|
|
1918
|
-
const al = data[i + 3] * wgt;
|
|
1919
|
-
r += data[i] * al;
|
|
1920
|
-
g += data[i + 1] * al;
|
|
1921
|
-
b += data[i + 2] * al;
|
|
1922
|
-
a += al;
|
|
1923
|
-
}
|
|
1924
|
-
out[0] = a > 0 ? r / a / 255 : 0;
|
|
1925
|
-
out[1] = a > 0 ? g / a / 255 : 0;
|
|
1926
|
-
out[2] = a > 0 ? b / a / 255 : 0;
|
|
1927
|
-
out[3] = a / 255;
|
|
1928
|
-
return out;
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
// src/paint-editor/stickers.ts
|
|
1932
|
-
function hexRgb(hex) {
|
|
1933
|
-
const n = parseInt(hex.slice(1, 7), 16);
|
|
1934
|
-
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
1935
|
-
}
|
|
1936
|
-
function stickerRect(s) {
|
|
1937
|
-
const r = s.size / 2 + 1;
|
|
1938
|
-
const x0 = Math.floor(s.x - r);
|
|
1939
|
-
const y0 = Math.floor(s.y - r);
|
|
1940
|
-
return { x: x0, y: y0, width: Math.ceil(s.x + r) - x0, height: Math.ceil(s.y + r) - y0 };
|
|
2080
|
+
var FUZZY_AMPLITUDE = 0.12;
|
|
2081
|
+
var NOMINAL_MS_PER_PX = 2;
|
|
2082
|
+
var RIBBON_SUB = 4;
|
|
2083
|
+
function chainDamp(damp, k) {
|
|
2084
|
+
return Math.min(damp, 1 / (1 + 0.8 * Math.sqrt(Math.max(0, k))) ** 2);
|
|
1941
2085
|
}
|
|
1942
|
-
function
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2086
|
+
function addPolygonArea(pts, rect, out, weight = 1) {
|
|
2087
|
+
const n = pts.length >> 1;
|
|
2088
|
+
if (n < 3) return;
|
|
2089
|
+
let y0 = Infinity;
|
|
2090
|
+
let y1 = -Infinity;
|
|
2091
|
+
for (let i = 1; i < pts.length; i += 2) {
|
|
2092
|
+
if (pts[i] < y0) y0 = pts[i];
|
|
2093
|
+
if (pts[i] > y1) y1 = pts[i];
|
|
2094
|
+
}
|
|
2095
|
+
const r0 = Math.max(0, Math.floor(y0) - rect.y);
|
|
2096
|
+
const r1 = Math.min(rect.height - 1, Math.floor(y1) - rect.y);
|
|
2097
|
+
const xs = [];
|
|
2098
|
+
const w = weight / RIBBON_SUB;
|
|
2099
|
+
for (let row = r0; row <= r1; row++) {
|
|
2100
|
+
const base = row * rect.width;
|
|
2101
|
+
for (let sub = 0; sub < RIBBON_SUB; sub++) {
|
|
2102
|
+
const y = rect.y + row + (sub + 0.5) / RIBBON_SUB;
|
|
2103
|
+
xs.length = 0;
|
|
2104
|
+
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
2105
|
+
const yi = pts[i * 2 + 1];
|
|
2106
|
+
const yj = pts[j * 2 + 1];
|
|
2107
|
+
if (yi <= y !== yj <= y) xs.push(pts[i * 2] + (y - yi) / (yj - yi) * (pts[j * 2] - pts[i * 2]));
|
|
2108
|
+
}
|
|
2109
|
+
if (xs.length < 2) continue;
|
|
2110
|
+
xs.sort((a, b) => a - b);
|
|
2111
|
+
for (let k = 0; k + 1 < xs.length; k += 2) {
|
|
2112
|
+
const a = Math.max(rect.x, xs[k]);
|
|
2113
|
+
const b = Math.min(rect.x + rect.width, xs[k + 1]);
|
|
2114
|
+
if (!(b > a)) continue;
|
|
2115
|
+
for (let px = Math.floor(a); px < b; px++) {
|
|
2116
|
+
const cover = Math.min(b, px + 1) - Math.max(a, px);
|
|
2117
|
+
if (cover > 0) out[base + px - rect.x] += cover * w;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
1948
2120
|
}
|
|
1949
2121
|
}
|
|
1950
|
-
return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
|
|
1951
2122
|
}
|
|
1952
|
-
function
|
|
1953
|
-
const
|
|
1954
|
-
|
|
1955
|
-
const
|
|
1956
|
-
|
|
2123
|
+
function curl(x, y, seed) {
|
|
2124
|
+
const n = (u, v) => {
|
|
2125
|
+
const x0 = Math.floor(u);
|
|
2126
|
+
const y0 = Math.floor(v);
|
|
2127
|
+
const fx = u - x0;
|
|
2128
|
+
const fy = v - y0;
|
|
2129
|
+
const sx = fx * fx * (3 - 2 * fx);
|
|
2130
|
+
const sy = fy * fy * (3 - 2 * fy);
|
|
2131
|
+
const h = (a2, b2) => rand01(seed, a2 * 73856093 + b2 * 19349663, 41);
|
|
2132
|
+
const a = h(x0, y0);
|
|
2133
|
+
const b = h(x0 + 1, y0);
|
|
2134
|
+
const c = h(x0, y0 + 1);
|
|
2135
|
+
const d = h(x0 + 1, y0 + 1);
|
|
2136
|
+
return a + (b - a) * sx + (c - a) * sy + (a - b - c + d) * sx * sy;
|
|
2137
|
+
};
|
|
2138
|
+
const e = 0.01;
|
|
2139
|
+
return [(n(x, y + e) - n(x, y - e)) / (2 * e), -(n(x + e, y) - n(x - e, y)) / (2 * e)];
|
|
2140
|
+
}
|
|
2141
|
+
var ParticleStroke = class extends MarkStroke {
|
|
2142
|
+
sets;
|
|
2143
|
+
/** Ribbon pairs per copy: particles in their order around the pen (non-flow kinds). */
|
|
2144
|
+
orders;
|
|
2145
|
+
s;
|
|
2146
|
+
spawn = 0;
|
|
2147
|
+
/** Textured dots drawn so far, per copy (sequence order, random picks). */
|
|
2148
|
+
dots;
|
|
2149
|
+
/** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
|
|
2150
|
+
filled = /* @__PURE__ */ new Map();
|
|
2151
|
+
pts = [];
|
|
2152
|
+
constructor(brush, options) {
|
|
2153
|
+
super(brush, options);
|
|
2154
|
+
this.s = brush.particle;
|
|
2155
|
+
this.sets = this.copies.map(() => []);
|
|
2156
|
+
this.orders = this.copies.map(() => []);
|
|
2157
|
+
this.dots = this.copies.map(() => 0);
|
|
1957
2158
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
const field = new Float32Array(n * 3);
|
|
1962
|
-
const rad2 = s.size / 2;
|
|
1963
|
-
const a = s.rotation * Math.PI / 180;
|
|
1964
|
-
const ca = Math.cos(a);
|
|
1965
|
-
const sa = Math.sin(a);
|
|
1966
|
-
const [r, g, b] = hexRgb(s.color);
|
|
1967
|
-
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
1968
|
-
for (let xx = 0, px = rect.x; xx < rect.width; xx++, p++, px++) {
|
|
1969
|
-
const dx = px + 0.5 - s.x;
|
|
1970
|
-
const dy = rect.y + yy + 0.5 - s.y;
|
|
1971
|
-
const u = (dx * ca + dy * sa) / rad2;
|
|
1972
|
-
const v = (-dx * sa + dy * ca) / rad2;
|
|
1973
|
-
if (u < -1.05 || u > 1.05 || v < -1.05 || v > 1.05) continue;
|
|
1974
|
-
let c = 0;
|
|
1975
|
-
for (let k = 0; k < 4; k++) c += stampCoverage(kind, u + ((k & 1) - 0.5) / rad2 / 2, v + ((k >> 1) - 0.5) / rad2 / 2, s.seed);
|
|
1976
|
-
c /= 4;
|
|
1977
|
-
if (c <= 0) continue;
|
|
1978
|
-
cov[p] = c;
|
|
1979
|
-
const shade = 1 + 0.12 * (-(u + v) / 2) + 0.06 * (hash2(px, rect.y + yy, s.seed) - 0.5);
|
|
1980
|
-
field[p * 3] = Math.min(1, Math.max(0, r * shade));
|
|
1981
|
-
field[p * 3 + 1] = Math.min(1, Math.max(0, g * shade));
|
|
1982
|
-
field[p * 3 + 2] = Math.min(1, Math.max(0, b * shade));
|
|
1983
|
-
}
|
|
2159
|
+
/** Strand kinds: a chain of masses behind each particle. */
|
|
2160
|
+
get strands() {
|
|
2161
|
+
return this.s.kind === "string" || this.s.kind === "elastic" || this.s.kind === "fuzzy";
|
|
1984
2162
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
field[p * 3 + 1] = art.tintable ? g : px[1];
|
|
2011
|
-
field[p * 3 + 2] = art.tintable ? b : px[2];
|
|
2012
|
-
}
|
|
2163
|
+
newParticle(x, y, ox, oy, k, phase, i, copy) {
|
|
2164
|
+
const s = this.s;
|
|
2165
|
+
const fuzzy = s.kind === "fuzzy";
|
|
2166
|
+
const jitter = fuzzy ? 1 : s.wavePhaseJitter;
|
|
2167
|
+
return {
|
|
2168
|
+
x,
|
|
2169
|
+
y,
|
|
2170
|
+
vx: 0,
|
|
2171
|
+
vy: 0,
|
|
2172
|
+
ox,
|
|
2173
|
+
oy,
|
|
2174
|
+
k,
|
|
2175
|
+
phase,
|
|
2176
|
+
age: 0,
|
|
2177
|
+
run: 0,
|
|
2178
|
+
bead: 0,
|
|
2179
|
+
alive: true,
|
|
2180
|
+
wavePhase: jitter > 0 ? jitter * Math.PI * 2 * rand01(this.seed, i, 43 + copy) : 0,
|
|
2181
|
+
waveScale: fuzzy ? 0.6 + 0.8 * rand01(this.seed, i, 47 + copy) : 1,
|
|
2182
|
+
dx: x,
|
|
2183
|
+
dy: y,
|
|
2184
|
+
px: NaN,
|
|
2185
|
+
py: NaN,
|
|
2186
|
+
fade: 1
|
|
2187
|
+
};
|
|
2013
2188
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
if (inGamut(at(mid))) lo = mid;
|
|
2031
|
-
else hi = mid;
|
|
2189
|
+
init(copy, d) {
|
|
2190
|
+
const s = this.s;
|
|
2191
|
+
const list = this.sets[copy];
|
|
2192
|
+
const size = Math.max(1, d.radius * 2);
|
|
2193
|
+
for (let i = 0; i < (s.kind === "flow" ? 0 : s.count); i++) {
|
|
2194
|
+
const r = rand01(this.seed, i, 3 + copy) * s.spread * size * 0.5;
|
|
2195
|
+
const a = rand01(this.seed, i, 5 + copy) * Math.PI * 2;
|
|
2196
|
+
const ox = Math.cos(a) * r;
|
|
2197
|
+
const oy = Math.sin(a) * r;
|
|
2198
|
+
const p = this.newParticle(d.x + ox, d.y + oy, ox, oy, 0.6 + 0.8 * rand01(this.seed, i, 7), a, i, copy);
|
|
2199
|
+
if (this.strands) p.strand = Array.from({ length: 6 }, () => ({ x: d.x + ox, y: d.y + oy, vx: 0, vy: 0 }));
|
|
2200
|
+
if (s.kind === "orbit") {
|
|
2201
|
+
p.ox = Math.max(1, r + size * 0.15);
|
|
2202
|
+
p.oy = 0;
|
|
2203
|
+
}
|
|
2204
|
+
list.push(p);
|
|
2032
2205
|
}
|
|
2033
|
-
|
|
2034
|
-
}
|
|
2035
|
-
return "#" + [out.r, out.g, out.b].map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join("");
|
|
2036
|
-
}
|
|
2037
|
-
var StickerSpray = class {
|
|
2038
|
-
constructor(init) {
|
|
2039
|
-
this.init = init;
|
|
2040
|
-
const b = init.brush;
|
|
2041
|
-
this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);
|
|
2042
|
-
this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
2043
|
-
const spacing = Math.max(0.05, 1 / Math.max(0.05, b.sticker.density * 4));
|
|
2044
|
-
const walk = { ...b, tip: { ...b.tip, spacing }, dynamics: { ...b.dynamics, scatter: 0, sizeJitter: 0, angleJitter: 0 } };
|
|
2045
|
-
this.walkers = this.copies.map((c, i) => new PenDabWalker(walk, init.seed + i * 7919 | 0, c.linear));
|
|
2046
|
-
this.medium = new TileMedium(init.target, {});
|
|
2047
|
-
this.next = init.nextId;
|
|
2206
|
+
this.orders[copy] = list.map((_, i) => i).sort((i, j) => list[i].phase - list[j].phase || i - j);
|
|
2048
2207
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
medium;
|
|
2058
|
-
lastRaw = null;
|
|
2059
|
-
lastFed = null;
|
|
2060
|
-
next;
|
|
2061
|
-
push(points) {
|
|
2062
|
-
const rects = [];
|
|
2063
|
-
for (const raw of points) {
|
|
2064
|
-
this.lastRaw = raw;
|
|
2065
|
-
const p = this.smoother.push(raw);
|
|
2066
|
-
this.lastFed = p;
|
|
2067
|
-
this.copies.forEach((c, i) => {
|
|
2068
|
-
const dabs = [];
|
|
2069
|
-
this.walkers[i].push(c.map(p), dabs);
|
|
2070
|
-
for (const d of dabs) this.drop(d, rects);
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
2073
|
-
return rects;
|
|
2074
|
-
}
|
|
2075
|
-
finish() {
|
|
2076
|
-
const rects = [];
|
|
2077
|
-
const raw = this.lastRaw;
|
|
2078
|
-
const fed = this.lastFed;
|
|
2079
|
-
this.copies.forEach((c, i) => {
|
|
2080
|
-
const dabs = [];
|
|
2081
|
-
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
2082
|
-
this.walkers[i].finish(dabs);
|
|
2083
|
-
for (const d of dabs) this.drop(d, rects);
|
|
2084
|
-
});
|
|
2085
|
-
return rects;
|
|
2086
|
-
}
|
|
2087
|
-
drop(d, rects) {
|
|
2088
|
-
const st = this.init.brush.sticker;
|
|
2089
|
-
const i = this.next;
|
|
2090
|
-
const seed = this.init.seed;
|
|
2091
|
-
const size = d.radius * 2 * (1 - st.sizeJitter * 0.7 * rand01(seed, i, 51));
|
|
2092
|
-
const off = st.scatter * d.radius * 2;
|
|
2093
|
-
const a = rand01(seed, i, 53) * Math.PI * 2;
|
|
2094
|
-
const dist = rand01(seed, i, 55) * off;
|
|
2095
|
-
const base = this.colorAt(d, Math.cos(a) * dist, Math.sin(a) * dist);
|
|
2096
|
-
if (!(base.alpha > 0)) {
|
|
2097
|
-
this.next++;
|
|
2098
|
-
return;
|
|
2099
|
-
}
|
|
2100
|
-
const ref2 = st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "editor:dot";
|
|
2101
|
-
const stamp = stickerStamp({ ref: ref2 });
|
|
2102
|
-
if (!stamp && !this.init.art?.(ref2)) {
|
|
2103
|
-
this.skipped.add(ref2);
|
|
2104
|
-
this.next++;
|
|
2105
|
-
return;
|
|
2106
|
-
}
|
|
2107
|
-
const sticker = {
|
|
2108
|
-
id: `st${this.next++}`,
|
|
2109
|
-
ref: ref2,
|
|
2110
|
-
...stamp ? { stamp } : {},
|
|
2111
|
-
x: Math.round((d.x + Math.cos(a) * dist) * 100) / 100,
|
|
2112
|
-
y: Math.round((d.y + Math.sin(a) * dist) * 100) / 100,
|
|
2113
|
-
size: Math.max(2, Math.round(size * 10) / 10),
|
|
2114
|
-
rotation: Math.round((-d.angle * 180 / Math.PI + (rand01(seed, i, 59) * 2 - 1) * 180 * st.rotationJitter) * 10) / 10,
|
|
2115
|
-
color: st.colorJitter > 0 ? jitterColor(base.color, st.colorJitter, seed, i) : base.color,
|
|
2116
|
-
opacity: Math.round(Math.min(1, d.opacity * this.init.brush.tip.opacity * base.alpha) * 1e3) / 1e3,
|
|
2117
|
-
seed: Math.floor(rand01(seed, i, 65) * 1e9)
|
|
2118
|
-
};
|
|
2119
|
-
this.stickers.push(sticker);
|
|
2120
|
-
rects.push(drawSticker(this.medium, sticker, this.init.art));
|
|
2121
|
-
this.dabCount++;
|
|
2208
|
+
/** The sideways wave of particle `p` at pen dab `d` (px along the normal to the pen's direction). */
|
|
2209
|
+
wave(p, d, size) {
|
|
2210
|
+
const s = this.s;
|
|
2211
|
+
const amp = s.kind === "fuzzy" ? Math.max(FUZZY_AMPLITUDE, s.waveAmplitude) : s.waveAmplitude;
|
|
2212
|
+
if (!(amp > 0)) return 0;
|
|
2213
|
+
const ms = d.time ?? d.travel * NOMINAL_MS_PER_PX;
|
|
2214
|
+
const phase = 2 * Math.PI * d.travel / (s.waveLength * p.waveScale) - s.waveSpeed * 0.025 * ms + p.wavePhase;
|
|
2215
|
+
return amp * size * Math.sin(phase);
|
|
2122
2216
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2217
|
+
advance(copy, d, rects) {
|
|
2218
|
+
const s = this.s;
|
|
2219
|
+
let list = this.sets[copy];
|
|
2220
|
+
if (d.index === 0 && !list.length) this.init(copy, d);
|
|
2221
|
+
const size = Math.max(1, d.radius * 2);
|
|
2222
|
+
const markR = Math.max(0.5, size * s.size / 2);
|
|
2223
|
+
const k = 0.02 + 0.3 * s.stiffness;
|
|
2224
|
+
const damp = 0.6 + 0.38 * (1 - s.damping);
|
|
2225
|
+
const g = s.gravity * 0.15;
|
|
2226
|
+
const op = d.opacity;
|
|
2227
|
+
const al = Math.max(0.05, d.alpha);
|
|
2228
|
+
const nx = Math.sin(d.direction);
|
|
2229
|
+
const ny = Math.cos(d.direction);
|
|
2230
|
+
if (s.kind === "flow") {
|
|
2231
|
+
this.spawn += s.count / 8;
|
|
2232
|
+
while (this.spawn >= 1) {
|
|
2233
|
+
this.spawn -= 1;
|
|
2234
|
+
const i = this.dabCount + list.length * 31 + Math.floor(d.index);
|
|
2235
|
+
const a = rand01(this.seed, i, 11 + copy) * Math.PI * 2;
|
|
2236
|
+
const r = rand01(this.seed, i, 13 + copy) * s.spread * size * 0.5;
|
|
2237
|
+
list.push(this.newParticle(d.x + Math.cos(a) * r, d.y + Math.sin(a) * r, 0, 0, 1, a, i, copy));
|
|
2238
|
+
}
|
|
2131
2239
|
}
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2240
|
+
const lifeSteps = 8 + 120 * s.life;
|
|
2241
|
+
for (let i = 0; i < list.length; i++) {
|
|
2242
|
+
const p = list[i];
|
|
2243
|
+
if (!p.alive) continue;
|
|
2244
|
+
switch (s.kind) {
|
|
2245
|
+
case "spring": {
|
|
2246
|
+
p.vx = (p.vx + (d.x + p.ox - p.x) * k * p.k) * damp;
|
|
2247
|
+
p.vy = (p.vy + (d.y + p.oy - p.y) * k * p.k + g) * damp;
|
|
2248
|
+
p.x += p.vx;
|
|
2249
|
+
p.y += p.vy;
|
|
2250
|
+
break;
|
|
2251
|
+
}
|
|
2252
|
+
case "string":
|
|
2253
|
+
case "fuzzy": {
|
|
2254
|
+
const strand = p.strand;
|
|
2255
|
+
const dm = s.kind === "fuzzy" ? chainDamp(damp, k * p.k) : damp;
|
|
2256
|
+
let ax = d.x + p.ox;
|
|
2257
|
+
let ay = d.y + p.oy;
|
|
2258
|
+
for (const m of strand) {
|
|
2259
|
+
m.vx = (m.vx + (ax - m.x) * k * p.k + 0) * dm;
|
|
2260
|
+
m.vy = (m.vy + (ay - m.y) * k * p.k + g) * dm;
|
|
2261
|
+
m.x += m.vx;
|
|
2262
|
+
m.y += m.vy;
|
|
2263
|
+
ax = m.x;
|
|
2264
|
+
ay = m.y;
|
|
2265
|
+
}
|
|
2266
|
+
p.x = strand[strand.length - 1].x;
|
|
2267
|
+
p.y = strand[strand.length - 1].y;
|
|
2268
|
+
break;
|
|
2269
|
+
}
|
|
2270
|
+
case "elastic": {
|
|
2271
|
+
const strand = p.strand;
|
|
2272
|
+
const rest = size * 0.12;
|
|
2273
|
+
const ke = Math.min(0.9, (0.1 + 0.6 * s.stiffness) * p.k);
|
|
2274
|
+
const dm = chainDamp(damp, ke);
|
|
2275
|
+
let ax = d.x + p.ox;
|
|
2276
|
+
let ay = d.y + p.oy;
|
|
2277
|
+
for (const m of strand) {
|
|
2278
|
+
const ex = ax - m.x;
|
|
2279
|
+
const ey = ay - m.y;
|
|
2280
|
+
const len = Math.hypot(ex, ey);
|
|
2281
|
+
const pull = len > rest ? (len - rest) / len : 0;
|
|
2282
|
+
m.vx = (m.vx + ex * pull * ke) * dm;
|
|
2283
|
+
m.vy = (m.vy + ey * pull * ke + g) * dm;
|
|
2284
|
+
m.x += m.vx;
|
|
2285
|
+
m.y += m.vy;
|
|
2286
|
+
ax = m.x;
|
|
2287
|
+
ay = m.y;
|
|
2288
|
+
}
|
|
2289
|
+
p.x = strand[strand.length - 1].x;
|
|
2290
|
+
p.y = strand[strand.length - 1].y;
|
|
2291
|
+
break;
|
|
2292
|
+
}
|
|
2293
|
+
case "orbit": {
|
|
2294
|
+
p.phase += (0.05 + 0.3 * s.speed) * (i % 2 ? -1 : 1) * (0.7 + 0.6 * rand01(this.seed, i, 17));
|
|
2295
|
+
p.x = d.x + Math.cos(p.phase) * p.ox;
|
|
2296
|
+
p.y = d.y + Math.sin(p.phase) * p.ox + g * p.ox;
|
|
2297
|
+
break;
|
|
2298
|
+
}
|
|
2299
|
+
case "flow": {
|
|
2300
|
+
const [fx, fy] = curl(p.x / Math.max(8, size * 1.5), p.y / Math.max(8, size * 1.5), this.seed);
|
|
2301
|
+
const push = size * 0.012 * (0.3 + 1.5 * s.speed);
|
|
2302
|
+
const fl = Math.hypot(fx, fy) || 1;
|
|
2303
|
+
p.vx = p.vx * 0.8 + fx / fl * push;
|
|
2304
|
+
p.vy = p.vy * 0.8 + fy / fl * push + g * 0.2;
|
|
2305
|
+
p.x += p.vx;
|
|
2306
|
+
p.y += p.vy;
|
|
2307
|
+
p.age++;
|
|
2308
|
+
if (p.age > lifeSteps) p.alive = false;
|
|
2309
|
+
break;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
const w = this.wave(p, d, size);
|
|
2313
|
+
const qx = w ? p.x + nx * w : p.x;
|
|
2314
|
+
const qy = w ? p.y + ny * w : p.y;
|
|
2315
|
+
const px = p.dx;
|
|
2316
|
+
const py = p.dy;
|
|
2317
|
+
p.px = p.dx;
|
|
2318
|
+
p.py = p.dy;
|
|
2319
|
+
p.dx = qx;
|
|
2320
|
+
p.dy = qy;
|
|
2321
|
+
const fade = s.kind === "flow" ? 1 - p.age / lifeSteps : 1;
|
|
2322
|
+
p.fade = fade;
|
|
2323
|
+
const moved = Math.hypot(qx - px, qy - py);
|
|
2324
|
+
if (d.index === 0 && s.kind !== "flow") continue;
|
|
2325
|
+
if (s.render === "dot") this.dot(copy, qx, qy, qx - px, qy - py, markR * fade, al * fade, op, d, rects);
|
|
2326
|
+
else if (s.render === "line" || s.render === "ribbon") this.segment(px, py, qx, qy, markR * fade, al * fade, op, d.pressure, rects);
|
|
2327
|
+
else {
|
|
2328
|
+
p.run += moved;
|
|
2329
|
+
const gap = Math.max(markR * 2, size * (0.05 + s.interval));
|
|
2330
|
+
while (p.run >= gap) {
|
|
2331
|
+
p.run -= gap;
|
|
2332
|
+
p.bead++;
|
|
2333
|
+
const t = moved > 0 ? 1 - p.run / Math.max(moved, 1e-6) : 1;
|
|
2334
|
+
const bx = px + (qx - px) * Math.min(1, Math.max(0, t));
|
|
2335
|
+
const by = py + (qy - py) * Math.min(1, Math.max(0, t));
|
|
2336
|
+
this.dot(copy, bx, by, qx - px, qy - py, markR * (0.7 + 0.6 * (p.bead % 3 / 2)) * fade, al * fade, op, d, rects);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2194
2339
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
if (only && base) {
|
|
2200
|
-
for (const [k, t] of base.tiles) if (!only.has(k)) tiles.set(k, t);
|
|
2201
|
-
}
|
|
2202
|
-
for (const [k, d] of live) {
|
|
2203
|
-
const t = finishTile(d, 4, 0);
|
|
2204
|
-
if (t) tiles.set(k, t);
|
|
2205
|
-
}
|
|
2206
|
-
return new TiledSurface(4, tiles, 0);
|
|
2207
|
-
}
|
|
2208
|
-
function stickerAt(stickers, x, y, art) {
|
|
2209
|
-
for (let i = stickers.length - 1; i >= 0; i--) {
|
|
2210
|
-
const s = stickers[i];
|
|
2211
|
-
const rad2 = s.size / 2;
|
|
2212
|
-
const a = s.rotation * Math.PI / 180;
|
|
2213
|
-
const dx = x - s.x;
|
|
2214
|
-
const dy = y - s.y;
|
|
2215
|
-
const u = (dx * Math.cos(a) + dy * Math.sin(a)) / rad2;
|
|
2216
|
-
const v = (-dx * Math.sin(a) + dy * Math.cos(a)) / rad2;
|
|
2217
|
-
if (Math.abs(u) > 1.05 || Math.abs(v) > 1.05) continue;
|
|
2218
|
-
if (s.size < 16) return s;
|
|
2219
|
-
const kind = stickerStamp(s);
|
|
2220
|
-
if (kind) {
|
|
2221
|
-
if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
|
|
2222
|
-
continue;
|
|
2340
|
+
if (s.render === "ribbon" && !(d.index === 0 && s.kind !== "flow")) this.ribbon(copy, list, op, al, d, rects);
|
|
2341
|
+
if (s.kind === "flow" && list.length > 512) {
|
|
2342
|
+
list = list.filter((p) => p.alive);
|
|
2343
|
+
this.sets[copy] = list;
|
|
2223
2344
|
}
|
|
2224
|
-
const pic = art?.(s.ref);
|
|
2225
|
-
if (!pic) return s;
|
|
2226
|
-
const scale = Math.max(pic.width, pic.height) / 2;
|
|
2227
|
-
if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
|
|
2228
2345
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
this.brush
|
|
2237
|
-
this.
|
|
2238
|
-
this.sampler = options.paint ? new PaintGradientSampler(options.paint, options.seed ?? 0) : null;
|
|
2239
|
-
this.tipTable = this.sampler?.acrossTip ? this.sampler.tipTable(TIP_GRADIENT_STEPS) : null;
|
|
2240
|
-
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
2241
|
-
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
2242
|
-
this.seed = options.seed ?? 0;
|
|
2243
|
-
const fine = { ...brush, tip: { ...brush.tip, spacing: Math.max(0.01, Math.min(0.25, STEP_PX / Math.max(1, brush.tip.size))) } };
|
|
2244
|
-
this.walkers = this.copies.map((c, i) => new PenDabWalker(fine, (options.seed ?? 0) + i * 7919 | 0, c.linear));
|
|
2245
|
-
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
2346
|
+
/** A dot (or bead): round, or a textured stamp turned by the particle's motion (mx, my), at random or to the tip angle. */
|
|
2347
|
+
dot(copy, x, y, mx, my, r, alpha, opacity, d, rects) {
|
|
2348
|
+
const s = this.s;
|
|
2349
|
+
const textures = s.dotTextures;
|
|
2350
|
+
if (!textures.length) return this.mark(x, y, r, alpha, opacity, d.pressure, rects);
|
|
2351
|
+
const n = this.dots[copy]++;
|
|
2352
|
+
const pick = s.dotTextureOrder === "random" ? Math.min(textures.length - 1, Math.floor(rand01(this.seed, n, 31 + copy) * textures.length)) : n % textures.length;
|
|
2353
|
+
const rotation = s.dotRotation === "random" ? rand01(this.seed, n, 37 + copy) * Math.PI * 2 : s.dotRotation === "follow" ? mx || my ? Math.atan2(my, mx) : -d.direction : -this.brush.tip.angle * Math.PI / 180;
|
|
2354
|
+
this.stamp(x, y, r, textures[pick], rotation, this.seed + n * 2654435761 >>> 0, alpha, opacity, d.pressure, rects);
|
|
2246
2355
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2356
|
+
/**
|
|
2357
|
+
* Ribbon fill for one step: the band between each pair of joined particles, from where they
|
|
2358
|
+
* were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
|
|
2359
|
+
* share edges exactly, so the fill has no seams; it is telescoped into the coverage
|
|
2360
|
+
* (`telescope`), so it does not build up at the joins.
|
|
2361
|
+
*/
|
|
2362
|
+
ribbon(copy, list, opacity, alpha, d, rects) {
|
|
2363
|
+
const s = this.s;
|
|
2364
|
+
if (!(s.ribbonFill > 0)) return;
|
|
2365
|
+
const live = (p) => !!p && p.alive && !Number.isNaN(p.px);
|
|
2366
|
+
let order;
|
|
2367
|
+
if (s.kind === "flow") order = list.filter(live);
|
|
2368
|
+
else order = this.orders[copy].map((i) => list[i]).filter(live);
|
|
2369
|
+
if (order.length < 2) return;
|
|
2370
|
+
const pairs = [];
|
|
2371
|
+
if (s.ribbonPairs === "first-last") pairs.push([order[0], order[order.length - 1]]);
|
|
2372
|
+
else for (let i = 0; i + 1 < order.length; i++) pairs.push([order[i], order[i + 1]]);
|
|
2373
|
+
const pts = this.pts;
|
|
2374
|
+
const ceil = Math.min(1, opacity * s.ribbonFill);
|
|
2375
|
+
for (const [a, b] of pairs) {
|
|
2376
|
+
pts.length = 0;
|
|
2377
|
+
pts.push(a.px, a.py, a.dx, a.dy, b.dx, b.dy, b.px, b.py);
|
|
2378
|
+
let x0 = Infinity;
|
|
2379
|
+
let y0 = Infinity;
|
|
2380
|
+
let x1 = -Infinity;
|
|
2381
|
+
let y1 = -Infinity;
|
|
2382
|
+
for (let i = 0; i < 8; i += 2) {
|
|
2383
|
+
x0 = Math.min(x0, pts[i]);
|
|
2384
|
+
x1 = Math.max(x1, pts[i]);
|
|
2385
|
+
y0 = Math.min(y0, pts[i + 1]);
|
|
2386
|
+
y1 = Math.max(y1, pts[i + 1]);
|
|
2387
|
+
}
|
|
2388
|
+
const rect = { x: Math.floor(x0), y: Math.floor(y0), width: Math.floor(x1) - Math.floor(x0) + 1, height: Math.floor(y1) - Math.floor(y0) + 1 };
|
|
2389
|
+
const area = this.scratch(rect.width * rect.height);
|
|
2390
|
+
addPolygonArea(pts, rect, area, Math.min(a.fade, b.fade) * alpha);
|
|
2391
|
+
if (this.telescope(rect, area)) this.lay(rect, area, ceil, d.pressure, (a.dx + b.dx) / 2, (a.dy + b.dy) / 2, rects);
|
|
2276
2392
|
}
|
|
2277
|
-
return rects;
|
|
2278
|
-
}
|
|
2279
|
-
step(copy, d, rects) {
|
|
2280
|
-
this.pen = d;
|
|
2281
|
-
if (this.sampler && !this.tipTable) this.sampler.dab(d, this.penColor);
|
|
2282
|
-
this.advance(copy, d, rects);
|
|
2283
|
-
}
|
|
2284
|
-
/** The colour of a mark at (x, y): its pen dab's, or across the tip by its offset from the pen. */
|
|
2285
|
-
markColor(x, y) {
|
|
2286
|
-
if (!this.sampler || !this.pen) return null;
|
|
2287
|
-
if (!this.tipTable) return this.penColor;
|
|
2288
|
-
const d = this.pen;
|
|
2289
|
-
const u = ((x - d.x) * Math.sin(d.direction) + (y - d.y) * Math.cos(d.direction)) / Math.max(0.5, d.radius);
|
|
2290
|
-
const last = this.tipTable.length - 1;
|
|
2291
|
-
return this.tipTable[Math.min(last, Math.max(0, Math.round((u + 1) / 2 * last)))];
|
|
2292
|
-
}
|
|
2293
|
-
finish() {
|
|
2294
|
-
const rects = [];
|
|
2295
|
-
const raw = this.lastRaw;
|
|
2296
|
-
const fed = this.lastFed;
|
|
2297
|
-
this.copies.forEach((c, i) => {
|
|
2298
|
-
const dabs = [];
|
|
2299
|
-
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
2300
|
-
this.walkers[i].finish(dabs);
|
|
2301
|
-
for (const d of dabs) this.step(i, d, rects);
|
|
2302
|
-
});
|
|
2303
|
-
return rects;
|
|
2304
|
-
}
|
|
2305
|
-
/** Stamps one round mark of radius `r` at (x, y) with flow `alpha` towards `opacity`. */
|
|
2306
|
-
mark(x, y, r, alpha, opacity, pressure, rects) {
|
|
2307
|
-
if (!(r > 0.05) || !(alpha > 0)) return;
|
|
2308
|
-
const dab = { x, y, radius: Math.max(0.5, r), alpha: Math.min(1, alpha), opacity: Math.min(1, opacity), angle: 0, roundness: 1 };
|
|
2309
|
-
const hardness = this.brush.tip.hardness;
|
|
2310
|
-
const rect = dabRect(dab, hardness);
|
|
2311
|
-
const n = rect.width * rect.height;
|
|
2312
|
-
if (n <= 0) return;
|
|
2313
|
-
const vals = this.scratch(n);
|
|
2314
|
-
rasterizeDab(dab, hardness, rect, vals);
|
|
2315
|
-
this.lay(rect, vals, dab.opacity, pressure, x, y, rects);
|
|
2316
2393
|
}
|
|
2317
2394
|
/**
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
2395
|
+
* Turns the area newly filled over `rect` into coverage to lay towards the fill's ceiling: the
|
|
2396
|
+
* share of what was left unfilled, so after any number of bands (in any grouping) a pixel's
|
|
2397
|
+
* coverage is the ceiling times the area filled there. False when nothing was added.
|
|
2320
2398
|
*/
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const reach = Math.max(0.5, r) * Math.SQRT2 + 1;
|
|
2324
|
-
const rect = { x: Math.floor(x - reach), y: Math.floor(y - reach), width: 0, height: 0 };
|
|
2325
|
-
rect.width = Math.ceil(x + reach) - rect.x;
|
|
2326
|
-
rect.height = Math.ceil(y + reach) - rect.y;
|
|
2327
|
-
const n = rect.width * rect.height;
|
|
2328
|
-
const vals = this.scratch(n);
|
|
2329
|
-
const ss = r < 10 ? 3 : 1;
|
|
2330
|
-
const c = Math.cos(rotation);
|
|
2331
|
-
const s = Math.sin(rotation);
|
|
2332
|
-
const inv = 1 / Math.max(0.5, r);
|
|
2333
|
-
const a = Math.min(1, alpha);
|
|
2399
|
+
telescope(rect, area) {
|
|
2400
|
+
let any = false;
|
|
2334
2401
|
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
2402
|
+
const gy = rect.y + yy;
|
|
2403
|
+
const ty = Math.floor(gy / TILE);
|
|
2404
|
+
let t;
|
|
2405
|
+
let tx = NaN;
|
|
2335
2406
|
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2407
|
+
const add = area[k];
|
|
2408
|
+
if (!(add > 0)) continue;
|
|
2409
|
+
const gx = rect.x + xx;
|
|
2410
|
+
const ntx = Math.floor(gx / TILE);
|
|
2411
|
+
if (ntx !== tx || !t) {
|
|
2412
|
+
tx = ntx;
|
|
2413
|
+
const key = tileKey(tx, ty);
|
|
2414
|
+
t = this.filled.get(key);
|
|
2415
|
+
if (!t) {
|
|
2416
|
+
t = new Float32Array(TILE * TILE);
|
|
2417
|
+
this.filled.set(key, t);
|
|
2342
2418
|
}
|
|
2343
2419
|
}
|
|
2344
|
-
|
|
2420
|
+
const ti = (gy - ty * TILE) * TILE + (gx - tx * TILE);
|
|
2421
|
+
const before = t[ti];
|
|
2422
|
+
const after = Math.min(1, before + add);
|
|
2423
|
+
t[ti] = after;
|
|
2424
|
+
area[k] = before < 1 ? (after - before) / (1 - before) : 0;
|
|
2425
|
+
any = true;
|
|
2345
2426
|
}
|
|
2346
2427
|
}
|
|
2347
|
-
|
|
2428
|
+
return any;
|
|
2348
2429
|
}
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2430
|
+
};
|
|
2431
|
+
var BristleStroke = class extends MarkStroke {
|
|
2432
|
+
tufts;
|
|
2433
|
+
s;
|
|
2434
|
+
constructor(brush, options) {
|
|
2435
|
+
super(brush, options);
|
|
2436
|
+
this.s = brush.bristle;
|
|
2437
|
+
const s = this.s;
|
|
2438
|
+
this.tufts = this.copies.map(
|
|
2439
|
+
(_, c) => Array.from({ length: s.count }, (_2, i) => {
|
|
2440
|
+
const a = rand01(this.seed, i, 21 + c) * Math.PI * 2;
|
|
2441
|
+
const r = Math.sqrt(rand01(this.seed, i, 23 + c));
|
|
2442
|
+
return {
|
|
2443
|
+
u: Math.cos(a) * r,
|
|
2444
|
+
v: Math.sin(a) * r,
|
|
2445
|
+
x: 0,
|
|
2446
|
+
y: 0,
|
|
2447
|
+
paint: 1 - s.jitter * 0.4 * rand01(this.seed, i, 25 + c),
|
|
2448
|
+
thirst: 1 + s.jitter * (rand01(this.seed, i, 27 + c) * 2 - 1),
|
|
2449
|
+
w: 1 + s.jitter * 0.6 * (rand01(this.seed, i, 29 + c) - 0.5),
|
|
2450
|
+
started: false
|
|
2451
|
+
};
|
|
2452
|
+
})
|
|
2453
|
+
);
|
|
2355
2454
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
const
|
|
2362
|
-
|
|
2363
|
-
const
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
const
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
const
|
|
2375
|
-
|
|
2376
|
-
|
|
2455
|
+
advance(copy, d, rects) {
|
|
2456
|
+
const s = this.s;
|
|
2457
|
+
const tuft = this.tufts[copy];
|
|
2458
|
+
const spread = d.radius * (0.55 + 0.9 * s.splay * d.pressure);
|
|
2459
|
+
const ca = Math.cos(d.angle);
|
|
2460
|
+
const sa = Math.sin(d.angle);
|
|
2461
|
+
const ro = Math.max(0.15, d.roundness);
|
|
2462
|
+
const tx = Math.cos(d.direction);
|
|
2463
|
+
const ty = -Math.sin(d.direction);
|
|
2464
|
+
const follow = 0.15 + 0.85 * s.stiffness;
|
|
2465
|
+
const bend = d.radius * (1 - s.stiffness) * d.pressure * 0.8;
|
|
2466
|
+
const lineR = Math.max(0.5, d.radius * 2 * s.thickness * 0.5);
|
|
2467
|
+
const dryRate = 4e-3 * (1 - s.load);
|
|
2468
|
+
for (const b of tuft) {
|
|
2469
|
+
const lu = b.u * spread;
|
|
2470
|
+
const lv = b.v * spread * ro;
|
|
2471
|
+
const ox = lu * ca - lv * sa;
|
|
2472
|
+
const oy = -(lu * sa + lv * ca);
|
|
2473
|
+
const lag = bend * (1 - 0.5 * b.v);
|
|
2474
|
+
const wx = d.x + ox - tx * lag;
|
|
2475
|
+
const wy = d.y + oy - ty * lag;
|
|
2476
|
+
if (!b.started) {
|
|
2477
|
+
b.x = wx;
|
|
2478
|
+
b.y = wy;
|
|
2479
|
+
b.started = true;
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
const nx = b.x + (wx - b.x) * follow;
|
|
2483
|
+
const ny = b.y + (wy - b.y) * follow;
|
|
2484
|
+
const paint = Math.max(0, b.paint);
|
|
2485
|
+
if (paint > 0.02) this.segment(b.x, b.y, nx, ny, lineR * b.w * (0.6 + 0.4 * d.pressure), d.alpha * Math.min(1, paint * 1.4), d.opacity, d.pressure, rects);
|
|
2486
|
+
b.paint -= dryRate * b.thirst * Math.hypot(nx - b.x, ny - b.y);
|
|
2487
|
+
b.x = nx;
|
|
2488
|
+
b.y = ny;
|
|
2377
2489
|
}
|
|
2378
|
-
rects.push(rect);
|
|
2379
|
-
this.dabCount++;
|
|
2380
|
-
}
|
|
2381
|
-
/** Marks along a segment, spaced by a fraction of the radius. */
|
|
2382
|
-
segment(x0, y0, x1, y1, r, alpha, opacity, pressure, rects) {
|
|
2383
|
-
const len = Math.hypot(x1 - x0, y1 - y0);
|
|
2384
|
-
const step = Math.max(0.35, r * 0.5);
|
|
2385
|
-
const n = Math.max(1, Math.ceil(len / step));
|
|
2386
|
-
for (let i = 1; i <= n; i++) this.mark(x0 + (x1 - x0) * i / n, y0 + (y1 - y0) * i / n, r, alpha, opacity, pressure, rects);
|
|
2387
2490
|
}
|
|
2388
2491
|
};
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
var
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
let y0 = Infinity;
|
|
2399
|
-
let y1 = -Infinity;
|
|
2400
|
-
for (let i = 1; i < pts.length; i += 2) {
|
|
2401
|
-
if (pts[i] < y0) y0 = pts[i];
|
|
2402
|
-
if (pts[i] > y1) y1 = pts[i];
|
|
2403
|
-
}
|
|
2404
|
-
const r0 = Math.max(0, Math.floor(y0) - rect.y);
|
|
2405
|
-
const r1 = Math.min(rect.height - 1, Math.floor(y1) - rect.y);
|
|
2406
|
-
const xs = [];
|
|
2407
|
-
const w = weight / RIBBON_SUB;
|
|
2408
|
-
for (let row = r0; row <= r1; row++) {
|
|
2409
|
-
const base = row * rect.width;
|
|
2410
|
-
for (let sub = 0; sub < RIBBON_SUB; sub++) {
|
|
2411
|
-
const y = rect.y + row + (sub + 0.5) / RIBBON_SUB;
|
|
2412
|
-
xs.length = 0;
|
|
2413
|
-
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
2414
|
-
const yi = pts[i * 2 + 1];
|
|
2415
|
-
const yj = pts[j * 2 + 1];
|
|
2416
|
-
if (yi <= y !== yj <= y) xs.push(pts[i * 2] + (y - yi) / (yj - yi) * (pts[j * 2] - pts[i * 2]));
|
|
2417
|
-
}
|
|
2418
|
-
if (xs.length < 2) continue;
|
|
2419
|
-
xs.sort((a, b) => a - b);
|
|
2420
|
-
for (let k = 0; k + 1 < xs.length; k += 2) {
|
|
2421
|
-
const a = Math.max(rect.x, xs[k]);
|
|
2422
|
-
const b = Math.min(rect.x + rect.width, xs[k + 1]);
|
|
2423
|
-
if (!(b > a)) continue;
|
|
2424
|
-
for (let px = Math.floor(a); px < b; px++) {
|
|
2425
|
-
const cover = Math.min(b, px + 1) - Math.max(a, px);
|
|
2426
|
-
if (cover > 0) out[base + px - rect.x] += cover * w;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2492
|
+
|
|
2493
|
+
// src/paint-editor/sticker-art.ts
|
|
2494
|
+
var STICKER_ART_SIZE = 256;
|
|
2495
|
+
var defaultStickerRasterizer = async (asset, size) => {
|
|
2496
|
+
if (asset.kind === "image") {
|
|
2497
|
+
const parsed = parseDataUrl(asset.url);
|
|
2498
|
+
if (parsed && parsed.mime === "image/png") {
|
|
2499
|
+
const png = decodePng(parsed.bytes);
|
|
2500
|
+
return { width: png.width, height: png.height, data: new Uint8ClampedArray(png.data) };
|
|
2429
2501
|
}
|
|
2430
2502
|
}
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2503
|
+
const g = globalThis;
|
|
2504
|
+
if (!g.createImageBitmap || !g.OffscreenCanvas) {
|
|
2505
|
+
throw new StickerError("unsupported_format", `This environment can't draw ${asset.kind === "svg" ? "SVG" : "non-PNG"} sticker art; pass rasterizeSticker in the paint editor's options (or use a provider that offers png).`);
|
|
2506
|
+
}
|
|
2507
|
+
const blob = asset.kind === "svg" ? new Blob([asset.svg], { type: "image/svg+xml" }) : await (await g.fetch(asset.url)).blob();
|
|
2508
|
+
const bitmap = await g.createImageBitmap(blob);
|
|
2509
|
+
const scale = size / Math.max(bitmap.width, bitmap.height, 1);
|
|
2510
|
+
const w = Math.max(1, Math.round(bitmap.width * scale));
|
|
2511
|
+
const h = Math.max(1, Math.round(bitmap.height * scale));
|
|
2512
|
+
const canvas = new g.OffscreenCanvas(w, h);
|
|
2513
|
+
const ctx = canvas.getContext("2d");
|
|
2514
|
+
ctx.drawImage(bitmap, 0, 0, w, h);
|
|
2515
|
+
const img = ctx.getImageData(0, 0, w, h);
|
|
2516
|
+
return { width: w, height: h, data: img.data };
|
|
2517
|
+
};
|
|
2518
|
+
var StickerArtCache = class {
|
|
2519
|
+
entries = /* @__PURE__ */ new Map();
|
|
2520
|
+
/** The art for `ref` when it has loaded (editor stamps never need any). */
|
|
2521
|
+
get = (ref2) => {
|
|
2522
|
+
const e = this.entries.get(ref2);
|
|
2523
|
+
return e?.status === "ready" ? e.art : void 0;
|
|
2446
2524
|
};
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
orders;
|
|
2454
|
-
s;
|
|
2455
|
-
spawn = 0;
|
|
2456
|
-
/** Textured dots drawn so far, per copy (sequence order, random picks). */
|
|
2457
|
-
dots;
|
|
2458
|
-
/** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
|
|
2459
|
-
filled = /* @__PURE__ */ new Map();
|
|
2460
|
-
pts = [];
|
|
2461
|
-
constructor(brush, options) {
|
|
2462
|
-
super(brush, options);
|
|
2463
|
-
this.s = brush.particle;
|
|
2464
|
-
this.sets = this.copies.map(() => []);
|
|
2465
|
-
this.orders = this.copies.map(() => []);
|
|
2466
|
-
this.dots = this.copies.map(() => 0);
|
|
2525
|
+
/** `ready`, `loading`, `failed` (with the reason) or `none` (never asked for). Editor stamps are always `ready`. */
|
|
2526
|
+
status(ref2) {
|
|
2527
|
+
if (editorStampOf(ref2)) return { status: "ready" };
|
|
2528
|
+
const e = this.entries.get(ref2);
|
|
2529
|
+
if (!e) return { status: "none" };
|
|
2530
|
+
return e.status === "failed" ? { status: "failed", error: e.error } : { status: e.status };
|
|
2467
2531
|
}
|
|
2468
|
-
/**
|
|
2469
|
-
|
|
2470
|
-
|
|
2532
|
+
/**
|
|
2533
|
+
* Loads and rasterises `ref` from `provider` once (later calls share the load). Unknown refs
|
|
2534
|
+
* reject with the closest known ones; failures are kept so they are reported, and retried on
|
|
2535
|
+
* the next call.
|
|
2536
|
+
*/
|
|
2537
|
+
ensure(provider, ref2, rasterize) {
|
|
2538
|
+
if (editorStampOf(ref2)) return Promise.resolve(null);
|
|
2539
|
+
const e = this.entries.get(ref2);
|
|
2540
|
+
if (e?.status === "ready") return Promise.resolve(e.art);
|
|
2541
|
+
if (e?.status === "loading") return e.promise;
|
|
2542
|
+
const promise = (async () => {
|
|
2543
|
+
const d = await describeStickerRef(provider, ref2);
|
|
2544
|
+
const id = stickerIdFor(provider, ref2);
|
|
2545
|
+
const max = provider.capabilities.maxSize ?? STICKER_ART_SIZE;
|
|
2546
|
+
const size = Math.min(STICKER_ART_SIZE, max);
|
|
2547
|
+
const asset = await provider.load(id, d.formats.includes("png") ? { format: "png", size } : { format: d.formats[0] ?? "svg", size });
|
|
2548
|
+
const px = await rasterize(asset, size);
|
|
2549
|
+
if (!(px.width > 0 && px.height > 0) || px.data.length !== px.width * px.height * 4) throw new StickerError("load_failed", `The art for "${ref2}" came back empty or the wrong size.`);
|
|
2550
|
+
return { ...px, tintable: d.tintable };
|
|
2551
|
+
})();
|
|
2552
|
+
this.entries.set(ref2, { status: "loading", promise });
|
|
2553
|
+
return promise.then(
|
|
2554
|
+
(art) => {
|
|
2555
|
+
this.entries.set(ref2, { status: "ready", art });
|
|
2556
|
+
return art;
|
|
2557
|
+
},
|
|
2558
|
+
(err) => {
|
|
2559
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2560
|
+
this.entries.set(ref2, { status: "failed", error: message });
|
|
2561
|
+
throw err;
|
|
2562
|
+
}
|
|
2563
|
+
);
|
|
2471
2564
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
const jitter = fuzzy ? 1 : s.wavePhaseJitter;
|
|
2476
|
-
return {
|
|
2477
|
-
x,
|
|
2478
|
-
y,
|
|
2479
|
-
vx: 0,
|
|
2480
|
-
vy: 0,
|
|
2481
|
-
ox,
|
|
2482
|
-
oy,
|
|
2483
|
-
k,
|
|
2484
|
-
phase,
|
|
2485
|
-
age: 0,
|
|
2486
|
-
run: 0,
|
|
2487
|
-
bead: 0,
|
|
2488
|
-
alive: true,
|
|
2489
|
-
wavePhase: jitter > 0 ? jitter * Math.PI * 2 * rand01(this.seed, i, 43 + copy) : 0,
|
|
2490
|
-
waveScale: fuzzy ? 0.6 + 0.8 * rand01(this.seed, i, 47 + copy) : 1,
|
|
2491
|
-
dx: x,
|
|
2492
|
-
dy: y,
|
|
2493
|
-
px: NaN,
|
|
2494
|
-
py: NaN,
|
|
2495
|
-
fade: 1
|
|
2496
|
-
};
|
|
2565
|
+
/** Forgets every load (a new provider). */
|
|
2566
|
+
clear() {
|
|
2567
|
+
this.entries.clear();
|
|
2497
2568
|
}
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2569
|
+
};
|
|
2570
|
+
function sampleArt(art, x, y, out) {
|
|
2571
|
+
const { width: w, height: h, data } = art;
|
|
2572
|
+
const fx = x - 0.5;
|
|
2573
|
+
const fy = y - 0.5;
|
|
2574
|
+
const x0 = Math.floor(fx);
|
|
2575
|
+
const y0 = Math.floor(fy);
|
|
2576
|
+
const tx = fx - x0;
|
|
2577
|
+
const ty = fy - y0;
|
|
2578
|
+
let r = 0;
|
|
2579
|
+
let g = 0;
|
|
2580
|
+
let b = 0;
|
|
2581
|
+
let a = 0;
|
|
2582
|
+
for (let k = 0; k < 4; k++) {
|
|
2583
|
+
const xx = x0 + (k & 1);
|
|
2584
|
+
const yy = y0 + (k >> 1);
|
|
2585
|
+
if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
|
|
2586
|
+
const wgt = (k & 1 ? tx : 1 - tx) * (k >> 1 ? ty : 1 - ty);
|
|
2587
|
+
const i = (yy * w + xx) * 4;
|
|
2588
|
+
const al = data[i + 3] * wgt;
|
|
2589
|
+
r += data[i] * al;
|
|
2590
|
+
g += data[i + 1] * al;
|
|
2591
|
+
b += data[i + 2] * al;
|
|
2592
|
+
a += al;
|
|
2593
|
+
}
|
|
2594
|
+
out[0] = a > 0 ? r / a / 255 : 0;
|
|
2595
|
+
out[1] = a > 0 ? g / a / 255 : 0;
|
|
2596
|
+
out[2] = a > 0 ? b / a / 255 : 0;
|
|
2597
|
+
out[3] = a / 255;
|
|
2598
|
+
return out;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/paint-editor/stickers.ts
|
|
2602
|
+
function hexRgb(hex2) {
|
|
2603
|
+
const n = parseInt(hex2.slice(1, 7), 16);
|
|
2604
|
+
return [(n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255];
|
|
2605
|
+
}
|
|
2606
|
+
function stickerRect(s) {
|
|
2607
|
+
const r = s.size / 2 + 1;
|
|
2608
|
+
const x0 = Math.floor(s.x - r);
|
|
2609
|
+
const y0 = Math.floor(s.y - r);
|
|
2610
|
+
return { x: x0, y: y0, width: Math.ceil(s.x + r) - x0, height: Math.ceil(s.y + r) - y0 };
|
|
2611
|
+
}
|
|
2612
|
+
function stickerStamp(s) {
|
|
2613
|
+
if (s.ref) {
|
|
2614
|
+
try {
|
|
2615
|
+
return editorStampOf(s.ref);
|
|
2616
|
+
} catch {
|
|
2617
|
+
return null;
|
|
2514
2618
|
}
|
|
2515
|
-
this.orders[copy] = list.map((_, i) => i).sort((i, j) => list[i].phase - list[j].phase || i - j);
|
|
2516
2619
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
return amp * size * Math.sin(phase);
|
|
2620
|
+
return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
|
|
2621
|
+
}
|
|
2622
|
+
function drawSticker(medium, s, art) {
|
|
2623
|
+
const kind = stickerStamp(s);
|
|
2624
|
+
if (!kind) {
|
|
2625
|
+
const a2 = art?.(s.ref);
|
|
2626
|
+
return a2 ? drawArtSticker(medium, s, a2) : stickerRect(s);
|
|
2525
2627
|
}
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2628
|
+
const rect = stickerRect(s);
|
|
2629
|
+
const n = rect.width * rect.height;
|
|
2630
|
+
const cov = new Float32Array(n);
|
|
2631
|
+
const field = new Float32Array(n * 3);
|
|
2632
|
+
const rad2 = s.size / 2;
|
|
2633
|
+
const a = s.rotation * Math.PI / 180;
|
|
2634
|
+
const ca = Math.cos(a);
|
|
2635
|
+
const sa = Math.sin(a);
|
|
2636
|
+
const [r, g, b] = hexRgb(s.color);
|
|
2637
|
+
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
2638
|
+
for (let xx = 0, px = rect.x; xx < rect.width; xx++, p++, px++) {
|
|
2639
|
+
const dx = px + 0.5 - s.x;
|
|
2640
|
+
const dy = rect.y + yy + 0.5 - s.y;
|
|
2641
|
+
const u = (dx * ca + dy * sa) / rad2;
|
|
2642
|
+
const v = (-dx * sa + dy * ca) / rad2;
|
|
2643
|
+
if (u < -1.05 || u > 1.05 || v < -1.05 || v > 1.05) continue;
|
|
2644
|
+
let c = 0;
|
|
2645
|
+
for (let k = 0; k < 4; k++) c += stampCoverage(kind, u + ((k & 1) - 0.5) / rad2 / 2, v + ((k >> 1) - 0.5) / rad2 / 2, s.seed);
|
|
2646
|
+
c /= 4;
|
|
2647
|
+
if (c <= 0) continue;
|
|
2648
|
+
cov[p] = c;
|
|
2649
|
+
const shade = 1 + 0.12 * (-(u + v) / 2) + 0.06 * (hash2(px, rect.y + yy, s.seed) - 0.5);
|
|
2650
|
+
field[p * 3] = Math.min(1, Math.max(0, r * shade));
|
|
2651
|
+
field[p * 3 + 1] = Math.min(1, Math.max(0, g * shade));
|
|
2652
|
+
field[p * 3 + 2] = Math.min(1, Math.max(0, b * shade));
|
|
2548
2653
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
break;
|
|
2578
|
-
}
|
|
2579
|
-
case "elastic": {
|
|
2580
|
-
const strand = p.strand;
|
|
2581
|
-
const rest = size * 0.12;
|
|
2582
|
-
const ke = Math.min(0.9, (0.1 + 0.6 * s.stiffness) * p.k);
|
|
2583
|
-
const dm = chainDamp(damp, ke);
|
|
2584
|
-
let ax = d.x + p.ox;
|
|
2585
|
-
let ay = d.y + p.oy;
|
|
2586
|
-
for (const m of strand) {
|
|
2587
|
-
const ex = ax - m.x;
|
|
2588
|
-
const ey = ay - m.y;
|
|
2589
|
-
const len = Math.hypot(ex, ey);
|
|
2590
|
-
const pull = len > rest ? (len - rest) / len : 0;
|
|
2591
|
-
m.vx = (m.vx + ex * pull * ke) * dm;
|
|
2592
|
-
m.vy = (m.vy + ey * pull * ke + g) * dm;
|
|
2593
|
-
m.x += m.vx;
|
|
2594
|
-
m.y += m.vy;
|
|
2595
|
-
ax = m.x;
|
|
2596
|
-
ay = m.y;
|
|
2597
|
-
}
|
|
2598
|
-
p.x = strand[strand.length - 1].x;
|
|
2599
|
-
p.y = strand[strand.length - 1].y;
|
|
2600
|
-
break;
|
|
2601
|
-
}
|
|
2602
|
-
case "orbit": {
|
|
2603
|
-
p.phase += (0.05 + 0.3 * s.speed) * (i % 2 ? -1 : 1) * (0.7 + 0.6 * rand01(this.seed, i, 17));
|
|
2604
|
-
p.x = d.x + Math.cos(p.phase) * p.ox;
|
|
2605
|
-
p.y = d.y + Math.sin(p.phase) * p.ox + g * p.ox;
|
|
2606
|
-
break;
|
|
2607
|
-
}
|
|
2608
|
-
case "flow": {
|
|
2609
|
-
const [fx, fy] = curl(p.x / Math.max(8, size * 1.5), p.y / Math.max(8, size * 1.5), this.seed);
|
|
2610
|
-
const push = size * 0.012 * (0.3 + 1.5 * s.speed);
|
|
2611
|
-
const fl = Math.hypot(fx, fy) || 1;
|
|
2612
|
-
p.vx = p.vx * 0.8 + fx / fl * push;
|
|
2613
|
-
p.vy = p.vy * 0.8 + fy / fl * push + g * 0.2;
|
|
2614
|
-
p.x += p.vx;
|
|
2615
|
-
p.y += p.vy;
|
|
2616
|
-
p.age++;
|
|
2617
|
-
if (p.age > lifeSteps) p.alive = false;
|
|
2618
|
-
break;
|
|
2619
|
-
}
|
|
2620
|
-
}
|
|
2621
|
-
const w = this.wave(p, d, size);
|
|
2622
|
-
const qx = w ? p.x + nx * w : p.x;
|
|
2623
|
-
const qy = w ? p.y + ny * w : p.y;
|
|
2624
|
-
const px = p.dx;
|
|
2625
|
-
const py = p.dy;
|
|
2626
|
-
p.px = p.dx;
|
|
2627
|
-
p.py = p.dy;
|
|
2628
|
-
p.dx = qx;
|
|
2629
|
-
p.dy = qy;
|
|
2630
|
-
const fade = s.kind === "flow" ? 1 - p.age / lifeSteps : 1;
|
|
2631
|
-
p.fade = fade;
|
|
2632
|
-
const moved = Math.hypot(qx - px, qy - py);
|
|
2633
|
-
if (d.index === 0 && s.kind !== "flow") continue;
|
|
2634
|
-
if (s.render === "dot") this.dot(copy, qx, qy, qx - px, qy - py, markR * fade, al * fade, op, d, rects);
|
|
2635
|
-
else if (s.render === "line" || s.render === "ribbon") this.segment(px, py, qx, qy, markR * fade, al * fade, op, d.pressure, rects);
|
|
2636
|
-
else {
|
|
2637
|
-
p.run += moved;
|
|
2638
|
-
const gap = Math.max(markR * 2, size * (0.05 + s.interval));
|
|
2639
|
-
while (p.run >= gap) {
|
|
2640
|
-
p.run -= gap;
|
|
2641
|
-
p.bead++;
|
|
2642
|
-
const t = moved > 0 ? 1 - p.run / Math.max(moved, 1e-6) : 1;
|
|
2643
|
-
const bx = px + (qx - px) * Math.min(1, Math.max(0, t));
|
|
2644
|
-
const by = py + (qy - py) * Math.min(1, Math.max(0, t));
|
|
2645
|
-
this.dot(copy, bx, by, qx - px, qy - py, markR * (0.7 + 0.6 * (p.bead % 3 / 2)) * fade, al * fade, op, d, rects);
|
|
2646
|
-
}
|
|
2647
|
-
}
|
|
2654
|
+
}
|
|
2655
|
+
medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
|
|
2656
|
+
return rect;
|
|
2657
|
+
}
|
|
2658
|
+
function drawArtSticker(medium, s, art) {
|
|
2659
|
+
const rect = stickerRect(s);
|
|
2660
|
+
const n = rect.width * rect.height;
|
|
2661
|
+
const cov = new Float32Array(n);
|
|
2662
|
+
const field = new Float32Array(n * 3);
|
|
2663
|
+
const a = s.rotation * Math.PI / 180;
|
|
2664
|
+
const ca = Math.cos(a);
|
|
2665
|
+
const sa = Math.sin(a);
|
|
2666
|
+
const scale = Math.max(art.width, art.height) / s.size;
|
|
2667
|
+
const [r, g, b] = hexRgb(s.color);
|
|
2668
|
+
const px = [0, 0, 0, 0];
|
|
2669
|
+
for (let yy = 0, p = 0; yy < rect.height; yy++) {
|
|
2670
|
+
for (let xx = 0; xx < rect.width; xx++, p++) {
|
|
2671
|
+
const dx = rect.x + xx + 0.5 - s.x;
|
|
2672
|
+
const dy = rect.y + yy + 0.5 - s.y;
|
|
2673
|
+
const ax = (dx * ca + dy * sa) * scale + art.width / 2;
|
|
2674
|
+
const ay = (-dx * sa + dy * ca) * scale + art.height / 2;
|
|
2675
|
+
if (ax < -1 || ay < -1 || ax > art.width + 1 || ay > art.height + 1) continue;
|
|
2676
|
+
sampleArt(art, ax, ay, px);
|
|
2677
|
+
if (!(px[3] > 0)) continue;
|
|
2678
|
+
cov[p] = px[3];
|
|
2679
|
+
field[p * 3] = art.tintable ? r : px[0];
|
|
2680
|
+
field[p * 3 + 1] = art.tintable ? g : px[1];
|
|
2681
|
+
field[p * 3 + 2] = art.tintable ? b : px[2];
|
|
2648
2682
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2683
|
+
}
|
|
2684
|
+
medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
|
|
2685
|
+
return rect;
|
|
2686
|
+
}
|
|
2687
|
+
function jitterColor(hex2, amount, seed, i) {
|
|
2688
|
+
const [r, g, b] = hexRgb(hex2);
|
|
2689
|
+
const lch = oklabToOklch(rgbToOklab({ r, g, b, a: 1 }));
|
|
2690
|
+
const l = Math.min(1, Math.max(0, lch.l + (rand01(seed, i, 61) - 0.5) * amount * 0.3));
|
|
2691
|
+
const h = lch.h + (rand01(seed, i, 63) - 0.5) * amount * 80;
|
|
2692
|
+
const at = (c) => oklabToRgb(oklchToOklab({ l, c, h, alpha: 1 }));
|
|
2693
|
+
const inGamut = (o) => [o.r, o.g, o.b].every((v) => v >= -1e-4 && v <= 1 + 1e-4);
|
|
2694
|
+
let out = at(lch.c);
|
|
2695
|
+
if (!inGamut(out)) {
|
|
2696
|
+
let lo = 0;
|
|
2697
|
+
let hi = lch.c;
|
|
2698
|
+
for (let k = 0; k < 12; k++) {
|
|
2699
|
+
const mid = (lo + hi) / 2;
|
|
2700
|
+
if (inGamut(at(mid))) lo = mid;
|
|
2701
|
+
else hi = mid;
|
|
2653
2702
|
}
|
|
2703
|
+
out = at(lo);
|
|
2654
2704
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
const
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2705
|
+
return "#" + [out.r, out.g, out.b].map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join("");
|
|
2706
|
+
}
|
|
2707
|
+
var StickerSpray = class {
|
|
2708
|
+
constructor(init) {
|
|
2709
|
+
this.init = init;
|
|
2710
|
+
const b = init.brush;
|
|
2711
|
+
this.smoother = new PenSmoother(b.tip.smoothing, b.pressureSmoothing);
|
|
2712
|
+
this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
2713
|
+
const spacing = Math.max(0.05, 1 / Math.max(0.05, b.sticker.density * 4));
|
|
2714
|
+
const walk = { ...b, tip: { ...b.tip, spacing }, dynamics: { ...b.dynamics, scatter: 0, sizeJitter: 0, angleJitter: 0 } };
|
|
2715
|
+
this.walkers = this.copies.map((c, i) => new PenDabWalker(walk, init.seed + i * 7919 | 0, c.linear));
|
|
2716
|
+
this.medium = new TileMedium(init.target, {});
|
|
2717
|
+
this.next = init.nextId;
|
|
2664
2718
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
const
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
let y0 = Infinity;
|
|
2689
|
-
let x1 = -Infinity;
|
|
2690
|
-
let y1 = -Infinity;
|
|
2691
|
-
for (let i = 0; i < 8; i += 2) {
|
|
2692
|
-
x0 = Math.min(x0, pts[i]);
|
|
2693
|
-
x1 = Math.max(x1, pts[i]);
|
|
2694
|
-
y0 = Math.min(y0, pts[i + 1]);
|
|
2695
|
-
y1 = Math.max(y1, pts[i + 1]);
|
|
2696
|
-
}
|
|
2697
|
-
const rect = { x: Math.floor(x0), y: Math.floor(y0), width: Math.floor(x1) - Math.floor(x0) + 1, height: Math.floor(y1) - Math.floor(y0) + 1 };
|
|
2698
|
-
const area = this.scratch(rect.width * rect.height);
|
|
2699
|
-
addPolygonArea(pts, rect, area, Math.min(a.fade, b.fade) * alpha);
|
|
2700
|
-
if (this.telescope(rect, area)) this.lay(rect, area, ceil, d.pressure, (a.dx + b.dx) / 2, (a.dy + b.dy) / 2, rects);
|
|
2719
|
+
init;
|
|
2720
|
+
dabCount = 0;
|
|
2721
|
+
stickers = [];
|
|
2722
|
+
/** Refs the spray picked but could not draw because their art had not loaded. */
|
|
2723
|
+
skipped = /* @__PURE__ */ new Set();
|
|
2724
|
+
smoother;
|
|
2725
|
+
walkers;
|
|
2726
|
+
copies;
|
|
2727
|
+
medium;
|
|
2728
|
+
lastRaw = null;
|
|
2729
|
+
lastFed = null;
|
|
2730
|
+
next;
|
|
2731
|
+
push(points) {
|
|
2732
|
+
const rects = [];
|
|
2733
|
+
for (const raw of points) {
|
|
2734
|
+
this.lastRaw = raw;
|
|
2735
|
+
const p = this.smoother.push(raw);
|
|
2736
|
+
this.lastFed = p;
|
|
2737
|
+
this.copies.forEach((c, i) => {
|
|
2738
|
+
const dabs = [];
|
|
2739
|
+
this.walkers[i].push(c.map(p), dabs);
|
|
2740
|
+
for (const d of dabs) this.drop(d, rects);
|
|
2741
|
+
});
|
|
2701
2742
|
}
|
|
2743
|
+
return rects;
|
|
2702
2744
|
}
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
const
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2745
|
+
finish() {
|
|
2746
|
+
const rects = [];
|
|
2747
|
+
const raw = this.lastRaw;
|
|
2748
|
+
const fed = this.lastFed;
|
|
2749
|
+
this.copies.forEach((c, i) => {
|
|
2750
|
+
const dabs = [];
|
|
2751
|
+
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) this.walkers[i].push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
2752
|
+
this.walkers[i].finish(dabs);
|
|
2753
|
+
for (const d of dabs) this.drop(d, rects);
|
|
2754
|
+
});
|
|
2755
|
+
return rects;
|
|
2756
|
+
}
|
|
2757
|
+
drop(d, rects) {
|
|
2758
|
+
const st = this.init.brush.sticker;
|
|
2759
|
+
const i = this.next;
|
|
2760
|
+
const seed = this.init.seed;
|
|
2761
|
+
const size = d.radius * 2 * (1 - st.sizeJitter * 0.7 * rand01(seed, i, 51));
|
|
2762
|
+
const off = st.scatter * d.radius * 2;
|
|
2763
|
+
const a = rand01(seed, i, 53) * Math.PI * 2;
|
|
2764
|
+
const dist = rand01(seed, i, 55) * off;
|
|
2765
|
+
const base = this.colorAt(d, Math.cos(a) * dist, Math.sin(a) * dist);
|
|
2766
|
+
if (!(base.alpha > 0)) {
|
|
2767
|
+
this.next++;
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const ref2 = st.stamps[Math.floor(rand01(seed, i, 57) * st.stamps.length)] ?? "editor:dot";
|
|
2771
|
+
const stamp = stickerStamp({ ref: ref2 });
|
|
2772
|
+
if (!stamp && !this.init.art?.(ref2)) {
|
|
2773
|
+
this.skipped.add(ref2);
|
|
2774
|
+
this.next++;
|
|
2775
|
+
return;
|
|
2776
|
+
}
|
|
2777
|
+
const sticker = {
|
|
2778
|
+
id: `st${this.next++}`,
|
|
2779
|
+
ref: ref2,
|
|
2780
|
+
...stamp ? { stamp } : {},
|
|
2781
|
+
x: Math.round((d.x + Math.cos(a) * dist) * 100) / 100,
|
|
2782
|
+
y: Math.round((d.y + Math.sin(a) * dist) * 100) / 100,
|
|
2783
|
+
size: Math.max(2, Math.round(size * 10) / 10),
|
|
2784
|
+
rotation: Math.round((-d.angle * 180 / Math.PI + (rand01(seed, i, 59) * 2 - 1) * 180 * st.rotationJitter) * 10) / 10,
|
|
2785
|
+
color: st.colorJitter > 0 ? jitterColor(base.color, st.colorJitter, seed, i) : base.color,
|
|
2786
|
+
opacity: Math.round(Math.min(1, d.opacity * this.init.brush.tip.opacity * base.alpha) * 1e3) / 1e3,
|
|
2787
|
+
seed: Math.floor(rand01(seed, i, 65) * 1e9)
|
|
2788
|
+
};
|
|
2789
|
+
this.stickers.push(sticker);
|
|
2790
|
+
rects.push(drawSticker(this.medium, sticker, this.init.art));
|
|
2791
|
+
this.dabCount++;
|
|
2792
|
+
}
|
|
2793
|
+
sampler;
|
|
2794
|
+
/** A sticker's colour: the paint colour, or gradient paint at its dab (`dx`, `dy` from the dab's centre). */
|
|
2795
|
+
colorAt(d, dx, dy) {
|
|
2796
|
+
const effect = this.init.effect;
|
|
2797
|
+
if (effect) {
|
|
2798
|
+
const rgb = [0, 0, 0];
|
|
2799
|
+
const alpha = sampleEffectAt(effect, d.x + dx, d.y + dy, rgb, d);
|
|
2800
|
+
return { color: "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join(""), alpha };
|
|
2801
|
+
}
|
|
2802
|
+
const g = this.init.paint;
|
|
2803
|
+
if (!g) return { color: this.init.color, alpha: 1 };
|
|
2804
|
+
this.sampler ??= new PaintGradientSampler(g, this.init.seed);
|
|
2805
|
+
const s = g.map.by === "across-tip" ? this.sampler.sample(((dx * Math.sin(d.direction) + dy * Math.cos(d.direction)) / Math.max(0.5, d.radius) + 1) / 2) : this.sampler.dab(d);
|
|
2806
|
+
return { color: "#" + s.rgb.map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255).toString(16).padStart(2, "0")).join(""), alpha: s.alpha };
|
|
2807
|
+
}
|
|
2808
|
+
};
|
|
2809
|
+
function renderStickers(stickers, target, art) {
|
|
2810
|
+
const medium = new TileMedium(target, {});
|
|
2811
|
+
for (const s of stickers) drawSticker(medium, s, art);
|
|
2812
|
+
}
|
|
2813
|
+
function normalizeStickerUpdate(u) {
|
|
2814
|
+
const out = {};
|
|
2815
|
+
if (u.ref !== void 0 && u.stamp !== void 0) throw new Error("Give ref or stamp, not both (stamp is the older name for an editor stamp).");
|
|
2816
|
+
if (u.stamp !== void 0) {
|
|
2817
|
+
if (!STAMP_KINDS.includes(u.stamp)) throw new Error(`stamp ${JSON.stringify(u.stamp)} is not a stamp; use ${STAMP_KINDS.join(", ")}, or ref for any sticker (find_stickers).`);
|
|
2818
|
+
out.ref = `editor:${u.stamp}`;
|
|
2819
|
+
out.stamp = u.stamp;
|
|
2820
|
+
}
|
|
2821
|
+
if (u.ref !== void 0) {
|
|
2822
|
+
out.ref = normalizeStickerRef(u.ref);
|
|
2823
|
+
const stamp = stickerStamp({ ref: out.ref });
|
|
2824
|
+
if (stamp) out.stamp = stamp;
|
|
2825
|
+
else out.stamp = void 0;
|
|
2826
|
+
}
|
|
2827
|
+
for (const k of ["x", "y"]) if (u[k] !== void 0) {
|
|
2828
|
+
if (!Number.isFinite(u[k])) throw new Error(`${k} must be a number of pixels.`);
|
|
2829
|
+
out[k] = u[k];
|
|
2830
|
+
}
|
|
2831
|
+
if (u.size !== void 0) {
|
|
2832
|
+
if (!(u.size >= 2 && u.size <= 2048)) throw new Error(`size ${u.size} is out of range; use 2..2048 px.`);
|
|
2833
|
+
out.size = u.size;
|
|
2834
|
+
}
|
|
2835
|
+
if (u.rotation !== void 0) {
|
|
2836
|
+
if (!Number.isFinite(u.rotation)) throw new Error("rotation must be degrees.");
|
|
2837
|
+
out.rotation = u.rotation;
|
|
2838
|
+
}
|
|
2839
|
+
if (u.color !== void 0) {
|
|
2840
|
+
if (!/^#[0-9a-f]{6}$/i.test(u.color)) throw new Error(`color ${JSON.stringify(u.color)} must be #rrggbb.`);
|
|
2841
|
+
out.color = u.color.toLowerCase();
|
|
2842
|
+
}
|
|
2843
|
+
if (u.opacity !== void 0) {
|
|
2844
|
+
if (!(u.opacity >= 0 && u.opacity <= 1)) throw new Error(`opacity ${u.opacity} is out of range; use 0..1.`);
|
|
2845
|
+
out.opacity = u.opacity;
|
|
2846
|
+
}
|
|
2847
|
+
return out;
|
|
2848
|
+
}
|
|
2849
|
+
function stickerSurface(stickers, base, dirty, art) {
|
|
2850
|
+
const only = base && dirty ? new Set(dirty.flatMap((r) => tilesInRect(r).map(({ tx, ty }) => tileKey(tx, ty)))) : null;
|
|
2851
|
+
const live = /* @__PURE__ */ new Map();
|
|
2852
|
+
let scratch = null;
|
|
2853
|
+
const target = {
|
|
2854
|
+
offset: { x: 0, y: 0 },
|
|
2855
|
+
lockTransparency: false,
|
|
2856
|
+
selection: null,
|
|
2857
|
+
readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? null,
|
|
2858
|
+
writeTile: (tx, ty) => {
|
|
2859
|
+
const k = tileKey(tx, ty);
|
|
2860
|
+
if (only && !only.has(k)) return (scratch ??= new Uint8ClampedArray(TILE * TILE * 4)).fill(0);
|
|
2861
|
+
let t = live.get(k);
|
|
2862
|
+
if (!t) live.set(k, t = new Uint8ClampedArray(TILE * TILE * 4));
|
|
2863
|
+
return t;
|
|
2736
2864
|
}
|
|
2737
|
-
|
|
2865
|
+
};
|
|
2866
|
+
const reaches = (s) => !only || tilesInRect(stickerRect(s)).some(({ tx, ty }) => only.has(tileKey(tx, ty)));
|
|
2867
|
+
renderStickers(stickers.filter(reaches), target, art);
|
|
2868
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
2869
|
+
if (only && base) {
|
|
2870
|
+
for (const [k, t] of base.tiles) if (!only.has(k)) tiles.set(k, t);
|
|
2738
2871
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
s;
|
|
2743
|
-
constructor(brush, options) {
|
|
2744
|
-
super(brush, options);
|
|
2745
|
-
this.s = brush.bristle;
|
|
2746
|
-
const s = this.s;
|
|
2747
|
-
this.tufts = this.copies.map(
|
|
2748
|
-
(_, c) => Array.from({ length: s.count }, (_2, i) => {
|
|
2749
|
-
const a = rand01(this.seed, i, 21 + c) * Math.PI * 2;
|
|
2750
|
-
const r = Math.sqrt(rand01(this.seed, i, 23 + c));
|
|
2751
|
-
return {
|
|
2752
|
-
u: Math.cos(a) * r,
|
|
2753
|
-
v: Math.sin(a) * r,
|
|
2754
|
-
x: 0,
|
|
2755
|
-
y: 0,
|
|
2756
|
-
paint: 1 - s.jitter * 0.4 * rand01(this.seed, i, 25 + c),
|
|
2757
|
-
thirst: 1 + s.jitter * (rand01(this.seed, i, 27 + c) * 2 - 1),
|
|
2758
|
-
w: 1 + s.jitter * 0.6 * (rand01(this.seed, i, 29 + c) - 0.5),
|
|
2759
|
-
started: false
|
|
2760
|
-
};
|
|
2761
|
-
})
|
|
2762
|
-
);
|
|
2872
|
+
for (const [k, d] of live) {
|
|
2873
|
+
const t = finishTile(d, 4, 0);
|
|
2874
|
+
if (t) tiles.set(k, t);
|
|
2763
2875
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2770
|
-
const
|
|
2771
|
-
const
|
|
2772
|
-
const
|
|
2773
|
-
const
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
const oy = -(lu * sa + lv * ca);
|
|
2782
|
-
const lag = bend * (1 - 0.5 * b.v);
|
|
2783
|
-
const wx = d.x + ox - tx * lag;
|
|
2784
|
-
const wy = d.y + oy - ty * lag;
|
|
2785
|
-
if (!b.started) {
|
|
2786
|
-
b.x = wx;
|
|
2787
|
-
b.y = wy;
|
|
2788
|
-
b.started = true;
|
|
2789
|
-
continue;
|
|
2790
|
-
}
|
|
2791
|
-
const nx = b.x + (wx - b.x) * follow;
|
|
2792
|
-
const ny = b.y + (wy - b.y) * follow;
|
|
2793
|
-
const paint = Math.max(0, b.paint);
|
|
2794
|
-
if (paint > 0.02) this.segment(b.x, b.y, nx, ny, lineR * b.w * (0.6 + 0.4 * d.pressure), d.alpha * Math.min(1, paint * 1.4), d.opacity, d.pressure, rects);
|
|
2795
|
-
b.paint -= dryRate * b.thirst * Math.hypot(nx - b.x, ny - b.y);
|
|
2796
|
-
b.x = nx;
|
|
2797
|
-
b.y = ny;
|
|
2876
|
+
return new TiledSurface(4, tiles, 0);
|
|
2877
|
+
}
|
|
2878
|
+
function stickerAt(stickers, x, y, art) {
|
|
2879
|
+
for (let i = stickers.length - 1; i >= 0; i--) {
|
|
2880
|
+
const s = stickers[i];
|
|
2881
|
+
const rad2 = s.size / 2;
|
|
2882
|
+
const a = s.rotation * Math.PI / 180;
|
|
2883
|
+
const dx = x - s.x;
|
|
2884
|
+
const dy = y - s.y;
|
|
2885
|
+
const u = (dx * Math.cos(a) + dy * Math.sin(a)) / rad2;
|
|
2886
|
+
const v = (-dx * Math.sin(a) + dy * Math.cos(a)) / rad2;
|
|
2887
|
+
if (Math.abs(u) > 1.05 || Math.abs(v) > 1.05) continue;
|
|
2888
|
+
if (s.size < 16) return s;
|
|
2889
|
+
const kind = stickerStamp(s);
|
|
2890
|
+
if (kind) {
|
|
2891
|
+
if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
|
|
2892
|
+
continue;
|
|
2798
2893
|
}
|
|
2894
|
+
const pic = art?.(s.ref);
|
|
2895
|
+
if (!pic) return s;
|
|
2896
|
+
const scale = Math.max(pic.width, pic.height) / 2;
|
|
2897
|
+
if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
|
|
2799
2898
|
}
|
|
2800
|
-
|
|
2899
|
+
return null;
|
|
2900
|
+
}
|
|
2801
2901
|
|
|
2802
2902
|
// src/paint-editor/stroke.ts
|
|
2803
2903
|
function defaultTile(surface) {
|
|
@@ -2861,7 +2961,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2861
2961
|
effect
|
|
2862
2962
|
});
|
|
2863
2963
|
} else {
|
|
2864
|
-
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect };
|
|
2964
|
+
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect, stampArt: init.stampArt };
|
|
2865
2965
|
this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
|
|
2866
2966
|
}
|
|
2867
2967
|
}
|
|
@@ -2897,7 +2997,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2897
2997
|
const init = this.init;
|
|
2898
2998
|
const grey = Math.round(init.brush.light.amount * 255);
|
|
2899
2999
|
const g = grey / 255;
|
|
2900
|
-
const
|
|
3000
|
+
const hex2 = "#" + grey.toString(16).padStart(2, "0").repeat(3);
|
|
2901
3001
|
const host = {
|
|
2902
3002
|
update: () => {
|
|
2903
3003
|
},
|
|
@@ -2906,14 +3006,14 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2906
3006
|
}
|
|
2907
3007
|
};
|
|
2908
3008
|
const effect = init.effect ? { source: init.effect.source, pixels: { ...init.effect.pixels, data: greyPixels(init.effect.pixels.data, grey) } } : null;
|
|
2909
|
-
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color:
|
|
3009
|
+
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color: hex2 })) } : null;
|
|
2910
3010
|
return new _PaintStrokeSession(host, {
|
|
2911
3011
|
...init,
|
|
2912
3012
|
surface: lightToShadow(light),
|
|
2913
3013
|
lockTransparency: false,
|
|
2914
3014
|
brush: lightTwinBrush(init.brush),
|
|
2915
3015
|
color: [grey, grey, grey, 255],
|
|
2916
|
-
colorHex:
|
|
3016
|
+
colorHex: hex2,
|
|
2917
3017
|
secondary: [grey, grey, grey, 255],
|
|
2918
3018
|
inputFilter: void 0,
|
|
2919
3019
|
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
@@ -4461,8 +4561,8 @@ var PaintWetLayers = class {
|
|
|
4461
4561
|
this.gpu = null;
|
|
4462
4562
|
}
|
|
4463
4563
|
};
|
|
4464
|
-
function wetPigmentOf(
|
|
4465
|
-
const [r, g, b] = hexToRgb8(
|
|
4564
|
+
function wetPigmentOf(hex2, body) {
|
|
4565
|
+
const [r, g, b] = hexToRgb8(hex2);
|
|
4466
4566
|
return Array.from(pigmentOf([r / 255, g / 255, b / 255], body));
|
|
4467
4567
|
}
|
|
4468
4568
|
|
|
@@ -4934,8 +5034,8 @@ var MixingPad = class {
|
|
|
4934
5034
|
A += a;
|
|
4935
5035
|
}
|
|
4936
5036
|
if (A === 0) return null;
|
|
4937
|
-
const
|
|
4938
|
-
return `#${
|
|
5037
|
+
const hex2 = (v) => Math.round(v / A).toString(16).padStart(2, "0");
|
|
5038
|
+
return `#${hex2(R)}${hex2(G)}${hex2(B)}`;
|
|
4939
5039
|
}
|
|
4940
5040
|
clear() {
|
|
4941
5041
|
this.surface = emptySurface(4);
|
|
@@ -4996,8 +5096,8 @@ var MixingPad = class {
|
|
|
4996
5096
|
paints2.map((p) => p.parts)
|
|
4997
5097
|
);
|
|
4998
5098
|
const core = normalizePaintBrush({ tip: { ...PAD_BLOB.tip, size: R * 1.1, hardness: 0.55 } }, PAD_BLOB);
|
|
4999
|
-
const
|
|
5000
|
-
this.run(core, [{ x: cx, y: cy }],
|
|
5099
|
+
const hex2 = "#" + mixed.map((v) => Math.round(v * 255).toString(16).padStart(2, "0")).join("");
|
|
5100
|
+
this.run(core, [{ x: cx, y: cy }], hex2);
|
|
5001
5101
|
return { color: this.pick(cx, cy, 2), bounds };
|
|
5002
5102
|
}
|
|
5003
5103
|
/** The pad's pixels as a PNG data URL (with alpha), for saving. */
|
|
@@ -5699,8 +5799,8 @@ function parseColor(value, what = "color") {
|
|
|
5699
5799
|
if (typeof value !== "string" || !HEX3.test(value.trim())) throw new PaintEditorError(`${what} is ${JSON.stringify(value)}; use #rrggbb, e.g. "#2a4b8d".`);
|
|
5700
5800
|
return value.trim().toLowerCase();
|
|
5701
5801
|
}
|
|
5702
|
-
function rgbaOf(
|
|
5703
|
-
const [r, g, b] = hexToRgb8(
|
|
5802
|
+
function rgbaOf(hex2) {
|
|
5803
|
+
const [r, g, b] = hexToRgb8(hex2);
|
|
5704
5804
|
return [r, g, b, 255];
|
|
5705
5805
|
}
|
|
5706
5806
|
function rgbToHex(r, g, b) {
|
|
@@ -5753,6 +5853,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5753
5853
|
stickerPreview = null;
|
|
5754
5854
|
encodeImage;
|
|
5755
5855
|
listeners = /* @__PURE__ */ new Set();
|
|
5856
|
+
commitListeners = /* @__PURE__ */ new Set();
|
|
5857
|
+
/** Origins set by `withOrigin`, innermost last. */
|
|
5858
|
+
origins = [];
|
|
5859
|
+
/** `group` in progress: commits fold into one step, recorded when it ends. */
|
|
5860
|
+
grouping = null;
|
|
5861
|
+
/** `previewPaint` in progress: nothing is emitted or recorded. */
|
|
5862
|
+
previewing = false;
|
|
5756
5863
|
version = 0;
|
|
5757
5864
|
cached = null;
|
|
5758
5865
|
session = null;
|
|
@@ -5824,6 +5931,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5824
5931
|
void this.refreshBrushes().catch(() => void 0);
|
|
5825
5932
|
this.pendingBrushId = start.brush.id === wanted ? null : wanted;
|
|
5826
5933
|
this.color = options.color ? parseColor(options.color) : "#1f2937";
|
|
5934
|
+
this.colorScheme = options.colorScheme === "dark" ? "dark" : "light";
|
|
5827
5935
|
this.hand = options.hand === "left" ? "left" : "right";
|
|
5828
5936
|
this.encodeImage = options.encodeImage;
|
|
5829
5937
|
this.guides = defaultGuides(this.doc.canvas);
|
|
@@ -5860,7 +5968,125 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5860
5968
|
this.listeners.add(listener);
|
|
5861
5969
|
return () => this.listeners.delete(listener);
|
|
5862
5970
|
}
|
|
5971
|
+
/** Hears each undo step recorded, merged, undone or redone, with who made it. Returns an unsubscribe function. */
|
|
5972
|
+
onCommit(listener) {
|
|
5973
|
+
this.commitListeners.add(listener);
|
|
5974
|
+
return () => {
|
|
5975
|
+
this.commitListeners.delete(listener);
|
|
5976
|
+
};
|
|
5977
|
+
}
|
|
5978
|
+
notifyCommit(event) {
|
|
5979
|
+
if (this.previewing) return;
|
|
5980
|
+
for (const l of [...this.commitListeners]) l(event);
|
|
5981
|
+
}
|
|
5982
|
+
/** The origin steps are recorded with now: the innermost `withOrigin`, else `user`. */
|
|
5983
|
+
getOrigin() {
|
|
5984
|
+
return this.origins[this.origins.length - 1] ?? "user";
|
|
5985
|
+
}
|
|
5986
|
+
/**
|
|
5987
|
+
* Records the steps `fn` makes as made by `origin` (`agent:<id>` for an agent). When `fn` returns
|
|
5988
|
+
* a promise, the origin holds until it settles. Satisfies `createOriginMiddleware`'s history target,
|
|
5989
|
+
* so agent tool calls are recorded as the calling agent's.
|
|
5990
|
+
*/
|
|
5991
|
+
withOrigin(origin, fn) {
|
|
5992
|
+
this.origins.push(origin);
|
|
5993
|
+
let pending = false;
|
|
5994
|
+
const pop = () => {
|
|
5995
|
+
const i = this.origins.lastIndexOf(origin);
|
|
5996
|
+
if (i >= 0) this.origins.splice(i, 1);
|
|
5997
|
+
};
|
|
5998
|
+
try {
|
|
5999
|
+
const result = fn();
|
|
6000
|
+
if (result && typeof result.then === "function") {
|
|
6001
|
+
pending = true;
|
|
6002
|
+
return Promise.resolve(result).finally(pop);
|
|
6003
|
+
}
|
|
6004
|
+
return result;
|
|
6005
|
+
} finally {
|
|
6006
|
+
if (!pending) pop();
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
/**
|
|
6010
|
+
* Runs `fn` as one undo step named `label`: every change it makes (strokes, fills, new layers) is
|
|
6011
|
+
* undone together. Wet paint can't be grouped. Nested groups join the outer one. If `fn` throws,
|
|
6012
|
+
* its changes are put back and nothing is recorded.
|
|
6013
|
+
*/
|
|
6014
|
+
group(label, fn) {
|
|
6015
|
+
if (this.grouping) return fn();
|
|
6016
|
+
this.requireIdle("grouping changes");
|
|
6017
|
+
const before = this.doc;
|
|
6018
|
+
const revision = this.docRevision;
|
|
6019
|
+
this.grouping = { label, before, changed: false };
|
|
6020
|
+
let result;
|
|
6021
|
+
try {
|
|
6022
|
+
result = fn();
|
|
6023
|
+
} catch (e) {
|
|
6024
|
+
this.grouping = null;
|
|
6025
|
+
if (this.doc !== before) {
|
|
6026
|
+
this.doc = before;
|
|
6027
|
+
this.docRevision = revision + 1;
|
|
6028
|
+
this.emit();
|
|
6029
|
+
}
|
|
6030
|
+
throw e;
|
|
6031
|
+
}
|
|
6032
|
+
const g = this.grouping;
|
|
6033
|
+
this.grouping = null;
|
|
6034
|
+
if (g.changed && this.doc !== before) {
|
|
6035
|
+
this.undoStack.push({ label, origin: this.getOrigin(), before, after: this.doc });
|
|
6036
|
+
if (this.undoStack.length > this.historyLimit) this.undoStack.shift();
|
|
6037
|
+
this.redoStack = [];
|
|
6038
|
+
this.notifyCommit({ kind: "execute", label, origin: this.getOrigin() });
|
|
6039
|
+
this.emit();
|
|
6040
|
+
}
|
|
6041
|
+
return result;
|
|
6042
|
+
}
|
|
6043
|
+
/**
|
|
6044
|
+
* What `fn` would paint, without changing anything: it runs on a new empty layer (above the
|
|
6045
|
+
* active layer, another one, or at the bottom), and the layer's pixels and the picture with them
|
|
6046
|
+
* come back while the document, history and listeners stay as they were. `fn` gets that layer's id and must paint on it with dry brushes (strokes, replays and
|
|
6047
|
+
* fills); wet paint can't be previewed. For ghost paint an agent suggests (`createPaintProposals`).
|
|
6048
|
+
*/
|
|
6049
|
+
previewPaint(fn, placement = {}) {
|
|
6050
|
+
this.requireIdle("previewing");
|
|
6051
|
+
if (this.previewing) throw new PaintEditorError("A preview is already running.");
|
|
6052
|
+
const saved = {
|
|
6053
|
+
doc: this.doc,
|
|
6054
|
+
undo: [...this.undoStack],
|
|
6055
|
+
redo: this.redoStack,
|
|
6056
|
+
revision: this.docRevision,
|
|
6057
|
+
strokeCounter: this.strokeCounter,
|
|
6058
|
+
seedCounter: this.seedCounter,
|
|
6059
|
+
mixerLoad: this.mixerLoad
|
|
6060
|
+
};
|
|
6061
|
+
this.previewing = true;
|
|
6062
|
+
try {
|
|
6063
|
+
const layer = this.addLayer({ name: "Preview", ...placement.aboveId ? { aboveId: placement.aboveId } : {} });
|
|
6064
|
+
if (placement.bottom) this.moveLayer(layer.id, 0, null);
|
|
6065
|
+
fn(layer.id);
|
|
6066
|
+
if (this.wetOp || this.pendingWetEvents.length) throw new PaintEditorError("Wet paint can't be previewed; suggest it with a dry brush.");
|
|
6067
|
+
const found = findLayer(this.doc.layers, layer.id)?.layer;
|
|
6068
|
+
const surface = found && isPixelLayer(found) ? found.surface : null;
|
|
6069
|
+
const bounds = surface ? contentBounds(surface) : null;
|
|
6070
|
+
const box = bounds ? clipToCanvas(bounds, this.doc.canvas) : null;
|
|
6071
|
+
if (!surface || !box) return { bounds: null, pixels: new Uint8ClampedArray(0), composite: new Uint8ClampedArray(0) };
|
|
6072
|
+
const composite = this.exportCompositor.composite(this.renderLayers({ wet: "display" }), box, this.sources());
|
|
6073
|
+
return { bounds: box, pixels: readRegion(surface, box), composite };
|
|
6074
|
+
} finally {
|
|
6075
|
+
this.wetOp = null;
|
|
6076
|
+
this.pendingWetEvents = [];
|
|
6077
|
+
this.doc = saved.doc;
|
|
6078
|
+
this.undoStack = saved.undo;
|
|
6079
|
+
this.redoStack = saved.redo;
|
|
6080
|
+
this.docRevision = saved.revision;
|
|
6081
|
+
this.strokeCounter = saved.strokeCounter;
|
|
6082
|
+
this.seedCounter = saved.seedCounter;
|
|
6083
|
+
this.mixerLoad = saved.mixerLoad;
|
|
6084
|
+
this.cached = null;
|
|
6085
|
+
this.previewing = false;
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
5863
6088
|
emit(dirty = "all") {
|
|
6089
|
+
if (this.previewing) return;
|
|
5864
6090
|
if (dirty === "all" || this.dirty === "all") this.dirty = "all";
|
|
5865
6091
|
else this.dirty.push(...dirty);
|
|
5866
6092
|
this.version++;
|
|
@@ -5877,6 +6103,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5877
6103
|
brush: this.brush,
|
|
5878
6104
|
brushes: this.library ??= this.brushEntries.map((e) => e.brush),
|
|
5879
6105
|
color: this.color,
|
|
6106
|
+
colorScheme: this.colorScheme,
|
|
6107
|
+
colorLocked: this.colorLocked,
|
|
5880
6108
|
secondaryColor: this.secondaryColor,
|
|
5881
6109
|
paint: this.gradient ?? { type: "color", color: this.color },
|
|
5882
6110
|
effectPaint: this.effectPaintPending ?? this.effectPaint,
|
|
@@ -5943,7 +6171,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5943
6171
|
if (this.session) throw new PaintEditorError(`A stroke is in progress; end or cancel it before ${what}.`);
|
|
5944
6172
|
}
|
|
5945
6173
|
commit(label, next, before = this.doc) {
|
|
5946
|
-
|
|
6174
|
+
if (this.grouping) {
|
|
6175
|
+
this.grouping.changed = true;
|
|
6176
|
+
this.doc = next;
|
|
6177
|
+
this.docRevision++;
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
6180
|
+
const entry = { label, origin: this.getOrigin(), before, after: next };
|
|
5947
6181
|
if (this.wetOp) {
|
|
5948
6182
|
entry.wet = { before: this.wetOp.before, after: this.wet.snapshot(), lost: false };
|
|
5949
6183
|
this.wetOp = null;
|
|
@@ -5954,6 +6188,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5954
6188
|
this.doc = next;
|
|
5955
6189
|
this.docRevision++;
|
|
5956
6190
|
this.trimWetHistory();
|
|
6191
|
+
this.notifyCommit({ kind: "execute", label, origin: entry.origin });
|
|
5957
6192
|
this.emit();
|
|
5958
6193
|
}
|
|
5959
6194
|
/**
|
|
@@ -5962,7 +6197,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5962
6197
|
*/
|
|
5963
6198
|
commitOrAmend(label, next, commitToHistory) {
|
|
5964
6199
|
const last = this.undoStack[this.undoStack.length - 1];
|
|
5965
|
-
if (commitToHistory || !last || last.label !== label || last.wet || this.wetOp) {
|
|
6200
|
+
if (commitToHistory || !last || last.label !== label || last.wet || this.wetOp || this.grouping) {
|
|
5966
6201
|
this.commit(label, next);
|
|
5967
6202
|
return;
|
|
5968
6203
|
}
|
|
@@ -5970,6 +6205,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5970
6205
|
this.redoStack = [];
|
|
5971
6206
|
this.doc = next;
|
|
5972
6207
|
this.docRevision++;
|
|
6208
|
+
this.notifyCommit({ kind: "merge", label, origin: last.origin });
|
|
5973
6209
|
this.emit();
|
|
5974
6210
|
}
|
|
5975
6211
|
/** Keeps the wet snapshots in history within the wet undo memory, dropping the oldest. */
|
|
@@ -6039,6 +6275,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6039
6275
|
this.wet.restore(e.wet.lost ? null : e.wet.before);
|
|
6040
6276
|
if (paper !== this.doc.paper) this.wet.paperChanged();
|
|
6041
6277
|
}
|
|
6278
|
+
this.notifyCommit({ kind: "undo", label: e.label, origin: this.getOrigin() });
|
|
6042
6279
|
this.emit();
|
|
6043
6280
|
return e.label;
|
|
6044
6281
|
}
|
|
@@ -6057,6 +6294,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6057
6294
|
this.wet.restore(e.wet.lost ? null : e.wet.after);
|
|
6058
6295
|
if (paper !== this.doc.paper) this.wet.paperChanged();
|
|
6059
6296
|
}
|
|
6297
|
+
this.notifyCommit({ kind: "redo", label: e.label, origin: this.getOrigin() });
|
|
6060
6298
|
this.emit();
|
|
6061
6299
|
return e.label;
|
|
6062
6300
|
}
|
|
@@ -7004,6 +7242,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7004
7242
|
if (changes.paint !== void 0 && changes.paint !== null) this.readStrokePaint(changes.paint);
|
|
7005
7243
|
const unchanged = !Object.keys(changes).length;
|
|
7006
7244
|
const next = normalizePaintBrush({ ...changes, id: base.id }, base);
|
|
7245
|
+
if (entry) this.takeBrushColour(entry.spec);
|
|
7007
7246
|
this.useBrush(next, unchanged ? baseSpec : brushSpecOf(next, baseSpec), unchanged && base.builtin === true);
|
|
7008
7247
|
return this.brush;
|
|
7009
7248
|
}
|
|
@@ -7020,6 +7259,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7020
7259
|
const base = copyJSON(entry ? entry.spec : this.brushSpec);
|
|
7021
7260
|
for (const k of unset) delete base[k];
|
|
7022
7261
|
const spec = this.readBrushPatch(base, patch, problems);
|
|
7262
|
+
if (entry || patch.colour?.default !== void 0) this.takeBrushColour(spec);
|
|
7023
7263
|
this.useBrush(unchanged && entry ? entry.brush : resolveBrush(spec), spec, unchanged && entry?.builtin === true);
|
|
7024
7264
|
return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
|
|
7025
7265
|
}
|
|
@@ -7069,7 +7309,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7069
7309
|
this.brushSpec = { ...copyJSON(spec), id: next.id, name: next.name };
|
|
7070
7310
|
this.pendingBrushId = null;
|
|
7071
7311
|
this.emit([]);
|
|
7072
|
-
|
|
7312
|
+
const refs = brushStickerRefs(next);
|
|
7313
|
+
if (refs.length) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
7073
7314
|
}
|
|
7074
7315
|
/**
|
|
7075
7316
|
* Saves the active brush (or `brush`, engine settings on top of it) into the brush library under
|
|
@@ -7114,9 +7355,54 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7114
7355
|
return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
|
|
7115
7356
|
}
|
|
7116
7357
|
// --- Colour --------------------------------------------------------------------
|
|
7358
|
+
colorScheme;
|
|
7359
|
+
colorLocked = false;
|
|
7360
|
+
/** The brush default colour the paint colour came from (resolved again when the scheme changes), or `null`. */
|
|
7361
|
+
brushColour = null;
|
|
7362
|
+
/**
|
|
7363
|
+
* The host's colour scheme: a paint colour that came from a brush's light / dark default
|
|
7364
|
+
* resolves to the new side. Editor state, not an undo step.
|
|
7365
|
+
*/
|
|
7366
|
+
setColorScheme(scheme) {
|
|
7367
|
+
const next = scheme === "dark" ? "dark" : "light";
|
|
7368
|
+
if (next === this.colorScheme) return;
|
|
7369
|
+
this.colorScheme = next;
|
|
7370
|
+
const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
|
|
7371
|
+
if (hex2) this.color = hex2;
|
|
7372
|
+
this.emit([]);
|
|
7373
|
+
}
|
|
7374
|
+
/** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
|
|
7375
|
+
setColorLocked(locked) {
|
|
7376
|
+
if (this.colorLocked === locked) return;
|
|
7377
|
+
this.colorLocked = locked;
|
|
7378
|
+
this.emit([]);
|
|
7379
|
+
}
|
|
7380
|
+
/**
|
|
7381
|
+
* After the user chooses a brush: its solid or light / dark default colour becomes the paint
|
|
7382
|
+
* colour (resolved for the colour scheme), unless the colour is locked. Gradient defaults are the
|
|
7383
|
+
* brush's own paint already; effect defaults stay a suggestion.
|
|
7384
|
+
*/
|
|
7385
|
+
takeBrushColour(spec) {
|
|
7386
|
+
if (this.colorLocked) return;
|
|
7387
|
+
const d = spec.colour?.default;
|
|
7388
|
+
const hex2 = d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
7389
|
+
if (!hex2) return;
|
|
7390
|
+
this.color = hex2;
|
|
7391
|
+
this.gradient = null;
|
|
7392
|
+
this.effectPaint = this.effectPaintPending = null;
|
|
7393
|
+
this.paintEpoch++;
|
|
7394
|
+
this.brushColour = d;
|
|
7395
|
+
}
|
|
7396
|
+
/** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
|
|
7397
|
+
ownColour(brushId) {
|
|
7398
|
+
const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
|
|
7399
|
+
const d = spec?.colour?.own ? spec.colour.default : void 0;
|
|
7400
|
+
return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
7401
|
+
}
|
|
7117
7402
|
/** Sets the paint colour (and goes back to one colour from gradient paint). */
|
|
7118
7403
|
setColor(color) {
|
|
7119
7404
|
this.color = parseColor(color);
|
|
7405
|
+
this.brushColour = null;
|
|
7120
7406
|
this.gradient = null;
|
|
7121
7407
|
this.effectPaint = this.effectPaintPending = null;
|
|
7122
7408
|
this.paintEpoch++;
|
|
@@ -7263,6 +7549,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7263
7549
|
return l;
|
|
7264
7550
|
})();
|
|
7265
7551
|
const brush = this.strokeBrush(init);
|
|
7552
|
+
if (this.previewing && isWetBrush(brush)) throw new PaintEditorError(`Wet paint can't be previewed; the ${brush.name} brush paints wet media. Suggest it with a dry brush.`);
|
|
7266
7553
|
const effect = this.strokeEffect(init, brush);
|
|
7267
7554
|
if (brush.light.amount > 0) {
|
|
7268
7555
|
if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (glow-gel, neon-marker; list_brush_presets lists them), or set light.amount to 0 (set_brush).`);
|
|
@@ -7299,7 +7586,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7299
7586
|
this.beginWetOp();
|
|
7300
7587
|
const world = this.wet.ensureLane(layer.id, brush.wet.medium);
|
|
7301
7588
|
const id = layer.id;
|
|
7302
|
-
const wetColor = init.color === void 0 ? this.color : parseColor(init.color);
|
|
7589
|
+
const wetColor = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7303
7590
|
const [r, g, b] = hexToRgb8(wetColor);
|
|
7304
7591
|
const wetGradient = this.strokeGradient(init, brush, wetColor);
|
|
7305
7592
|
const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);
|
|
@@ -7327,7 +7614,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7327
7614
|
const dried = findLayer(this.doc.layers, layer.id).layer;
|
|
7328
7615
|
layer = dried;
|
|
7329
7616
|
}
|
|
7330
|
-
let color = init.color === void 0 ? this.color : parseColor(init.color);
|
|
7617
|
+
let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7331
7618
|
const tracing = this.guides.tracing;
|
|
7332
7619
|
if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
|
|
7333
7620
|
const perspective = this.guides.perspective;
|
|
@@ -7366,6 +7653,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7366
7653
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
7367
7654
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
7368
7655
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
|
|
7656
|
+
stampArt: this.stickerArt.get,
|
|
7369
7657
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
7370
7658
|
effect
|
|
7371
7659
|
});
|
|
@@ -9566,6 +9854,13 @@ function resampleInto(src, rect, width, height) {
|
|
|
9566
9854
|
}
|
|
9567
9855
|
return { width, height, data: out };
|
|
9568
9856
|
}
|
|
9857
|
+
function clipToCanvas(rect, canvas) {
|
|
9858
|
+
const x0 = Math.max(0, rect.x);
|
|
9859
|
+
const y0 = Math.max(0, rect.y);
|
|
9860
|
+
const x1 = Math.min(canvas.width, rect.x + rect.width);
|
|
9861
|
+
const y1 = Math.min(canvas.height, rect.y + rect.height);
|
|
9862
|
+
return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
|
|
9863
|
+
}
|
|
9569
9864
|
|
|
9570
9865
|
// src/paint-editor/presence.ts
|
|
9571
9866
|
var PaintPresence = class extends PresenceStore {
|
|
@@ -10786,8 +11081,8 @@ var POINTS = {
|
|
|
10786
11081
|
}
|
|
10787
11082
|
};
|
|
10788
11083
|
function loadResult(load) {
|
|
10789
|
-
const
|
|
10790
|
-
return { reservoir:
|
|
11084
|
+
const hex2 = (c) => c ? "#" + c.map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("") : null;
|
|
11085
|
+
return { reservoir: hex2(load.reservoir), amount: Math.round(load.amount * 1e3) / 1e3, pickup: hex2(load.pickup) };
|
|
10791
11086
|
}
|
|
10792
11087
|
function createPaintEditorToolProvider(controller, options = {}) {
|
|
10793
11088
|
const c = typeof controller === "function" ? lazyController(controller) : controller;
|
|
@@ -11133,18 +11428,24 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11133
11428
|
add({
|
|
11134
11429
|
name: "set_color",
|
|
11135
11430
|
title: "Set the paint colour",
|
|
11136
|
-
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours.",
|
|
11431
|
+
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default, and whether it is locked. Choosing a brush with a default colour (set_brush id, e.g. the highlighter's yellow) makes that the paint colour unless the colour is locked; a brush with colour.own always paints its own. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours and locked.",
|
|
11137
11432
|
inputSchema: {
|
|
11138
11433
|
type: "object",
|
|
11139
|
-
properties: {
|
|
11434
|
+
properties: {
|
|
11435
|
+
color: { ...COLOR3, description: "Paint colour #rrggbb." },
|
|
11436
|
+
secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
|
|
11437
|
+
locked: { type: "boolean", description: "Lock the paint colour: brushes chosen later keep it instead of taking their default colour (default: unchanged; the editor starts unlocked)." }
|
|
11438
|
+
},
|
|
11140
11439
|
additionalProperties: false
|
|
11141
11440
|
},
|
|
11142
11441
|
annotations: { title: "Set the paint colour", ...SET4 },
|
|
11143
|
-
handler: ({ color, secondaryColor }) => {
|
|
11442
|
+
handler: ({ color, secondaryColor, locked }) => {
|
|
11443
|
+
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
11144
11444
|
if (color !== void 0) c.setColor(color);
|
|
11145
11445
|
if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
|
|
11446
|
+
if (locked !== void 0) c.setColorLocked(locked);
|
|
11146
11447
|
const s = c.getState();
|
|
11147
|
-
return { color: s.color, secondaryColor: s.secondaryColor };
|
|
11448
|
+
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
|
|
11148
11449
|
}
|
|
11149
11450
|
});
|
|
11150
11451
|
add({
|
|
@@ -11226,7 +11527,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11226
11527
|
await c.wetIdle();
|
|
11227
11528
|
const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
|
|
11228
11529
|
const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
|
|
11229
|
-
if (
|
|
11530
|
+
if (base) {
|
|
11531
|
+
const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
|
|
11532
|
+
if (refs.length) await c.prepareStickers(refs);
|
|
11533
|
+
}
|
|
11230
11534
|
const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
|
|
11231
11535
|
const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
|
|
11232
11536
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
@@ -11603,6 +11907,7 @@ function strokeSummary(s, withPoints, live) {
|
|
|
11603
11907
|
function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
11604
11908
|
return createBrushLibraryToolProvider(controller.brushLibrary, {
|
|
11605
11909
|
profile: PAINT_PROFILE,
|
|
11910
|
+
stickers: () => controller.stickers.provider,
|
|
11606
11911
|
...options,
|
|
11607
11912
|
renderPreview: (brush, { width, height, color }) => {
|
|
11608
11913
|
const t = renderBrushThumbnail(resolveBrush(brush), { paper: controller.getState().document.paper, width, height, color: color.toLowerCase() });
|
|
@@ -12405,6 +12710,392 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
12405
12710
|
readOnly: () => controller.busyReason() ?? false
|
|
12406
12711
|
});
|
|
12407
12712
|
}
|
|
12713
|
+
|
|
12714
|
+
// src/paint-editor/collaboration.ts
|
|
12715
|
+
var LAYER_FIELDS = ["name", "visible", "opacity", "blend"];
|
|
12716
|
+
function createPaintChangeFeed(controller, options = {}) {
|
|
12717
|
+
const now = options.now ?? (() => Date.now());
|
|
12718
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12719
|
+
let seq = 0;
|
|
12720
|
+
let pending;
|
|
12721
|
+
const take = () => {
|
|
12722
|
+
const state = controller.getState();
|
|
12723
|
+
const doc = state.document;
|
|
12724
|
+
const layers = /* @__PURE__ */ new Map();
|
|
12725
|
+
for (const l of controller.listLayers()) layers.set(l.id, { name: l.name, visible: l.visible, opacity: l.opacity, blend: l.blend });
|
|
12726
|
+
return { doc, strokes: new Map(doc.strokes.map((s) => [s.id, s])), events: doc.paintEvents?.length ?? 0, layers, stroking: state.stroking };
|
|
12727
|
+
};
|
|
12728
|
+
let last = take();
|
|
12729
|
+
const check = () => {
|
|
12730
|
+
const state = controller.getState();
|
|
12731
|
+
if (state.document === last.doc && state.stroking === last.stroking) return null;
|
|
12732
|
+
const next = take();
|
|
12733
|
+
const addedStrokes = [...next.strokes.values()].filter((s) => !last.strokes.has(s.id));
|
|
12734
|
+
const removedStrokes = [...last.strokes.values()].filter((s) => !next.strokes.has(s.id));
|
|
12735
|
+
const allEvents = state.document.paintEvents ?? [];
|
|
12736
|
+
const events = next.events > last.events ? allEvents.slice(last.events) : [];
|
|
12737
|
+
const addedLayers = [];
|
|
12738
|
+
const changedLayers = [];
|
|
12739
|
+
for (const [id, l] of next.layers) {
|
|
12740
|
+
const before = last.layers.get(id);
|
|
12741
|
+
if (!before) addedLayers.push({ id, name: String(l.name) });
|
|
12742
|
+
else {
|
|
12743
|
+
const fields = LAYER_FIELDS.filter((f) => before[f] !== l[f]);
|
|
12744
|
+
if (fields.length) changedLayers.push({ id, name: String(l.name), fields });
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
const removedLayers = [...last.layers].filter(([id]) => !next.layers.has(id)).map(([id, l]) => ({ id, name: String(l.name) }));
|
|
12748
|
+
const gestureStarted = next.stroking && !last.stroking;
|
|
12749
|
+
const gestureEnded = !next.stroking && last.stroking;
|
|
12750
|
+
last = next;
|
|
12751
|
+
const edited = addedStrokes.length + removedStrokes.length + events.length + addedLayers.length + removedLayers.length + changedLayers.length > 0;
|
|
12752
|
+
if (!edited && !gestureStarted && !gestureEnded) return null;
|
|
12753
|
+
const commit = edited ? pending : void 0;
|
|
12754
|
+
if (edited) pending = void 0;
|
|
12755
|
+
const change = {
|
|
12756
|
+
seq: ++seq,
|
|
12757
|
+
origin: commit?.origin ?? "user",
|
|
12758
|
+
...commit ? { commit } : {},
|
|
12759
|
+
addedStrokes,
|
|
12760
|
+
removedStrokes,
|
|
12761
|
+
events,
|
|
12762
|
+
addedLayers,
|
|
12763
|
+
removedLayers,
|
|
12764
|
+
changedLayers,
|
|
12765
|
+
inGesture: next.stroking,
|
|
12766
|
+
...gestureStarted ? { gestureStarted: true } : {},
|
|
12767
|
+
...gestureEnded ? { gestureEnded: true } : {},
|
|
12768
|
+
at: now()
|
|
12769
|
+
};
|
|
12770
|
+
listeners.forEach((l) => l(change));
|
|
12771
|
+
return change;
|
|
12772
|
+
};
|
|
12773
|
+
const offCommit = controller.onCommit((event) => {
|
|
12774
|
+
pending = event;
|
|
12775
|
+
});
|
|
12776
|
+
const offState = controller.subscribe(() => {
|
|
12777
|
+
check();
|
|
12778
|
+
});
|
|
12779
|
+
return {
|
|
12780
|
+
check,
|
|
12781
|
+
subscribe(listener) {
|
|
12782
|
+
listeners.add(listener);
|
|
12783
|
+
return () => {
|
|
12784
|
+
listeners.delete(listener);
|
|
12785
|
+
};
|
|
12786
|
+
},
|
|
12787
|
+
reset() {
|
|
12788
|
+
last = take();
|
|
12789
|
+
pending = void 0;
|
|
12790
|
+
},
|
|
12791
|
+
dispose() {
|
|
12792
|
+
offCommit();
|
|
12793
|
+
offState();
|
|
12794
|
+
listeners.clear();
|
|
12795
|
+
}
|
|
12796
|
+
};
|
|
12797
|
+
}
|
|
12798
|
+
var HEX4 = /^#[0-9a-f]{6}$/i;
|
|
12799
|
+
var DRY_ENGINES = /* @__PURE__ */ new Set(["dab", "particle", "bristle", "mixer"]);
|
|
12800
|
+
function suggestionBrushRefusal(brush) {
|
|
12801
|
+
if (!DRY_ENGINES.has(brush.engine)) return `The ${brush.name} brush (${brush.engine}) can't paint a suggestion: suggestions are dry paint on a layer of their own. Use a dab, particle, bristle or mixer brush.`;
|
|
12802
|
+
if (brush.behavior.pickup > 0) return `The ${brush.name} brush picks up paint, which a suggestion on its own layer can't; choose a brush without pickup.`;
|
|
12803
|
+
return null;
|
|
12804
|
+
}
|
|
12805
|
+
function createPaintProposals(options) {
|
|
12806
|
+
const { controller } = options;
|
|
12807
|
+
const now = options.now ?? (() => Date.now());
|
|
12808
|
+
const maxPoints = options.maxPoints ?? 4e3;
|
|
12809
|
+
let seq = 0;
|
|
12810
|
+
const makeId = options.idGenerator ?? (() => `paint-proposal-${++seq}`);
|
|
12811
|
+
let state = { proposals: [] };
|
|
12812
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12813
|
+
const set = (proposals) => {
|
|
12814
|
+
state = { proposals };
|
|
12815
|
+
listeners.forEach((l) => l(state));
|
|
12816
|
+
};
|
|
12817
|
+
const find = (id) => {
|
|
12818
|
+
const p = state.proposals.find((x) => x.id === id);
|
|
12819
|
+
if (!p) throw new PaintEditorError(`No pending paint proposal "${id}". Call list_paint_proposals for the ids.`);
|
|
12820
|
+
return p;
|
|
12821
|
+
};
|
|
12822
|
+
const strokeIds = () => new Set(controller.getStrokes().map((s) => s.id));
|
|
12823
|
+
const validate = (input) => {
|
|
12824
|
+
if (!input.title?.trim()) throw new PaintEditorError("A proposal needs a title: what it adds, in a few words.");
|
|
12825
|
+
if (!Array.isArray(input.ops) || !input.ops.length) throw new PaintEditorError("A proposal needs at least one op (stroke, wash or echo).");
|
|
12826
|
+
const place = input.place ?? "top";
|
|
12827
|
+
if (place === "above") {
|
|
12828
|
+
if (!input.layerId) throw new PaintEditorError('place "above" needs layerId: the layer to put the suggestion above (list_layers).');
|
|
12829
|
+
if (!controller.listLayers().some((l) => l.id === input.layerId)) throw new PaintEditorError(`Unknown layer "${input.layerId}". Call list_layers for the ids.`);
|
|
12830
|
+
}
|
|
12831
|
+
const known = strokeIds();
|
|
12832
|
+
let points = 0;
|
|
12833
|
+
input.ops.forEach((op, i) => {
|
|
12834
|
+
const at = `ops[${i}]`;
|
|
12835
|
+
if (op.kind === "stroke") {
|
|
12836
|
+
if (!op.points?.length) throw new PaintEditorError(`${at}: a stroke needs points in document pixels.`);
|
|
12837
|
+
points += op.points.length;
|
|
12838
|
+
if (op.color !== void 0 && !HEX4.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
12839
|
+
const brush = op.brushId ? controller.getBrush(op.brushId) : controller.getState().brush;
|
|
12840
|
+
if (!brush) throw new PaintEditorError(`${at}: unknown brush "${op.brushId}". Call list_brush_presets for the ids.`);
|
|
12841
|
+
const refusal = suggestionBrushRefusal(brush);
|
|
12842
|
+
if (refusal) throw new PaintEditorError(`${at}: ${refusal}`);
|
|
12843
|
+
if (op.size !== void 0 && !(op.size > 0)) throw new PaintEditorError(`${at}: size must be above 0 (document pixels).`);
|
|
12844
|
+
} else if (op.kind === "wash") {
|
|
12845
|
+
if (!HEX4.test(op.color ?? "")) throw new PaintEditorError(`${at}: a wash needs color as #rrggbb.`);
|
|
12846
|
+
if (op.opacity !== void 0 && !(op.opacity > 0 && op.opacity <= 1)) throw new PaintEditorError(`${at}: opacity must be in (0, 1].`);
|
|
12847
|
+
} else if (op.kind === "echo") {
|
|
12848
|
+
if (!op.strokeIds?.length) throw new PaintEditorError(`${at}: an echo needs strokeIds (list_strokes).`);
|
|
12849
|
+
const unknown = op.strokeIds.filter((id) => !known.has(id));
|
|
12850
|
+
if (unknown.length) throw new PaintEditorError(`${at}: unknown stroke ${unknown.map((u) => `"${u}"`).join(", ")}. Call list_strokes for the ids.`);
|
|
12851
|
+
if (op.color !== void 0 && !HEX4.test(op.color)) throw new PaintEditorError(`${at}: color "${op.color}" is not #rrggbb.`);
|
|
12852
|
+
if (op.repeat !== void 0 && !(Number.isInteger(op.repeat) && op.repeat >= 1 && op.repeat <= 50)) throw new PaintEditorError(`${at}: repeat must be a whole number from 1 to 50.`);
|
|
12853
|
+
for (const id of op.strokeIds) {
|
|
12854
|
+
const stroke = controller.getStrokes().find((s) => s.id === id);
|
|
12855
|
+
const refusal = suggestionBrushRefusal(stroke.brush);
|
|
12856
|
+
if (refusal) throw new PaintEditorError(`${at}: stroke "${id}": ${refusal}`);
|
|
12857
|
+
points += stroke.points.length * (op.repeat ?? 1);
|
|
12858
|
+
}
|
|
12859
|
+
} else {
|
|
12860
|
+
throw new PaintEditorError(`${at}: unknown kind "${op.kind}"; use stroke, wash or echo.`);
|
|
12861
|
+
}
|
|
12862
|
+
});
|
|
12863
|
+
if (points > maxPoints) throw new PaintEditorError(`The proposal paints ${points} points; the most is ${maxPoints}. Suggest less, or echo fewer strokes.`);
|
|
12864
|
+
};
|
|
12865
|
+
const paint = (ops, layerId) => {
|
|
12866
|
+
const before = strokeIds();
|
|
12867
|
+
for (const op of ops) {
|
|
12868
|
+
if (op.kind === "stroke") {
|
|
12869
|
+
controller.paintStroke({
|
|
12870
|
+
layerId,
|
|
12871
|
+
points: op.points,
|
|
12872
|
+
...op.brushId ? { brushId: op.brushId } : {},
|
|
12873
|
+
...op.color ? { color: op.color } : {},
|
|
12874
|
+
...op.size ? { brush: { tip: { size: op.size } } } : {},
|
|
12875
|
+
stencils: false
|
|
12876
|
+
});
|
|
12877
|
+
} else if (op.kind === "wash") {
|
|
12878
|
+
controller.fillLayer({ layerId, color: op.color, opacity: op.opacity ?? 0.35, grain: op.grain ?? 0.4 });
|
|
12879
|
+
} else {
|
|
12880
|
+
const { kind: _kind, strokeIds: ids, color, repeat, ...variation } = op;
|
|
12881
|
+
controller.replayStrokes({ strokeIds: ids, layerId, ...color ? { color } : {}, ...repeat ? { repeat } : {}, ...variation });
|
|
12882
|
+
}
|
|
12883
|
+
}
|
|
12884
|
+
return controller.getStrokes().map((s) => s.id).filter((id) => !before.has(id));
|
|
12885
|
+
};
|
|
12886
|
+
const missing = (id) => {
|
|
12887
|
+
const p = state.proposals.find((x) => x.id === id);
|
|
12888
|
+
if (!p) return [];
|
|
12889
|
+
const known = strokeIds();
|
|
12890
|
+
return p.anchors.filter((a) => !known.has(a));
|
|
12891
|
+
};
|
|
12892
|
+
const placementOf = (input) => ({
|
|
12893
|
+
place: input.place ?? "top",
|
|
12894
|
+
...input.layerId ? { layerId: input.layerId } : {},
|
|
12895
|
+
...input.layerName ? { layerName: input.layerName } : {}
|
|
12896
|
+
});
|
|
12897
|
+
const topLayerId = () => controller.listLayers().find((l) => l.parentId === null)?.id;
|
|
12898
|
+
return {
|
|
12899
|
+
getState: () => state,
|
|
12900
|
+
subscribe(listener) {
|
|
12901
|
+
listeners.add(listener);
|
|
12902
|
+
return () => {
|
|
12903
|
+
listeners.delete(listener);
|
|
12904
|
+
};
|
|
12905
|
+
},
|
|
12906
|
+
propose(input) {
|
|
12907
|
+
validate(input);
|
|
12908
|
+
const placement = placementOf(input);
|
|
12909
|
+
const preview = controller.previewPaint(
|
|
12910
|
+
(layerId) => {
|
|
12911
|
+
paint(input.ops, layerId);
|
|
12912
|
+
},
|
|
12913
|
+
placement.place === "bottom" ? { bottom: true } : { aboveId: placement.place === "above" ? placement.layerId : topLayerId() }
|
|
12914
|
+
);
|
|
12915
|
+
const proposal = {
|
|
12916
|
+
id: makeId(),
|
|
12917
|
+
title: input.title.trim().slice(0, 120),
|
|
12918
|
+
...input.rationale ? { rationale: input.rationale } : {},
|
|
12919
|
+
origin: input.origin ?? options.origin ?? "agent:anonymous",
|
|
12920
|
+
ops: input.ops.map((op) => ({ ...op })),
|
|
12921
|
+
placement,
|
|
12922
|
+
bounds: preview.bounds,
|
|
12923
|
+
ghost: preview.composite,
|
|
12924
|
+
anchors: [...new Set(input.ops.flatMap((op) => op.kind === "echo" ? op.strokeIds : []))],
|
|
12925
|
+
createdAt: now()
|
|
12926
|
+
};
|
|
12927
|
+
set([...state.proposals, proposal]);
|
|
12928
|
+
return proposal;
|
|
12929
|
+
},
|
|
12930
|
+
accept(id) {
|
|
12931
|
+
const p = find(id);
|
|
12932
|
+
const gone = missing(id);
|
|
12933
|
+
if (gone.length) throw new PaintEditorError(`Paint proposal "${p.title}" echoes strokes that are no longer on the canvas (${gone.join(", ")}); dismiss it or propose it again.`);
|
|
12934
|
+
const placement = p.placement;
|
|
12935
|
+
if (placement.place === "above" && !controller.listLayers().some((l) => l.id === placement.layerId)) {
|
|
12936
|
+
throw new PaintEditorError(`Paint proposal "${p.title}" goes above layer "${placement.layerId}", which is gone; dismiss it or propose it again.`);
|
|
12937
|
+
}
|
|
12938
|
+
const active = controller.getState().document.activeLayerId;
|
|
12939
|
+
const result = controller.withOrigin(
|
|
12940
|
+
p.origin,
|
|
12941
|
+
() => controller.group(p.title, () => {
|
|
12942
|
+
const layer = controller.addLayer({ name: placement.layerName ?? p.title, ...placement.place === "above" ? { aboveId: placement.layerId } : placement.place === "top" ? { aboveId: topLayerId() } : {} });
|
|
12943
|
+
if (placement.place === "bottom") controller.moveLayer(layer.id, 0, null);
|
|
12944
|
+
return { layerId: layer.id, strokeIds: paint(p.ops, layer.id) };
|
|
12945
|
+
})
|
|
12946
|
+
);
|
|
12947
|
+
if (active && controller.listLayers().some((l) => l.id === active)) controller.setActiveLayer(active);
|
|
12948
|
+
set(state.proposals.filter((x) => x.id !== id));
|
|
12949
|
+
return { proposalId: id, layerId: result.layerId, strokeIds: result.strokeIds, label: p.title };
|
|
12950
|
+
},
|
|
12951
|
+
reject(id) {
|
|
12952
|
+
const p = find(id);
|
|
12953
|
+
set(state.proposals.filter((x) => x.id !== id));
|
|
12954
|
+
return p;
|
|
12955
|
+
},
|
|
12956
|
+
clear(origin) {
|
|
12957
|
+
const dropped = state.proposals.filter((p) => !origin || p.origin === origin);
|
|
12958
|
+
if (dropped.length) set(state.proposals.filter((p) => !dropped.includes(p)));
|
|
12959
|
+
return dropped.map((p) => p.id);
|
|
12960
|
+
},
|
|
12961
|
+
get: (id) => state.proposals.find((p) => p.id === id),
|
|
12962
|
+
missing
|
|
12963
|
+
};
|
|
12964
|
+
}
|
|
12965
|
+
function paintProposalGhost(proposal) {
|
|
12966
|
+
const b = proposal.bounds;
|
|
12967
|
+
if (!b || !b.width || !b.height || proposal.ghost.length !== b.width * b.height * 4) return null;
|
|
12968
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height, data: proposal.ghost };
|
|
12969
|
+
}
|
|
12970
|
+
|
|
12971
|
+
// src/paint-editor/collaboration-tools.ts
|
|
12972
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
12973
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
12974
|
+
var proposalIdArg = {
|
|
12975
|
+
type: "object",
|
|
12976
|
+
properties: { proposalId: { type: "string", minLength: 1, description: "Id of a pending paint proposal, from propose_paint or list_paint_proposals." } },
|
|
12977
|
+
required: ["proposalId"],
|
|
12978
|
+
additionalProperties: false
|
|
12979
|
+
};
|
|
12980
|
+
var hex = { type: "string", pattern: "^#[0-9a-fA-F]{6}$" };
|
|
12981
|
+
var opSchema = {
|
|
12982
|
+
type: "object",
|
|
12983
|
+
description: `One thing to paint. kind "stroke": a stroke along points with a dry brush. kind "wash": an even wash over the suggestion's layer (a ground, a glaze). kind "echo": strokes already on the canvas painted again, moved, turned, scaled or mirrored (grass along a horizon, rain, hatching).`,
|
|
12984
|
+
properties: {
|
|
12985
|
+
kind: { type: "string", enum: ["stroke", "wash", "echo"], description: "What this op paints (see above)." },
|
|
12986
|
+
points: {
|
|
12987
|
+
type: "array",
|
|
12988
|
+
minItems: 1,
|
|
12989
|
+
description: "stroke: pen samples in document pixels, in drawing order.",
|
|
12990
|
+
items: {
|
|
12991
|
+
type: "object",
|
|
12992
|
+
properties: {
|
|
12993
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
12994
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
12995
|
+
pressure: { type: "number", minimum: 0, maximum: 1, description: "Pen pressure 0..1 (default 0.5)." }
|
|
12996
|
+
},
|
|
12997
|
+
required: ["x", "y"],
|
|
12998
|
+
additionalProperties: false
|
|
12999
|
+
}
|
|
13000
|
+
},
|
|
13001
|
+
brushId: { type: "string", description: "stroke: a dry brush id from list_brush_presets (dab, particle, bristle or mixer without pickup). Default the editor brush." },
|
|
13002
|
+
size: { type: "number", exclusiveMinimum: 0, description: "stroke: brush size in document pixels. Default the brush's." },
|
|
13003
|
+
color: { ...hex, description: "stroke and echo: #rrggbb (stroke default the editor colour; echo default each stroke's colour). wash: required." },
|
|
13004
|
+
opacity: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "wash: 0..1 (default 0.35)." },
|
|
13005
|
+
grain: { type: "number", minimum: 0, maximum: 1, description: "wash: how much it catches the paper grain, 0..1 (default 0.4)." },
|
|
13006
|
+
strokeIds: { type: "array", minItems: 1, items: { type: "string" }, description: "echo: ids of strokes on the canvas (list_strokes)." },
|
|
13007
|
+
dx: { type: "number", description: "echo: move right by this many document pixels (default 0)." },
|
|
13008
|
+
dy: { type: "number", description: "echo: move down by this many document pixels (default 0)." },
|
|
13009
|
+
rotate: { type: "number", description: "echo: turn this many degrees clockwise about the strokes' centre (default 0)." },
|
|
13010
|
+
scale: { type: "number", exclusiveMinimum: 0, description: "echo: scale about the centre; brush size scales too (default 1)." },
|
|
13011
|
+
flipX: { type: "boolean", description: "echo: mirror horizontally about the centre (default false)." },
|
|
13012
|
+
repeat: { type: "integer", minimum: 1, maximum: 50, description: "echo: repeat the move this many times, each on top of the last (default 1)." }
|
|
13013
|
+
},
|
|
13014
|
+
required: ["kind"],
|
|
13015
|
+
additionalProperties: false
|
|
13016
|
+
};
|
|
13017
|
+
function createPaintProposalToolProvider(proposals, options = {}) {
|
|
13018
|
+
const readOnlyReason = () => {
|
|
13019
|
+
const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
|
|
13020
|
+
return ro ? typeof ro === "string" ? ro : "The painting is read-only: paint proposals can be made and rejected but not accepted." : null;
|
|
13021
|
+
};
|
|
13022
|
+
const describe = (p) => ({
|
|
13023
|
+
id: p.id,
|
|
13024
|
+
title: p.title,
|
|
13025
|
+
...p.rationale ? { rationale: p.rationale } : {},
|
|
13026
|
+
origin: p.origin,
|
|
13027
|
+
ops: p.ops.map((op) => op.kind),
|
|
13028
|
+
placement: p.placement,
|
|
13029
|
+
bounds: p.bounds,
|
|
13030
|
+
anchors: p.anchors,
|
|
13031
|
+
missing: proposals.missing(p.id),
|
|
13032
|
+
createdAt: new Date(p.createdAt).toISOString()
|
|
13033
|
+
});
|
|
13034
|
+
const proposeTool = {
|
|
13035
|
+
name: "propose_paint",
|
|
13036
|
+
title: "Propose paint",
|
|
13037
|
+
description: "Suggests paint without painting it: the user sees it as a ghost over the canvas and accepts or dismisses it, so use it for contributions they did not ask for. Accepted, it is painted on a new layer of its own (on top by default, at the bottom for a ground, or above a layer) as one undo step recorded as yours. Prefer what agents do well: a wash or ground, an echo of the user's strokes, a few deliberate strokes. Dry brushes only. Returns the proposal id, where the ghost sits (bounds, document pixels) and the strokes it builds on. Follow with accept_paint_proposal only when the user asked for it.",
|
|
13038
|
+
inputSchema: {
|
|
13039
|
+
type: "object",
|
|
13040
|
+
properties: {
|
|
13041
|
+
title: { type: "string", minLength: 1, maxLength: 120, description: `What the suggestion adds, in a few words, e.g. "Warm ground behind the figure". The undo step's name once accepted.` },
|
|
13042
|
+
rationale: { type: "string", description: "Why it fits what the user is painting, in one sentence. Shown beside the ghost." },
|
|
13043
|
+
ops: { type: "array", minItems: 1, items: opSchema, description: "What to paint, in order, onto the suggestion's layer." },
|
|
13044
|
+
place: { type: "string", enum: ["top", "bottom", "above"], description: "Where the new layer goes: top (default), bottom (behind everything: grounds and washes), or above layerId." },
|
|
13045
|
+
layerId: { type: "string", description: 'With place "above": the layer to put it above (list_layers).' },
|
|
13046
|
+
layerName: { type: "string", description: "The new layer's name (default the title)." }
|
|
13047
|
+
},
|
|
13048
|
+
required: ["title", "ops"],
|
|
13049
|
+
additionalProperties: false
|
|
13050
|
+
},
|
|
13051
|
+
annotations: { title: "Propose paint", ...WRITE6 },
|
|
13052
|
+
handler: (args, context) => {
|
|
13053
|
+
const origin = context.actor?.id ? `agent:${context.actor.id}` : options.fallbackOrigin;
|
|
13054
|
+
const proposal = proposals.propose({ ...args, ...origin ? { origin } : {} });
|
|
13055
|
+
const { id: _id, missing: _missing, createdAt: _createdAt, ...rest } = describe(proposal);
|
|
13056
|
+
return { proposalId: proposal.id, ...rest };
|
|
13057
|
+
}
|
|
13058
|
+
};
|
|
13059
|
+
const listTool = {
|
|
13060
|
+
name: "list_paint_proposals",
|
|
13061
|
+
title: "List paint proposals",
|
|
13062
|
+
description: "Lists the pending paint proposals (ghost paint not yet on the canvas): id, title, rationale, who proposed it, its op kinds, where it goes, where the ghost sits, the strokes it echoes and any of those since removed (missing: it can no longer be accepted), and whether accepting is allowed. Call before accept_paint_proposal or reject_paint_proposal.",
|
|
13063
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
13064
|
+
annotations: { title: "List paint proposals", ...READ7 },
|
|
13065
|
+
handler: () => ({ proposals: proposals.getState().proposals.map(describe), readOnly: readOnlyReason() })
|
|
13066
|
+
};
|
|
13067
|
+
const acceptTool = {
|
|
13068
|
+
name: "accept_paint_proposal",
|
|
13069
|
+
title: "Accept a paint proposal",
|
|
13070
|
+
description: "Paints a pending proposal on a new layer as one undo step, recorded as made by whoever proposed it, and drops it from the pending list; the user's active layer stays theirs. Refuses, changing nothing, when the painting is read-only or a stroke it echoes was removed. Returns the new layer id, the stroke ids painted and the undo step name.",
|
|
13071
|
+
inputSchema: proposalIdArg,
|
|
13072
|
+
annotations: { title: "Accept a paint proposal", ...WRITE6 },
|
|
13073
|
+
handler: (args) => {
|
|
13074
|
+
const reason = readOnlyReason();
|
|
13075
|
+
if (reason) throw new Error(reason);
|
|
13076
|
+
return proposals.accept(args.proposalId);
|
|
13077
|
+
}
|
|
13078
|
+
};
|
|
13079
|
+
const rejectTool = {
|
|
13080
|
+
name: "reject_paint_proposal",
|
|
13081
|
+
title: "Reject a paint proposal",
|
|
13082
|
+
description: "Dismisses a pending paint proposal: its ghost disappears and the painting and its history are unchanged. Use it to withdraw a suggestion that no longer fits. Returns the dismissed proposal id and title.",
|
|
13083
|
+
inputSchema: proposalIdArg,
|
|
13084
|
+
annotations: { title: "Reject a paint proposal", ...WRITE6 },
|
|
13085
|
+
handler: (args) => {
|
|
13086
|
+
const p = proposals.reject(args.proposalId);
|
|
13087
|
+
return { proposalId: p.id, title: p.title, rejected: true };
|
|
13088
|
+
}
|
|
13089
|
+
};
|
|
13090
|
+
return {
|
|
13091
|
+
id: options.id ?? "paint-proposals",
|
|
13092
|
+
...options.namespace !== void 0 ? { namespace: options.namespace } : {},
|
|
13093
|
+
title: "Paint proposals",
|
|
13094
|
+
description: "Suggest paint as a ghost the user accepts or dismisses, and accept or reject pending suggestions.",
|
|
13095
|
+
skill: "pixerate-paint-editor",
|
|
13096
|
+
tools: [proposeTool, listTool, acceptTool, rejectTool]
|
|
13097
|
+
};
|
|
13098
|
+
}
|
|
12408
13099
|
export {
|
|
12409
13100
|
BACKLIGHT_SPILL,
|
|
12410
13101
|
BLEND_MENU_GROUPS,
|
|
@@ -12429,6 +13120,7 @@ export {
|
|
|
12429
13120
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
12430
13121
|
DEFAULT_PAPER,
|
|
12431
13122
|
DEFAULT_PARTICLE,
|
|
13123
|
+
DEFAULT_SPRAY,
|
|
12432
13124
|
DEFAULT_STICKER,
|
|
12433
13125
|
DEFAULT_SWATCHES,
|
|
12434
13126
|
DEFAULT_VIEW,
|
|
@@ -12565,12 +13257,15 @@ export {
|
|
|
12565
13257
|
composeAffine,
|
|
12566
13258
|
contactSheet,
|
|
12567
13259
|
createPaintBrushLibraryToolProvider,
|
|
13260
|
+
createPaintChangeFeed,
|
|
12568
13261
|
createPaintCompositor,
|
|
12569
13262
|
createPaintDocument,
|
|
12570
13263
|
createPaintEditorDocument,
|
|
12571
13264
|
createPaintEditorToolProvider,
|
|
12572
13265
|
createPaintEffectsToolProvider,
|
|
12573
13266
|
createPaintLayer,
|
|
13267
|
+
createPaintProposalToolProvider,
|
|
13268
|
+
createPaintProposals,
|
|
12574
13269
|
createPaintReplayTools,
|
|
12575
13270
|
createPaintStickerTools,
|
|
12576
13271
|
createPaintThickTools,
|
|
@@ -12655,6 +13350,7 @@ export {
|
|
|
12655
13350
|
padSampleRadius,
|
|
12656
13351
|
paintGradientMappingFor,
|
|
12657
13352
|
paintPlaybackPanel,
|
|
13353
|
+
paintProposalGhost,
|
|
12658
13354
|
paintSourceColor,
|
|
12659
13355
|
paintSourceCss,
|
|
12660
13356
|
paintToMultiColor,
|