@pixerate/editor 0.256.0 → 0.257.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/index.cjs +40 -38
- package/dist/agent/index.js +9 -7
- package/dist/animation/index.cjs +4 -2
- package/dist/animation/index.js +4 -2
- package/dist/animation-editor/index.cjs +63 -61
- package/dist/animation-editor/index.js +7 -5
- package/dist/brushes/colour.d.cts +11 -0
- package/dist/brushes/colour.d.ts +11 -0
- package/dist/brushes/index.cjs +51 -30
- package/dist/brushes/index.d.cts +1 -0
- package/dist/brushes/index.d.ts +1 -0
- package/dist/brushes/index.js +25 -4
- package/dist/brushes/resolve.d.cts +2 -0
- package/dist/brushes/resolve.d.ts +2 -0
- package/dist/brushes/schema.d.cts +1 -0
- package/dist/brushes/schema.d.ts +1 -0
- package/dist/brushes/tools.d.cts +7 -0
- package/dist/brushes/tools.d.ts +7 -0
- package/dist/brushes/types.d.cts +10 -2
- package/dist/brushes/types.d.ts +10 -2
- package/dist/brushes/ui.d.cts +14 -3
- package/dist/brushes/ui.d.ts +14 -3
- package/dist/{brushstrokeRender-HCOQUSWW.js → brushstrokeRender-JI4W6F2G.js} +2 -2
- package/dist/{brushstrokeRender-ZZOXI5O5.cjs → brushstrokeRender-MQ6PSDM5.cjs} +11 -11
- package/dist/canvas/index.cjs +20 -18
- package/dist/canvas/index.js +7 -5
- package/dist/chart/index.cjs +159 -157
- package/dist/chart/index.js +9 -7
- package/dist/{chunk-7272D3K2.js → chunk-2EUW2XZX.js} +27 -276
- package/dist/{chunk-L6Y7TSQ4.js → chunk-2VJ6OVJV.js} +1 -1
- package/dist/{chunk-6ZZKRH7R.js → chunk-47V3WXDG.js} +2 -2
- package/dist/{chunk-H7ET2ISK.cjs → chunk-4IMD2TLY.cjs} +52 -52
- package/dist/{chunk-VUKUTT2W.js → chunk-4M4E3N6X.js} +1 -1
- package/dist/{chunk-TBAVCLEQ.cjs → chunk-5Q5MLXAD.cjs} +23 -23
- package/dist/{chunk-MONXNVZK.js → chunk-6I23ZCGK.js} +5 -3
- package/dist/{chunk-GOBOCZZI.js → chunk-6NQPKNUZ.js} +1 -1
- package/dist/{chunk-WYGSR3IJ.js → chunk-AORZ5M5C.js} +1 -1
- package/dist/chunk-AVTPZO4Z.cjs +142 -0
- package/dist/{chunk-WKMUBATU.js → chunk-C2WEIEUB.js} +1 -1
- package/dist/{chunk-P3K2JCJA.js → chunk-D5BXSOSU.js} +3 -3
- package/dist/chunk-E2V5DA3W.js +36 -0
- package/dist/{chunk-WZOGDKCQ.js → chunk-F7CPSRLN.js} +0 -133
- package/dist/chunk-FJBQTZS4.js +17 -0
- package/dist/{chunk-SOSBOS5T.cjs → chunk-G23BTQA5.cjs} +22 -22
- package/dist/{chunk-EUIDTZNQ.cjs → chunk-I3Y2Q3PM.cjs} +59 -308
- package/dist/{chunk-YHUMGBZD.js → chunk-ICTPLVSR.js} +22 -4
- package/dist/{chunk-LRNUZKWV.cjs → chunk-IPUTIUGQ.cjs} +1 -1
- package/dist/{chunk-6GRUVGKU.js → chunk-ITURMC2M.js} +3 -3
- package/dist/{chunk-HYWRU5VX.js → chunk-IXQZHCOF.js} +1 -1
- package/dist/chunk-J5UQHWVL.js +142 -0
- package/dist/{chunk-YBAPT7NL.js → chunk-JC2SHA4A.js} +1 -23
- package/dist/chunk-LRTR4ASZ.cjs +17 -0
- package/dist/{chunk-HUAISCUX.cjs → chunk-M2HG5DJ2.cjs} +18 -18
- package/dist/{chunk-N76AMOKE.cjs → chunk-MDLEGVEN.cjs} +42 -42
- package/dist/chunk-MEGSQUQO.cjs +36 -0
- package/dist/chunk-MLLTWAXH.cjs +276 -0
- package/dist/{chunk-RHPSE2YR.js → chunk-MQ6MGIQM.js} +2 -2
- package/dist/{chunk-PGPFWC35.cjs → chunk-MQGQTKVW.cjs} +51 -33
- package/dist/{chunk-5YWWQ36H.cjs → chunk-O3ZVZSNJ.cjs} +9 -9
- package/dist/{chunk-GCTUOTCW.cjs → chunk-P3PZ63M3.cjs} +35 -35
- package/dist/{chunk-WRZ6C4TU.cjs → chunk-P6WYDBTH.cjs} +4 -4
- package/dist/{chunk-CJ6TBL2N.cjs → chunk-RHEMMGCN.cjs} +16 -16
- package/dist/{chunk-HIMGBZAM.cjs → chunk-RSIKDWOP.cjs} +201 -38
- package/dist/{chunk-SABJMH6M.cjs → chunk-RVGP2XLF.cjs} +2 -135
- package/dist/{chunk-JVRLGV3C.js → chunk-SMTUQWGY.js} +184 -21
- package/dist/{chunk-DMTEQ6SH.js → chunk-SSFMWJDK.js} +4 -4
- package/dist/{chunk-TFF2GTGN.cjs → chunk-SXQHWXEL.cjs} +2 -24
- package/dist/{chunk-IWEVV63O.cjs → chunk-UEYLANO3.cjs} +18 -16
- package/dist/chunk-VR6OGHU5.js +276 -0
- package/dist/{chunk-AF23QK5F.js → chunk-YQ4PQTBB.js} +1 -1
- package/dist/{chunk-B7V34GIH.cjs → chunk-YSGW7VV4.cjs} +88 -88
- package/dist/{chunk-3NS5C7FB.cjs → chunk-YYBW6DF2.cjs} +76 -76
- package/dist/{chunk-UFEDHACQ.js → chunk-Z7BX4TWO.js} +2 -2
- package/dist/{chunk-RUONPPMC.cjs → chunk-ZSE25IZA.cjs} +8 -8
- package/dist/color/index.cjs +7 -3
- package/dist/color/index.js +18 -14
- package/dist/export/index.cjs +8 -6
- package/dist/export/index.js +7 -5
- package/dist/extensions/index.cjs +11 -9
- package/dist/extensions/index.js +10 -8
- package/dist/gradient/index.cjs +33 -31
- package/dist/gradient/index.js +5 -3
- package/dist/image-editor/brush-presets.d.cts +4 -1
- package/dist/image-editor/brush-presets.d.ts +4 -1
- package/dist/image-editor/controller.d.cts +10 -5
- package/dist/image-editor/controller.d.ts +10 -5
- package/dist/image-editor/index.cjs +1090 -1073
- package/dist/image-editor/index.js +37 -20
- package/dist/index.cjs +17 -15
- package/dist/index.js +11 -9
- package/dist/paint-editor/brushes.d.cts +18 -1
- package/dist/paint-editor/brushes.d.ts +18 -1
- package/dist/paint-editor/controller.d.cts +29 -0
- package/dist/paint-editor/controller.d.ts +29 -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 +1443 -1280
- package/dist/paint-editor/index.js +1117 -954
- package/dist/paint-editor/mark-settings.d.cts +1 -1
- package/dist/paint-editor/mark-settings.d.ts +1 -1
- package/dist/paint-editor/procedural.d.cts +4 -2
- package/dist/paint-editor/procedural.d.ts +4 -2
- package/dist/paint-editor/stamp-tip.d.cts +45 -0
- package/dist/paint-editor/stamp-tip.d.ts +45 -0
- package/dist/paint-editor/stroke.d.cts +2 -0
- package/dist/paint-editor/stroke.d.ts +2 -0
- package/dist/stickers/index.cjs +4 -2
- package/dist/stickers/index.js +6 -4
- package/dist/typography/index.cjs +3 -2
- package/dist/typography/index.js +2 -1
- package/dist/vector/index.cjs +971 -969
- package/dist/vector/index.js +8 -6
- package/dist/video-editor/index.cjs +4 -2
- package/dist/video-editor/index.js +4 -2
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +21 -11
- package/skills/pixerate-brushes-design/SKILL.md +6 -2
- package/skills/pixerate-brushes-design/evals/canvas-chalk.json +8 -8
- package/skills/pixerate-canvas/SKILL.md +3 -2
- package/skills/pixerate-image-editor/SKILL.md +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +4 -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;
|
|
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);
|
|
2086
2216
|
}
|
|
2087
|
-
|
|
2088
|
-
const
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const size =
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2094
|
-
const
|
|
2095
|
-
const
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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
|
+
}
|
|
2099
2239
|
}
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
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
|
+
}
|
|
2106
2339
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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 };
|
|
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;
|
|
2131
2344
|
}
|
|
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
2345
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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();
|
|
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);
|
|
2172
2355
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2356
|
+
/**
|
|
2357
|
+
* Ribbon fill for one step: the band between each pair of joined particles, from where they
|
|
2358
|
+
* were drawn last step to where they are drawn now. Bands of one step, and of consecutive steps,
|
|
2359
|
+
* share edges exactly, so the fill has no seams; it is telescoped into the coverage
|
|
2360
|
+
* (`telescope`), so it does not build up at the joins.
|
|
2361
|
+
*/
|
|
2362
|
+
ribbon(copy, list, opacity, alpha, d, rects) {
|
|
2363
|
+
const s = this.s;
|
|
2364
|
+
if (!(s.ribbonFill > 0)) return;
|
|
2365
|
+
const live = (p) => !!p && p.alive && !Number.isNaN(p.px);
|
|
2366
|
+
let order;
|
|
2367
|
+
if (s.kind === "flow") order = list.filter(live);
|
|
2368
|
+
else order = this.orders[copy].map((i) => list[i]).filter(live);
|
|
2369
|
+
if (order.length < 2) return;
|
|
2370
|
+
const pairs = [];
|
|
2371
|
+
if (s.ribbonPairs === "first-last") pairs.push([order[0], order[order.length - 1]]);
|
|
2372
|
+
else for (let i = 0; i + 1 < order.length; i++) pairs.push([order[i], order[i + 1]]);
|
|
2373
|
+
const pts = this.pts;
|
|
2374
|
+
const ceil = Math.min(1, opacity * s.ribbonFill);
|
|
2375
|
+
for (const [a, b] of pairs) {
|
|
2376
|
+
pts.length = 0;
|
|
2377
|
+
pts.push(a.px, a.py, a.dx, a.dy, b.dx, b.dy, b.px, b.py);
|
|
2378
|
+
let x0 = Infinity;
|
|
2379
|
+
let y0 = Infinity;
|
|
2380
|
+
let x1 = -Infinity;
|
|
2381
|
+
let y1 = -Infinity;
|
|
2382
|
+
for (let i = 0; i < 8; i += 2) {
|
|
2383
|
+
x0 = Math.min(x0, pts[i]);
|
|
2384
|
+
x1 = Math.max(x1, pts[i]);
|
|
2385
|
+
y0 = Math.min(y0, pts[i + 1]);
|
|
2386
|
+
y1 = Math.max(y1, pts[i + 1]);
|
|
2387
|
+
}
|
|
2388
|
+
const rect = { x: Math.floor(x0), y: Math.floor(y0), width: Math.floor(x1) - Math.floor(x0) + 1, height: Math.floor(y1) - Math.floor(y0) + 1 };
|
|
2389
|
+
const area = this.scratch(rect.width * rect.height);
|
|
2390
|
+
addPolygonArea(pts, rect, area, Math.min(a.fade, b.fade) * alpha);
|
|
2391
|
+
if (this.telescope(rect, area)) this.lay(rect, area, ceil, d.pressure, (a.dx + b.dx) / 2, (a.dy + b.dy) / 2, rects);
|
|
2392
|
+
}
|
|
2176
2393
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
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;
|
|
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;
|
|
2406
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
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);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
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;
|
|
2426
|
+
}
|
|
2194
2427
|
}
|
|
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);
|
|
2428
|
+
return any;
|
|
2201
2429
|
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
+
);
|
|
2205
2454
|
}
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
const
|
|
2211
|
-
const
|
|
2212
|
-
const
|
|
2213
|
-
const
|
|
2214
|
-
const
|
|
2215
|
-
const
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
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;
|
|
2223
2489
|
}
|
|
2224
|
-
const pic = art?.(s.ref);
|
|
2225
|
-
if (!pic) return s;
|
|
2226
|
-
const scale = Math.max(pic.width, pic.height) / 2;
|
|
2227
|
-
if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
|
|
2228
2490
|
}
|
|
2229
|
-
|
|
2230
|
-
}
|
|
2491
|
+
};
|
|
2231
2492
|
|
|
2232
|
-
// src/paint-editor/
|
|
2233
|
-
var
|
|
2234
|
-
var
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
2241
|
-
this.copies = options.copies?.length ? options.copies : [IDENTITY_COPY];
|
|
2242
|
-
this.seed = options.seed ?? 0;
|
|
2243
|
-
const fine = { ...brush, tip: { ...brush.tip, spacing: Math.max(0.01, Math.min(0.25, STEP_PX / Math.max(1, brush.tip.size))) } };
|
|
2244
|
-
this.walkers = this.copies.map((c, i) => new PenDabWalker(fine, (options.seed ?? 0) + i * 7919 | 0, c.linear));
|
|
2245
|
-
this.depth = options.grain && !options.grain.flat ? Math.min(1, Math.max(0, options.paperDepth * brush.grain)) : 0;
|
|
2246
|
-
}
|
|
2247
|
-
brush;
|
|
2248
|
-
options;
|
|
2249
|
-
coverage = new CoverageMap();
|
|
2250
|
-
dabCount = 0;
|
|
2251
|
-
smoother;
|
|
2252
|
-
copies;
|
|
2253
|
-
walkers;
|
|
2254
|
-
depth;
|
|
2255
|
-
buf = new Float32Array(0);
|
|
2256
|
-
field = new Float32Array(0);
|
|
2257
|
-
lastRaw = null;
|
|
2258
|
-
lastFed = null;
|
|
2259
|
-
seed;
|
|
2260
|
-
sampler;
|
|
2261
|
-
tipTable;
|
|
2262
|
-
/** The pen dab the marks being made belong to, and its gradient colour. */
|
|
2263
|
-
pen = null;
|
|
2264
|
-
penColor = { rgb: [0, 0, 0], alpha: 1 };
|
|
2265
|
-
push(points) {
|
|
2266
|
-
const rects = [];
|
|
2267
|
-
for (const raw of points) {
|
|
2268
|
-
this.lastRaw = raw;
|
|
2269
|
-
const p = this.smoother.push(raw);
|
|
2270
|
-
this.lastFed = p;
|
|
2271
|
-
this.copies.forEach((c, i) => {
|
|
2272
|
-
const dabs = [];
|
|
2273
|
-
this.walkers[i].push(c.map(p), dabs);
|
|
2274
|
-
for (const d of dabs) this.step(i, d, rects);
|
|
2275
|
-
});
|
|
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) };
|
|
2276
2501
|
}
|
|
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
2502
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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;
|
|
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).`);
|
|
2304
2506
|
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
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 };
|
|
2316
2531
|
}
|
|
2317
2532
|
/**
|
|
2318
|
-
*
|
|
2319
|
-
*
|
|
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.
|
|
2320
2536
|
*/
|
|
2321
|
-
|
|
2322
|
-
if (
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
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;
|
|
2345
2562
|
}
|
|
2346
|
-
|
|
2347
|
-
this.lay(rect, vals, Math.min(1, opacity), pressure, x, y, rects);
|
|
2563
|
+
);
|
|
2348
2564
|
}
|
|
2349
|
-
/**
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
const vals = this.buf.subarray(0, n);
|
|
2353
|
-
vals.fill(0);
|
|
2354
|
-
return vals;
|
|
2565
|
+
/** Forgets every load (a new provider). */
|
|
2566
|
+
clear() {
|
|
2567
|
+
this.entries.clear();
|
|
2355
2568
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2569
|
+
};
|
|
2570
|
+
function sampleArt(art, x, y, out) {
|
|
2571
|
+
const { width: w, height: h, data } = art;
|
|
2572
|
+
const fx = x - 0.5;
|
|
2573
|
+
const fy = y - 0.5;
|
|
2574
|
+
const x0 = Math.floor(fx);
|
|
2575
|
+
const y0 = Math.floor(fy);
|
|
2576
|
+
const tx = fx - x0;
|
|
2577
|
+
const ty = fy - y0;
|
|
2578
|
+
let r = 0;
|
|
2579
|
+
let g = 0;
|
|
2580
|
+
let b = 0;
|
|
2581
|
+
let a = 0;
|
|
2582
|
+
for (let k = 0; k < 4; k++) {
|
|
2583
|
+
const xx = x0 + (k & 1);
|
|
2584
|
+
const yy = y0 + (k >> 1);
|
|
2585
|
+
if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue;
|
|
2586
|
+
const wgt = (k & 1 ? tx : 1 - tx) * (k >> 1 ? ty : 1 - ty);
|
|
2587
|
+
const i = (yy * w + xx) * 4;
|
|
2588
|
+
const al = data[i + 3] * wgt;
|
|
2589
|
+
r += data[i] * al;
|
|
2590
|
+
g += data[i + 1] * al;
|
|
2591
|
+
b += data[i + 2] * al;
|
|
2592
|
+
a += al;
|
|
2380
2593
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
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;
|
|
2618
|
+
}
|
|
2387
2619
|
}
|
|
2388
|
-
|
|
2389
|
-
var FUZZY_AMPLITUDE = 0.12;
|
|
2390
|
-
var NOMINAL_MS_PER_PX = 2;
|
|
2391
|
-
var RIBBON_SUB = 4;
|
|
2392
|
-
function chainDamp(damp, k) {
|
|
2393
|
-
return Math.min(damp, 1 / (1 + 0.8 * Math.sqrt(Math.max(0, k))) ** 2);
|
|
2620
|
+
return s.stamp && STAMP_KINDS.includes(s.stamp) ? s.stamp : null;
|
|
2394
2621
|
}
|
|
2395
|
-
function
|
|
2396
|
-
const
|
|
2397
|
-
if (
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
for (let i = 1; i < pts.length; i += 2) {
|
|
2401
|
-
if (pts[i] < y0) y0 = pts[i];
|
|
2402
|
-
if (pts[i] > y1) y1 = pts[i];
|
|
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);
|
|
2403
2627
|
}
|
|
2404
|
-
const
|
|
2405
|
-
const
|
|
2406
|
-
const
|
|
2407
|
-
const
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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));
|
|
2429
2653
|
}
|
|
2430
2654
|
}
|
|
2655
|
+
medium.deposit(rect, cov, s.opacity, [r, g, b], null, field);
|
|
2656
|
+
return rect;
|
|
2431
2657
|
}
|
|
2432
|
-
function
|
|
2433
|
-
const
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
dots;
|
|
2458
|
-
/** Ribbon fill: the area each pixel has had so far this stroke (telescoped into the coverage). */
|
|
2459
|
-
filled = /* @__PURE__ */ new Map();
|
|
2460
|
-
pts = [];
|
|
2461
|
-
constructor(brush, options) {
|
|
2462
|
-
super(brush, options);
|
|
2463
|
-
this.s = brush.particle;
|
|
2464
|
-
this.sets = this.copies.map(() => []);
|
|
2465
|
-
this.orders = this.copies.map(() => []);
|
|
2466
|
-
this.dots = this.copies.map(() => 0);
|
|
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 };
|
|
2654
2807
|
}
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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;
|
|
2664
2820
|
}
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
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
|
-
}
|
|
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;
|
|
2702
2826
|
}
|
|
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
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2827
|
+
for (const k of ["x", "y"]) if (u[k] !== void 0) {
|
|
2828
|
+
if (!Number.isFinite(u[k])) throw new Error(`${k} must be a number of pixels.`);
|
|
2829
|
+
out[k] = u[k];
|
|
2830
|
+
}
|
|
2831
|
+
if (u.size !== void 0) {
|
|
2832
|
+
if (!(u.size >= 2 && u.size <= 2048)) throw new Error(`size ${u.size} is out of range; use 2..2048 px.`);
|
|
2833
|
+
out.size = u.size;
|
|
2834
|
+
}
|
|
2835
|
+
if (u.rotation !== void 0) {
|
|
2836
|
+
if (!Number.isFinite(u.rotation)) throw new Error("rotation must be degrees.");
|
|
2837
|
+
out.rotation = u.rotation;
|
|
2838
|
+
}
|
|
2839
|
+
if (u.color !== void 0) {
|
|
2840
|
+
if (!/^#[0-9a-f]{6}$/i.test(u.color)) throw new Error(`color ${JSON.stringify(u.color)} must be #rrggbb.`);
|
|
2841
|
+
out.color = u.color.toLowerCase();
|
|
2842
|
+
}
|
|
2843
|
+
if (u.opacity !== void 0) {
|
|
2844
|
+
if (!(u.opacity >= 0 && u.opacity <= 1)) throw new Error(`opacity ${u.opacity} is out of range; use 0..1.`);
|
|
2845
|
+
out.opacity = u.opacity;
|
|
2846
|
+
}
|
|
2847
|
+
return out;
|
|
2848
|
+
}
|
|
2849
|
+
function stickerSurface(stickers, base, dirty, art) {
|
|
2850
|
+
const only = base && dirty ? new Set(dirty.flatMap((r) => tilesInRect(r).map(({ tx, ty }) => tileKey(tx, ty)))) : null;
|
|
2851
|
+
const live = /* @__PURE__ */ new Map();
|
|
2852
|
+
let scratch = null;
|
|
2853
|
+
const target = {
|
|
2854
|
+
offset: { x: 0, y: 0 },
|
|
2855
|
+
lockTransparency: false,
|
|
2856
|
+
selection: null,
|
|
2857
|
+
readTile: (tx, ty) => live.get(tileKey(tx, ty)) ?? null,
|
|
2858
|
+
writeTile: (tx, ty) => {
|
|
2859
|
+
const k = tileKey(tx, ty);
|
|
2860
|
+
if (only && !only.has(k)) return (scratch ??= new Uint8ClampedArray(TILE * TILE * 4)).fill(0);
|
|
2861
|
+
let t = live.get(k);
|
|
2862
|
+
if (!t) live.set(k, t = new Uint8ClampedArray(TILE * TILE * 4));
|
|
2863
|
+
return t;
|
|
2736
2864
|
}
|
|
2737
|
-
|
|
2865
|
+
};
|
|
2866
|
+
const reaches = (s) => !only || tilesInRect(stickerRect(s)).some(({ tx, ty }) => only.has(tileKey(tx, ty)));
|
|
2867
|
+
renderStickers(stickers.filter(reaches), target, art);
|
|
2868
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
2869
|
+
if (only && base) {
|
|
2870
|
+
for (const [k, t] of base.tiles) if (!only.has(k)) tiles.set(k, t);
|
|
2738
2871
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
s;
|
|
2743
|
-
constructor(brush, options) {
|
|
2744
|
-
super(brush, options);
|
|
2745
|
-
this.s = brush.bristle;
|
|
2746
|
-
const s = this.s;
|
|
2747
|
-
this.tufts = this.copies.map(
|
|
2748
|
-
(_, c) => Array.from({ length: s.count }, (_2, i) => {
|
|
2749
|
-
const a = rand01(this.seed, i, 21 + c) * Math.PI * 2;
|
|
2750
|
-
const r = Math.sqrt(rand01(this.seed, i, 23 + c));
|
|
2751
|
-
return {
|
|
2752
|
-
u: Math.cos(a) * r,
|
|
2753
|
-
v: Math.sin(a) * r,
|
|
2754
|
-
x: 0,
|
|
2755
|
-
y: 0,
|
|
2756
|
-
paint: 1 - s.jitter * 0.4 * rand01(this.seed, i, 25 + c),
|
|
2757
|
-
thirst: 1 + s.jitter * (rand01(this.seed, i, 27 + c) * 2 - 1),
|
|
2758
|
-
w: 1 + s.jitter * 0.6 * (rand01(this.seed, i, 29 + c) - 0.5),
|
|
2759
|
-
started: false
|
|
2760
|
-
};
|
|
2761
|
-
})
|
|
2762
|
-
);
|
|
2872
|
+
for (const [k, d] of live) {
|
|
2873
|
+
const t = finishTile(d, 4, 0);
|
|
2874
|
+
if (t) tiles.set(k, t);
|
|
2763
2875
|
}
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
2770
|
-
const
|
|
2771
|
-
const
|
|
2772
|
-
const
|
|
2773
|
-
const
|
|
2774
|
-
const
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
const oy = -(lu * sa + lv * ca);
|
|
2782
|
-
const lag = bend * (1 - 0.5 * b.v);
|
|
2783
|
-
const wx = d.x + ox - tx * lag;
|
|
2784
|
-
const wy = d.y + oy - ty * lag;
|
|
2785
|
-
if (!b.started) {
|
|
2786
|
-
b.x = wx;
|
|
2787
|
-
b.y = wy;
|
|
2788
|
-
b.started = true;
|
|
2789
|
-
continue;
|
|
2790
|
-
}
|
|
2791
|
-
const nx = b.x + (wx - b.x) * follow;
|
|
2792
|
-
const ny = b.y + (wy - b.y) * follow;
|
|
2793
|
-
const paint = Math.max(0, b.paint);
|
|
2794
|
-
if (paint > 0.02) this.segment(b.x, b.y, nx, ny, lineR * b.w * (0.6 + 0.4 * d.pressure), d.alpha * Math.min(1, paint * 1.4), d.opacity, d.pressure, rects);
|
|
2795
|
-
b.paint -= dryRate * b.thirst * Math.hypot(nx - b.x, ny - b.y);
|
|
2796
|
-
b.x = nx;
|
|
2797
|
-
b.y = ny;
|
|
2876
|
+
return new TiledSurface(4, tiles, 0);
|
|
2877
|
+
}
|
|
2878
|
+
function stickerAt(stickers, x, y, art) {
|
|
2879
|
+
for (let i = stickers.length - 1; i >= 0; i--) {
|
|
2880
|
+
const s = stickers[i];
|
|
2881
|
+
const rad2 = s.size / 2;
|
|
2882
|
+
const a = s.rotation * Math.PI / 180;
|
|
2883
|
+
const dx = x - s.x;
|
|
2884
|
+
const dy = y - s.y;
|
|
2885
|
+
const u = (dx * Math.cos(a) + dy * Math.sin(a)) / rad2;
|
|
2886
|
+
const v = (-dx * Math.sin(a) + dy * Math.cos(a)) / rad2;
|
|
2887
|
+
if (Math.abs(u) > 1.05 || Math.abs(v) > 1.05) continue;
|
|
2888
|
+
if (s.size < 16) return s;
|
|
2889
|
+
const kind = stickerStamp(s);
|
|
2890
|
+
if (kind) {
|
|
2891
|
+
if (stampCoverage(kind, u, v, s.seed) > 0.25) return s;
|
|
2892
|
+
continue;
|
|
2798
2893
|
}
|
|
2894
|
+
const pic = art?.(s.ref);
|
|
2895
|
+
if (!pic) return s;
|
|
2896
|
+
const scale = Math.max(pic.width, pic.height) / 2;
|
|
2897
|
+
if (sampleArt(pic, u * scale + pic.width / 2, v * scale + pic.height / 2, [0, 0, 0, 0])[3] > 0.25) return s;
|
|
2799
2898
|
}
|
|
2800
|
-
|
|
2899
|
+
return null;
|
|
2900
|
+
}
|
|
2801
2901
|
|
|
2802
2902
|
// src/paint-editor/stroke.ts
|
|
2803
2903
|
function defaultTile(surface) {
|
|
@@ -2861,7 +2961,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2861
2961
|
effect
|
|
2862
2962
|
});
|
|
2863
2963
|
} else {
|
|
2864
|
-
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect };
|
|
2964
|
+
const opts = { grain: init.grain, paperDepth: init.paperDepth, seed: init.seed, copies: this.copies, paint: init.paint ?? null, effect, stampArt: init.stampArt };
|
|
2865
2965
|
this.dry = brush.engine === "particle" ? new ParticleStroke(brush, opts) : brush.engine === "bristle" ? new BristleStroke(brush, opts) : new DryStroke(brush, opts);
|
|
2866
2966
|
}
|
|
2867
2967
|
}
|
|
@@ -5831,6 +5931,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
5831
5931
|
void this.refreshBrushes().catch(() => void 0);
|
|
5832
5932
|
this.pendingBrushId = start.brush.id === wanted ? null : wanted;
|
|
5833
5933
|
this.color = options.color ? parseColor(options.color) : "#1f2937";
|
|
5934
|
+
this.colorScheme = options.colorScheme === "dark" ? "dark" : "light";
|
|
5834
5935
|
this.hand = options.hand === "left" ? "left" : "right";
|
|
5835
5936
|
this.encodeImage = options.encodeImage;
|
|
5836
5937
|
this.guides = defaultGuides(this.doc.canvas);
|
|
@@ -6002,6 +6103,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6002
6103
|
brush: this.brush,
|
|
6003
6104
|
brushes: this.library ??= this.brushEntries.map((e) => e.brush),
|
|
6004
6105
|
color: this.color,
|
|
6106
|
+
colorScheme: this.colorScheme,
|
|
6107
|
+
colorLocked: this.colorLocked,
|
|
6005
6108
|
secondaryColor: this.secondaryColor,
|
|
6006
6109
|
paint: this.gradient ?? { type: "color", color: this.color },
|
|
6007
6110
|
effectPaint: this.effectPaintPending ?? this.effectPaint,
|
|
@@ -7139,6 +7242,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7139
7242
|
if (changes.paint !== void 0 && changes.paint !== null) this.readStrokePaint(changes.paint);
|
|
7140
7243
|
const unchanged = !Object.keys(changes).length;
|
|
7141
7244
|
const next = normalizePaintBrush({ ...changes, id: base.id }, base);
|
|
7245
|
+
if (entry) this.takeBrushColour(entry.spec);
|
|
7142
7246
|
this.useBrush(next, unchanged ? baseSpec : brushSpecOf(next, baseSpec), unchanged && base.builtin === true);
|
|
7143
7247
|
return this.brush;
|
|
7144
7248
|
}
|
|
@@ -7155,6 +7259,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7155
7259
|
const base = copyJSON(entry ? entry.spec : this.brushSpec);
|
|
7156
7260
|
for (const k of unset) delete base[k];
|
|
7157
7261
|
const spec = this.readBrushPatch(base, patch, problems);
|
|
7262
|
+
if (entry || patch.colour?.default !== void 0) this.takeBrushColour(spec);
|
|
7158
7263
|
this.useBrush(unchanged && entry ? entry.brush : resolveBrush(spec), spec, unchanged && entry?.builtin === true);
|
|
7159
7264
|
return { brush: this.brush, spec: copyJSON(this.brushSpec), problems };
|
|
7160
7265
|
}
|
|
@@ -7204,7 +7309,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7204
7309
|
this.brushSpec = { ...copyJSON(spec), id: next.id, name: next.name };
|
|
7205
7310
|
this.pendingBrushId = null;
|
|
7206
7311
|
this.emit([]);
|
|
7207
|
-
|
|
7312
|
+
const refs = brushStickerRefs(next);
|
|
7313
|
+
if (refs.length) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
7208
7314
|
}
|
|
7209
7315
|
/**
|
|
7210
7316
|
* Saves the active brush (or `brush`, engine settings on top of it) into the brush library under
|
|
@@ -7249,9 +7355,54 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7249
7355
|
return renderBrushThumbnail(b, { paper: this.doc.paper, width: options.width, height: options.height, color: options.color ? parseColor(options.color) : this.color });
|
|
7250
7356
|
}
|
|
7251
7357
|
// --- Colour --------------------------------------------------------------------
|
|
7358
|
+
colorScheme;
|
|
7359
|
+
colorLocked = false;
|
|
7360
|
+
/** The brush default colour the paint colour came from (resolved again when the scheme changes), or `null`. */
|
|
7361
|
+
brushColour = null;
|
|
7362
|
+
/**
|
|
7363
|
+
* The host's colour scheme: a paint colour that came from a brush's light / dark default
|
|
7364
|
+
* resolves to the new side. Editor state, not an undo step.
|
|
7365
|
+
*/
|
|
7366
|
+
setColorScheme(scheme) {
|
|
7367
|
+
const next = scheme === "dark" ? "dark" : "light";
|
|
7368
|
+
if (next === this.colorScheme) return;
|
|
7369
|
+
this.colorScheme = next;
|
|
7370
|
+
const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
|
|
7371
|
+
if (hex2) this.color = hex2;
|
|
7372
|
+
this.emit([]);
|
|
7373
|
+
}
|
|
7374
|
+
/** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
|
|
7375
|
+
setColorLocked(locked) {
|
|
7376
|
+
if (this.colorLocked === locked) return;
|
|
7377
|
+
this.colorLocked = locked;
|
|
7378
|
+
this.emit([]);
|
|
7379
|
+
}
|
|
7380
|
+
/**
|
|
7381
|
+
* After the user chooses a brush: its solid or light / dark default colour becomes the paint
|
|
7382
|
+
* colour (resolved for the colour scheme), unless the colour is locked. Gradient defaults are the
|
|
7383
|
+
* brush's own paint already; effect defaults stay a suggestion.
|
|
7384
|
+
*/
|
|
7385
|
+
takeBrushColour(spec) {
|
|
7386
|
+
if (this.colorLocked) return;
|
|
7387
|
+
const d = spec.colour?.default;
|
|
7388
|
+
const hex2 = d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
7389
|
+
if (!hex2) return;
|
|
7390
|
+
this.color = hex2;
|
|
7391
|
+
this.gradient = null;
|
|
7392
|
+
this.effectPaint = this.effectPaintPending = null;
|
|
7393
|
+
this.paintEpoch++;
|
|
7394
|
+
this.brushColour = d;
|
|
7395
|
+
}
|
|
7396
|
+
/** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
|
|
7397
|
+
ownColour(brushId) {
|
|
7398
|
+
const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
|
|
7399
|
+
const d = spec?.colour?.own ? spec.colour.default : void 0;
|
|
7400
|
+
return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
7401
|
+
}
|
|
7252
7402
|
/** Sets the paint colour (and goes back to one colour from gradient paint). */
|
|
7253
7403
|
setColor(color) {
|
|
7254
7404
|
this.color = parseColor(color);
|
|
7405
|
+
this.brushColour = null;
|
|
7255
7406
|
this.gradient = null;
|
|
7256
7407
|
this.effectPaint = this.effectPaintPending = null;
|
|
7257
7408
|
this.paintEpoch++;
|
|
@@ -7435,7 +7586,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7435
7586
|
this.beginWetOp();
|
|
7436
7587
|
const world = this.wet.ensureLane(layer.id, brush.wet.medium);
|
|
7437
7588
|
const id = layer.id;
|
|
7438
|
-
const wetColor = init.color === void 0 ? this.color : parseColor(init.color);
|
|
7589
|
+
const wetColor = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7439
7590
|
const [r, g, b] = hexToRgb8(wetColor);
|
|
7440
7591
|
const wetGradient = this.strokeGradient(init, brush, wetColor);
|
|
7441
7592
|
const load = init.load ?? brushLoadForStroke(brush, this.mixerLoad, [r / 255, g / 255, b / 255]);
|
|
@@ -7463,7 +7614,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7463
7614
|
const dried = findLayer(this.doc.layers, layer.id).layer;
|
|
7464
7615
|
layer = dried;
|
|
7465
7616
|
}
|
|
7466
|
-
let color = init.color === void 0 ? this.color : parseColor(init.color);
|
|
7617
|
+
let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
7467
7618
|
const tracing = this.guides.tracing;
|
|
7468
7619
|
if (init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors) color = this.sampleTracingColor(init.start.x, init.start.y) ?? color;
|
|
7469
7620
|
const perspective = this.guides.perspective;
|
|
@@ -7502,6 +7653,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7502
7653
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
7503
7654
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
7504
7655
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get } : void 0,
|
|
7656
|
+
stampArt: this.stickerArt.get,
|
|
7505
7657
|
paint: effect ? null : this.strokeGradient(init, brush, color),
|
|
7506
7658
|
effect
|
|
7507
7659
|
});
|
|
@@ -11276,18 +11428,24 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11276
11428
|
add({
|
|
11277
11429
|
name: "set_color",
|
|
11278
11430
|
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.",
|
|
11431
|
+
description: "Sets the paint colour (and optionally the second colour) that strokes and fills use by default, and whether it is locked. Choosing a brush with a default colour (set_brush id, e.g. the highlighter's yellow) makes that the paint colour unless the colour is locked; a brush with colour.own always paints its own. Setting color goes back to one colour from gradient paint (set_paint) or effect paint (set_effect_paint). Editor state, not an undo step. Returns both colours and locked.",
|
|
11280
11432
|
inputSchema: {
|
|
11281
11433
|
type: "object",
|
|
11282
|
-
properties: {
|
|
11434
|
+
properties: {
|
|
11435
|
+
color: { ...COLOR3, description: "Paint colour #rrggbb." },
|
|
11436
|
+
secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
|
|
11437
|
+
locked: { type: "boolean", description: "Lock the paint colour: brushes chosen later keep it instead of taking their default colour (default: unchanged; the editor starts unlocked)." }
|
|
11438
|
+
},
|
|
11283
11439
|
additionalProperties: false
|
|
11284
11440
|
},
|
|
11285
11441
|
annotations: { title: "Set the paint colour", ...SET4 },
|
|
11286
|
-
handler: ({ color, secondaryColor }) => {
|
|
11442
|
+
handler: ({ color, secondaryColor, locked }) => {
|
|
11443
|
+
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
11287
11444
|
if (color !== void 0) c.setColor(color);
|
|
11288
11445
|
if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
|
|
11446
|
+
if (locked !== void 0) c.setColorLocked(locked);
|
|
11289
11447
|
const s = c.getState();
|
|
11290
|
-
return { color: s.color, secondaryColor: s.secondaryColor };
|
|
11448
|
+
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
|
|
11291
11449
|
}
|
|
11292
11450
|
});
|
|
11293
11451
|
add({
|
|
@@ -11369,7 +11527,10 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
11369
11527
|
await c.wetIdle();
|
|
11370
11528
|
const base = a.brushId ? c.getBrush(a.brushId) : c.getState().brush;
|
|
11371
11529
|
const brush = a.brush ? c.brushChanges(a.brush, a.brushId) : void 0;
|
|
11372
|
-
if (
|
|
11530
|
+
if (base) {
|
|
11531
|
+
const refs = brushStickerRefs(normalizePaintBrush({ ...brush ?? {}, id: base.id }, base));
|
|
11532
|
+
if (refs.length) await c.prepareStickers(refs);
|
|
11533
|
+
}
|
|
11373
11534
|
const stroke = { points: a.points, layerId: a.layerId, brushId: a.brushId, brush, color: a.color, paint: a.paint, seed: a.seed, snapToPerspective: a.snapToPerspective };
|
|
11374
11535
|
const r = await act({ kind: "stroke", stroke }, () => c.paintStroke(stroke));
|
|
11375
11536
|
return r.mixerLoad ? { ...r, mixerLoad: loadResult(r.mixerLoad) } : r;
|
|
@@ -11746,6 +11907,7 @@ function strokeSummary(s, withPoints, live) {
|
|
|
11746
11907
|
function createPaintBrushLibraryToolProvider(controller, options = {}) {
|
|
11747
11908
|
return createBrushLibraryToolProvider(controller.brushLibrary, {
|
|
11748
11909
|
profile: PAINT_PROFILE,
|
|
11910
|
+
stickers: () => controller.stickers.provider,
|
|
11749
11911
|
...options,
|
|
11750
11912
|
renderPreview: (brush, { width, height, color }) => {
|
|
11751
11913
|
const t = renderBrushThumbnail(resolveBrush(brush), { paper: controller.getState().document.paper, width, height, color: color.toLowerCase() });
|
|
@@ -12958,6 +13120,7 @@ export {
|
|
|
12958
13120
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
12959
13121
|
DEFAULT_PAPER,
|
|
12960
13122
|
DEFAULT_PARTICLE,
|
|
13123
|
+
DEFAULT_SPRAY,
|
|
12961
13124
|
DEFAULT_STICKER,
|
|
12962
13125
|
DEFAULT_SWATCHES,
|
|
12963
13126
|
DEFAULT_VIEW,
|