@pixerate/editor 0.256.0 → 0.258.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.d.cts +38 -0
- package/dist/paint-editor/collaboration.d.ts +38 -0
- package/dist/paint-editor/controller.d.cts +60 -0
- package/dist/paint-editor/controller.d.ts +60 -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 +1564 -1324
- package/dist/paint-editor/index.js +1198 -958
- 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 +4 -0
- package/dist/paint-editor/stroke.d.ts +4 -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 +5 -1
- 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,
|
|
@@ -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(hex2) {
|
|
1933
|
-
const n = parseInt(hex2.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
|
-
|
|
2206
|
+
this.orders[copy] = list.map((_, i) => i).sort((i, j) => list[i].phase - list[j].phase || i - j);
|
|
2034
2207
|
}
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2042
|
-
|
|
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;
|
|
2048
|
-
}
|
|
2049
|
-
init;
|
|
2050
|
-
dabCount = 0;
|
|
2051
|
-
stickers = [];
|
|
2052
|
-
/** Refs the spray picked but could not draw because their art had not loaded. */
|
|
2053
|
-
skipped = /* @__PURE__ */ new Set();
|
|
2054
|
-
smoother;
|
|
2055
|
-
walkers;
|
|
2056
|
-
copies;
|
|
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++;
|
|
2122
|
-
}
|
|
2123
|
-
sampler;
|
|
2124
|
-
/** A sticker's colour: the paint colour, or gradient paint at its dab (`dx`, `dy` from the dab's centre). */
|
|
2125
|
-
colorAt(d, dx, dy) {
|
|
2126
|
-
const effect = this.init.effect;
|
|
2127
|
-
if (effect) {
|
|
2128
|
-
const rgb = [0, 0, 0];
|
|
2129
|
-
const alpha = sampleEffectAt(effect, d.x + dx, d.y + dy, rgb, d);
|
|
2130
|
-
return { color: "#" + rgb.map((c) => Math.round(c * 255).toString(16).padStart(2, "0")).join(""), alpha };
|
|
2131
|
-
}
|
|
2132
|
-
const g = this.init.paint;
|
|
2133
|
-
if (!g) return { color: this.init.color, alpha: 1 };
|
|
2134
|
-
this.sampler ??= new PaintGradientSampler(g, this.init.seed);
|
|
2135
|
-
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);
|
|
2136
|
-
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 };
|
|
2137
|
-
}
|
|
2138
|
-
};
|
|
2139
|
-
function renderStickers(stickers, target, art) {
|
|
2140
|
-
const medium = new TileMedium(target, {});
|
|
2141
|
-
for (const s of stickers) drawSticker(medium, s, art);
|
|
2142
|
-
}
|
|
2143
|
-
function normalizeStickerUpdate(u) {
|
|
2144
|
-
const out = {};
|
|
2145
|
-
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).");
|
|
2146
|
-
if (u.stamp !== void 0) {
|
|
2147
|
-
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).`);
|
|
2148
|
-
out.ref = `editor:${u.stamp}`;
|
|
2149
|
-
out.stamp = u.stamp;
|
|
2150
|
-
}
|
|
2151
|
-
if (u.ref !== void 0) {
|
|
2152
|
-
out.ref = normalizeStickerRef(u.ref);
|
|
2153
|
-
const stamp = stickerStamp({ ref: out.ref });
|
|
2154
|
-
if (stamp) out.stamp = stamp;
|
|
2155
|
-
else out.stamp = void 0;
|
|
2156
|
-
}
|
|
2157
|
-
for (const k of ["x", "y"]) if (u[k] !== void 0) {
|
|
2158
|
-
if (!Number.isFinite(u[k])) throw new Error(`${k} must be a number of pixels.`);
|
|
2159
|
-
out[k] = u[k];
|
|
2160
|
-
}
|
|
2161
|
-
if (u.size !== void 0) {
|
|
2162
|
-
if (!(u.size >= 2 && u.size <= 2048)) throw new Error(`size ${u.size} is out of range; use 2..2048 px.`);
|
|
2163
|
-
out.size = u.size;
|
|
2164
|
-
}
|
|
2165
|
-
if (u.rotation !== void 0) {
|
|
2166
|
-
if (!Number.isFinite(u.rotation)) throw new Error("rotation must be degrees.");
|
|
2167
|
-
out.rotation = u.rotation;
|
|
2168
|
-
}
|
|
2169
|
-
if (u.color !== void 0) {
|
|
2170
|
-
if (!/^#[0-9a-f]{6}$/i.test(u.color)) throw new Error(`color ${JSON.stringify(u.color)} must be #rrggbb.`);
|
|
2171
|
-
out.color = u.color.toLowerCase();
|
|
2172
|
-
}
|
|
2173
|
-
if (u.opacity !== void 0) {
|
|
2174
|
-
if (!(u.opacity >= 0 && u.opacity <= 1)) throw new Error(`opacity ${u.opacity} is out of range; use 0..1.`);
|
|
2175
|
-
out.opacity = u.opacity;
|
|
2176
|
-
}
|
|
2177
|
-
return out;
|
|
2178
|
-
}
|
|
2179
|
-
function stickerSurface(stickers, base, dirty, art) {
|
|
2180
|
-
const only = base && dirty ? new Set(dirty.flatMap((r) => tilesInRect(r).map(({ tx, ty }) => tileKey(tx, ty)))) : null;
|
|
2181
|
-
const live = /* @__PURE__ */ new Map();
|
|
2182
|
-
let scratch = null;
|
|
2183
|
-
const target = {
|
|
2184
|
-
offset: { x: 0, y: 0 },
|
|
2185
|
-
lockTransparency: false,
|
|
2186
|
-
selection: null,
|
|
2187
|
-
readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? null,
|
|
2188
|
-
writeTile: (tx, ty) => {
|
|
2189
|
-
const k = tileKey(tx, ty);
|
|
2190
|
-
if (only && !only.has(k)) return (scratch ??= new Uint8ClampedArray(TILE * TILE * 4)).fill(0);
|
|
2191
|
-
let t = live.get(k);
|
|
2192
|
-
if (!t) live.set(k, t = new Uint8ClampedArray(TILE * TILE * 4));
|
|
2193
|
-
return t;
|
|
2194
|
-
}
|
|
2195
|
-
};
|
|
2196
|
-
const reaches = (s) => !only || tilesInRect(stickerRect(s)).some(({ tx, ty }) => only.has(tileKey(tx, ty)));
|
|
2197
|
-
renderStickers(stickers.filter(reaches), target, art);
|
|
2198
|
-
const tiles = /* @__PURE__ */ new Map();
|
|
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);
|
|
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);
|
|
2205
2216
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
const
|
|
2211
|
-
const
|
|
2212
|
-
const
|
|
2213
|
-
const
|
|
2214
|
-
const
|
|
2215
|
-
const
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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
|
+
}
|
|
2223
2239
|
}
|
|
2224
|
-
const
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
+
}
|
|
2339
|
+
}
|
|
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;
|
|
2276
2344
|
}
|
|
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
2345
|
}
|
|
2305
|
-
/**
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
const
|
|
2309
|
-
|
|
2310
|
-
const
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
rasterizeDab(dab, hardness, rect, vals);
|
|
2315
|
-
this.lay(rect, vals, dab.opacity, pressure, x, y, rects);
|
|
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);
|
|
2316
2355
|
}
|
|
2317
2356
|
/**
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
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.
|
|
2320
2361
|
*/
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
const
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
const
|
|
2333
|
-
const
|
|
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);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
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.
|
|
2398
|
+
*/
|
|
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
|
-
|
|
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;
|
|
2367
2489
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
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) };
|
|
2377
2501
|
}
|
|
2378
|
-
rects.push(rect);
|
|
2379
|
-
this.dabCount++;
|
|
2380
2502
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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;
|
|
2524
|
+
};
|
|
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 };
|
|
2531
|
+
}
|
|
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
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
/** Forgets every load (a new provider). */
|
|
2566
|
+
clear() {
|
|
2567
|
+
this.entries.clear();
|
|
2387
2568
|
}
|
|
2388
2569
|
};
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
const
|
|
2397
|
-
|
|
2398
|
-
let
|
|
2399
|
-
let
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
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;
|
|
2403
2593
|
}
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
}
|
|
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;
|
|
2429
2618
|
}
|
|
2430
2619
|
}
|
|
2620
|
+
return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
|
|
2431
2621
|
}
|
|
2432
|
-
function
|
|
2433
|
-
const
|
|
2434
|
-
|
|
2435
|
-
const
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
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);
|
|
2627
|
+
}
|
|
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));
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
|
|
2656
|
+
return rect;
|
|
2449
2657
|
}
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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];
|
|
2682
|
+
}
|
|
2467
2683
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
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;
|
|
2702
|
+
}
|
|
2703
|
+
out = at(lo);
|
|
2471
2704
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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
|
-
};
|
|
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;
|
|
2497
2718
|
}
|
|
2498
|
-
init
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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
|
+
});
|
|
2514
2742
|
}
|
|
2515
|
-
|
|
2743
|
+
return rects;
|
|
2516
2744
|
}
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
const
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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;
|
|
2525
2756
|
}
|
|
2526
|
-
|
|
2527
|
-
const
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
const size =
|
|
2531
|
-
const
|
|
2532
|
-
const
|
|
2533
|
-
const
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
const ny = Math.cos(d.direction);
|
|
2539
|
-
if (s.kind === "flow") {
|
|
2540
|
-
this.spawn += s.count / 8;
|
|
2541
|
-
while (this.spawn >= 1) {
|
|
2542
|
-
this.spawn -= 1;
|
|
2543
|
-
const i = this.dabCount + list.length * 31 + Math.floor(d.index);
|
|
2544
|
-
const a = rand01(this.seed, i, 11 + copy) * Math.PI * 2;
|
|
2545
|
-
const r = rand01(this.seed, i, 13 + copy) * s.spread * size * 0.5;
|
|
2546
|
-
list.push(this.newParticle(d.x + Math.cos(a) * r, d.y + Math.sin(a) * r, 0, 0, 1, a, i, copy));
|
|
2547
|
-
}
|
|
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;
|
|
2548
2769
|
}
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
p.vx = (p.vx + (d.x + p.ox - p.x) * k * p.k) * damp;
|
|
2556
|
-
p.vy = (p.vy + (d.y + p.oy - p.y) * k * p.k + g) * damp;
|
|
2557
|
-
p.x += p.vx;
|
|
2558
|
-
p.y += p.vy;
|
|
2559
|
-
break;
|
|
2560
|
-
}
|
|
2561
|
-
case "string":
|
|
2562
|
-
case "fuzzy": {
|
|
2563
|
-
const strand = p.strand;
|
|
2564
|
-
const dm = s.kind === "fuzzy" ? chainDamp(damp, k * p.k) : damp;
|
|
2565
|
-
let ax = d.x + p.ox;
|
|
2566
|
-
let ay = d.y + p.oy;
|
|
2567
|
-
for (const m of strand) {
|
|
2568
|
-
m.vx = (m.vx + (ax - m.x) * k * p.k + 0) * dm;
|
|
2569
|
-
m.vy = (m.vy + (ay - m.y) * k * p.k + g) * dm;
|
|
2570
|
-
m.x += m.vx;
|
|
2571
|
-
m.y += m.vy;
|
|
2572
|
-
ax = m.x;
|
|
2573
|
-
ay = m.y;
|
|
2574
|
-
}
|
|
2575
|
-
p.x = strand[strand.length - 1].x;
|
|
2576
|
-
p.y = strand[strand.length - 1].y;
|
|
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
|
-
}
|
|
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;
|
|
2648
2776
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
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 };
|
|
2653
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;
|
|
2654
2826
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
const textures = s.dotTextures;
|
|
2659
|
-
if (!textures.length) return this.mark(x, y, r, alpha, opacity, d.pressure, rects);
|
|
2660
|
-
const n = this.dots[copy]++;
|
|
2661
|
-
const pick = s.dotTextureOrder === "random" ? Math.min(textures.length - 1, Math.floor(rand01(this.seed, n, 31 + copy) * textures.length)) : n % textures.length;
|
|
2662
|
-
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;
|
|
2663
|
-
this.stamp(x, y, r, textures[pick], rotation, this.seed + n * 2654435761 >>> 0, alpha, opacity, d.pressure, rects);
|
|
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];
|
|
2664
2830
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
* share edges exactly, so the fill has no seams; it is telescoped into the coverage
|
|
2669
|
-
* (`telescope`), so it does not build up at the joins.
|
|
2670
|
-
*/
|
|
2671
|
-
ribbon(copy, list, opacity, alpha, d, rects) {
|
|
2672
|
-
const s = this.s;
|
|
2673
|
-
if (!(s.ribbonFill > 0)) return;
|
|
2674
|
-
const live = (p) => !!p && p.alive && !Number.isNaN(p.px);
|
|
2675
|
-
let order;
|
|
2676
|
-
if (s.kind === "flow") order = list.filter(live);
|
|
2677
|
-
else order = this.orders[copy].map((i) => list[i]).filter(live);
|
|
2678
|
-
if (order.length < 2) return;
|
|
2679
|
-
const pairs = [];
|
|
2680
|
-
if (s.ribbonPairs === "first-last") pairs.push([order[0], order[order.length - 1]]);
|
|
2681
|
-
else for (let i = 0; i + 1 < order.length; i++) pairs.push([order[i], order[i + 1]]);
|
|
2682
|
-
const pts = this.pts;
|
|
2683
|
-
const ceil = Math.min(1, opacity * s.ribbonFill);
|
|
2684
|
-
for (const [a, b] of pairs) {
|
|
2685
|
-
pts.length = 0;
|
|
2686
|
-
pts.push(a.px, a.py, a.dx, a.dy, b.dx, b.dy, b.px, b.py);
|
|
2687
|
-
let x0 = Infinity;
|
|
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);
|
|
2701
|
-
}
|
|
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;
|
|
2702
2834
|
}
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
t[ti] = after;
|
|
2733
|
-
area[k] = before < 1 ? (after - before) / (1 - before) : 0;
|
|
2734
|
-
any = true;
|
|
2735
|
-
}
|
|
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
|
}
|
|
@@ -2955,6 +3055,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2955
3055
|
if (filter) points = points.map(filter);
|
|
2956
3056
|
for (const p of points) this.points.push(p);
|
|
2957
3057
|
this.lastRaw = points[points.length - 1];
|
|
3058
|
+
this.host.sampled?.(this, points);
|
|
2958
3059
|
this.lightTwin?.push(points);
|
|
2959
3060
|
if (this.wetDriver) {
|
|
2960
3061
|
this.wetUpdate(this.wetDriver.push(points));
|
|
@@ -5754,6 +5855,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5754
5855
|
encodeImage;
|
|
5755
5856
|
listeners = /* @__PURE__ */ new Set();
|
|
5756
5857
|
commitListeners = /* @__PURE__ */ new Set();
|
|
5858
|
+
sampleListeners = /* @__PURE__ */ new Set();
|
|
5757
5859
|
/** Origins set by `withOrigin`, innermost last. */
|
|
5758
5860
|
origins = [];
|
|
5759
5861
|
/** `group` in progress: commits fold into one step, recorded when it ends. */
|
|
@@ -5831,6 +5933,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5831
5933
|
void this.refreshBrushes().catch(() => void 0);
|
|
5832
5934
|
this.pendingBrushId = start.brush.id === wanted ? null : wanted;
|
|
5833
5935
|
this.color = options.color ? parseColor(options.color) : "#1f2937";
|
|
5936
|
+
this.colorScheme = options.colorScheme === "dark" ? "dark" : "light";
|
|
5834
5937
|
this.hand = options.hand === "left" ? "left" : "right";
|
|
5835
5938
|
this.encodeImage = options.encodeImage;
|
|
5836
5939
|
this.guides = defaultGuides(this.doc.canvas);
|
|
@@ -5867,6 +5970,27 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5867
5970
|
this.listeners.add(listener);
|
|
5868
5971
|
return () => this.listeners.delete(listener);
|
|
5869
5972
|
}
|
|
5973
|
+
/**
|
|
5974
|
+
* Hears the user's live strokes as they are painted (`beginStroke` sessions, not strokes painted
|
|
5975
|
+
* from data): when the pen goes down, each batch of samples, and the end or cancel. For agents
|
|
5976
|
+
* that paint along in real time. Returns an unsubscribe function.
|
|
5977
|
+
*/
|
|
5978
|
+
onStrokeSample(listener) {
|
|
5979
|
+
this.sampleListeners.add(listener);
|
|
5980
|
+
return () => {
|
|
5981
|
+
this.sampleListeners.delete(listener);
|
|
5982
|
+
};
|
|
5983
|
+
}
|
|
5984
|
+
notifySample(phase, session, added = [], strokeId) {
|
|
5985
|
+
if (!this.sampleListeners.size || this.previewing) return;
|
|
5986
|
+
const sample = { phase, layerId: session.layerId, brush: session.init.brush, color: session.init.colorHex, points: session.points, added, ...strokeId !== void 0 ? { strokeId } : {} };
|
|
5987
|
+
for (const l of [...this.sampleListeners]) l(sample);
|
|
5988
|
+
}
|
|
5989
|
+
/** The stroke being painted now, or `null`: its layer, brush, colour and samples so far. */
|
|
5990
|
+
getLiveStroke() {
|
|
5991
|
+
const s = this.session;
|
|
5992
|
+
return s ? { layerId: s.layerId, brushId: s.init.brush.id, brushName: s.init.brush.name, color: s.init.colorHex, points: s.points } : null;
|
|
5993
|
+
}
|
|
5870
5994
|
/** Hears each undo step recorded, merged, undone or redone, with who made it. Returns an unsubscribe function. */
|
|
5871
5995
|
onCommit(listener) {
|
|
5872
5996
|
this.commitListeners.add(listener);
|
|
@@ -6002,6 +6126,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6002
6126
|
brush: this.brush,
|
|
6003
6127
|
brushes: this.library ??= this.brushEntries.map((e) => e.brush),
|
|
6004
6128
|
color: this.color,
|
|
6129
|
+
colorScheme: this.colorScheme,
|
|
6130
|
+
colorLocked: this.colorLocked,
|
|
6005
6131
|
secondaryColor: this.secondaryColor,
|
|
6006
6132
|
paint: this.gradient ?? { type: "color", color: this.color },
|
|
6007
6133
|
effectPaint: this.effectPaintPending ?? this.effectPaint,
|
|
@@ -7139,6 +7265,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7139
7265
|
if (changes.paint !== void 0 && changes.paint !== null) this.readStrokePaint(changes.paint);
|
|
7140
7266
|
const unchanged = !Object.keys(changes).length;
|
|
7141
7267
|
const next = normalizePaintBrush({ ...changes, id: base.id }, base);
|
|
7268
|
+
if (entry) this.takeBrushColour(entry.spec);
|
|
7142
7269
|
this.useBrush(next, unchanged ? baseSpec : brushSpecOf(next, baseSpec), unchanged && base.builtin === true);
|
|
7143
7270
|
return this.brush;
|
|
7144
7271
|
}
|
|
@@ -7155,6 +7282,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7155
7282
|
const base = copyJSON(entry ? entry.spec : this.brushSpec);
|
|
7156
7283
|
for (const k of unset) delete base[k];
|
|
7157
7284
|
const spec = this.readBrushPatch(base, patch, problems);
|
|
7285
|
+
if (entry || patch.colour?.default !== void 0) this.takeBrushColour(spec);
|
|
7158
7286
|
this.useBrush(unchanged && entry ? entry.brush : resolveBrush(spec), spec, unchanged && entry?.builtin === true);
|
|
7159
7287
|
return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
|
|
7160
7288
|
}
|
|
@@ -7204,7 +7332,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7204
7332
|
this.brushSpec = { ...copyJSON(spec), id: next.id, name: next.name };
|
|
7205
7333
|
this.pendingBrushId = null;
|
|
7206
7334
|
this.emit([]);
|
|
7207
|
-
|
|
7335
|
+
const refs = brushStickerRefs(next);
|
|
7336
|
+
if (refs.length) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
7208
7337
|
}
|
|
7209
7338
|
/**
|
|
7210
7339
|
* Saves the active brush (or `brush`, engine settings on top of it) into the brush library under
|
|
@@ -7249,9 +7378,54 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7249
7378
|
return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
|
|
7250
7379
|
}
|
|
7251
7380
|
// --- Colour --------------------------------------------------------------------
|
|
7381
|
+
colorScheme;
|
|
7382
|
+
colorLocked = false;
|
|
7383
|
+
/** The brush default colour the paint colour came from (resolved again when the scheme changes), or `null`. */
|
|
7384
|
+
brushColour = null;
|
|
7385
|
+
/**
|
|
7386
|
+
* The host's colour scheme: a paint colour that came from a brush's light / dark default
|
|
7387
|
+
* resolves to the new side. Editor state, not an undo step.
|
|
7388
|
+
*/
|
|
7389
|
+
setColorScheme(scheme) {
|
|
7390
|
+
const next = scheme === "dark" ? "dark" : "light";
|
|
7391
|
+
if (next === this.colorScheme) return;
|
|
7392
|
+
this.colorScheme = next;
|
|
7393
|
+
const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
|
|
7394
|
+
if (hex2) this.color = hex2;
|
|
7395
|
+
this.emit([]);
|
|
7396
|
+
}
|
|
7397
|
+
/** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
|
|
7398
|
+
setColorLocked(locked) {
|
|
7399
|
+
if (this.colorLocked === locked) return;
|
|
7400
|
+
this.colorLocked = locked;
|
|
7401
|
+
this.emit([]);
|
|
7402
|
+
}
|
|
7403
|
+
/**
|
|
7404
|
+
* After the user chooses a brush: its solid or light / dark default colour becomes the paint
|
|
7405
|
+
* colour (resolved for the colour scheme), unless the colour is locked. Gradient defaults are the
|
|
7406
|
+
* brush's own paint already; effect defaults stay a suggestion.
|
|
7407
|
+
*/
|
|
7408
|
+
takeBrushColour(spec) {
|
|
7409
|
+
if (this.colorLocked) return;
|
|
7410
|
+
const d = spec.colour?.default;
|
|
7411
|
+
const hex2 = d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
7412
|
+
if (!hex2) return;
|
|
7413
|
+
this.color = hex2;
|
|
7414
|
+
this.gradient = null;
|
|
7415
|
+
this.effectPaint = this.effectPaintPending = null;
|
|
7416
|
+
this.paintEpoch++;
|
|
7417
|
+
this.brushColour = d;
|
|
7418
|
+
}
|
|
7419
|
+
/** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
|
|
7420
|
+
ownColour(brushId) {
|
|
7421
|
+
const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
|
|
7422
|
+
const d = spec?.colour?.own ? spec.colour.default : void 0;
|
|
7423
|
+
return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
7424
|
+
}
|
|
7252
7425
|
/** Sets the paint colour (and goes back to one colour from gradient paint). */
|
|
7253
7426
|
setColor(color) {
|
|
7254
7427
|
this.color = parseColor(color);
|
|
7428
|
+
this.brushColour = null;
|
|
7255
7429
|
this.gradient = null;
|
|
7256
7430
|
this.effectPaint = this.effectPaintPending = null;
|
|
7257
7431
|
this.paintEpoch++;
|
|
@@ -7435,7 +7609,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7435
7609
|
this.beginWetOp();
|
|
7436
7610
|
const world = this.wet.ensureLane(layer.id, brush.wet.medium);
|
|
7437
7611
|
const id = layer.id;
|
|
7438
|
-
const wetColor = init.color === void 0 ? this.color : parseColor(init.color);
|
|
7612
|
+
const wetColor = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7439
7613
|
const [r, g, b] = hexToRgb8(wetColor);
|
|
7440
7614
|
const wetGradient = this.strokeGradient(init, brush, wetColor);
|
|
7441
7615
|
const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);
|
|
@@ -7463,7 +7637,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7463
7637
|
const dried = findLayer(this.doc.layers, layer.id).layer;
|
|
7464
7638
|
layer = dried;
|
|
7465
7639
|
}
|
|
7466
|
-
let color = init.color === void 0 ? this.color : parseColor(init.color);
|
|
7640
|
+
let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7467
7641
|
const tracing = this.guides.tracing;
|
|
7468
7642
|
if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
|
|
7469
7643
|
const perspective = this.guides.perspective;
|
|
@@ -7502,6 +7676,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7502
7676
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
7503
7677
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
7504
7678
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
|
|
7679
|
+
stampArt: this.stickerArt.get,
|
|
7505
7680
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
7506
7681
|
effect
|
|
7507
7682
|
});
|
|
@@ -7548,6 +7723,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7548
7723
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
7549
7724
|
this.emit(rects);
|
|
7550
7725
|
},
|
|
7726
|
+
sampled: (s, points) => this.notifySample("points", s, points),
|
|
7551
7727
|
commit: (s, surface, cancelled) => {
|
|
7552
7728
|
this.session = null;
|
|
7553
7729
|
this.live = null;
|
|
@@ -7567,6 +7743,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7567
7743
|
this.wet.syncDisplays();
|
|
7568
7744
|
}
|
|
7569
7745
|
this.emit();
|
|
7746
|
+
this.notifySample(cancelled ? "cancel" : "end", s, [], cancelled ? void 0 : null);
|
|
7570
7747
|
return;
|
|
7571
7748
|
}
|
|
7572
7749
|
const stroke = this.record(s);
|
|
@@ -7586,6 +7763,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7586
7763
|
const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
|
|
7587
7764
|
const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
|
|
7588
7765
|
this.commit(`${brush.name} stroke`, this.withWetEvents(withLiveStroke({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {}, ...light ? { light } : {} }, stroke)), wetStroke ? before : this.doc);
|
|
7766
|
+
this.notifySample("end", s, [], stroke.id);
|
|
7589
7767
|
}
|
|
7590
7768
|
};
|
|
7591
7769
|
let session;
|
|
@@ -7597,6 +7775,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7597
7775
|
}
|
|
7598
7776
|
this.session = session;
|
|
7599
7777
|
this.emit([]);
|
|
7778
|
+
this.notifySample("begin", session);
|
|
7600
7779
|
return session;
|
|
7601
7780
|
}
|
|
7602
7781
|
/** The current live stroke, if any. */
|
|
@@ -11276,18 +11455,24 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11276
11455
|
add({
|
|
11277
11456
|
name: "set_color",
|
|
11278
11457
|
title: "Set the paint colour",
|
|
11279
|
-
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.",
|
|
11458
|
+
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.",
|
|
11280
11459
|
inputSchema: {
|
|
11281
11460
|
type: "object",
|
|
11282
|
-
properties: {
|
|
11461
|
+
properties: {
|
|
11462
|
+
color: { ...COLOR3, description: "Paint colour #rrggbb." },
|
|
11463
|
+
secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
|
|
11464
|
+
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)." }
|
|
11465
|
+
},
|
|
11283
11466
|
additionalProperties: false
|
|
11284
11467
|
},
|
|
11285
11468
|
annotations: { title: "Set the paint colour", ...SET4 },
|
|
11286
|
-
handler: ({ color, secondaryColor }) => {
|
|
11469
|
+
handler: ({ color, secondaryColor, locked }) => {
|
|
11470
|
+
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
11287
11471
|
if (color !== void 0) c.setColor(color);
|
|
11288
11472
|
if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
|
|
11473
|
+
if (locked !== void 0) c.setColorLocked(locked);
|
|
11289
11474
|
const s = c.getState();
|
|
11290
|
-
return { color: s.color, secondaryColor: s.secondaryColor };
|
|
11475
|
+
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
|
|
11291
11476
|
}
|
|
11292
11477
|
});
|
|
11293
11478
|
add({
|
|
@@ -11369,12 +11554,32 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11369
11554
|
await c.wetIdle();
|
|
11370
11555
|
const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
|
|
11371
11556
|
const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
|
|
11372
|
-
if (
|
|
11557
|
+
if (base) {
|
|
11558
|
+
const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
|
|
11559
|
+
if (refs.length) await c.prepareStickers(refs);
|
|
11560
|
+
}
|
|
11373
11561
|
const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
|
|
11374
11562
|
const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
|
|
11375
11563
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
11376
11564
|
}
|
|
11377
11565
|
});
|
|
11566
|
+
add({
|
|
11567
|
+
name: "get_live_stroke",
|
|
11568
|
+
title: "Get the stroke being painted",
|
|
11569
|
+
description: "Reads the stroke the user is painting right now, if any: its layer, brush id and name, colour, and the pen samples so far (document pixels; at most the last `limit`). Returns live: false when no stroke is in progress. For agents that paint along with the user: hosts stream the same samples to them live (onStrokeSample); this is a snapshot.",
|
|
11570
|
+
inputSchema: {
|
|
11571
|
+
type: "object",
|
|
11572
|
+
properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
|
|
11573
|
+
additionalProperties: false
|
|
11574
|
+
},
|
|
11575
|
+
annotations: { title: "Get the stroke being painted", ...READ6 },
|
|
11576
|
+
handler: (args) => {
|
|
11577
|
+
const live = c.getLiveStroke();
|
|
11578
|
+
if (!live) return { live: false };
|
|
11579
|
+
const limit = args.limit ?? 500;
|
|
11580
|
+
return { live: true, layerId: live.layerId, brushId: live.brushId, brushName: live.brushName, color: live.color, total: live.points.length, points: live.points.slice(-limit) };
|
|
11581
|
+
}
|
|
11582
|
+
});
|
|
11378
11583
|
add({
|
|
11379
11584
|
name: "list_strokes",
|
|
11380
11585
|
title: "List recorded strokes",
|
|
@@ -11746,6 +11951,7 @@ function strokeSummary(s, withPoints, live) {
|
|
|
11746
11951
|
function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
11747
11952
|
return createBrushLibraryToolProvider(controller.brushLibrary, {
|
|
11748
11953
|
profile: PAINT_PROFILE,
|
|
11954
|
+
stickers: () => controller.stickers.provider,
|
|
11749
11955
|
...options,
|
|
11750
11956
|
renderPreview: (brush, { width, height, color }) => {
|
|
11751
11957
|
const t = renderBrushThumbnail(resolveBrush(brush), { paper: controller.getState().document.paper, width, height, color: color.toLowerCase() });
|
|
@@ -12805,6 +13011,37 @@ function paintProposalGhost(proposal) {
|
|
|
12805
13011
|
if (!b || !b.width || !b.height || proposal.ghost.length !== b.width * b.height * 4) return null;
|
|
12806
13012
|
return { x: b.x, y: b.y, width: b.width, height: b.height, data: proposal.ghost };
|
|
12807
13013
|
}
|
|
13014
|
+
function createPaintAgentStrokes() {
|
|
13015
|
+
let state = { strokes: [] };
|
|
13016
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
13017
|
+
const set = (strokes) => {
|
|
13018
|
+
state = { strokes };
|
|
13019
|
+
listeners.forEach((l) => l(state));
|
|
13020
|
+
};
|
|
13021
|
+
return {
|
|
13022
|
+
getState: () => state,
|
|
13023
|
+
subscribe(listener) {
|
|
13024
|
+
listeners.add(listener);
|
|
13025
|
+
return () => {
|
|
13026
|
+
listeners.delete(listener);
|
|
13027
|
+
};
|
|
13028
|
+
},
|
|
13029
|
+
set(stroke) {
|
|
13030
|
+
const i = state.strokes.findIndex((s) => s.id === stroke.id);
|
|
13031
|
+
set(i < 0 ? [...state.strokes, stroke] : state.strokes.map((s, j) => j === i ? stroke : s));
|
|
13032
|
+
},
|
|
13033
|
+
remove(id) {
|
|
13034
|
+
if (state.strokes.some((s) => s.id === id)) set(state.strokes.filter((s) => s.id !== id));
|
|
13035
|
+
},
|
|
13036
|
+
clear(origin) {
|
|
13037
|
+
const keep = origin ? state.strokes.filter((s) => s.origin !== origin) : [];
|
|
13038
|
+
if (keep.length !== state.strokes.length) set(keep);
|
|
13039
|
+
}
|
|
13040
|
+
};
|
|
13041
|
+
}
|
|
13042
|
+
function paintAgentStrokePoints(stroke) {
|
|
13043
|
+
return stroke.points.map((p) => `${Math.round(p.x * 10) / 10},${Math.round(p.y * 10) / 10}`).join(" ");
|
|
13044
|
+
}
|
|
12808
13045
|
|
|
12809
13046
|
// src/paint-editor/collaboration-tools.ts
|
|
12810
13047
|
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -12958,6 +13195,7 @@ export {
|
|
|
12958
13195
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
12959
13196
|
DEFAULT_PAPER,
|
|
12960
13197
|
DEFAULT_PARTICLE,
|
|
13198
|
+
DEFAULT_SPRAY,
|
|
12961
13199
|
DEFAULT_STICKER,
|
|
12962
13200
|
DEFAULT_SWATCHES,
|
|
12963
13201
|
DEFAULT_VIEW,
|
|
@@ -13093,6 +13331,7 @@ export {
|
|
|
13093
13331
|
colorHarmony2 as colorHarmony,
|
|
13094
13332
|
composeAffine,
|
|
13095
13333
|
contactSheet,
|
|
13334
|
+
createPaintAgentStrokes,
|
|
13096
13335
|
createPaintBrushLibraryToolProvider,
|
|
13097
13336
|
createPaintChangeFeed,
|
|
13098
13337
|
createPaintCompositor,
|
|
@@ -13185,6 +13424,7 @@ export {
|
|
|
13185
13424
|
normalizeWetParams,
|
|
13186
13425
|
overPixels,
|
|
13187
13426
|
padSampleRadius,
|
|
13427
|
+
paintAgentStrokePoints,
|
|
13188
13428
|
paintGradientMappingFor,
|
|
13189
13429
|
paintPlaybackPanel,
|
|
13190
13430
|
paintProposalGhost,
|