@pixerate/editor 0.276.0 → 0.278.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation-editor/index.cjs +59 -59
- package/dist/animation-editor/index.js +3 -3
- package/dist/brushes/index.cjs +69 -67
- package/dist/brushes/index.js +6 -4
- package/dist/brushes/patch.d.cts +4 -4
- package/dist/brushes/patch.d.ts +4 -4
- package/dist/brushes/profiles.d.cts +2 -0
- package/dist/brushes/profiles.d.ts +2 -0
- package/dist/brushes/schema.d.cts +1 -0
- package/dist/brushes/schema.d.ts +1 -0
- package/dist/brushes/types.d.cts +10 -0
- package/dist/brushes/types.d.ts +10 -0
- package/dist/{brushstrokeRender-I6MYFCOU.cjs → brushstrokeRender-7HOMKZNO.cjs} +15 -15
- package/dist/{brushstrokeRender-FK3ASUUJ.js → brushstrokeRender-EIK3625T.js} +3 -3
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-U26LUXCJ.cjs → chunk-4NZTK7LM.cjs} +35 -35
- package/dist/{chunk-CVBTJVRN.js → chunk-DWPFGWW5.js} +1 -1
- package/dist/{chunk-XMSH66VU.js → chunk-FNIWHSLI.js} +1 -1
- package/dist/{chunk-QNNZA3V5.cjs → chunk-H3EME3S5.cjs} +2 -2
- package/dist/{chunk-4EM5NGCL.js → chunk-HHX7ST6Z.js} +1 -1
- package/dist/{chunk-Q33V5PEF.cjs → chunk-HRUK5TFW.cjs} +73 -12
- package/dist/{chunk-5HR5CSLC.cjs → chunk-JVFGK3DK.cjs} +69 -58
- package/dist/{chunk-JNMNM3SL.js → chunk-LZE2QZCT.js} +1 -1
- package/dist/{chunk-OS6OG7DN.js → chunk-N4MCWQZ7.js} +1 -1
- package/dist/{chunk-IJ6NSWKM.js → chunk-NJLPW4MM.js} +17 -6
- package/dist/{chunk-OY24XRYF.cjs → chunk-PEQWARCP.cjs} +12 -12
- package/dist/{chunk-XVPQNX7O.js → chunk-PPDUCZZV.js} +66 -5
- package/dist/{chunk-X7XBEX3O.cjs → chunk-QBZEC2JJ.cjs} +86 -86
- package/dist/{chunk-GHYJAMMS.js → chunk-ROURHJYT.js} +1 -1
- package/dist/{chunk-ZIE6EJGR.cjs → chunk-VNF7A2JA.cjs} +23 -23
- package/dist/{chunk-ISCAJBPQ.cjs → chunk-WFSYZO5Z.cjs} +1 -1
- package/dist/image-editor/index.cjs +788 -788
- package/dist/image-editor/index.js +4 -4
- package/dist/paint-editor/brush-model.d.cts +15 -1
- package/dist/paint-editor/brush-model.d.ts +15 -1
- package/dist/paint-editor/controller.d.cts +90 -9
- package/dist/paint-editor/controller.d.ts +90 -9
- package/dist/paint-editor/document.d.cts +8 -1
- package/dist/paint-editor/document.d.ts +8 -1
- package/dist/paint-editor/ferro-tools.d.cts +10 -0
- package/dist/paint-editor/ferro-tools.d.ts +10 -0
- package/dist/paint-editor/ferro.d.cts +134 -0
- package/dist/paint-editor/ferro.d.ts +134 -0
- package/dist/paint-editor/index.cjs +2138 -872
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +1446 -180
- package/dist/paint-editor/serialize.d.cts +3 -1
- package/dist/paint-editor/serialize.d.ts +3 -1
- package/dist/paint-editor/stroke.d.cts +17 -0
- package/dist/paint-editor/stroke.d.ts +17 -0
- package/dist/paint-editor/strokes.d.cts +11 -1
- package/dist/paint-editor/strokes.d.ts +11 -1
- package/dist/paint-editor/thick.d.cts +15 -2
- package/dist/paint-editor/thick.d.ts +15 -2
- package/dist/paint-editor/ui.d.cts +2 -0
- package/dist/paint-editor/ui.d.ts +2 -0
- package/dist/vector/index.cjs +998 -998
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -0
- package/skills/pixerate-paint-editor/SKILL.md +12 -30
- package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
- package/skills/pixerate-paint-editor/references/ferrofluid.md +36 -0
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
mergeBrushPatch,
|
|
15
15
|
multiColorSliders,
|
|
16
16
|
patchBrushValue
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-NJLPW4MM.js";
|
|
18
18
|
import {
|
|
19
19
|
createStickerTools,
|
|
20
20
|
describeStickerRef
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
BRUSH_PREVIEW_DARK_PAPER,
|
|
24
24
|
brushPreviewsOnDark
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-ROURHJYT.js";
|
|
26
26
|
import {
|
|
27
27
|
toVideoFfmpeg
|
|
28
28
|
} from "../chunk-S2XPXA47.js";
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
resolveExternalAssets,
|
|
43
43
|
serializeLayers,
|
|
44
44
|
serializeLayersExternal
|
|
45
|
-
} from "../chunk-
|
|
45
|
+
} from "../chunk-FNIWHSLI.js";
|
|
46
46
|
import "../chunk-ADMRNWR7.js";
|
|
47
47
|
import "../chunk-WJ4WQJCC.js";
|
|
48
48
|
import {
|
|
@@ -125,7 +125,7 @@ import {
|
|
|
125
125
|
wetPaperInput,
|
|
126
126
|
wetPigmentOf,
|
|
127
127
|
withStrokePath
|
|
128
|
-
} from "../chunk-
|
|
128
|
+
} from "../chunk-HHX7ST6Z.js";
|
|
129
129
|
import {
|
|
130
130
|
BRUSH_ID,
|
|
131
131
|
BRUSH_PRESETS,
|
|
@@ -137,6 +137,7 @@ import {
|
|
|
137
137
|
DEFAULT_BRISTLE,
|
|
138
138
|
DEFAULT_BRUSH_ID,
|
|
139
139
|
DEFAULT_DYNAMICS,
|
|
140
|
+
DEFAULT_FERRO,
|
|
140
141
|
DEFAULT_FLUORESCENCE,
|
|
141
142
|
DEFAULT_GHOST,
|
|
142
143
|
DEFAULT_IMPASTO,
|
|
@@ -182,6 +183,7 @@ import {
|
|
|
182
183
|
normalizeBristle,
|
|
183
184
|
normalizeBrushSpec,
|
|
184
185
|
normalizeDynamics,
|
|
186
|
+
normalizeFerro,
|
|
185
187
|
normalizeFluorescence,
|
|
186
188
|
normalizeGhost,
|
|
187
189
|
normalizeImpasto,
|
|
@@ -198,7 +200,7 @@ import {
|
|
|
198
200
|
readBrushSource,
|
|
199
201
|
resolveBrush,
|
|
200
202
|
usesPaintExchange
|
|
201
|
-
} from "../chunk-
|
|
203
|
+
} from "../chunk-PPDUCZZV.js";
|
|
202
204
|
import {
|
|
203
205
|
STAMP_KINDS,
|
|
204
206
|
StickersSlot,
|
|
@@ -366,7 +368,10 @@ import {
|
|
|
366
368
|
wheelToHue
|
|
367
369
|
} from "../chunk-2EUW2XZX.js";
|
|
368
370
|
import "../chunk-E2V5DA3W.js";
|
|
369
|
-
import
|
|
371
|
+
import {
|
|
372
|
+
isDualColor,
|
|
373
|
+
normalizeDualColor
|
|
374
|
+
} from "../chunk-VR6OGHU5.js";
|
|
370
375
|
import "../chunk-JC2SHA4A.js";
|
|
371
376
|
import {
|
|
372
377
|
linearToSrgb,
|
|
@@ -412,6 +417,442 @@ function createPaintDocument(options = {}) {
|
|
|
412
417
|
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
413
418
|
}
|
|
414
419
|
|
|
420
|
+
// src/paint-editor/ferro.ts
|
|
421
|
+
var DEFAULT_MAGNET_STRENGTH = 0.75;
|
|
422
|
+
var DEFAULT_MAGNET_RADIUS = 72;
|
|
423
|
+
var FERRO_CARRIER_RGB = [17, 24, 39];
|
|
424
|
+
var FERRO_DEFAULT_GLOSS = 224;
|
|
425
|
+
var FERRO_DEFAULT_METALLIC = 166;
|
|
426
|
+
var FERRO_DEFAULT_RIM = 64;
|
|
427
|
+
var SQRT3_HALF = 0.8660254037844386;
|
|
428
|
+
function normalizeMagnet(raw, base) {
|
|
429
|
+
const x = raw.x ?? base?.x;
|
|
430
|
+
const y = raw.y ?? base?.y;
|
|
431
|
+
const strength = raw.strength ?? base?.strength ?? DEFAULT_MAGNET_STRENGTH;
|
|
432
|
+
const radius = raw.radius ?? base?.radius ?? DEFAULT_MAGNET_RADIUS;
|
|
433
|
+
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
434
|
+
throw new Error(`x ${String(x)} is not a finite coordinate; pass a number in canvas pixels.`);
|
|
435
|
+
}
|
|
436
|
+
if (typeof y !== "number" || !Number.isFinite(y)) {
|
|
437
|
+
throw new Error(`y ${String(y)} is not a finite coordinate; pass a number in canvas pixels.`);
|
|
438
|
+
}
|
|
439
|
+
if (typeof strength !== "number" || !Number.isFinite(strength) || strength < -1 || strength > 1) {
|
|
440
|
+
throw new Error(`strength ${String(strength)} is out of range; use -1..1.`);
|
|
441
|
+
}
|
|
442
|
+
if (typeof radius !== "number" || !Number.isFinite(radius) || radius < 8 || radius > 2e3) {
|
|
443
|
+
throw new Error(`radius ${String(radius)} is out of range; use 8..2000 px.`);
|
|
444
|
+
}
|
|
445
|
+
return { id: raw.id, x, y, strength, radius };
|
|
446
|
+
}
|
|
447
|
+
function emptyFerro() {
|
|
448
|
+
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
449
|
+
}
|
|
450
|
+
var full = (t, channels) => {
|
|
451
|
+
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
452
|
+
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
453
|
+
const u = t.uniform ?? 0;
|
|
454
|
+
if (channels === 1) out.fill(u & 255);
|
|
455
|
+
else {
|
|
456
|
+
const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
|
|
457
|
+
for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
};
|
|
461
|
+
function ferroToShadow(ferro) {
|
|
462
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
463
|
+
for (const [key, tile] of ferro.tiles) {
|
|
464
|
+
const f = full(tile, 1);
|
|
465
|
+
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
466
|
+
for (let i = 0, j = 0; i < f.length; i++, j += 4) {
|
|
467
|
+
if (!f[i]) continue;
|
|
468
|
+
d[j] = d[j + 1] = d[j + 2] = 255;
|
|
469
|
+
d[j + 3] = f[i];
|
|
470
|
+
}
|
|
471
|
+
const t = finishTile(d, 4, 0);
|
|
472
|
+
if (t) tiles.set(key, t);
|
|
473
|
+
}
|
|
474
|
+
return new TiledSurface(4, tiles, 0);
|
|
475
|
+
}
|
|
476
|
+
function shadowToFerro(shadow) {
|
|
477
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
478
|
+
for (const [key, tile] of shadow.tiles) {
|
|
479
|
+
const s = full(tile, 4);
|
|
480
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
481
|
+
for (let i = 0, j = 0; i < d.length; i++, j += 4) {
|
|
482
|
+
if (s[j + 3]) d[i] = Math.round((s[j] + s[j + 1] + s[j + 2]) / 3 * s[j + 3] / 255);
|
|
483
|
+
}
|
|
484
|
+
const t = finishTile(d, 1, 0);
|
|
485
|
+
if (t) tiles.set(key, t);
|
|
486
|
+
}
|
|
487
|
+
return new TiledSurface(1, tiles, 0);
|
|
488
|
+
}
|
|
489
|
+
function ferroTwinBrush(brush) {
|
|
490
|
+
return normalizePaintBrush(
|
|
491
|
+
{
|
|
492
|
+
tip: { ...brush.tip, blend: "normal" },
|
|
493
|
+
behavior: { ...brush.behavior, mixing: "rgb" },
|
|
494
|
+
paint: null,
|
|
495
|
+
...brush.engine === "mixer" ? { mixer: { ...brush.mixer, mixing: "rgb" } } : {},
|
|
496
|
+
...brush.engine === "blender" ? { blender: { ...brush.blender, mixing: "rgb" } } : {},
|
|
497
|
+
...brush.engine === "smudge" ? { smudge: { ...brush.smudge, mixing: "rgb" } } : {}
|
|
498
|
+
},
|
|
499
|
+
brush
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
function scaleFerro(ferro, k) {
|
|
503
|
+
if (k >= 1) return ferro;
|
|
504
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
505
|
+
if (k > 0) {
|
|
506
|
+
for (const [key, tile] of ferro.tiles) {
|
|
507
|
+
const d = Uint8ClampedArray.from(full(tile, 1), (v) => Math.round(v * k));
|
|
508
|
+
const t = finishTile(d, 1, 0);
|
|
509
|
+
if (t) tiles.set(key, t);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return new TiledSurface(1, tiles, 0);
|
|
513
|
+
}
|
|
514
|
+
function mergeFerro(lower, upper, upperColor) {
|
|
515
|
+
if (!upper?.tiles.size) return lower;
|
|
516
|
+
const keys = /* @__PURE__ */ new Set([...lower?.tiles.keys() ?? [], ...upper.tiles.keys()]);
|
|
517
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
518
|
+
for (const key of keys) {
|
|
519
|
+
const lo = lower?.tiles.get(key);
|
|
520
|
+
const up = upper.tiles.get(key);
|
|
521
|
+
const col = upperColor.tiles.get(key);
|
|
522
|
+
const l = lo ? full(lo, 1) : null;
|
|
523
|
+
const u = up ? full(up, 1) : null;
|
|
524
|
+
const c = col ? full(col, 4) : null;
|
|
525
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
526
|
+
for (let i = 0; i < d.length; i++) {
|
|
527
|
+
const uf = u ? u[i] : 0;
|
|
528
|
+
const ca = c ? c[i * 4 + 3] : 0;
|
|
529
|
+
const a = Math.max(ca, uf) / 255;
|
|
530
|
+
d[i] = Math.min(255, Math.round(uf + (l ? l[i] : 0) * (1 - a)));
|
|
531
|
+
}
|
|
532
|
+
const t = finishTile(d, 1, 0);
|
|
533
|
+
if (t) tiles.set(key, t);
|
|
534
|
+
}
|
|
535
|
+
return new TiledSurface(1, tiles, 0);
|
|
536
|
+
}
|
|
537
|
+
function ferroFieldAt(x, y, magnets) {
|
|
538
|
+
let bx = 0;
|
|
539
|
+
let by = 0;
|
|
540
|
+
let bz = 0;
|
|
541
|
+
for (let i = 0; i < magnets.length; i++) {
|
|
542
|
+
const m = magnets[i];
|
|
543
|
+
if (m.strength === 0) continue;
|
|
544
|
+
const invR = 1 / Math.max(8, m.radius);
|
|
545
|
+
const u = (x - m.x) * invR;
|
|
546
|
+
const v = (y - m.y) * invR;
|
|
547
|
+
const r2 = u * u + v * v;
|
|
548
|
+
const s = 1 + r2;
|
|
549
|
+
const d = 1 / (s * s * Math.sqrt(s));
|
|
550
|
+
bx += m.strength * 1.5 * u * d;
|
|
551
|
+
by += m.strength * 1.5 * v * d;
|
|
552
|
+
bz += m.strength * (1 - 0.5 * r2) * d;
|
|
553
|
+
}
|
|
554
|
+
const magnitude = Math.hypot(bx, by, bz);
|
|
555
|
+
return { bx, by, bz, magnitude };
|
|
556
|
+
}
|
|
557
|
+
function hexWave(px, py, k) {
|
|
558
|
+
const c0 = Math.cos(k * px);
|
|
559
|
+
const c1 = Math.cos(k * (-0.5 * px + SQRT3_HALF * py));
|
|
560
|
+
const c2 = Math.cos(k * (-0.5 * px - SQRT3_HALF * py));
|
|
561
|
+
return (c0 + c1 + c2 + 1.5) / 4.5;
|
|
562
|
+
}
|
|
563
|
+
function ferroSpikeHeightAt(x, y, ferroByte, magnets, settings = DEFAULT_FERRO) {
|
|
564
|
+
if (ferroByte <= 0 || magnets.length === 0) return 0;
|
|
565
|
+
const field = ferroFieldAt(x, y, magnets);
|
|
566
|
+
const bc = Math.max(0.01, settings.threshold);
|
|
567
|
+
if (field.magnitude <= bc) return 0;
|
|
568
|
+
const amp = Math.sqrt(1 - bc / field.magnitude) * (ferroByte / 255);
|
|
569
|
+
const wl = Math.max(4, settings.wavelength);
|
|
570
|
+
const k = 2 * Math.PI / wl;
|
|
571
|
+
const w0 = hexWave(x, y, k);
|
|
572
|
+
const sz = field.bz < 0 ? -1 : 1;
|
|
573
|
+
const tx = sz * field.bx / field.magnitude;
|
|
574
|
+
const ty = sz * field.by / field.magnitude;
|
|
575
|
+
const shift = 0.32 * wl * amp * w0;
|
|
576
|
+
const w = Math.max(0, hexWave(x - tx * shift, y - ty * shift, k));
|
|
577
|
+
const profile = 0.15 + 0.85 * w * Math.sqrt(w);
|
|
578
|
+
return Math.min(255, Math.max(1, Math.round(255 * amp * profile)));
|
|
579
|
+
}
|
|
580
|
+
function advectFerro(ferro, magnets, offset = { x: 0, y: 0 }, steps = 4) {
|
|
581
|
+
if (!ferro.tiles.size || !magnets.length || steps <= 0) return ferro;
|
|
582
|
+
const b = ferro.bounds;
|
|
583
|
+
if (!b) return ferro;
|
|
584
|
+
let active = false;
|
|
585
|
+
for (let i = 0; i < magnets.length; i++) {
|
|
586
|
+
if (magnets[i].strength !== 0) {
|
|
587
|
+
active = true;
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!active) return ferro;
|
|
592
|
+
const pad = Math.min(32, Math.max(8, steps * 4));
|
|
593
|
+
const rect = {
|
|
594
|
+
x: b.x - pad,
|
|
595
|
+
y: b.y - pad,
|
|
596
|
+
width: b.width + 2 * pad,
|
|
597
|
+
height: b.height + 2 * pad
|
|
598
|
+
};
|
|
599
|
+
const w = rect.width;
|
|
600
|
+
const h = rect.height;
|
|
601
|
+
const raw = readRegion(ferro, rect);
|
|
602
|
+
let cur = new Float64Array(w * h);
|
|
603
|
+
for (let i = 0; i < raw.length; i++) cur[i] = raw[i];
|
|
604
|
+
const vx = new Float64Array(w * h);
|
|
605
|
+
const vy = new Float64Array(w * h);
|
|
606
|
+
for (let gy = 0; gy < h; gy++) {
|
|
607
|
+
const docY = rect.y + gy + offset.y + 0.5;
|
|
608
|
+
for (let gx = 0; gx < w; gx++) {
|
|
609
|
+
const docX = rect.x + gx + offset.x + 0.5;
|
|
610
|
+
let fx = 0;
|
|
611
|
+
let fy = 0;
|
|
612
|
+
for (let mIdx = 0; mIdx < magnets.length; mIdx++) {
|
|
613
|
+
const m = magnets[mIdx];
|
|
614
|
+
if (m.strength === 0) continue;
|
|
615
|
+
const dx = m.x - docX;
|
|
616
|
+
const dy = m.y - docY;
|
|
617
|
+
const r = Math.hypot(dx, dy);
|
|
618
|
+
if (r <= 1e-6) continue;
|
|
619
|
+
const R = Math.max(8, m.radius);
|
|
620
|
+
const u = r / R;
|
|
621
|
+
const s = 1 + u * u;
|
|
622
|
+
const damp = Math.min(1, r * 0.5);
|
|
623
|
+
const pull = Math.abs(m.strength) * damp * 1.5 / (s * Math.sqrt(s));
|
|
624
|
+
fx += dx / r * pull;
|
|
625
|
+
fy += dy / r * pull;
|
|
626
|
+
}
|
|
627
|
+
const mag = Math.hypot(fx, fy);
|
|
628
|
+
if (mag > 2.5) {
|
|
629
|
+
const scale = 2.5 / mag;
|
|
630
|
+
fx *= scale;
|
|
631
|
+
fy *= scale;
|
|
632
|
+
}
|
|
633
|
+
const idx = gy * w + gx;
|
|
634
|
+
vx[idx] = fx;
|
|
635
|
+
vy[idx] = fy;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
for (let step = 0; step < steps; step++) {
|
|
639
|
+
const next = new Float64Array(w * h);
|
|
640
|
+
for (let gy = 0; gy < h; gy++) {
|
|
641
|
+
for (let gx = 0; gx < w; gx++) {
|
|
642
|
+
const idx = gy * w + gx;
|
|
643
|
+
const val = cur[idx];
|
|
644
|
+
if (val <= 0) continue;
|
|
645
|
+
const nx = Math.min(w - 1, Math.max(0, gx + vx[idx]));
|
|
646
|
+
const ny = Math.min(h - 1, Math.max(0, gy + vy[idx]));
|
|
647
|
+
const x0 = Math.floor(nx);
|
|
648
|
+
const y0 = Math.floor(ny);
|
|
649
|
+
const x1 = Math.min(w - 1, x0 + 1);
|
|
650
|
+
const y1 = Math.min(h - 1, y0 + 1);
|
|
651
|
+
const tx = nx - x0;
|
|
652
|
+
const ty = ny - y0;
|
|
653
|
+
next[y0 * w + x0] += val * (1 - tx) * (1 - ty);
|
|
654
|
+
next[y0 * w + x1] += val * tx * (1 - ty);
|
|
655
|
+
next[y1 * w + x0] += val * (1 - tx) * ty;
|
|
656
|
+
next[y1 * w + x1] += val * tx * ty;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
for (let i = 0; i < next.length; i++) {
|
|
660
|
+
cur[i] = Math.min(255, next[i]);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const out = new Uint8ClampedArray(w * h);
|
|
664
|
+
for (let i = 0; i < out.length; i++) {
|
|
665
|
+
out[i] = Math.min(255, Math.max(0, Math.round(cur[i])));
|
|
666
|
+
}
|
|
667
|
+
return surfaceFromPixels({ width: w, height: h, data: out }, 1, rect.x, rect.y);
|
|
668
|
+
}
|
|
669
|
+
function ferroInfo(ferro, magnets = [], settings = DEFAULT_FERRO, offset = { x: 0, y: 0 }) {
|
|
670
|
+
if (!ferro || !ferro.tiles.size) {
|
|
671
|
+
return {
|
|
672
|
+
bounds: null,
|
|
673
|
+
peak: 0,
|
|
674
|
+
liquidPixels: 0,
|
|
675
|
+
maxSpikeHeight: 0,
|
|
676
|
+
spikePixels: 0,
|
|
677
|
+
magnetCount: magnets.length
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
let peak = 0;
|
|
681
|
+
let liquidPixels = 0;
|
|
682
|
+
let maxSpikeHeight = 0;
|
|
683
|
+
let spikePixels = 0;
|
|
684
|
+
for (const [key, tile] of ferro.tiles) {
|
|
685
|
+
const tx = key % 65536 - 32768;
|
|
686
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
687
|
+
const d = full(tile, 1);
|
|
688
|
+
for (let y = 0, i = 0; y < TILE; y++) {
|
|
689
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
690
|
+
for (let x = 0; x < TILE; x++, i++) {
|
|
691
|
+
const fb = d[i];
|
|
692
|
+
if (!fb) continue;
|
|
693
|
+
liquidPixels++;
|
|
694
|
+
if (fb > peak) peak = fb;
|
|
695
|
+
if (magnets.length > 0) {
|
|
696
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
697
|
+
const hs = ferroSpikeHeightAt(docX, docY, fb, magnets, settings);
|
|
698
|
+
if (hs > 0) {
|
|
699
|
+
spikePixels++;
|
|
700
|
+
if (hs > maxSpikeHeight) maxSpikeHeight = hs;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
bounds: peak ? ferro.bounds : null,
|
|
708
|
+
peak,
|
|
709
|
+
liquidPixels,
|
|
710
|
+
maxSpikeHeight,
|
|
711
|
+
spikePixels,
|
|
712
|
+
magnetCount: magnets.length
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function ferroCacheKey(ferro) {
|
|
716
|
+
if (!ferro || !ferro.surface.tiles.size) return "";
|
|
717
|
+
const mKey = ferro.magnets.map((m) => `${m.id}:${m.x.toFixed(2)},${m.y.toFixed(2)},${m.strength.toFixed(3)},${m.radius.toFixed(1)}`).join(";");
|
|
718
|
+
const s = ferro.settings;
|
|
719
|
+
return `${ferro.offset.x},${ferro.offset.y}|${s.wavelength.toFixed(2)},${s.threshold.toFixed(3)}|${mKey}`;
|
|
720
|
+
}
|
|
721
|
+
function bakeFerroIntoThick(color, ferro, thick, magnets, settings = DEFAULT_FERRO, offset = { x: 0, y: 0 }) {
|
|
722
|
+
const baseThick = thick ?? emptyThickChannels();
|
|
723
|
+
if (!ferro.tiles.size) {
|
|
724
|
+
return { color, thick: baseThick, maxSpikeHeight: 0, spikePixels: 0, curedPixels: 0 };
|
|
725
|
+
}
|
|
726
|
+
const colorTiles = new Map(color.tiles);
|
|
727
|
+
const heightTiles = new Map(baseThick.height.tiles);
|
|
728
|
+
const materialTiles = new Map(baseThick.material.tiles);
|
|
729
|
+
let maxSpikeHeight = 0;
|
|
730
|
+
let spikePixels = 0;
|
|
731
|
+
let curedPixels = 0;
|
|
732
|
+
for (const [key, fTile] of ferro.tiles) {
|
|
733
|
+
const tx = key % 65536 - 32768;
|
|
734
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
735
|
+
const fd = full(fTile, 1);
|
|
736
|
+
const cTile = colorTiles.get(key);
|
|
737
|
+
const hTile = heightTiles.get(key);
|
|
738
|
+
const mTile = materialTiles.get(key);
|
|
739
|
+
const cd = cTile ? new Uint8ClampedArray(full(cTile, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
740
|
+
const hd = hTile ? new Uint8ClampedArray(full(hTile, 1)) : new Uint8ClampedArray(TILE * TILE);
|
|
741
|
+
const md = mTile ? new Uint8ClampedArray(full(mTile, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
742
|
+
let changedColor = false;
|
|
743
|
+
let changedHeight = false;
|
|
744
|
+
let changedMaterial = false;
|
|
745
|
+
for (let y = 0, p = 0; y < TILE; y++) {
|
|
746
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
747
|
+
for (let x = 0; x < TILE; x++, p++) {
|
|
748
|
+
const fb = fd[p];
|
|
749
|
+
if (!fb) continue;
|
|
750
|
+
curedPixels++;
|
|
751
|
+
const i = p * 4;
|
|
752
|
+
if (cd[i + 3] < fb) {
|
|
753
|
+
const a0 = cd[i + 3] / fb;
|
|
754
|
+
cd[i] = Math.round(cd[i] * a0 + FERRO_CARRIER_RGB[0] * (1 - a0));
|
|
755
|
+
cd[i + 1] = Math.round(cd[i + 1] * a0 + FERRO_CARRIER_RGB[1] * (1 - a0));
|
|
756
|
+
cd[i + 2] = Math.round(cd[i + 2] * a0 + FERRO_CARRIER_RGB[2] * (1 - a0));
|
|
757
|
+
cd[i + 3] = fb;
|
|
758
|
+
changedColor = true;
|
|
759
|
+
}
|
|
760
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
761
|
+
const hs = magnets.length > 0 ? ferroSpikeHeightAt(docX, docY, fb, magnets, settings) : 0;
|
|
762
|
+
if (hs > 0) {
|
|
763
|
+
spikePixels++;
|
|
764
|
+
if (hs > maxSpikeHeight) maxSpikeHeight = hs;
|
|
765
|
+
}
|
|
766
|
+
const bead = Math.max(hs, Math.round(fb * 28 / 255));
|
|
767
|
+
const nh = Math.min(255, hd[p] + bead);
|
|
768
|
+
if (nh !== hd[p]) {
|
|
769
|
+
hd[p] = nh;
|
|
770
|
+
changedHeight = true;
|
|
771
|
+
}
|
|
772
|
+
const w = fb / 255;
|
|
773
|
+
const ng = Math.max(md[i], Math.round(FERRO_DEFAULT_GLOSS * w));
|
|
774
|
+
const nm = Math.max(md[i + 1], Math.round(FERRO_DEFAULT_METALLIC * w));
|
|
775
|
+
const nr = Math.max(md[i + 2], Math.round(FERRO_DEFAULT_RIM * w));
|
|
776
|
+
const na = md[i + 3] || 255;
|
|
777
|
+
if (ng !== md[i] || nm !== md[i + 1] || nr !== md[i + 2] || na !== md[i + 3]) {
|
|
778
|
+
md[i] = ng;
|
|
779
|
+
md[i + 1] = nm;
|
|
780
|
+
md[i + 2] = nr;
|
|
781
|
+
md[i + 3] = na;
|
|
782
|
+
changedMaterial = true;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (changedColor) {
|
|
787
|
+
const ct = finishTile(cd, 4, 0);
|
|
788
|
+
if (ct) colorTiles.set(key, ct);
|
|
789
|
+
}
|
|
790
|
+
if (changedHeight) {
|
|
791
|
+
const ht = finishTile(hd, 1, 0);
|
|
792
|
+
if (ht) heightTiles.set(key, ht);
|
|
793
|
+
}
|
|
794
|
+
if (changedMaterial) {
|
|
795
|
+
const mt = finishTile(md, 4, 0);
|
|
796
|
+
if (mt) materialTiles.set(key, mt);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
color: new TiledSurface(4, colorTiles, color.defaultValue),
|
|
801
|
+
thick: {
|
|
802
|
+
height: new TiledSurface(1, heightTiles, 0),
|
|
803
|
+
material: new TiledSurface(4, materialTiles, 0),
|
|
804
|
+
film: baseThick.film
|
|
805
|
+
},
|
|
806
|
+
maxSpikeHeight,
|
|
807
|
+
spikePixels,
|
|
808
|
+
curedPixels
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function ferroToJSON(ferro, settings) {
|
|
812
|
+
const b = ferroInfo(ferro).bounds;
|
|
813
|
+
if (!b) return null;
|
|
814
|
+
const out = {
|
|
815
|
+
rect: { ...b },
|
|
816
|
+
ferro: pngDataUrl(b.width, b.height, readRegion(ferro, b), 1)
|
|
817
|
+
};
|
|
818
|
+
if (settings) {
|
|
819
|
+
if (settings.wavelength !== DEFAULT_FERRO.wavelength) out.wavelength = settings.wavelength;
|
|
820
|
+
if (settings.threshold !== DEFAULT_FERRO.threshold) out.threshold = settings.threshold;
|
|
821
|
+
}
|
|
822
|
+
return out;
|
|
823
|
+
}
|
|
824
|
+
function ferroFromJSON(raw) {
|
|
825
|
+
const r = raw;
|
|
826
|
+
if (!r || typeof r !== "object" || !r.rect || typeof r.ferro !== "string") {
|
|
827
|
+
throw new Error("not a saved ferrofluid channel");
|
|
828
|
+
}
|
|
829
|
+
const parsed = parseDataUrl(r.ferro);
|
|
830
|
+
if (!parsed) throw new Error("the ferrofluid channel is not a PNG data URL");
|
|
831
|
+
const img = decodePng(parsed.bytes, { keepGray: true });
|
|
832
|
+
const data = img.channels === 1 ? img.data : Uint8ClampedArray.from({ length: img.width * img.height }, (_, i) => img.data[i * 4]);
|
|
833
|
+
const surface = surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
|
|
834
|
+
const settings = normalizeFerro({
|
|
835
|
+
amount: 0,
|
|
836
|
+
wavelength: r.wavelength,
|
|
837
|
+
threshold: r.threshold
|
|
838
|
+
});
|
|
839
|
+
return { surface, settings };
|
|
840
|
+
}
|
|
841
|
+
function magnetsFromJSON(raw) {
|
|
842
|
+
if (!Array.isArray(raw)) return [];
|
|
843
|
+
const out = [];
|
|
844
|
+
for (const item of raw) {
|
|
845
|
+
if (!item || typeof item !== "object") continue;
|
|
846
|
+
const m = item;
|
|
847
|
+
if (typeof m.id !== "string" || !m.id || typeof m.x !== "number" || typeof m.y !== "number") continue;
|
|
848
|
+
try {
|
|
849
|
+
out.push(normalizeMagnet({ id: m.id, x: m.x, y: m.y, strength: m.strength, radius: m.radius }));
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return out;
|
|
854
|
+
}
|
|
855
|
+
|
|
415
856
|
// src/paint-editor/thick.ts
|
|
416
857
|
var DEFAULT_LIGHTING = { azimuth: 315, elevation: 40, relief: 0.7 };
|
|
417
858
|
function normalizeLighting(raw = {}, base = DEFAULT_LIGHTING) {
|
|
@@ -551,8 +992,14 @@ function lightTile(color, height, heightAround, material, light, film = null, vi
|
|
|
551
992
|
}
|
|
552
993
|
var LitCache = class {
|
|
553
994
|
tiles = /* @__PURE__ */ new Map();
|
|
554
|
-
/**
|
|
555
|
-
|
|
995
|
+
/**
|
|
996
|
+
* The layer's pixels lit (tiles without relief, film or ferrofluid nearby are the colour tiles
|
|
997
|
+
* themselves). Without ferrofluid this is the thick-paint / thin-film pass; with it, the liquid
|
|
998
|
+
* ferrofluid adds its carrier colour, glossy metallic material and (near magnets) Rosensweig
|
|
999
|
+
* spike heights before the same lighting.
|
|
1000
|
+
*/
|
|
1001
|
+
lit(color, ch, light, viewTilt = DEFAULT_VIEW_TILT, ferro) {
|
|
1002
|
+
if (ferro && ferro.surface.tiles.size > 0) return this.litFerro(color, ch, light, viewTilt, ferro);
|
|
556
1003
|
const hasRelief = ch.height.tiles.size > 0 && light.relief > 0;
|
|
557
1004
|
const hasFilm = ch.film.tiles.size > 0;
|
|
558
1005
|
if (!hasRelief && !hasFilm) return color;
|
|
@@ -601,6 +1048,101 @@ var LitCache = class {
|
|
|
601
1048
|
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
602
1049
|
return new TiledSurface(4, out, color.defaultValue);
|
|
603
1050
|
}
|
|
1051
|
+
/**
|
|
1052
|
+
* The ferrofluid lighting pass: every tile with ferrofluid or thick relief nearby is lit, with the
|
|
1053
|
+
* liquid's carrier colour and gloss / metal material blended in and its spikes added to the
|
|
1054
|
+
* height field. Thick relief is lit even when `light.relief` is 0 here (the spikes need it), and
|
|
1055
|
+
* a thin film on the same layer still colours the lit tiles.
|
|
1056
|
+
*/
|
|
1057
|
+
litFerro(color, ch, light, viewTilt, ferro) {
|
|
1058
|
+
const hasFerroSpikes = ferro.magnets.length > 0;
|
|
1059
|
+
const lk = `${lightingKey(light)}|${viewTiltKey(viewTilt)}`;
|
|
1060
|
+
const fk = ferroCacheKey(ferro);
|
|
1061
|
+
const out = new Map(color.tiles);
|
|
1062
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1063
|
+
for (const key of color.tiles.keys()) keys.add(key);
|
|
1064
|
+
for (const key of ferro.surface.tiles.keys()) keys.add(key);
|
|
1065
|
+
for (const key of keys) {
|
|
1066
|
+
const tile = color.tiles.get(key) ?? null;
|
|
1067
|
+
const tx = key % 65536 - 32768;
|
|
1068
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
1069
|
+
const aroundH = (dx, dy) => ch.height.tiles.get(tileKey(tx + dx, ty + dy)) ?? null;
|
|
1070
|
+
const aroundF = (dx, dy) => ferro.surface.tiles.get(tileKey(tx + dx, ty + dy)) ?? null;
|
|
1071
|
+
const film = ch.film.tiles.get(key) ?? null;
|
|
1072
|
+
let any = !!film;
|
|
1073
|
+
for (let dy = -1; dy <= 1 && !any; dy++) {
|
|
1074
|
+
for (let dx = -1; dx <= 1 && !any; dx++) {
|
|
1075
|
+
if (aroundH(dx, dy) || aroundF(dx, dy)) any = true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (!any) continue;
|
|
1079
|
+
const m = ch.material.tiles.get(key) ?? null;
|
|
1080
|
+
const f = ferro.surface.tiles.get(key) ?? null;
|
|
1081
|
+
if (!tile && !f) continue;
|
|
1082
|
+
const inputs = [tile, lk, m, fk, film];
|
|
1083
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(aroundH(dx, dy));
|
|
1084
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(aroundF(dx, dy));
|
|
1085
|
+
const cached = this.tiles.get(key);
|
|
1086
|
+
if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
1087
|
+
out.set(key, cached.tile);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
let cd = tile ? expand(tile, 4) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
1091
|
+
let md = m ? expand(m, 4) : null;
|
|
1092
|
+
if (f) {
|
|
1093
|
+
const fd = expand(f, 1);
|
|
1094
|
+
cd = new Uint8ClampedArray(cd);
|
|
1095
|
+
const mBuf = md ? new Uint8ClampedArray(md) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
1096
|
+
for (let p = 0; p < fd.length; p++) {
|
|
1097
|
+
const fb = fd[p];
|
|
1098
|
+
if (!fb) continue;
|
|
1099
|
+
const i = p * 4;
|
|
1100
|
+
if (cd[i + 3] < fb) {
|
|
1101
|
+
const a0 = cd[i + 3] / fb;
|
|
1102
|
+
cd[i] = Math.round(cd[i] * a0 + FERRO_CARRIER_RGB[0] * (1 - a0));
|
|
1103
|
+
cd[i + 1] = Math.round(cd[i + 1] * a0 + FERRO_CARRIER_RGB[1] * (1 - a0));
|
|
1104
|
+
cd[i + 2] = Math.round(cd[i + 2] * a0 + FERRO_CARRIER_RGB[2] * (1 - a0));
|
|
1105
|
+
cd[i + 3] = fb;
|
|
1106
|
+
}
|
|
1107
|
+
const w = fb / 255;
|
|
1108
|
+
mBuf[i] = Math.max(mBuf[i], Math.round(FERRO_DEFAULT_GLOSS * w));
|
|
1109
|
+
mBuf[i + 1] = Math.max(mBuf[i + 1], Math.round(FERRO_DEFAULT_METALLIC * w));
|
|
1110
|
+
mBuf[i + 2] = Math.max(mBuf[i + 2], Math.round(FERRO_DEFAULT_RIM * w));
|
|
1111
|
+
mBuf[i + 3] = mBuf[i + 3] || 255;
|
|
1112
|
+
}
|
|
1113
|
+
md = mBuf;
|
|
1114
|
+
}
|
|
1115
|
+
const buildHeightTile = (ntx, nty) => {
|
|
1116
|
+
const nkey = tileKey(ntx, nty);
|
|
1117
|
+
const ht = ch.height.tiles.get(nkey) ?? null;
|
|
1118
|
+
const ft = hasFerroSpikes ? ferro.surface.tiles.get(nkey) ?? null : null;
|
|
1119
|
+
if (!ht && !ft) return null;
|
|
1120
|
+
if (!ft) return expand(ht, 1);
|
|
1121
|
+
const hBase = ht ? expand(ht, 1) : null;
|
|
1122
|
+
const fData = expand(ft, 1);
|
|
1123
|
+
const outH = hBase ? new Uint8ClampedArray(hBase) : new Uint8ClampedArray(TILE * TILE);
|
|
1124
|
+
const ox = ntx * TILE + ferro.offset.x + 0.5;
|
|
1125
|
+
const oy = nty * TILE + ferro.offset.y + 0.5;
|
|
1126
|
+
for (let py = 0, p = 0; py < TILE; py++) {
|
|
1127
|
+
const docY = oy + py;
|
|
1128
|
+
for (let px = 0; px < TILE; px++, p++) {
|
|
1129
|
+
const fb = fData[p];
|
|
1130
|
+
if (!fb) continue;
|
|
1131
|
+
const hs = ferroSpikeHeightAt(ox + px, docY, fb, ferro.magnets, ferro.settings);
|
|
1132
|
+
if (hs > 0) outH[p] = Math.min(255, outH[p] + hs);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return outH;
|
|
1136
|
+
};
|
|
1137
|
+
const hd = buildHeightTile(tx, ty);
|
|
1138
|
+
const data = lightTile(cd, hd, (dx, dy) => buildHeightTile(tx + dx, ty + dy), md, light, film ? expand(film, 4) : null, viewTilt);
|
|
1139
|
+
const lit = { data, uniform: null };
|
|
1140
|
+
this.tiles.set(key, { inputs, tile: lit });
|
|
1141
|
+
out.set(key, lit);
|
|
1142
|
+
}
|
|
1143
|
+
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
1144
|
+
return new TiledSurface(4, out, color.defaultValue);
|
|
1145
|
+
}
|
|
604
1146
|
};
|
|
605
1147
|
function expand(t, channels) {
|
|
606
1148
|
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
@@ -924,27 +1466,27 @@ var ThickStrokeDriver = class {
|
|
|
924
1466
|
const u = ((dx + 0.5 - d.x) * nxs + (dy + 0.5 - d.y) * nys) / r;
|
|
925
1467
|
const h0 = this.getH(lx, ly);
|
|
926
1468
|
let h = h0;
|
|
927
|
-
const
|
|
1469
|
+
const full3 = 255 * thick * k;
|
|
928
1470
|
switch (im.tool) {
|
|
929
1471
|
case "brush": {
|
|
930
1472
|
const ridge = 0.6 + 0.4 * Math.sin(u * 9 + hash2(Math.round(u * 6), 3, seed) * 6.28) + 0.15 * (hash2(lx, ly, seed) - 0.5);
|
|
931
|
-
const target =
|
|
1473
|
+
const target = full3 * c * ridge;
|
|
932
1474
|
h = Math.max(h0 * (1 - 0.25 * c * im.drag), Math.min(255, h0 * 0.35 + target));
|
|
933
1475
|
break;
|
|
934
1476
|
}
|
|
935
1477
|
case "tube": {
|
|
936
1478
|
const dome = Math.sqrt(Math.max(0, c));
|
|
937
|
-
h = Math.max(h0, Math.min(255,
|
|
1479
|
+
h = Math.max(h0, Math.min(255, full3 * dome * 1.1));
|
|
938
1480
|
break;
|
|
939
1481
|
}
|
|
940
1482
|
case "gloop": {
|
|
941
1483
|
const dome = Math.sqrt(Math.max(0, 1 - Math.min(1, Math.abs(u)) ** 2));
|
|
942
|
-
h = Math.max(h0, Math.min(255,
|
|
1484
|
+
h = Math.max(h0, Math.min(255, full3 * dome * (c > 0.2 ? 1 : c * 5)));
|
|
943
1485
|
break;
|
|
944
1486
|
}
|
|
945
1487
|
case "roller": {
|
|
946
1488
|
const stipple = 0.75 + 0.5 * hash2(lx >> 1, ly >> 1, seed);
|
|
947
|
-
h = h0 + (Math.min(255,
|
|
1489
|
+
h = h0 + (Math.min(255, full3 * 0.6 * stipple) - h0) * Math.min(1, c * 1.5);
|
|
948
1490
|
break;
|
|
949
1491
|
}
|
|
950
1492
|
}
|
|
@@ -1068,7 +1610,7 @@ function normalizeLightSpill(raw, base = DEFAULT_LIGHT_SPILL) {
|
|
|
1068
1610
|
function emptyLight() {
|
|
1069
1611
|
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
1070
1612
|
}
|
|
1071
|
-
var
|
|
1613
|
+
var full2 = (t, channels) => {
|
|
1072
1614
|
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
1073
1615
|
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
1074
1616
|
const u = t.uniform ?? 0;
|
|
@@ -1082,7 +1624,7 @@ var full = (t, channels) => {
|
|
|
1082
1624
|
function lightToShadow(light) {
|
|
1083
1625
|
const tiles = /* @__PURE__ */ new Map();
|
|
1084
1626
|
for (const [key, tile] of light.tiles) {
|
|
1085
|
-
const l =
|
|
1627
|
+
const l = full2(tile, 1);
|
|
1086
1628
|
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1087
1629
|
for (let i = 0, j = 0; i < l.length; i++, j += 4) {
|
|
1088
1630
|
if (!l[i]) continue;
|
|
@@ -1097,7 +1639,7 @@ function lightToShadow(light) {
|
|
|
1097
1639
|
function shadowToLight(shadow) {
|
|
1098
1640
|
const tiles = /* @__PURE__ */ new Map();
|
|
1099
1641
|
for (const [key, tile] of shadow.tiles) {
|
|
1100
|
-
const s =
|
|
1642
|
+
const s = full2(tile, 4);
|
|
1101
1643
|
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1102
1644
|
for (let i = 0, j = 0; i < d.length; i++, j += 4) if (s[j + 3]) d[i] = Math.round((s[j] + s[j + 1] + s[j + 2]) / 3 * s[j + 3] / 255);
|
|
1103
1645
|
const t = finishTile(d, 1, 0);
|
|
@@ -1132,7 +1674,7 @@ function scaleLight(light, k) {
|
|
|
1132
1674
|
const tiles = /* @__PURE__ */ new Map();
|
|
1133
1675
|
if (k > 0) {
|
|
1134
1676
|
for (const [key, tile] of light.tiles) {
|
|
1135
|
-
const d = Uint8ClampedArray.from(
|
|
1677
|
+
const d = Uint8ClampedArray.from(full2(tile, 1), (v) => Math.round(v * k));
|
|
1136
1678
|
const t = finishTile(d, 1, 0);
|
|
1137
1679
|
if (t) tiles.set(key, t);
|
|
1138
1680
|
}
|
|
@@ -1147,9 +1689,9 @@ function mergeLight(lower, upper, upperColor) {
|
|
|
1147
1689
|
const lo = lower?.tiles.get(key);
|
|
1148
1690
|
const up = upper.tiles.get(key);
|
|
1149
1691
|
const col = upperColor.tiles.get(key);
|
|
1150
|
-
const l = lo ?
|
|
1151
|
-
const u = up ?
|
|
1152
|
-
const c = col ?
|
|
1692
|
+
const l = lo ? full2(lo, 1) : null;
|
|
1693
|
+
const u = up ? full2(up, 1) : null;
|
|
1694
|
+
const c = col ? full2(col, 4) : null;
|
|
1153
1695
|
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1154
1696
|
for (let i = 0; i < d.length; i++) {
|
|
1155
1697
|
const a = c ? c[i * 4 + 3] / 255 : 0;
|
|
@@ -1186,10 +1728,10 @@ var LightCache = class {
|
|
|
1186
1728
|
out.set(key, cached.tile);
|
|
1187
1729
|
continue;
|
|
1188
1730
|
}
|
|
1189
|
-
const c =
|
|
1190
|
-
const ld = l ?
|
|
1191
|
-
const md = m ?
|
|
1192
|
-
const hd = h ?
|
|
1731
|
+
const c = full2(tile, 4);
|
|
1732
|
+
const ld = l ? full2(l, 1) : null;
|
|
1733
|
+
const md = m ? full2(m, 4) : null;
|
|
1734
|
+
const hd = h ? full2(h, 1) : null;
|
|
1193
1735
|
const d = new Uint8ClampedArray(c);
|
|
1194
1736
|
for (let y = 0, i = 0; y < TILE; y++) {
|
|
1195
1737
|
for (let x = 0; x < TILE; x++, i++) {
|
|
@@ -2916,6 +3458,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2916
3458
|
this.invisibleDaylight = brush.fluorescence.amount > 0 && brush.fluorescence.invisible && brush.engine !== "eraser" && brush.engine !== "blender" && brush.engine !== "smudge";
|
|
2917
3459
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
2918
3460
|
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
3461
|
+
if (init.ferro && !init.wet && !init.sticker) this.ferroTwin = this.makeFerroTwin(init.ferro.surface);
|
|
2919
3462
|
if (init.fluor && !init.wet && !init.sticker) this.fluorTwin = this.makeFluorTwin(init.fluor.surface);
|
|
2920
3463
|
if (init.thermo && !init.wet && !init.sticker) this.thermoTwin = this.makeThermoTwin(init.thermo.surface);
|
|
2921
3464
|
this.apply = {
|
|
@@ -2948,6 +3491,9 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2948
3491
|
/** Light paint: the stroke's twin over the light channel, and what it left there. */
|
|
2949
3492
|
lightTwin = null;
|
|
2950
3493
|
lightResult = null;
|
|
3494
|
+
/** Ferrofluid paint: the stroke's twin over the ferrofluid channel, and what it left there. */
|
|
3495
|
+
ferroTwin = null;
|
|
3496
|
+
ferroResult = null;
|
|
2951
3497
|
/** UV-fluorescent ink: the stroke's twin over the fluorescence channel, and what it left there. */
|
|
2952
3498
|
fluorTwin = null;
|
|
2953
3499
|
fluorResult = null;
|
|
@@ -3046,6 +3592,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3046
3592
|
paint,
|
|
3047
3593
|
effect,
|
|
3048
3594
|
light: null,
|
|
3595
|
+
ferro: null,
|
|
3049
3596
|
fluor: null,
|
|
3050
3597
|
thermo: null
|
|
3051
3598
|
});
|
|
@@ -3069,7 +3616,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3069
3616
|
inputFilter: void 0,
|
|
3070
3617
|
light: null,
|
|
3071
3618
|
fluor: null,
|
|
3072
|
-
thermo: null
|
|
3619
|
+
thermo: null,
|
|
3620
|
+
ferro: null
|
|
3073
3621
|
});
|
|
3074
3622
|
}
|
|
3075
3623
|
/** The stroke's twin over the 4-channel thermochromic surface (see `heat.ts`). */
|
|
@@ -3104,7 +3652,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3104
3652
|
effect: null,
|
|
3105
3653
|
light: null,
|
|
3106
3654
|
fluor: null,
|
|
3107
|
-
thermo: null
|
|
3655
|
+
thermo: null,
|
|
3656
|
+
ferro: null
|
|
3108
3657
|
});
|
|
3109
3658
|
}
|
|
3110
3659
|
/** Light paint: the layer's light channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
@@ -3119,6 +3668,44 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3119
3668
|
thermoChannel() {
|
|
3120
3669
|
return this.thermoResult;
|
|
3121
3670
|
}
|
|
3671
|
+
/** The stroke's twin over the ferrofluid channel: the same marks in grey at the brush's ferro amount (see `ferro.ts`). */
|
|
3672
|
+
makeFerroTwin(ferro) {
|
|
3673
|
+
const init = this.init;
|
|
3674
|
+
const grey = Math.round(init.brush.ferro.amount * 255);
|
|
3675
|
+
const g = grey / 255;
|
|
3676
|
+
const hex2 = "#" + grey.toString(16).padStart(2, "0").repeat(3);
|
|
3677
|
+
const host = {
|
|
3678
|
+
update: () => {
|
|
3679
|
+
},
|
|
3680
|
+
commit: (_s, surface) => {
|
|
3681
|
+
this.ferroResult = surface ? shadowToFerro(surface) : null;
|
|
3682
|
+
}
|
|
3683
|
+
};
|
|
3684
|
+
const effect = init.effect ? { source: init.effect.source, pixels: { ...init.effect.pixels, data: greyPixels(init.effect.pixels.data, grey) } } : null;
|
|
3685
|
+
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color: hex2 })) } : null;
|
|
3686
|
+
return new _PaintStrokeSession(host, {
|
|
3687
|
+
...init,
|
|
3688
|
+
surface: ferroToShadow(ferro),
|
|
3689
|
+
lockTransparency: false,
|
|
3690
|
+
brush: ferroTwinBrush(init.brush),
|
|
3691
|
+
color: [grey, grey, grey, 255],
|
|
3692
|
+
colorHex: hex2,
|
|
3693
|
+
secondary: [grey, grey, grey, 255],
|
|
3694
|
+
inputFilter: void 0,
|
|
3695
|
+
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
3696
|
+
thick: init.thick,
|
|
3697
|
+
paint,
|
|
3698
|
+
effect,
|
|
3699
|
+
light: null,
|
|
3700
|
+
ferro: null,
|
|
3701
|
+
fluor: null,
|
|
3702
|
+
thermo: null
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
/** Ferrofluid paint: the layer's ferrofluid channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
3706
|
+
ferroChannel() {
|
|
3707
|
+
return this.ferroResult;
|
|
3708
|
+
}
|
|
3122
3709
|
tileTarget() {
|
|
3123
3710
|
return {
|
|
3124
3711
|
offset: this.init.offset,
|
|
@@ -3149,6 +3736,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3149
3736
|
this.lastRaw = points[points.length - 1];
|
|
3150
3737
|
this.host.sampled?.(this, points);
|
|
3151
3738
|
this.lightTwin?.push(points);
|
|
3739
|
+
this.ferroTwin?.push(points);
|
|
3152
3740
|
this.fluorTwin?.push(points);
|
|
3153
3741
|
this.thermoTwin?.push(points);
|
|
3154
3742
|
if (this.wetDriver) {
|
|
@@ -3242,6 +3830,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3242
3830
|
this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet.finish());
|
|
3243
3831
|
this.closed = true;
|
|
3244
3832
|
if (this.lightTwin?.open) this.lightTwin.end();
|
|
3833
|
+
if (this.ferroTwin?.open) this.ferroTwin.end();
|
|
3245
3834
|
if (this.fluorTwin?.open) this.fluorTwin.end();
|
|
3246
3835
|
if (this.thermoTwin?.open) this.thermoTwin.end();
|
|
3247
3836
|
const ch = this.base.channels;
|
|
@@ -3276,6 +3865,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3276
3865
|
if (this.fluorTwin?.open) this.fluorTwin.cancel();
|
|
3277
3866
|
if (this.thermoTwin?.open) this.thermoTwin.cancel();
|
|
3278
3867
|
this.lightResult = null;
|
|
3868
|
+
if (this.ferroTwin?.open) this.ferroTwin.cancel();
|
|
3869
|
+
this.ferroResult = null;
|
|
3279
3870
|
this.fluorResult = null;
|
|
3280
3871
|
this.thermoResult = null;
|
|
3281
3872
|
this.host.commit(this, null, true);
|
|
@@ -3392,8 +3983,8 @@ function nearestOnStencil(s, p) {
|
|
|
3392
3983
|
if (s.kind === "arc") {
|
|
3393
3984
|
const c = s.center;
|
|
3394
3985
|
const ang = Math.atan2(p.y - c.y, p.x - c.x);
|
|
3395
|
-
const
|
|
3396
|
-
if (
|
|
3986
|
+
const full3 = Math.abs(s.endAngle - s.startAngle) >= 360;
|
|
3987
|
+
if (full3) {
|
|
3397
3988
|
const q = { x: c.x + Math.cos(ang) * s.radius, y: c.y + Math.sin(ang) * s.radius };
|
|
3398
3989
|
return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };
|
|
3399
3990
|
}
|
|
@@ -3584,6 +4175,8 @@ function normalizeBrushStroke(raw) {
|
|
|
3584
4175
|
points,
|
|
3585
4176
|
seed: finite2(r.seed) ? r.seed | 0 : 0
|
|
3586
4177
|
};
|
|
4178
|
+
const dual = r.dualColor === void 0 ? null : normalizeDualColor(r.dualColor);
|
|
4179
|
+
if (dual && /^#[0-9a-f]{6}$/.test(dual.light) && /^#[0-9a-f]{6}$/.test(dual.dark)) out.dualColor = dual;
|
|
3587
4180
|
const copies = Array.isArray(r.copies) ? r.copies.map(readAffine).filter((m) => m !== null) : [];
|
|
3588
4181
|
if (copies.length) out.copies = copies;
|
|
3589
4182
|
const load = readLoad(r.load);
|
|
@@ -3792,9 +4385,77 @@ function serializePaintDocument(state, options = {}) {
|
|
|
3792
4385
|
const { layers, assets } = serializeLayers(state.layers);
|
|
3793
4386
|
return { ...header(state, options), layers, assets };
|
|
3794
4387
|
}
|
|
4388
|
+
var NO_EXTENSION_PNGS = /* @__PURE__ */ new Set(["format", "version", "width", "height", "dpi", "paper", "layers", "assets", "activeLayerId", "strokes", "wetEvents", "paintEvents", "origin"]);
|
|
4389
|
+
var EXTENSION_ASSET_REF = "asset:";
|
|
4390
|
+
var PNG_DATA_URL = "data:image/png;base64,";
|
|
4391
|
+
var storedPngs = /* @__PURE__ */ new WeakMap();
|
|
4392
|
+
function fnv1a(text) {
|
|
4393
|
+
let h = 2166136261;
|
|
4394
|
+
for (let i = 0; i < text.length; i++) {
|
|
4395
|
+
h ^= text.charCodeAt(i);
|
|
4396
|
+
h = Math.imul(h, 16777619);
|
|
4397
|
+
}
|
|
4398
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
4399
|
+
}
|
|
4400
|
+
async function externalizePngs(value, assets, store) {
|
|
4401
|
+
if (typeof value === "string") {
|
|
4402
|
+
if (!value.startsWith(PNG_DATA_URL)) return value;
|
|
4403
|
+
const id = `ext-${fnv1a(value)}-${value.length.toString(36)}`;
|
|
4404
|
+
if (!assets[id]) {
|
|
4405
|
+
let refs = storedPngs.get(store);
|
|
4406
|
+
if (!refs) storedPngs.set(store, refs = /* @__PURE__ */ new Map());
|
|
4407
|
+
let ref2 = refs.get(value);
|
|
4408
|
+
if (!ref2) {
|
|
4409
|
+
const parsed = parseDataUrl(value);
|
|
4410
|
+
if (!parsed) return value;
|
|
4411
|
+
const b = parsed.bytes;
|
|
4412
|
+
const u32 = (o) => (b[o] << 24 | b[o + 1] << 16 | b[o + 2] << 8 | b[o + 3]) >>> 0;
|
|
4413
|
+
ref2 = await store.put(new Blob([b], { type: "image/png" }), { id, width: b.length >= 24 ? u32(16) : 0, height: b.length >= 24 ? u32(20) : 0 });
|
|
4414
|
+
if (typeof ref2 !== "string" || !ref2) throw new Error(`The asset store returned no reference for "${id}".`);
|
|
4415
|
+
refs.set(value, ref2);
|
|
4416
|
+
}
|
|
4417
|
+
assets[id] = ref2;
|
|
4418
|
+
}
|
|
4419
|
+
return `${EXTENSION_ASSET_REF}${id}`;
|
|
4420
|
+
}
|
|
4421
|
+
if (Array.isArray(value)) {
|
|
4422
|
+
const out = [];
|
|
4423
|
+
for (const v of value) out.push(await externalizePngs(v, assets, store));
|
|
4424
|
+
return out;
|
|
4425
|
+
}
|
|
4426
|
+
if (value && typeof value === "object") {
|
|
4427
|
+
const out = {};
|
|
4428
|
+
for (const [k, v] of Object.entries(value)) out[k] = await externalizePngs(v, assets, store);
|
|
4429
|
+
return out;
|
|
4430
|
+
}
|
|
4431
|
+
return value;
|
|
4432
|
+
}
|
|
4433
|
+
function inlinePngs(value, assets, problems, field, used) {
|
|
4434
|
+
if (typeof value === "string") {
|
|
4435
|
+
if (!value.startsWith(EXTENSION_ASSET_REF)) return value;
|
|
4436
|
+
const id = value.slice(EXTENSION_ASSET_REF.length);
|
|
4437
|
+
const url = assets[id];
|
|
4438
|
+
if (typeof url === "string" && url.startsWith("data:")) {
|
|
4439
|
+
used.add(id);
|
|
4440
|
+
return url;
|
|
4441
|
+
}
|
|
4442
|
+
const msg = `"${field}" refers to asset "${id}", which could not be read; that part loads empty.`;
|
|
4443
|
+
if (!problems.includes(msg)) problems.push(msg);
|
|
4444
|
+
return value;
|
|
4445
|
+
}
|
|
4446
|
+
if (Array.isArray(value)) return value.map((v) => inlinePngs(v, assets, problems, field, used));
|
|
4447
|
+
if (value && typeof value === "object") {
|
|
4448
|
+
const out = {};
|
|
4449
|
+
for (const [k, v] of Object.entries(value)) out[k] = inlinePngs(v, assets, problems, field, used);
|
|
4450
|
+
return out;
|
|
4451
|
+
}
|
|
4452
|
+
return value;
|
|
4453
|
+
}
|
|
3795
4454
|
async function serializePaintDocumentExternal(state, store, options = {}) {
|
|
3796
4455
|
const { layers, assets } = await serializeLayersExternal(state.layers, store);
|
|
3797
|
-
|
|
4456
|
+
const out = { ...header(state, options), layers, assets };
|
|
4457
|
+
for (const key of Object.keys(out)) if (!NO_EXTENSION_PNGS.has(key)) out[key] = await externalizePngs(out[key], assets, store);
|
|
4458
|
+
return out;
|
|
3798
4459
|
}
|
|
3799
4460
|
function paintLayersOnly(layers, problems) {
|
|
3800
4461
|
const out = [];
|
|
@@ -3827,16 +4488,22 @@ function deserializePaintDocument(raw) {
|
|
|
3827
4488
|
throw new Error(`Paint document version ${JSON.stringify(r.version)} is newer than this editor reads (${PAINT_SCHEMA_VERSION}); update @pixerate/editor.`);
|
|
3828
4489
|
}
|
|
3829
4490
|
const problems = [];
|
|
4491
|
+
const rawAssets = r.assets && typeof r.assets === "object" ? r.assets : {};
|
|
4492
|
+
const used = /* @__PURE__ */ new Set();
|
|
4493
|
+
const inlined = { ...r };
|
|
4494
|
+
for (const key of Object.keys(r)) if (!NO_EXTENSION_PNGS.has(key)) inlined[key] = inlinePngs(r[key], rawAssets, problems, key, used);
|
|
4495
|
+
return readDocument(inlined, Object.fromEntries(Object.entries(rawAssets).filter(([id]) => !used.has(id))), problems);
|
|
4496
|
+
}
|
|
4497
|
+
function readDocument(r, assets, problems) {
|
|
3830
4498
|
const canvas = normalizeCanvas({ width: r.width, height: r.height, dpi: r.dpi ?? void 0 });
|
|
3831
4499
|
const paper = normalizePaper(r.paper ?? {});
|
|
3832
|
-
const assets = r.assets && typeof r.assets === "object" ? r.assets : {};
|
|
3833
4500
|
const layers = paintLayersOnly(deserializeLayers(r.layers, { assets, problems }), problems);
|
|
3834
4501
|
const wanted = typeof r.activeLayerId === "string" ? r.activeLayerId : null;
|
|
3835
4502
|
const activeLayerId = wanted && findLayer(layers, wanted) ? wanted : flattenLayers(layers).filter((l) => l.type === "pixel").pop()?.id ?? null;
|
|
3836
4503
|
const wetEvents = Array.isArray(r.wetEvents) ? r.wetEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.clock) && typeof e.kind === "string") : [];
|
|
3837
4504
|
const state = { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) };
|
|
3838
4505
|
if (wetEvents.length) state.wetEvents = wetEvents;
|
|
3839
|
-
const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "fill-region", "clear", "flatten", "stickers", "live-strokes"].includes(e.kind)) : [];
|
|
4506
|
+
const paintEvents = Array.isArray(r.paintEvents) ? r.paintEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.at) && typeof e.layerId === "string" && ["fill", "fill-region", "clear", "flatten", "stickers", "live-strokes", "magnets", "cure-ferrofluid"].includes(e.kind)) : [];
|
|
3840
4507
|
if (paintEvents.length) state.paintEvents = paintEvents;
|
|
3841
4508
|
const o = r.origin;
|
|
3842
4509
|
if (o && typeof o === "object" && Number.isFinite(o.x) && Number.isFinite(o.y) && (o.x || o.y)) state.origin = { x: Math.round(o.x), y: Math.round(o.y) };
|
|
@@ -4768,6 +5435,7 @@ function liveStrokeRefusal(brush) {
|
|
|
4768
5435
|
if (brush.light.amount > 0) return `The ${brush.name} brush lays light, which a stroke layer doesn't redraw yet; paint light on a pixel layer (add_layer), or set light.amount to 0 (set_brush).`;
|
|
4769
5436
|
if (brush.fluorescence.amount > 0) return `The ${brush.name} brush lays UV-fluorescent ink, which a stroke layer doesn't redraw yet; paint it on a pixel layer (add_layer), or set fluorescence.amount to 0 (set_brush).`;
|
|
4770
5437
|
if (brush.thermochromic.hotColor !== null) return `The ${brush.name} brush lays thermochromic ink, which a stroke layer doesn't redraw yet; paint it on a pixel layer (add_layer), or remove thermochromic (set_brush).`;
|
|
5438
|
+
if (brush.ferro.amount > 0) return `The ${brush.name} brush lays ferrofluid, which a stroke layer doesn't redraw; paint ferrofluid on a pixel layer (add_layer), or set ferro.amount to 0 (set_brush).`;
|
|
4771
5439
|
if (brush.behavior.pickup > 0) return `The ${brush.name} brush picks up the paint under it (pickup ${brush.behavior.pickup}), so it can't be redrawn on its own; set behavior.pickup to 0 (set_brush) or paint on a pixel layer.`;
|
|
4772
5440
|
return null;
|
|
4773
5441
|
}
|
|
@@ -5036,6 +5704,12 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5036
5704
|
if (strokes >= maxStrokes && item.kind === "stroke") break;
|
|
5037
5705
|
if (item.kind === "paint") {
|
|
5038
5706
|
const e = item.event;
|
|
5707
|
+
if (e.kind === "magnets") {
|
|
5708
|
+
const d = e.data;
|
|
5709
|
+
const list = (d?.magnets ?? []).map((m) => s === 1 ? m : { ...m, x: m.x * s, y: m.y * s, radius: Math.min(2e3, Math.max(8, m.radius * s)) });
|
|
5710
|
+
target.replaceMagnets(list, Boolean(d?.advect));
|
|
5711
|
+
continue;
|
|
5712
|
+
}
|
|
5039
5713
|
if (!target.getLayer(e.layerId)) continue;
|
|
5040
5714
|
if (e.kind === "fill") target.fillLayer({ layerId: e.layerId, ...e.data });
|
|
5041
5715
|
else if (e.kind === "fill-region") {
|
|
@@ -5050,6 +5724,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5050
5724
|
...d.expand !== void 0 ? { expand: Math.max(-16, Math.min(16, d.expand * s)) } : {}
|
|
5051
5725
|
});
|
|
5052
5726
|
} else if (e.kind === "clear") target.clearLayer(e.layerId);
|
|
5727
|
+
else if (e.kind === "cure-ferrofluid") target.cureFerrofluid(e.layerId);
|
|
5053
5728
|
else if (e.kind === "live-strokes") {
|
|
5054
5729
|
const d = e.data;
|
|
5055
5730
|
if (d?.rasterized) target.replaceLiveStrokes(e.layerId, null);
|
|
@@ -6363,6 +7038,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6363
7038
|
litCaches = /* @__PURE__ */ new Map();
|
|
6364
7039
|
/** The live impasto stroke's channels. */
|
|
6365
7040
|
liveThick = null;
|
|
7041
|
+
/** The live ferrofluid stroke's channel. */
|
|
7042
|
+
liveFerro = null;
|
|
7043
|
+
nextMagnetId = 1;
|
|
6366
7044
|
/** The UV / blacklight setting (saved with the document, not undo steps). */
|
|
6367
7045
|
uvLight = { ...DEFAULT_UV_LIGHT };
|
|
6368
7046
|
uvCaches = /* @__PURE__ */ new Map();
|
|
@@ -6892,6 +7570,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6892
7570
|
paper: options.paper === void 0 ? { ...this.doc.paper, offset: void 0 } : this.resolvePaper(options.paper)
|
|
6893
7571
|
});
|
|
6894
7572
|
this.extensions = {};
|
|
7573
|
+
this.nextMagnetId = 1;
|
|
6895
7574
|
this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
|
|
6896
7575
|
this.tracingVersion++;
|
|
6897
7576
|
if (this.wet.world || this.wet.patterns.size) {
|
|
@@ -7049,7 +7728,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7049
7728
|
describeLayer(l, parentId = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0) {
|
|
7050
7729
|
let bounds = null;
|
|
7051
7730
|
if (l.type === "pixel") {
|
|
7052
|
-
const b = unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null);
|
|
7731
|
+
const b = unionRect(unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null), this.doc.ferro?.[l.id]?.bounds ?? null);
|
|
7053
7732
|
bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;
|
|
7054
7733
|
} else for (const c of flattenLayers(l.children)) if (c.type === "pixel") bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);
|
|
7055
7734
|
return {
|
|
@@ -7250,7 +7929,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7250
7929
|
...this.liveStrokesWithout(removedIds),
|
|
7251
7930
|
...this.lightWithout(removedIds),
|
|
7252
7931
|
...this.fluorWithout(removedIds),
|
|
7253
|
-
...this.thermoWithout(removedIds)
|
|
7932
|
+
...this.thermoWithout(removedIds),
|
|
7933
|
+
...this.ferroWithout(removedIds)
|
|
7254
7934
|
});
|
|
7255
7935
|
return { removed: id, activeLayerId: active };
|
|
7256
7936
|
}
|
|
@@ -7266,12 +7946,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7266
7946
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
7267
7947
|
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
7268
7948
|
const light = { ...this.doc.light ?? {} };
|
|
7949
|
+
const ferro = { ...this.doc.ferro ?? {} };
|
|
7950
|
+
const ferroSettings = { ...this.doc.ferroSettings ?? {} };
|
|
7269
7951
|
const fluor = { ...this.doc.fluor ?? {} };
|
|
7270
7952
|
const thermo = { ...this.doc.thermo ?? {} };
|
|
7271
7953
|
const copy = (l) => {
|
|
7272
7954
|
const id2 = newPaintLayerId();
|
|
7273
7955
|
if (light[l.id]) light[id2] = light[l.id];
|
|
7274
7956
|
if (this.lightSpills[l.id]) this.lightSpills[id2] = { ...this.lightSpills[l.id] };
|
|
7957
|
+
if (ferro[l.id]) ferro[id2] = ferro[l.id];
|
|
7958
|
+
if (ferroSettings[l.id]) ferroSettings[id2] = { ...ferroSettings[l.id] };
|
|
7275
7959
|
if (fluor[l.id]) fluor[id2] = fluor[l.id];
|
|
7276
7960
|
if (thermo[l.id]) thermo[id2] = thermo[l.id];
|
|
7277
7961
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
@@ -7292,7 +7976,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7292
7976
|
...this.doc.liveStrokes ? { liveStrokes } : {},
|
|
7293
7977
|
...this.doc.light ? { light } : {},
|
|
7294
7978
|
...this.doc.fluor ? { fluor } : {},
|
|
7295
|
-
...this.doc.thermo ? { thermo } : {}
|
|
7979
|
+
...this.doc.thermo ? { thermo } : {},
|
|
7980
|
+
...this.doc.ferro ? { ferro } : {},
|
|
7981
|
+
...this.doc.ferroSettings ? { ferroSettings } : {}
|
|
7296
7982
|
})
|
|
7297
7983
|
);
|
|
7298
7984
|
return this.describeLayer(dup);
|
|
@@ -7345,6 +8031,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7345
8031
|
const mergedLight = this.doc.light?.[upper.id] || this.doc.light?.[below.id] ? mergeLight(toDoc(this.doc.light?.[below.id], below), upper.visible ? toDoc(this.doc.light?.[upper.id], upper) : void 0, upperDoc) : void 0;
|
|
7346
8032
|
const lightAfter = this.lightWithout([upper.id, below.id]).light ?? (this.doc.light ? { ...this.doc.light } : void 0);
|
|
7347
8033
|
const light = mergedLight ? { ...lightAfter ?? {}, [below.id]: mergedLight } : lightAfter;
|
|
8034
|
+
const mergedFerro = this.doc.ferro?.[upper.id] || this.doc.ferro?.[below.id] ? mergeFerro(toDoc(this.doc.ferro?.[below.id], below), upper.visible ? toDoc(this.doc.ferro?.[upper.id], upper) : void 0, translateSurface(upper.surface, upper.offset.x, upper.offset.y)) : void 0;
|
|
8035
|
+
const ferroRemoved = this.ferroWithout([upper.id, below.id]);
|
|
8036
|
+
const ferroAfter = ferroRemoved.ferro ?? (this.doc.ferro ? { ...this.doc.ferro } : void 0);
|
|
8037
|
+
const ferro = mergedFerro?.tiles.size ? { ...ferroAfter ?? {}, [below.id]: mergedFerro } : ferroAfter;
|
|
8038
|
+
const ferroSettingsAfter = ferroRemoved.ferroSettings ?? (this.doc.ferroSettings ? { ...this.doc.ferroSettings } : void 0);
|
|
8039
|
+
const mergedFerroSettings = upper.visible && this.doc.ferroSettings?.[upper.id] ? this.doc.ferroSettings[upper.id] : this.doc.ferroSettings?.[below.id];
|
|
8040
|
+
const ferroSettings = mergedFerro?.tiles.size && mergedFerroSettings ? { ...ferroSettingsAfter ?? {}, [below.id]: mergedFerroSettings } : ferroSettingsAfter;
|
|
7348
8041
|
const mergedFluor = this.doc.fluor?.[upper.id] || this.doc.fluor?.[below.id] ? mergeFourChannel(toDoc(this.doc.fluor?.[below.id], below), upper.visible ? toDoc(this.doc.fluor?.[upper.id], upper) : void 0) : void 0;
|
|
7349
8042
|
const fluorAfter = this.fluorWithout([upper.id, below.id]).fluor ?? (this.doc.fluor ? { ...this.doc.fluor } : void 0);
|
|
7350
8043
|
const fluor = mergedFluor ? { ...fluorAfter ?? {}, [below.id]: mergedFluor } : fluorAfter;
|
|
@@ -7362,7 +8055,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7362
8055
|
...this.liveStrokesWithout([upper.id, below.id]),
|
|
7363
8056
|
...light ? { light } : {},
|
|
7364
8057
|
...fluor ? { fluor } : {},
|
|
7365
|
-
...thermo ? { thermo } : {}
|
|
8058
|
+
...thermo ? { thermo } : {},
|
|
8059
|
+
...ferro !== void 0 ? { ferro } : {},
|
|
8060
|
+
...ferroSettings !== void 0 ? { ferroSettings } : {}
|
|
7366
8061
|
})
|
|
7367
8062
|
);
|
|
7368
8063
|
return this.describeLayer(merged);
|
|
@@ -7387,7 +8082,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7387
8082
|
...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {},
|
|
7388
8083
|
...this.lightWithout([l.id]),
|
|
7389
8084
|
...this.fluorWithout([l.id]),
|
|
7390
|
-
...this.thermoWithout([l.id])
|
|
8085
|
+
...this.thermoWithout([l.id]),
|
|
8086
|
+
...this.ferroWithout([l.id])
|
|
7391
8087
|
},
|
|
7392
8088
|
{ kind: "clear", layerId: l.id }
|
|
7393
8089
|
)
|
|
@@ -7437,6 +8133,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7437
8133
|
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
7438
8134
|
const lit = this.doc.light?.[l.id];
|
|
7439
8135
|
const light = lit ? withLayerLight(this.doc.light, l.id, scaleLight(lit, 1 - opacity)) : void 0;
|
|
8136
|
+
const fer = this.doc.ferro?.[l.id];
|
|
8137
|
+
const ferro = fer ? withLayerFerro(this.doc.ferro, l.id, scaleFerro(fer, 1 - opacity)) : void 0;
|
|
8138
|
+
const ferroSettings = fer ? withLayerFerroSettings(this.doc.ferroSettings, ferro, l.id, this.doc.ferroSettings?.[l.id] ?? null) : void 0;
|
|
7440
8139
|
const fl = this.doc.fluor?.[l.id];
|
|
7441
8140
|
const fluor = fl ? withLayerLight(this.doc.fluor, l.id, scaleFluor(fl, 1 - opacity)) : void 0;
|
|
7442
8141
|
const th = this.doc.thermo?.[l.id];
|
|
@@ -7449,7 +8148,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7449
8148
|
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
7450
8149
|
...light ? { light } : {},
|
|
7451
8150
|
...fluor ? { fluor } : {},
|
|
7452
|
-
...thermo ? { thermo } : {}
|
|
8151
|
+
...thermo ? { thermo } : {},
|
|
8152
|
+
...fer ? { ferro, ferroSettings } : {}
|
|
7453
8153
|
}),
|
|
7454
8154
|
{ kind: "fill", layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }
|
|
7455
8155
|
)
|
|
@@ -8404,7 +9104,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8404
9104
|
brushColour = null;
|
|
8405
9105
|
/**
|
|
8406
9106
|
* The host's colour scheme: a paint colour that came from a brush's light / dark default
|
|
8407
|
-
* resolves to the new side
|
|
9107
|
+
* resolves to the new side, and stroke layers show their dual-colour strokes in it. Only the
|
|
9108
|
+
* view changes: the document, history and change listeners don't hear of it.
|
|
8408
9109
|
*/
|
|
8409
9110
|
setColorScheme(scheme) {
|
|
8410
9111
|
const next = scheme === "dark" ? "dark" : "light";
|
|
@@ -8412,9 +9113,77 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8412
9113
|
this.colorScheme = next;
|
|
8413
9114
|
const hex2 = this.brushColour ? brushColourHex(this.brushColour, next) : null;
|
|
8414
9115
|
if (hex2) this.color = hex2;
|
|
9116
|
+
this.liveLayers = null;
|
|
8415
9117
|
const refs = brushStickerRefs(this.brush);
|
|
8416
9118
|
if (refs.some((r) => !editorStampOf(r))) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
8417
|
-
this.emit(
|
|
9119
|
+
this.emit();
|
|
9120
|
+
}
|
|
9121
|
+
/**
|
|
9122
|
+
* Stroke layers in the colour scheme: the document keeps every stroke layer drawn with each
|
|
9123
|
+
* stroke's `color` (a dual-colour stroke's light side), so saved and shared documents never
|
|
9124
|
+
* depend on who is looking. The view (display, image exports, the base a pointer stroke paints
|
|
9125
|
+
* on) draws dual-colour strokes in the scheme's side instead. One cached surface per layer,
|
|
9126
|
+
* brought up to date from the strokes that changed since it was drawn.
|
|
9127
|
+
*/
|
|
9128
|
+
schemeViews = /* @__PURE__ */ new Map();
|
|
9129
|
+
schemeCopies = /* @__PURE__ */ new WeakMap();
|
|
9130
|
+
/** Set while a stroke is painted for the document rather than the view (`runStroke` on a stroke layer): dual colours take this side. */
|
|
9131
|
+
paintScheme = null;
|
|
9132
|
+
/** A stroke as the scheme shows it: itself, or (a dual-colour stroke) a copy in the scheme's side. */
|
|
9133
|
+
schemeStroke(st, scheme) {
|
|
9134
|
+
const want = st.dualColor?.[scheme];
|
|
9135
|
+
if (!want || want === st.color) return st;
|
|
9136
|
+
const had = this.schemeCopies.get(st);
|
|
9137
|
+
if (had?.scheme === scheme) return had.stroke;
|
|
9138
|
+
const out = { ...st, color: want };
|
|
9139
|
+
if (st.load) {
|
|
9140
|
+
const load = brushLoadForStroke(st.brush, EMPTY_MIXER_LOAD, hexToRgb8(want).map((v) => v / 255));
|
|
9141
|
+
if (load.reservoir || load.pickup) out.load = load;
|
|
9142
|
+
else delete out.load;
|
|
9143
|
+
}
|
|
9144
|
+
this.schemeCopies.set(st, { scheme, stroke: out });
|
|
9145
|
+
return out;
|
|
9146
|
+
}
|
|
9147
|
+
/** A stroke layer's surface in the scheme, or `null` when the document's pixels already show it. */
|
|
9148
|
+
schemeSurface(layer, list) {
|
|
9149
|
+
const scheme = this.colorScheme;
|
|
9150
|
+
if (!list.some((st) => st.dualColor && st.dualColor[scheme] !== st.color)) {
|
|
9151
|
+
this.schemeViews.delete(layer.id);
|
|
9152
|
+
return null;
|
|
9153
|
+
}
|
|
9154
|
+
const had = this.schemeViews.get(layer.id);
|
|
9155
|
+
if (had && had.scheme === scheme && had.list === list && had.base === layer.surface) return had.surface;
|
|
9156
|
+
const view = list.map((st) => this.schemeStroke(st, scheme));
|
|
9157
|
+
let surface;
|
|
9158
|
+
if (had && had.scheme === scheme && had.list !== list && !layer.offset.x && !layer.offset.y) {
|
|
9159
|
+
const now = new Set(list);
|
|
9160
|
+
const before = new Set(had.list);
|
|
9161
|
+
const gone = had.list.filter((st) => !now.has(st));
|
|
9162
|
+
const kept = list.filter((st) => before.has(st));
|
|
9163
|
+
const sameOrder = kept.every((st, i) => st === had.list.filter((x) => now.has(x))[i]);
|
|
9164
|
+
surface = sameOrder ? this.drawLiveStrokes({ ...layer, surface: had.surface }, view, this.strokeAreas(gone.map((st) => this.schemeStroke(st, scheme)))) : this.drawLiveStrokes(layer, view);
|
|
9165
|
+
} else surface = this.drawLiveStrokes(layer, view);
|
|
9166
|
+
this.schemeViews.set(layer.id, { scheme, list, base: layer.surface, surface });
|
|
9167
|
+
return surface;
|
|
9168
|
+
}
|
|
9169
|
+
/** `layers` with each stroke layer that holds dual-colour strokes shown in the scheme. */
|
|
9170
|
+
schemeLayers(layers) {
|
|
9171
|
+
const live = this.doc.liveStrokes;
|
|
9172
|
+
if (!live) return layers;
|
|
9173
|
+
let out = layers;
|
|
9174
|
+
for (const [id, list] of Object.entries(live)) {
|
|
9175
|
+
const l = findLayer(out, id)?.layer;
|
|
9176
|
+
if (!isPixelLayer(l)) continue;
|
|
9177
|
+
const surface = this.schemeSurface(l, list);
|
|
9178
|
+
if (surface) out = mapLayer(out, id, () => ({ ...l, surface }));
|
|
9179
|
+
}
|
|
9180
|
+
return out;
|
|
9181
|
+
}
|
|
9182
|
+
/** The paint colour's light / dark pair for a stroke with `brushId`, when the colour came from one (a brush's own or default colour, or `setColor`). */
|
|
9183
|
+
dualPaint(brushId) {
|
|
9184
|
+
const spec = brushId !== void 0 ? this.findEntry(brushId)?.spec : this.brushSpec;
|
|
9185
|
+
if (spec?.colour?.own && spec.colour.default !== void 0) return dualHex(spec.colour.default);
|
|
9186
|
+
return this.brushColour ? dualHex(this.brushColour) : null;
|
|
8418
9187
|
}
|
|
8419
9188
|
/** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
|
|
8420
9189
|
setColorLocked(locked) {
|
|
@@ -8444,10 +9213,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8444
9213
|
const d = spec?.colour?.own ? spec.colour.default : void 0;
|
|
8445
9214
|
return d !== void 0 ? brushColourHex(d, this.colorScheme) : null;
|
|
8446
9215
|
}
|
|
8447
|
-
/**
|
|
9216
|
+
/**
|
|
9217
|
+
* Sets the paint colour (and goes back to one colour from gradient paint). A light / dark pair
|
|
9218
|
+
* paints the colour scheme's side, follows `setColorScheme`, and is recorded with each stroke
|
|
9219
|
+
* painted in it. Returns the colour as #rrggbb.
|
|
9220
|
+
*/
|
|
8448
9221
|
setColor(color) {
|
|
8449
|
-
|
|
8450
|
-
|
|
9222
|
+
const dual = typeof color === "string" ? null : dualHex(color);
|
|
9223
|
+
if (typeof color !== "string" && !dual) throw new PaintEditorError(`color ${JSON.stringify(color)} is not a colour; use #rrggbb, or { light, dark } with a colour each.`);
|
|
9224
|
+
this.color = dual ? dual[this.colorScheme] : parseColor(color);
|
|
9225
|
+
this.brushColour = dual;
|
|
8451
9226
|
this.gradient = null;
|
|
8452
9227
|
this.effectPaint = this.effectPaintPending = null;
|
|
8453
9228
|
this.paintEpoch++;
|
|
@@ -8576,11 +9351,20 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8576
9351
|
strokeCopies() {
|
|
8577
9352
|
return symmetryCopies(this.guides.symmetry);
|
|
8578
9353
|
}
|
|
8579
|
-
sessionFor(doc,
|
|
9354
|
+
sessionFor(doc, given, host, points) {
|
|
9355
|
+
let dualColor = null;
|
|
9356
|
+
if (given.color !== void 0 && typeof given.color !== "string") {
|
|
9357
|
+
dualColor = dualHex(given.color);
|
|
9358
|
+
if (!dualColor) throw new PaintEditorError(`color ${JSON.stringify(given.color)} is not a colour; use #rrggbb, or { light, dark } with a colour each.`);
|
|
9359
|
+
}
|
|
9360
|
+
const scheme = this.paintScheme ?? this.colorScheme;
|
|
9361
|
+
let init = dualColor ? { ...given, color: dualColor[scheme] } : given;
|
|
8580
9362
|
if (init.paint !== void 0 && !isPaintEffectPaint(init.paint)) {
|
|
8581
9363
|
const p = this.readStrokePaint(init.paint);
|
|
8582
|
-
if (p.type === "color")
|
|
8583
|
-
|
|
9364
|
+
if (p.type === "color") {
|
|
9365
|
+
init = { ...init, color: p.color, paint: void 0 };
|
|
9366
|
+
dualColor = null;
|
|
9367
|
+
} else init = { ...init, paint: p };
|
|
8584
9368
|
}
|
|
8585
9369
|
let layer = (() => {
|
|
8586
9370
|
const id = init.layerId ?? doc.activeLayerId;
|
|
@@ -8625,6 +9409,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8625
9409
|
if (brush.thermochromic.hotColor !== null && brush.engine === "sticker") {
|
|
8626
9410
|
throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take thermochromic ink; paint thermochromic ink with a dab or particle brush (thermo-ink; list_brush_presets lists them), or set thermochromic.hotColor to null (set_brush).`);
|
|
8627
9411
|
}
|
|
9412
|
+
if (brush.ferro.amount > 0) {
|
|
9413
|
+
if (brush.engine === "sticker") throw new PaintEditorError(`Sticker brushes spray objects and cannot lay ferrofluid (${brush.name}); paint ferrofluid with a dab or particle brush (ferrofluid, ferro-ink; list_brush_presets lists them), or set ferro.amount to 0 (set_brush).`);
|
|
9414
|
+
if (brush.engine === "wet") throw new PaintEditorError(`Wet brushes cannot lay ferrofluid (${brush.name}); paint ferrofluid with a dry or impasto brush (ferrofluid, ferro-ink), or set ferro.amount to 0 (set_brush).`);
|
|
9415
|
+
}
|
|
8628
9416
|
if (doc.liveStrokes?.[layer.id]) {
|
|
8629
9417
|
const why = liveStrokeRefusal(brush);
|
|
8630
9418
|
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
@@ -8716,7 +9504,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8716
9504
|
}
|
|
8717
9505
|
let color = init.color === void 0 ? this.ownColour(init.brushId) ?? this.color : parseColor(init.color);
|
|
8718
9506
|
const tracing = this.guides.tracing;
|
|
8719
|
-
|
|
9507
|
+
const traced = init.color === void 0 && init.start && tracing?.visible && tracing.sampleColors ? this.sampleTracingColor(init.start.x, init.start.y) : null;
|
|
9508
|
+
if (traced) color = traced;
|
|
9509
|
+
else if (init.color === void 0) {
|
|
9510
|
+
dualColor = this.dualPaint(init.brushId);
|
|
9511
|
+
if (dualColor) color = dualColor[scheme];
|
|
9512
|
+
}
|
|
8720
9513
|
const perspective = this.guides.perspective;
|
|
8721
9514
|
const snapper = init.snapToPerspective && perspective.enabled && perspective.snap ? new PerspectiveSnapper(perspective) : null;
|
|
8722
9515
|
const useStencils = init.stencils !== false;
|
|
@@ -8732,6 +9525,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8732
9525
|
const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
|
|
8733
9526
|
const hasThick = Boolean(doc.thick?.[layer.id]);
|
|
8734
9527
|
const needThick = brush.engine === "impasto" || Boolean(brush.iridescence) || brush.engine === "eraser" && hasThick || Boolean(wet?.driver.init.film);
|
|
9528
|
+
const gradient = effect ? null : this.strokeGradient(init, brush, color);
|
|
8735
9529
|
const session = new PaintStrokeSession(host, {
|
|
8736
9530
|
layerId: layer.id,
|
|
8737
9531
|
surface: layer.surface,
|
|
@@ -8744,6 +9538,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8744
9538
|
paperDepth: doc.paper.depth,
|
|
8745
9539
|
seed,
|
|
8746
9540
|
colorHex: color,
|
|
9541
|
+
// Only a stroke that paints its one colour keeps the pair (gradient and effect paint have their own colours).
|
|
9542
|
+
...dualColor && !gradient && !effect && dualColor[scheme] === color ? { dualColor } : {},
|
|
8747
9543
|
affineCopies: copies,
|
|
8748
9544
|
copies: [IDENTITY_AFFINE, ...copies].map(affineCopy),
|
|
8749
9545
|
inputFilter,
|
|
@@ -8754,11 +9550,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8754
9550
|
thick: needThick ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
8755
9551
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
8756
9552
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
9553
|
+
// Ferrofluid paint: a twin of the stroke paints the liquid ferrofluid channel.
|
|
9554
|
+
ferro: !wet && brush.engine !== "sticker" && (brush.ferro.amount > 0 || doc.ferro?.[layer.id]) ? { surface: doc.ferro?.[layer.id] ?? emptyFerro() } : null,
|
|
8757
9555
|
fluor: brush.engine !== "sticker" && (brush.fluorescence.amount > 0 || doc.fluor?.[layer.id]) ? { surface: doc.fluor?.[layer.id] ?? emptyFluor() } : null,
|
|
8758
9556
|
thermo: brush.engine !== "sticker" && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
|
|
8759
9557
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
8760
9558
|
stampArt: this.stampArt,
|
|
8761
|
-
paint:
|
|
9559
|
+
paint: gradient,
|
|
8762
9560
|
effect
|
|
8763
9561
|
});
|
|
8764
9562
|
if (wet) this.wet.driver = wet.driver;
|
|
@@ -8775,6 +9573,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8775
9573
|
points: session.points.map((p) => ({ ...p })),
|
|
8776
9574
|
seed: init.seed
|
|
8777
9575
|
};
|
|
9576
|
+
if (init.dualColor) {
|
|
9577
|
+
stroke.dualColor = { ...init.dualColor };
|
|
9578
|
+
if (this.doc.liveStrokes?.[session.layerId]) stroke.color = init.dualColor.light;
|
|
9579
|
+
}
|
|
8778
9580
|
if (init.affineCopies.length) stroke.copies = init.affineCopies.map((m) => [...m]);
|
|
8779
9581
|
if (init.effect) stroke.paint = JSON.parse(JSON.stringify(init.effect.source));
|
|
8780
9582
|
else if (init.paint) stroke.paint = JSON.parse(JSON.stringify(init.paint));
|
|
@@ -8804,6 +9606,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8804
9606
|
this.live = { layerId, surface };
|
|
8805
9607
|
const ch = this.session?.thickChannels();
|
|
8806
9608
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
9609
|
+
const fc = this.session?.ferroChannel();
|
|
9610
|
+
const fb = this.session?.init.brush.ferro;
|
|
9611
|
+
this.liveFerro = fc ? { layerId, surface: fc, settings: fb && fb.amount > 0 ? fb : this.doc.ferroSettings?.[layerId] ?? DEFAULT_FERRO } : null;
|
|
8807
9612
|
const fl = this.session?.fluorChannel();
|
|
8808
9613
|
this.liveFluor = fl ? { layerId, surface: fl } : null;
|
|
8809
9614
|
const th = this.session?.thermoChannel();
|
|
@@ -8826,6 +9631,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8826
9631
|
this.live = null;
|
|
8827
9632
|
this.liveLayers = null;
|
|
8828
9633
|
this.liveThick = null;
|
|
9634
|
+
this.liveFerro = null;
|
|
8829
9635
|
this.liveFluor = null;
|
|
8830
9636
|
this.liveThermo = null;
|
|
8831
9637
|
const wetStroke = Boolean(s.init.wet);
|
|
@@ -8874,34 +9680,48 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8874
9680
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
8875
9681
|
const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
|
|
8876
9682
|
const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
|
|
9683
|
+
const fc = s.ferroChannel();
|
|
9684
|
+
const ferro = fc ? withLayerFerro(this.doc.ferro, l.id, fc) : this.doc.ferro;
|
|
9685
|
+
const ferroSettings = fc ? withLayerFerroSettings(this.doc.ferroSettings, ferro, l.id, brush.ferro.amount > 0 ? brush.ferro : this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO) : this.doc.ferroSettings;
|
|
8877
9686
|
const fluor = withLayerLight(this.doc.fluor, l.id, s.fluorChannel());
|
|
8878
9687
|
const thermo = withLayerLight(this.doc.thermo, l.id, s.thermoChannel());
|
|
8879
|
-
this.
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
{
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
),
|
|
8896
|
-
before
|
|
9688
|
+
let next = this.withWetEvents(
|
|
9689
|
+
withLiveStroke(
|
|
9690
|
+
{
|
|
9691
|
+
...this.doc,
|
|
9692
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
9693
|
+
strokes: [...this.doc.strokes, stroke],
|
|
9694
|
+
...thick ? { thick } : {},
|
|
9695
|
+
...stickers ? { stickers } : {},
|
|
9696
|
+
...light ? { light } : {},
|
|
9697
|
+
...fluor ? { fluor } : {},
|
|
9698
|
+
...thermo ? { thermo } : {},
|
|
9699
|
+
...ferro ? { ferro } : { ferro: void 0 },
|
|
9700
|
+
...ferroSettings ? { ferroSettings } : { ferroSettings: void 0 }
|
|
9701
|
+
},
|
|
9702
|
+
stroke
|
|
9703
|
+
)
|
|
8897
9704
|
);
|
|
9705
|
+
const list = next.liveStrokes?.[l.id];
|
|
9706
|
+
if (list && (viewBase || stroke.color !== s.init.colorHex)) {
|
|
9707
|
+
const canonical = this.withLiveStrokes({ ...next, layers: mapLayer(next.layers, l.id, () => l) }, l, list, s.result?.bounds ? [s.result.bounds] : null, { record: false });
|
|
9708
|
+
const drawn = findLayer(canonical.layers, l.id)?.layer;
|
|
9709
|
+
this.schemeViews.set(l.id, { scheme: viewScheme, list, base: drawn.surface, surface });
|
|
9710
|
+
next = canonical;
|
|
9711
|
+
}
|
|
9712
|
+
this.commit(`${brush.name} stroke`, next, before);
|
|
8898
9713
|
this.recordMutationStroke(stroke, s.result?.bounds ?? null);
|
|
8899
9714
|
this.notifySample("end", s, [], stroke.id);
|
|
8900
9715
|
}
|
|
8901
9716
|
};
|
|
9717
|
+
const targetId = init.layerId ?? this.doc.activeLayerId;
|
|
9718
|
+
const target = targetId ? findLayer(this.doc.layers, targetId)?.layer : void 0;
|
|
9719
|
+
const targetStrokes = isPixelLayer(target) ? this.doc.liveStrokes?.[target.id] : void 0;
|
|
9720
|
+
const viewBase = targetStrokes && isPixelLayer(target) ? this.schemeSurface(target, targetStrokes) : null;
|
|
9721
|
+
const viewScheme = this.colorScheme;
|
|
8902
9722
|
let session;
|
|
8903
9723
|
try {
|
|
8904
|
-
session = this.sessionFor(this.doc, { snapToPerspective: true, ...init }, host);
|
|
9724
|
+
session = this.sessionFor(viewBase && target ? { ...this.doc, layers: mapLayer(this.doc.layers, target.id, (x) => ({ ...x, surface: viewBase })) } : this.doc, { snapToPerspective: true, ...init }, host);
|
|
8905
9725
|
} catch (e) {
|
|
8906
9726
|
this.wetOp = null;
|
|
8907
9727
|
throw e;
|
|
@@ -8973,11 +9793,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8973
9793
|
this.live = null;
|
|
8974
9794
|
this.liveLayers = null;
|
|
8975
9795
|
this.liveThick = null;
|
|
9796
|
+
this.liveFerro = null;
|
|
8976
9797
|
this.liveFluor = null;
|
|
8977
9798
|
this.liveThermo = null;
|
|
8978
9799
|
const masks = s.init.selection ? (s.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask") : [];
|
|
8979
9800
|
const load = s.init.mixerLoad;
|
|
8980
|
-
const result = this.paintStroke({ layerId: s.layerId, points: s.points, color: s.init.colorHex, brush: brushToJSON(s.init.brush), seed: s.init.seed, copies: s.init.affineCopies, ...load.reservoir || load.pickup ? { load } : {}, paint, snapToPerspective: false, stencils: false, maskStencils: masks });
|
|
9801
|
+
const result = this.paintStroke({ layerId: s.layerId, points: s.points, color: s.init.dualColor ?? s.init.colorHex, brush: brushToJSON(s.init.brush), seed: s.init.seed, copies: s.init.affineCopies, ...load.reservoir || load.pickup ? { load } : {}, paint, snapToPerspective: false, stencils: false, maskStencils: masks });
|
|
8981
9802
|
Object.assign(s.result, result);
|
|
8982
9803
|
this.notifySample("end", s, [], result.strokeId);
|
|
8983
9804
|
};
|
|
@@ -8993,6 +9814,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8993
9814
|
this.live = null;
|
|
8994
9815
|
this.liveLayers = null;
|
|
8995
9816
|
this.liveThick = null;
|
|
9817
|
+
this.liveFerro = null;
|
|
8996
9818
|
this.liveFluor = null;
|
|
8997
9819
|
this.liveThermo = null;
|
|
8998
9820
|
this.emit();
|
|
@@ -9037,6 +9859,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9037
9859
|
const thick = withLayerThick(out.thick, l.id, ch);
|
|
9038
9860
|
const sprayed = s.stickers();
|
|
9039
9861
|
const lit = s.lightChannel() ?? (s.init.wet && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null);
|
|
9862
|
+
const fc = s.ferroChannel();
|
|
9863
|
+
const nextFerro = fc ? withLayerFerro(out.ferro, l.id, fc) : out.ferro;
|
|
9864
|
+
const nextFerroSettings = fc ? withLayerFerroSettings(
|
|
9865
|
+
out.ferroSettings,
|
|
9866
|
+
nextFerro,
|
|
9867
|
+
l.id,
|
|
9868
|
+
s.init.brush.ferro.amount > 0 ? s.init.brush.ferro : out.ferroSettings?.[l.id] ?? DEFAULT_FERRO
|
|
9869
|
+
) : out.ferroSettings;
|
|
9040
9870
|
const fl = s.fluorChannel();
|
|
9041
9871
|
const th = s.thermoChannel();
|
|
9042
9872
|
out = withLiveStroke({
|
|
@@ -9046,12 +9876,20 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9046
9876
|
...thick ? { thick } : {},
|
|
9047
9877
|
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
|
|
9048
9878
|
...lit ? { light: withLayerLight(out.light, l.id, lit) } : {},
|
|
9879
|
+
...fc ? { ferro: nextFerro, ferroSettings: nextFerroSettings } : {},
|
|
9049
9880
|
...fl ? { fluor: withLayerLight(out.fluor, l.id, fl) } : {},
|
|
9050
9881
|
...th ? { thermo: withLayerLight(out.thermo, l.id, th) } : {}
|
|
9051
9882
|
}, stroke);
|
|
9052
9883
|
}
|
|
9053
9884
|
};
|
|
9054
|
-
const
|
|
9885
|
+
const target = init.layerId ?? doc.activeLayerId;
|
|
9886
|
+
this.paintScheme = target && doc.liveStrokes?.[target] ? "light" : null;
|
|
9887
|
+
let session;
|
|
9888
|
+
try {
|
|
9889
|
+
session = this.sessionFor(doc, init, host, points);
|
|
9890
|
+
} finally {
|
|
9891
|
+
this.paintScheme = null;
|
|
9892
|
+
}
|
|
9055
9893
|
session.push(points);
|
|
9056
9894
|
const result = session.end();
|
|
9057
9895
|
if (session.init.wet) {
|
|
@@ -9440,6 +10278,178 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9440
10278
|
this.commit("Flatten relief", this.withPaintEvent({ ...this.doc, ...t }, { kind: "flatten", layerId: l.id }));
|
|
9441
10279
|
return { layerId: l.id, flattened: true };
|
|
9442
10280
|
}
|
|
10281
|
+
// --- Ferrofluid paint and magnets -----------------------------------------------
|
|
10282
|
+
/** The document's `ferro` and `ferroSettings` without these layers. */
|
|
10283
|
+
ferroWithout(ids) {
|
|
10284
|
+
const out = {};
|
|
10285
|
+
const f = this.doc.ferro;
|
|
10286
|
+
if (f && ids.some((id) => f[id])) {
|
|
10287
|
+
const next = { ...f };
|
|
10288
|
+
for (const id of ids) delete next[id];
|
|
10289
|
+
out.ferro = Object.keys(next).length ? next : void 0;
|
|
10290
|
+
}
|
|
10291
|
+
const fs = this.doc.ferroSettings;
|
|
10292
|
+
if (fs && ids.some((id) => fs[id])) {
|
|
10293
|
+
const next = { ...fs };
|
|
10294
|
+
for (const id of ids) delete next[id];
|
|
10295
|
+
out.ferroSettings = Object.keys(next).length ? next : void 0;
|
|
10296
|
+
}
|
|
10297
|
+
return out;
|
|
10298
|
+
}
|
|
10299
|
+
/** Runs deterministic advection on all pixel layers with liquid ferrofluid toward `magnets`. */
|
|
10300
|
+
advectDocumentFerro(doc, magnets, steps = 4) {
|
|
10301
|
+
const f = doc.ferro;
|
|
10302
|
+
if (!f || !magnets.length) return f ? { ...f } : void 0;
|
|
10303
|
+
const out = {};
|
|
10304
|
+
for (const [id, surf] of Object.entries(f)) {
|
|
10305
|
+
const l = findLayer(doc.layers, id)?.layer;
|
|
10306
|
+
if (isPixelLayer(l) && surf.tiles.size > 0) {
|
|
10307
|
+
const next = advectFerro(surf, magnets, l.offset, steps);
|
|
10308
|
+
if (next.tiles.size > 0) out[id] = next;
|
|
10309
|
+
} else if (surf.tiles.size > 0) {
|
|
10310
|
+
out[id] = surf;
|
|
10311
|
+
}
|
|
10312
|
+
}
|
|
10313
|
+
return Object.keys(out).length ? out : void 0;
|
|
10314
|
+
}
|
|
10315
|
+
/** Lists the magnets on the canvas. */
|
|
10316
|
+
listMagnets() {
|
|
10317
|
+
return (this.doc.magnets ?? []).map((m) => ({ ...m }));
|
|
10318
|
+
}
|
|
10319
|
+
/** Places a dipole magnet on the canvas and advects liquid ferrofluid toward it (one undo step). */
|
|
10320
|
+
addMagnet(input) {
|
|
10321
|
+
this.requireIdle("adding a magnet");
|
|
10322
|
+
const existing = this.doc.magnets ?? [];
|
|
10323
|
+
const usedIds = new Set(existing.map((m) => m.id));
|
|
10324
|
+
while (usedIds.has(`mag${this.nextMagnetId}`)) this.nextMagnetId++;
|
|
10325
|
+
const id = `mag${this.nextMagnetId++}`;
|
|
10326
|
+
let magnet;
|
|
10327
|
+
try {
|
|
10328
|
+
magnet = normalizeMagnet({ id, x: input.x, y: input.y, strength: input.strength, radius: input.radius });
|
|
10329
|
+
} catch (e) {
|
|
10330
|
+
throw new PaintEditorError(e.message);
|
|
10331
|
+
}
|
|
10332
|
+
const magnets = [...existing, magnet];
|
|
10333
|
+
const ferro = this.advectDocumentFerro(this.doc, magnets, 4);
|
|
10334
|
+
const nextDoc = this.withPaintEvent(
|
|
10335
|
+
{ ...this.doc, magnets, ...this.doc.ferro ? { ferro } : {} },
|
|
10336
|
+
{ kind: "magnets", layerId: "", data: { magnets: magnets.map((m) => ({ ...m })), advect: true } }
|
|
10337
|
+
);
|
|
10338
|
+
this.commit("Add magnet", nextDoc);
|
|
10339
|
+
return { ...magnet };
|
|
10340
|
+
}
|
|
10341
|
+
/** Moves or retunes a magnet and advects liquid ferrofluid toward the magnets (one undo step). */
|
|
10342
|
+
updateMagnet(id, update, commitToHistory = true) {
|
|
10343
|
+
this.requireIdle("updating a magnet");
|
|
10344
|
+
if (update.x === void 0 && update.y === void 0 && update.strength === void 0 && update.radius === void 0) {
|
|
10345
|
+
throw new PaintEditorError("Pass at least one change: x, y, strength or radius.");
|
|
10346
|
+
}
|
|
10347
|
+
const existing = this.doc.magnets ?? [];
|
|
10348
|
+
const idx = existing.findIndex((m) => m.id === id);
|
|
10349
|
+
if (idx < 0) throw new PaintEditorError(`Unknown magnet "${id}". Call list_magnets for the ids.`);
|
|
10350
|
+
const current = existing[idx];
|
|
10351
|
+
let magnet;
|
|
10352
|
+
try {
|
|
10353
|
+
magnet = normalizeMagnet(
|
|
10354
|
+
{
|
|
10355
|
+
id,
|
|
10356
|
+
x: update.x ?? current.x,
|
|
10357
|
+
y: update.y ?? current.y,
|
|
10358
|
+
strength: update.strength ?? current.strength,
|
|
10359
|
+
radius: update.radius ?? current.radius
|
|
10360
|
+
},
|
|
10361
|
+
current
|
|
10362
|
+
);
|
|
10363
|
+
} catch (e) {
|
|
10364
|
+
throw new PaintEditorError(e.message);
|
|
10365
|
+
}
|
|
10366
|
+
const magnets = existing.map((m, i) => i === idx ? magnet : m);
|
|
10367
|
+
const last = this.undoStack[this.undoStack.length - 1];
|
|
10368
|
+
const amending = !commitToHistory && Boolean(last && last.label === "Update magnet" && !last.wet && !this.wetOp && !this.grouping);
|
|
10369
|
+
const baseDoc = amending && last ? last.before : this.doc;
|
|
10370
|
+
const ferro = this.advectDocumentFerro(baseDoc, magnets, 4);
|
|
10371
|
+
const nextDoc = this.withPaintEvent(
|
|
10372
|
+
{ ...this.doc, paintEvents: baseDoc.paintEvents, magnets, ...baseDoc.ferro ? { ferro } : {} },
|
|
10373
|
+
{ kind: "magnets", layerId: "", data: { magnets: magnets.map((m) => ({ ...m })), advect: true } }
|
|
10374
|
+
);
|
|
10375
|
+
this.commitOrAmend("Update magnet", nextDoc, commitToHistory);
|
|
10376
|
+
return { ...magnet };
|
|
10377
|
+
}
|
|
10378
|
+
/** Removes magnets by id, or all magnets when `ids` is omitted or empty (one undo step). Returns the removed ids. */
|
|
10379
|
+
removeMagnets(ids) {
|
|
10380
|
+
this.requireIdle("removing magnets");
|
|
10381
|
+
const existing = this.doc.magnets ?? [];
|
|
10382
|
+
if (ids?.length) {
|
|
10383
|
+
for (const id of ids) {
|
|
10384
|
+
if (!existing.some((m) => m.id === id)) throw new PaintEditorError(`Unknown magnet "${id}". Call list_magnets for the ids.`);
|
|
10385
|
+
}
|
|
10386
|
+
}
|
|
10387
|
+
const removed = ids?.length ? [...ids] : existing.map((m) => m.id);
|
|
10388
|
+
if (!removed.length) return [];
|
|
10389
|
+
const next = existing.filter((m) => !removed.includes(m.id));
|
|
10390
|
+
const nextDoc = this.withPaintEvent(
|
|
10391
|
+
{ ...this.doc, magnets: next.length ? next : void 0 },
|
|
10392
|
+
{ kind: "magnets", layerId: "", data: { magnets: next.map((m) => ({ ...m })), advect: false } }
|
|
10393
|
+
);
|
|
10394
|
+
this.commit("Remove magnets", nextDoc);
|
|
10395
|
+
return removed;
|
|
10396
|
+
}
|
|
10397
|
+
/** Replaces the canvas magnets (used by replay). */
|
|
10398
|
+
replaceMagnets(magnets, advect = false) {
|
|
10399
|
+
const next = magnets.map((m) => normalizeMagnet(m));
|
|
10400
|
+
const ferro = advect ? this.advectDocumentFerro(this.doc, next, 4) : this.doc.ferro;
|
|
10401
|
+
this.commit("Magnets", { ...this.doc, magnets: next.length ? next : void 0, ...this.doc.ferro ? { ferro } : {} });
|
|
10402
|
+
}
|
|
10403
|
+
/** Reads a layer's liquid ferrofluid coverage and active Rosensweig spike relief under the canvas magnets. */
|
|
10404
|
+
getFerrofluid(layerId) {
|
|
10405
|
+
const l = this.wetLayer(layerId, "read the ferrofluid of");
|
|
10406
|
+
const settings = this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO;
|
|
10407
|
+
const magnets = this.doc.magnets ?? [];
|
|
10408
|
+
const info = ferroInfo(this.doc.ferro?.[l.id], magnets, settings, l.offset);
|
|
10409
|
+
const bounds = info.bounds ? { ...info.bounds, x: info.bounds.x + l.offset.x, y: info.bounds.y + l.offset.y } : null;
|
|
10410
|
+
return {
|
|
10411
|
+
layerId: l.id,
|
|
10412
|
+
ferro: info.peak > 0,
|
|
10413
|
+
bounds,
|
|
10414
|
+
peak: info.peak,
|
|
10415
|
+
coverage: info.liquidPixels,
|
|
10416
|
+
activeSpikeArea: info.spikePixels,
|
|
10417
|
+
maxSpikeHeight: info.maxSpikeHeight,
|
|
10418
|
+
magnetCount: info.magnetCount,
|
|
10419
|
+
wavelength: settings.wavelength,
|
|
10420
|
+
threshold: settings.threshold
|
|
10421
|
+
};
|
|
10422
|
+
}
|
|
10423
|
+
/** Cures a layer's liquid ferrofluid: bakes the current Rosensweig spikes and glossy metallic material into `doc.thick` and clears `doc.ferro` (one undo step). */
|
|
10424
|
+
cureFerrofluid(layerId) {
|
|
10425
|
+
this.requireIdle("curing ferrofluid");
|
|
10426
|
+
const l = this.paintTarget(layerId, "cure the ferrofluid of");
|
|
10427
|
+
const fSurf = this.doc.ferro?.[l.id];
|
|
10428
|
+
if (!fSurf || !fSurf.tiles.size) {
|
|
10429
|
+
return { layerId: l.id, cured: false, bounds: null, maxSpikeHeight: 0, activeSpikeArea: 0 };
|
|
10430
|
+
}
|
|
10431
|
+
const settings = this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO;
|
|
10432
|
+
const magnets = this.doc.magnets ?? [];
|
|
10433
|
+
const fb = ferroInfo(fSurf, magnets, settings, l.offset).bounds;
|
|
10434
|
+
const bounds = fb ? { ...fb, x: fb.x + l.offset.x, y: fb.y + l.offset.y } : null;
|
|
10435
|
+
const baked = bakeFerroIntoThick(l.surface, fSurf, this.doc.thick?.[l.id], magnets, settings, l.offset);
|
|
10436
|
+
const removed = this.ferroWithout([l.id]);
|
|
10437
|
+
const nextDoc = {
|
|
10438
|
+
...this.doc,
|
|
10439
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: baked.color })),
|
|
10440
|
+
thick: { ...this.doc.thick ?? {}, [l.id]: baked.thick },
|
|
10441
|
+
ferro: removed.ferro,
|
|
10442
|
+
ferroSettings: removed.ferroSettings
|
|
10443
|
+
};
|
|
10444
|
+
this.commit("Cure ferrofluid", this.withPaintEvent(nextDoc, { kind: "cure-ferrofluid", layerId: l.id }));
|
|
10445
|
+
return {
|
|
10446
|
+
layerId: l.id,
|
|
10447
|
+
cured: true,
|
|
10448
|
+
bounds,
|
|
10449
|
+
maxSpikeHeight: baked.maxSpikeHeight,
|
|
10450
|
+
activeSpikeArea: baked.spikePixels
|
|
10451
|
+
};
|
|
10452
|
+
}
|
|
9443
10453
|
// --- Replay: time-lapse and high-resolution export ------------------------------
|
|
9444
10454
|
/**
|
|
9445
10455
|
* A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain
|
|
@@ -9850,9 +10860,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9850
10860
|
const override = this.frameOverride;
|
|
9851
10861
|
if (override) return Array.from(override).reduce((ls, [id, surface]) => mapLayer(ls, id, (l) => ({ ...l, surface })), this.doc.layers);
|
|
9852
10862
|
const live = this.live;
|
|
9853
|
-
if (!live) return this.animatedView();
|
|
10863
|
+
if (!live) return this.schemeLayers(this.animatedView());
|
|
9854
10864
|
if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;
|
|
9855
|
-
const layers = mapLayer(this.doc.layers, live.layerId, (l) => ({ ...l, surface: live.surface }));
|
|
10865
|
+
const layers = mapLayer(this.schemeLayers(this.doc.layers), live.layerId, (l) => ({ ...l, surface: live.surface }));
|
|
9856
10866
|
this.liveLayers = { key: live.surface, layers };
|
|
9857
10867
|
return layers;
|
|
9858
10868
|
}
|
|
@@ -9942,20 +10952,33 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9942
10952
|
};
|
|
9943
10953
|
return visit(layers);
|
|
9944
10954
|
}
|
|
9945
|
-
/** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
|
|
10955
|
+
/** The layers with thick paint and ferrofluid spikes lit (each thick or ferrofluid layer's pixels replaced by its lit tiles). */
|
|
9946
10956
|
litLayers(layers) {
|
|
9947
10957
|
const thick = this.doc.thick;
|
|
9948
10958
|
const live = this.liveThick;
|
|
9949
|
-
|
|
10959
|
+
const ferro = this.doc.ferro;
|
|
10960
|
+
const liveFerro = this.liveFerro;
|
|
10961
|
+
if (!thick && !live && !ferro && !liveFerro) return layers;
|
|
9950
10962
|
let out = layers;
|
|
9951
|
-
const ids = /* @__PURE__ */ new Set([
|
|
10963
|
+
const ids = /* @__PURE__ */ new Set([
|
|
10964
|
+
...Object.keys(thick ?? {}),
|
|
10965
|
+
...live ? [live.layerId] : [],
|
|
10966
|
+
...Object.keys(ferro ?? {}),
|
|
10967
|
+
...liveFerro ? [liveFerro.layerId] : []
|
|
10968
|
+
]);
|
|
10969
|
+
const magnets = this.doc.magnets ?? [];
|
|
9952
10970
|
for (const id of ids) {
|
|
9953
|
-
const
|
|
9954
|
-
|
|
9955
|
-
|
|
10971
|
+
const l = findLayer(out, id)?.layer;
|
|
10972
|
+
if (!isPixelLayer(l)) continue;
|
|
10973
|
+
const ch = (live?.layerId === id ? live.channels : thick?.[id]) ?? emptyThickChannels();
|
|
10974
|
+
const fSurf = liveFerro?.layerId === id ? liveFerro.surface : ferro?.[id];
|
|
10975
|
+
const fSet = (liveFerro?.layerId === id ? liveFerro.settings : this.doc.ferroSettings?.[id]) ?? DEFAULT_FERRO;
|
|
10976
|
+
const hasThick = ch.height.tiles.size > 0 || ch.material.tiles.size > 0 || ch.film.tiles.size > 0;
|
|
10977
|
+
const hasFerro = Boolean(fSurf && fSurf.tiles.size > 0);
|
|
10978
|
+
if (!hasThick && !hasFerro) continue;
|
|
9956
10979
|
let cache = this.litCaches.get(id);
|
|
9957
10980
|
if (!cache) this.litCaches.set(id, cache = new LitCache());
|
|
9958
|
-
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt);
|
|
10981
|
+
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt, hasFerro ? { surface: fSurf, magnets, settings: fSet, offset: l.offset } : null);
|
|
9959
10982
|
if (lit !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: lit }));
|
|
9960
10983
|
}
|
|
9961
10984
|
return out;
|
|
@@ -10044,13 +11067,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10044
11067
|
}
|
|
10045
11068
|
/** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */
|
|
10046
11069
|
renderPreview(options = {}) {
|
|
10047
|
-
const
|
|
11070
|
+
const full3 = this.compositePixels({ ...options, wet: options.wet ?? "display" });
|
|
10048
11071
|
const side = Math.max(1, Math.round(options.maxSide ?? 512));
|
|
10049
|
-
const k = Math.min(1, side / Math.max(
|
|
10050
|
-
if (k >= 1) return
|
|
10051
|
-
const w = Math.max(1, Math.round(
|
|
10052
|
-
const h = Math.max(1, Math.round(
|
|
10053
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
11072
|
+
const k = Math.min(1, side / Math.max(full3.width, full3.height));
|
|
11073
|
+
if (k >= 1) return full3;
|
|
11074
|
+
const w = Math.max(1, Math.round(full3.width * k));
|
|
11075
|
+
const h = Math.max(1, Math.round(full3.height * k));
|
|
11076
|
+
return { width: w, height: h, data: downsampleRgba(full3.data, full3.width, full3.height, w, h) };
|
|
10054
11077
|
}
|
|
10055
11078
|
// --- Save, load and export -------------------------------------------------------
|
|
10056
11079
|
/** The document as JSON with inline PNG assets. */
|
|
@@ -10166,6 +11189,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10166
11189
|
const spills = Object.fromEntries(Object.entries(this.lightSpills).filter(([id]) => findLayer(this.doc.layers, id)));
|
|
10167
11190
|
if (Object.keys(spills).length) ext.lightSpill = spills;
|
|
10168
11191
|
else delete ext.lightSpill;
|
|
11192
|
+
const ferro = {};
|
|
11193
|
+
for (const [id, f] of Object.entries(this.doc.ferro ?? {})) {
|
|
11194
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
11195
|
+
const saved = ferroToJSON(f, this.doc.ferroSettings?.[id]);
|
|
11196
|
+
if (saved) ferro[id] = saved;
|
|
11197
|
+
}
|
|
11198
|
+
if (Object.keys(ferro).length) ext.ferro = ferro;
|
|
11199
|
+
else delete ext.ferro;
|
|
11200
|
+
if (this.doc.magnets?.length) ext.magnets = this.doc.magnets.map((m) => ({ ...m }));
|
|
11201
|
+
else delete ext.magnets;
|
|
10169
11202
|
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
10170
11203
|
if (effectLayers) ext.effectLayers = effectLayers;
|
|
10171
11204
|
else delete ext.effectLayers;
|
|
@@ -10303,6 +11336,25 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10303
11336
|
if (heat) state = { ...state, heat };
|
|
10304
11337
|
else problems.push("The heat field could not be read; the canvas loads cool.");
|
|
10305
11338
|
}
|
|
11339
|
+
if (ext.ferro && typeof ext.ferro === "object") {
|
|
11340
|
+
const ferro = {};
|
|
11341
|
+
const ferroSettings = {};
|
|
11342
|
+
for (const [id, v] of Object.entries(ext.ferro)) {
|
|
11343
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
11344
|
+
try {
|
|
11345
|
+
const loaded = ferroFromJSON(v);
|
|
11346
|
+
ferro[id] = loaded.surface;
|
|
11347
|
+
ferroSettings[id] = loaded.settings;
|
|
11348
|
+
} catch (e) {
|
|
11349
|
+
problems.push(`The ferrofluid of layer "${id}" could not be read (${e.message}); it loads without ferrofluid.`);
|
|
11350
|
+
}
|
|
11351
|
+
}
|
|
11352
|
+
if (Object.keys(ferro).length) state = { ...state, ferro, ferroSettings };
|
|
11353
|
+
}
|
|
11354
|
+
if (ext.magnets !== void 0) {
|
|
11355
|
+
const magnets = magnetsFromJSON(ext.magnets);
|
|
11356
|
+
if (magnets.length) state = { ...state, magnets };
|
|
11357
|
+
}
|
|
10306
11358
|
const raw = ext.layerChannels;
|
|
10307
11359
|
if (!raw || typeof raw !== "object") return state;
|
|
10308
11360
|
const thick = {};
|
|
@@ -10337,6 +11389,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10337
11389
|
this.redoStack = [];
|
|
10338
11390
|
this.resetMutations();
|
|
10339
11391
|
this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);
|
|
11392
|
+
this.nextMagnetId = 1;
|
|
11393
|
+
for (const m of this.doc.magnets ?? []) {
|
|
11394
|
+
const n = Number(/^mag(\d+)$/.exec(m.id)?.[1] ?? 0);
|
|
11395
|
+
if (n >= this.nextMagnetId) this.nextMagnetId = n + 1;
|
|
11396
|
+
}
|
|
10340
11397
|
for (const list of Object.values(this.doc.liveStrokes ?? {})) {
|
|
10341
11398
|
for (const s of list) {
|
|
10342
11399
|
const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
|
|
@@ -10347,6 +11404,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10347
11404
|
delete this.extensions.layerChannels;
|
|
10348
11405
|
delete this.extensions.light;
|
|
10349
11406
|
delete this.extensions.lightSpill;
|
|
11407
|
+
delete this.extensions.ferro;
|
|
11408
|
+
delete this.extensions.magnets;
|
|
10350
11409
|
delete this.extensions.lighting;
|
|
10351
11410
|
delete this.extensions.stickers;
|
|
10352
11411
|
delete this.extensions.liveStrokes;
|
|
@@ -10358,6 +11417,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10358
11417
|
delete this.extensions.heat;
|
|
10359
11418
|
delete this.extensions.uvLight;
|
|
10360
11419
|
this.viewCompositor.clear();
|
|
11420
|
+
this.schemeViews.clear();
|
|
10361
11421
|
this.emit();
|
|
10362
11422
|
const refs = Object.values(this.doc.stickers ?? {}).flatMap((list) => list.map((x) => x.ref));
|
|
10363
11423
|
if (refs.some((r) => !editorStampOf(r))) void this.prepareStickers(refs).catch(() => this.emit([]));
|
|
@@ -10387,13 +11447,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10387
11447
|
return encodePng(img.width, img.height, img.data);
|
|
10388
11448
|
}
|
|
10389
11449
|
scaled(options) {
|
|
10390
|
-
const
|
|
11450
|
+
const full3 = this.compositePixels(options);
|
|
10391
11451
|
const s = options.scale ?? 1;
|
|
10392
11452
|
if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);
|
|
10393
|
-
if (s === 1) return
|
|
10394
|
-
const w = Math.max(1, Math.round(
|
|
10395
|
-
const h = Math.max(1, Math.round(
|
|
10396
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
11453
|
+
if (s === 1) return full3;
|
|
11454
|
+
const w = Math.max(1, Math.round(full3.width * s));
|
|
11455
|
+
const h = Math.max(1, Math.round(full3.height * s));
|
|
11456
|
+
return { width: w, height: h, data: downsampleRgba(full3.data, full3.width, full3.height, w, h) };
|
|
10397
11457
|
}
|
|
10398
11458
|
/**
|
|
10399
11459
|
* The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.
|
|
@@ -10416,7 +11476,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10416
11476
|
* pixels), and the composite as the merged image.
|
|
10417
11477
|
*/
|
|
10418
11478
|
exportPsd() {
|
|
10419
|
-
const driedBase = this.driedLayers(this.doc.layers);
|
|
11479
|
+
const driedBase = this.driedLayers(this.schemeLayers(this.doc.layers));
|
|
10420
11480
|
const backlight = this.backlightLayer(driedBase);
|
|
10421
11481
|
const dried = [...this.lightLayers(this.uvLayers(this.litLayers(this.thermoLayers(driedBase)))), ...backlight ? [backlight] : []];
|
|
10422
11482
|
const { width, height, dpi } = this.doc.canvas;
|
|
@@ -10970,7 +12030,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10970
12030
|
next.seed = update.seed;
|
|
10971
12031
|
}
|
|
10972
12032
|
if (update.color !== void 0) {
|
|
10973
|
-
|
|
12033
|
+
const dual = typeof update.color === "string" ? null : dualHex(update.color);
|
|
12034
|
+
if (typeof update.color !== "string" && !dual) throw new PaintEditorError(`color ${JSON.stringify(update.color)} is not a colour; use #rrggbb, or { light, dark } with a colour each.`);
|
|
12035
|
+
next.color = dual ? dual.light : parseColor(update.color);
|
|
12036
|
+
if (dual) next.dualColor = dual;
|
|
12037
|
+
else delete next.dualColor;
|
|
10974
12038
|
delete next.paint;
|
|
10975
12039
|
}
|
|
10976
12040
|
if (update.paint !== void 0) {
|
|
@@ -10983,6 +12047,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10983
12047
|
const p = this.readStrokePaint(update.paint);
|
|
10984
12048
|
if (p.type === "color") {
|
|
10985
12049
|
next.color = p.color;
|
|
12050
|
+
delete next.dualColor;
|
|
10986
12051
|
delete next.paint;
|
|
10987
12052
|
} else next.paint = JSON.parse(JSON.stringify(p));
|
|
10988
12053
|
}
|
|
@@ -11098,8 +12163,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11098
12163
|
commitLiveStrokes(label, layer, list, dirty) {
|
|
11099
12164
|
this.commit(label, this.withLiveStrokes(this.doc, layer, list, dirty));
|
|
11100
12165
|
}
|
|
11101
|
-
|
|
11102
|
-
|
|
12166
|
+
/**
|
|
12167
|
+
* The document with a layer's strokes set and its pixels redrawn. With `record` false (the same
|
|
12168
|
+
* strokes drawn for the colour scheme) the pixel lock doesn't apply and no event is recorded for replays.
|
|
12169
|
+
*/
|
|
12170
|
+
withLiveStrokes(doc, layer, list, dirty, options = {}) {
|
|
12171
|
+
const record = options.record !== false;
|
|
12172
|
+
if (record && isLayerLocked(layer, "pixels")) throw new PaintEditorError(`Layer "${layer.name}" has its pixels locked; unlock it (update_layer locks) first.`);
|
|
11103
12173
|
const current = findLayer(doc.layers, layer.id)?.layer;
|
|
11104
12174
|
const hasFilm = list.some((s) => Boolean(s.brush.iridescence)) || Boolean(doc.thick?.[layer.id]);
|
|
11105
12175
|
const surface = this.drawLiveStrokes(current ?? layer, list, hasFilm ? null : dirty);
|
|
@@ -11119,6 +12189,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11119
12189
|
thick = withLayerThick(doc.thick, layer.id, ch);
|
|
11120
12190
|
}
|
|
11121
12191
|
const next = { ...doc, layers: mapLayer(doc.layers, layer.id, (l) => ({ ...l, surface, offset: { x: 0, y: 0 } })), liveStrokes: { ...doc.liveStrokes ?? {}, [layer.id]: [...list] }, ...thick ? { thick } : {} };
|
|
12192
|
+
if (!record) return next;
|
|
11122
12193
|
return this.withPaintEvent(next, { kind: "live-strokes", layerId: layer.id, data: { strokes: list.map((x) => JSON.parse(JSON.stringify(x))) } });
|
|
11123
12194
|
}
|
|
11124
12195
|
/** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
|
|
@@ -11596,11 +12667,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11596
12667
|
width,
|
|
11597
12668
|
height
|
|
11598
12669
|
) ?? { x: 0, y: 0, width, height };
|
|
11599
|
-
const
|
|
12670
|
+
const full3 = this.compositePixels({ wet: "display" });
|
|
11600
12671
|
const cropData = new Uint8ClampedArray(cropRect.width * cropRect.height * 4);
|
|
11601
12672
|
for (let y = 0; y < cropRect.height; y++) {
|
|
11602
12673
|
const srcRow = ((cropRect.y + y) * width + cropRect.x) * 4;
|
|
11603
|
-
cropData.set(
|
|
12674
|
+
cropData.set(full3.data.subarray(srcRow, srcRow + cropRect.width * 4), y * cropRect.width * 4);
|
|
11604
12675
|
}
|
|
11605
12676
|
const cropPngBase64 = bytesToBase64(encodePng(cropRect.width, cropRect.height, cropData));
|
|
11606
12677
|
const preview = this.renderPreview({ wet: "display", maxSide: options.maxSide ?? 512 });
|
|
@@ -12231,6 +13302,19 @@ function withLayerThick(thick, layerId, next) {
|
|
|
12231
13302
|
else delete out[layerId];
|
|
12232
13303
|
return out;
|
|
12233
13304
|
}
|
|
13305
|
+
function withLayerFerro(ferro, layerId, next) {
|
|
13306
|
+
if (!next) return ferro ? { ...ferro } : void 0;
|
|
13307
|
+
const out = { ...ferro ?? {} };
|
|
13308
|
+
if (next.tiles.size) out[layerId] = next;
|
|
13309
|
+
else delete out[layerId];
|
|
13310
|
+
return Object.keys(out).length ? out : void 0;
|
|
13311
|
+
}
|
|
13312
|
+
function withLayerFerroSettings(settings, ferro, layerId, next) {
|
|
13313
|
+
const out = { ...settings ?? {} };
|
|
13314
|
+
if (next && ferro?.[layerId]?.tiles.size) out[layerId] = { amount: next.amount, wavelength: next.wavelength, threshold: next.threshold };
|
|
13315
|
+
else if (!ferro?.[layerId]?.tiles.size) delete out[layerId];
|
|
13316
|
+
return Object.keys(out).length ? out : void 0;
|
|
13317
|
+
}
|
|
12234
13318
|
var BACKLIGHT_LAYER_ID = "__backlight";
|
|
12235
13319
|
function insertAfter(layers, id, node) {
|
|
12236
13320
|
const i = layers.findIndex((l) => l.id === id);
|
|
@@ -12272,6 +13356,7 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12272
13356
|
for (const l of flattenLayers(doc.layers)) if (l.type === "pixel") layers = mapLayer(layers, l.id, () => ({ ...l, offset: movePoint(l.offset, dx, dy) }));
|
|
12273
13357
|
const m = [1, 0, 0, 1, dx, dy];
|
|
12274
13358
|
const moveStickers = (list) => list.map((s) => ({ ...s, x: Math.round((s.x + dx) * 100) / 100, y: Math.round((s.y + dy) * 100) / 100 }));
|
|
13359
|
+
const moveMagnets = (list) => list.map((mag) => ({ ...mag, x: Math.round((mag.x + dx) * 100) / 100, y: Math.round((mag.y + dy) * 100) / 100 }));
|
|
12275
13360
|
const out = {
|
|
12276
13361
|
...doc,
|
|
12277
13362
|
layers,
|
|
@@ -12287,11 +13372,14 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12287
13372
|
return s.stencils ? { ...moved, stencils: s.stencils.map((x) => translateStencil(x, dx, dy)) } : moved;
|
|
12288
13373
|
};
|
|
12289
13374
|
if (doc.stickers) out.stickers = Object.fromEntries(Object.entries(doc.stickers).map(([id, list]) => [id, moveStickers(list)]));
|
|
13375
|
+
if (doc.magnets) out.magnets = moveMagnets(doc.magnets);
|
|
12290
13376
|
if (doc.liveStrokes) out.liveStrokes = Object.fromEntries(Object.entries(doc.liveStrokes).map(([id, list]) => [id, list.map(moveStroke)]));
|
|
12291
13377
|
if (doc.paintEvents) {
|
|
12292
13378
|
out.paintEvents = doc.paintEvents.map((e) => {
|
|
12293
13379
|
const list = e.kind === "stickers" ? e.data?.stickers : void 0;
|
|
12294
13380
|
if (list) return { ...e, data: { ...e.data, stickers: moveStickers(list) } };
|
|
13381
|
+
const magnets = e.kind === "magnets" ? e.data?.magnets : void 0;
|
|
13382
|
+
if (magnets) return { ...e, data: { ...e.data, magnets: moveMagnets(magnets) } };
|
|
12295
13383
|
const strokes = e.kind === "live-strokes" ? e.data?.strokes : void 0;
|
|
12296
13384
|
if (strokes) return { ...e, data: { ...e.data, strokes: strokes.map(moveStroke) } };
|
|
12297
13385
|
if (e.kind === "fill-region" && e.data) {
|
|
@@ -12391,6 +13479,12 @@ function readWetGhost(raw) {
|
|
|
12391
13479
|
const linger = typeof g.linger === "number" && Number.isFinite(g.linger) ? Math.max(0.5, Math.min(60, g.linger)) : 4;
|
|
12392
13480
|
return { pigment: [g.pigment[0], g.pigment[1], g.pigment[2], g.pigment[3]], remaining, total, linger };
|
|
12393
13481
|
}
|
|
13482
|
+
function dualHex(value) {
|
|
13483
|
+
if (!isDualColor(value)) return null;
|
|
13484
|
+
const light = brushColourHex(value.light, "light");
|
|
13485
|
+
const dark = brushColourHex(value.dark, "dark");
|
|
13486
|
+
return light && dark ? { light, dark } : null;
|
|
13487
|
+
}
|
|
12394
13488
|
|
|
12395
13489
|
// src/paint-editor/presence.ts
|
|
12396
13490
|
var PaintPresence = class extends PresenceStore {
|
|
@@ -12647,7 +13741,7 @@ function unionRect2(a, b) {
|
|
|
12647
13741
|
|
|
12648
13742
|
// src/paint-editor/playback.ts
|
|
12649
13743
|
var WET = /^(advance_wet_paint|dry_paint|wet_paper|set_wet_settings|set_flow_map|set_canvas_tilt|pause_wet_paint|set_wet_engine|set_water_overlay)$/;
|
|
12650
|
-
var PAPER = /^(set_paper|set_lighting|flatten_relief|set_instant_dry)$/;
|
|
13744
|
+
var PAPER = /^(set_paper|set_lighting|flatten_relief|set_instant_dry|add_magnet|update_magnet|remove_magnets|cure_ferrofluid)$/;
|
|
12651
13745
|
var LAYERS = /^(add_layer|update_layer|set_active_layer|remove_layer|duplicate_layer|move_layer|merge_layer_down|clear_layer|update_sticker|arrange_stickers|remove_stickers)$/;
|
|
12652
13746
|
var PAD = /^(mixing_pad_stroke|pick_mixing_pad_color|clear_mixing_pad|mix_on_pad|save_mixing_pad|load_mixing_pad|remove_mixing_pad|mix_colors)$/;
|
|
12653
13747
|
var COLOR = /^(set_color|set_paint|color_harmony|add_swatch|remove_swatch|load_mixer_brush|clean_mixer_brush)$/;
|
|
@@ -13838,8 +14932,111 @@ function createPaintThickTools(c) {
|
|
|
13838
14932
|
];
|
|
13839
14933
|
}
|
|
13840
14934
|
|
|
13841
|
-
// src/paint-editor/
|
|
14935
|
+
// src/paint-editor/ferro-tools.ts
|
|
13842
14936
|
var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14937
|
+
var SET4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14938
|
+
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14939
|
+
var EMPTY3 = { type: "object", properties: {}, additionalProperties: false };
|
|
14940
|
+
var LAYER_ID4 = { type: "string", description: "Pixel layer id from list_layers (default the active layer)." };
|
|
14941
|
+
function createFerroTools(c) {
|
|
14942
|
+
return [
|
|
14943
|
+
{
|
|
14944
|
+
name: "add_magnet",
|
|
14945
|
+
title: "Place a magnet",
|
|
14946
|
+
description: "Places a dipole magnet on the canvas at (x, y) in document pixels. Liquid ferrofluid on every layer gathers toward the magnet (deterministic advection steps) and rises into hexagonal Rosensweig relief spikes along the magnetic field lines wherever |B| exceeds the layer\u2019s threshold, shaded live by set_lighting. One undo step. Returns the magnet (id, x, y, strength, radius).",
|
|
14947
|
+
inputSchema: {
|
|
14948
|
+
type: "object",
|
|
14949
|
+
properties: {
|
|
14950
|
+
x: { type: "number", description: "Magnet centre x in document pixels." },
|
|
14951
|
+
y: { type: "number", description: "Magnet centre y in document pixels." },
|
|
14952
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Dipole strength -1..1 (default 0.8; negative flips the pole)." },
|
|
14953
|
+
radius: { type: "number", minimum: 4, maximum: 2048, description: "Dipole falloff radius in pixels (default 80)." }
|
|
14954
|
+
},
|
|
14955
|
+
required: ["x", "y"],
|
|
14956
|
+
additionalProperties: false
|
|
14957
|
+
},
|
|
14958
|
+
annotations: { title: "Place a magnet", ...WRITE5 },
|
|
14959
|
+
coordinates: { in: [""], out: [""] },
|
|
14960
|
+
handler: (a) => c.addMagnet(a)
|
|
14961
|
+
},
|
|
14962
|
+
{
|
|
14963
|
+
name: "update_magnet",
|
|
14964
|
+
title: "Move or tune a magnet",
|
|
14965
|
+
description: "Moves a magnet or changes its dipole strength (-1..1) or falloff radius (4..2048 px) by id from list_magnets. Liquid ferrofluid gathers toward the updated field and the lit relief spikes update at once. One undo step. Returns the updated magnet.",
|
|
14966
|
+
inputSchema: {
|
|
14967
|
+
type: "object",
|
|
14968
|
+
properties: {
|
|
14969
|
+
id: { type: "string", description: "Magnet id from list_magnets." },
|
|
14970
|
+
x: { type: "number", description: "Magnet centre x in document pixels." },
|
|
14971
|
+
y: { type: "number", description: "Magnet centre y in document pixels." },
|
|
14972
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Dipole strength -1..1." },
|
|
14973
|
+
radius: { type: "number", minimum: 4, maximum: 2048, description: "Dipole falloff radius in pixels (4..2048)." }
|
|
14974
|
+
},
|
|
14975
|
+
required: ["id"],
|
|
14976
|
+
additionalProperties: false
|
|
14977
|
+
},
|
|
14978
|
+
annotations: { title: "Move or tune a magnet", ...SET4 },
|
|
14979
|
+
coordinates: { in: [""], out: [""] },
|
|
14980
|
+
handler: ({ id, ...u }) => c.updateMagnet(id, u)
|
|
14981
|
+
},
|
|
14982
|
+
{
|
|
14983
|
+
name: "remove_magnets",
|
|
14984
|
+
title: "Remove magnets",
|
|
14985
|
+
description: "Removes magnets by id from list_magnets, or every magnet on the canvas when ids is omitted or empty. Uncured ferrofluid relaxes as the field changes; cured relief stays. One undo step. Returns the removed magnet ids.",
|
|
14986
|
+
inputSchema: {
|
|
14987
|
+
type: "object",
|
|
14988
|
+
properties: {
|
|
14989
|
+
ids: {
|
|
14990
|
+
type: "array",
|
|
14991
|
+
items: { type: "string", description: "Magnet id from list_magnets." },
|
|
14992
|
+
description: "Magnet ids to remove (default: remove all magnets)."
|
|
14993
|
+
}
|
|
14994
|
+
},
|
|
14995
|
+
additionalProperties: false
|
|
14996
|
+
},
|
|
14997
|
+
annotations: { title: "Remove magnets", ...WRITE5 },
|
|
14998
|
+
handler: ({ ids }) => ({ removed: c.removeMagnets(ids) })
|
|
14999
|
+
},
|
|
15000
|
+
{
|
|
15001
|
+
name: "list_magnets",
|
|
15002
|
+
title: "List magnets",
|
|
15003
|
+
description: "Lists the magnets placed on the canvas in order: id, x, y (document pixels), strength (-1..1) and radius (px). Changes nothing.",
|
|
15004
|
+
inputSchema: EMPTY3,
|
|
15005
|
+
annotations: { title: "List magnets", ...READ5 },
|
|
15006
|
+
coordinates: { out: ["magnets[]"] },
|
|
15007
|
+
handler: () => ({ magnets: c.listMagnets() })
|
|
15008
|
+
},
|
|
15009
|
+
{
|
|
15010
|
+
name: "get_ferrofluid",
|
|
15011
|
+
title: "Read a layer\u2019s ferrofluid and spikes",
|
|
15012
|
+
description: "Reads a layer\u2019s liquid ferrofluid (laid by brushes with ferro.amount above 0, such as ferrofluid and ferro-ink) and its live Rosensweig spikes under the current magnets: whether it has any (ferro), its bounds in document pixels, peak liquid level (0..255), liquid coverage in pixels, activeSpikeArea (pixels currently raised into spikes), maxSpikeHeight (0..255), magnetCount, and the layer\u2019s capillary wavelength and threshold. Changes nothing.",
|
|
15013
|
+
inputSchema: {
|
|
15014
|
+
type: "object",
|
|
15015
|
+
properties: { layerId: LAYER_ID4 },
|
|
15016
|
+
additionalProperties: false
|
|
15017
|
+
},
|
|
15018
|
+
annotations: { title: "Read a layer\u2019s ferrofluid and spikes", ...READ5 },
|
|
15019
|
+
coordinates: { out: ["bounds"] },
|
|
15020
|
+
handler: ({ layerId }) => c.getFerrofluid(layerId)
|
|
15021
|
+
},
|
|
15022
|
+
{
|
|
15023
|
+
name: "cure_ferrofluid",
|
|
15024
|
+
title: "Cure ferrofluid into solid relief",
|
|
15025
|
+
description: "Cures a layer\u2019s liquid ferrofluid: bakes its current magnetic Rosensweig spikes into the layer\u2019s solid impasto height and dark glossy metallic material, and clears the liquid ferrofluid channel so moving or removing magnets no longer changes this layer. One undo step. Returns the layer id, whether anything was cured, the cured bounds, maxSpikeHeight and activeSpikeArea.",
|
|
15026
|
+
inputSchema: {
|
|
15027
|
+
type: "object",
|
|
15028
|
+
properties: { layerId: LAYER_ID4 },
|
|
15029
|
+
additionalProperties: false
|
|
15030
|
+
},
|
|
15031
|
+
annotations: { title: "Cure ferrofluid into solid relief", ...WRITE5 },
|
|
15032
|
+
coordinates: { out: ["bounds"] },
|
|
15033
|
+
handler: ({ layerId }) => c.cureFerrofluid(layerId)
|
|
15034
|
+
}
|
|
15035
|
+
];
|
|
15036
|
+
}
|
|
15037
|
+
|
|
15038
|
+
// src/paint-editor/replay-tools.ts
|
|
15039
|
+
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13843
15040
|
function createPaintReplayTools(c) {
|
|
13844
15041
|
return [
|
|
13845
15042
|
{
|
|
@@ -13858,7 +15055,7 @@ function createPaintReplayTools(c) {
|
|
|
13858
15055
|
},
|
|
13859
15056
|
additionalProperties: false
|
|
13860
15057
|
},
|
|
13861
|
-
annotations: { title: "Export a time-lapse", ...
|
|
15058
|
+
annotations: { title: "Export a time-lapse", ...READ6 },
|
|
13862
15059
|
handler: async (a) => {
|
|
13863
15060
|
await c.wetIdle();
|
|
13864
15061
|
const tl = await c.timeLapse({ frames: a.frames, fps: a.fps, maxSide: a.maxSide, wetStepsPerFrame: a.wetStepsPerFrame, holdMs: a.holdMs });
|
|
@@ -13890,7 +15087,7 @@ function createPaintReplayTools(c) {
|
|
|
13890
15087
|
required: ["scale"],
|
|
13891
15088
|
additionalProperties: false
|
|
13892
15089
|
},
|
|
13893
|
-
annotations: { title: "Export at a higher resolution", ...
|
|
15090
|
+
annotations: { title: "Export at a higher resolution", ...READ6 },
|
|
13894
15091
|
handler: async ({ scale, paper }) => {
|
|
13895
15092
|
await c.wetIdle();
|
|
13896
15093
|
const img = await c.renderHighRes({ scale, paper });
|
|
@@ -13902,9 +15099,9 @@ function createPaintReplayTools(c) {
|
|
|
13902
15099
|
}
|
|
13903
15100
|
|
|
13904
15101
|
// src/paint-editor/mutation-tools.ts
|
|
13905
|
-
var
|
|
13906
|
-
var
|
|
13907
|
-
var
|
|
15102
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15103
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
15104
|
+
var SET5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13908
15105
|
var POINTS = {
|
|
13909
15106
|
type: "array",
|
|
13910
15107
|
minItems: 1,
|
|
@@ -13943,7 +15140,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13943
15140
|
},
|
|
13944
15141
|
additionalProperties: false
|
|
13945
15142
|
},
|
|
13946
|
-
annotations: { title: "List pending mutations", ...
|
|
15143
|
+
annotations: { title: "List pending mutations", ...READ7 },
|
|
13947
15144
|
coordinates: { out: ["mutations[].bounds"] },
|
|
13948
15145
|
handler: (a) => ({ mutations: c.listMutations({ includeAll: Boolean(a.includeAll) }) })
|
|
13949
15146
|
},
|
|
@@ -13974,7 +15171,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13974
15171
|
required: ["id"],
|
|
13975
15172
|
additionalProperties: false
|
|
13976
15173
|
},
|
|
13977
|
-
annotations: { title: "Read and claim a mutation", ...
|
|
15174
|
+
annotations: { title: "Read and claim a mutation", ...WRITE6 },
|
|
13978
15175
|
coordinates: { out: ["bounds", "strokes[].bounds", "strokes[].points[]", "cropPng.rect"] },
|
|
13979
15176
|
handler: (a) => c.readMutation(a.id, {
|
|
13980
15177
|
actor: options.actor?.id,
|
|
@@ -14051,7 +15248,7 @@ function createMutationTools(c, options = {}) {
|
|
|
14051
15248
|
required: ["id", "strokes"],
|
|
14052
15249
|
additionalProperties: false
|
|
14053
15250
|
},
|
|
14054
|
-
annotations: { title: "Answer a mutation with strokes", ...
|
|
15251
|
+
annotations: { title: "Answer a mutation with strokes", ...WRITE6 },
|
|
14055
15252
|
coordinates: { in: ["strokes[].points[]"], out: ["bounds"] },
|
|
14056
15253
|
handler: async (a) => {
|
|
14057
15254
|
await c.wetIdle();
|
|
@@ -14105,7 +15302,7 @@ function createMutationTools(c, options = {}) {
|
|
|
14105
15302
|
required: ["id"],
|
|
14106
15303
|
additionalProperties: false
|
|
14107
15304
|
},
|
|
14108
|
-
annotations: { title: "Decline a mutation", ...
|
|
15305
|
+
annotations: { title: "Decline a mutation", ...SET5 },
|
|
14109
15306
|
coordinates: { out: ["bounds"] },
|
|
14110
15307
|
handler: (a) => c.declineMutation(a.id, a.reason)
|
|
14111
15308
|
}
|
|
@@ -14113,8 +15310,8 @@ function createMutationTools(c, options = {}) {
|
|
|
14113
15310
|
}
|
|
14114
15311
|
|
|
14115
15312
|
// src/paint-editor/evolution-tools.ts
|
|
14116
|
-
var
|
|
14117
|
-
var
|
|
15313
|
+
var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15314
|
+
var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14118
15315
|
var UNIT2 = (description) => ({ type: "number", minimum: 0, maximum: 1, description });
|
|
14119
15316
|
var EVOLUTION_MUTATION_SCHEMA = {
|
|
14120
15317
|
type: "object",
|
|
@@ -14159,7 +15356,7 @@ function createPaintEvolutionTools(c) {
|
|
|
14159
15356
|
required: ["strokeIds"],
|
|
14160
15357
|
additionalProperties: false
|
|
14161
15358
|
},
|
|
14162
|
-
annotations: { title: "Breed stroke variants", ...
|
|
15359
|
+
annotations: { title: "Breed stroke variants", ...WRITE7 },
|
|
14163
15360
|
coordinates: { out: ["proposals[].bounds"] },
|
|
14164
15361
|
handler: ({ strokeIds, count, mutation, seed }, context) => {
|
|
14165
15362
|
const actor = context?.actor?.id;
|
|
@@ -14202,7 +15399,7 @@ function createPaintEvolutionTools(c) {
|
|
|
14202
15399
|
required: ["proposalIds"],
|
|
14203
15400
|
additionalProperties: false
|
|
14204
15401
|
},
|
|
14205
|
-
annotations: { title: "Breed from paint proposals", ...
|
|
15402
|
+
annotations: { title: "Breed from paint proposals", ...WRITE7 },
|
|
14206
15403
|
coordinates: { out: ["proposals[].bounds"] },
|
|
14207
15404
|
handler: ({ proposalIds, count, mutation, seed }, context) => {
|
|
14208
15405
|
const actor = context?.actor?.id;
|
|
@@ -14230,7 +15427,7 @@ function createPaintEvolutionTools(c) {
|
|
|
14230
15427
|
required: ["proposalId"],
|
|
14231
15428
|
additionalProperties: false
|
|
14232
15429
|
},
|
|
14233
|
-
annotations: { title: "Get a proposal\u2019s stroke genome", ...
|
|
15430
|
+
annotations: { title: "Get a proposal\u2019s stroke genome", ...READ8 },
|
|
14234
15431
|
coordinates: { out: ["bounds", "strokes[].points[]"] },
|
|
14235
15432
|
handler: ({ proposalId }) => c.getProposalGenome(proposalId)
|
|
14236
15433
|
}
|
|
@@ -14249,13 +15446,13 @@ function createPaintEvolutionToolProvider(controller, options = {}) {
|
|
|
14249
15446
|
}
|
|
14250
15447
|
|
|
14251
15448
|
// src/paint-editor/tools.ts
|
|
14252
|
-
var
|
|
14253
|
-
var
|
|
14254
|
-
var
|
|
15449
|
+
var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15450
|
+
var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
15451
|
+
var SET6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14255
15452
|
var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
14256
|
-
var
|
|
15453
|
+
var EMPTY4 = { type: "object", properties: {}, additionalProperties: false };
|
|
14257
15454
|
var COLOR3 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
|
|
14258
|
-
var
|
|
15455
|
+
var LAYER_ID5 = { type: "string", description: "Layer id from list_layers (default the active layer)." };
|
|
14259
15456
|
var TIME = { type: "number", minimum: 0, description: "Draw live effect paint (strokes on stroke layers painted with live effect paint) this many ms into its animation (default: as painted, its own time)." };
|
|
14260
15457
|
var POINTS2 = {
|
|
14261
15458
|
type: "array",
|
|
@@ -14298,8 +15495,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14298
15495
|
name: "get_canvas_info",
|
|
14299
15496
|
title: "Get the canvas",
|
|
14300
15497
|
description: "Reads the canvas: size in pixels and dpi, its origin (where (0, 0) is relative to the canvas it was extended from), bounds (the rectangle the painting covers, in the coordinates every other tool takes), the paper (preset, colour, grain depth), the active layer, the active brush id, the paint colour, the view (zoom, rotation, mirror), the hand, how many strokes are recorded, the undo / redo labels, and the blend modes. Call it first.",
|
|
14301
|
-
inputSchema:
|
|
14302
|
-
annotations: { title: "Get the canvas", ...
|
|
15498
|
+
inputSchema: EMPTY4,
|
|
15499
|
+
annotations: { title: "Get the canvas", ...READ9 },
|
|
14303
15500
|
coordinates: { out: ["bounds"] },
|
|
14304
15501
|
handler: () => {
|
|
14305
15502
|
const s = c.getState();
|
|
@@ -14360,7 +15557,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14360
15557
|
},
|
|
14361
15558
|
additionalProperties: false
|
|
14362
15559
|
},
|
|
14363
|
-
annotations: { title: "Resize the canvas", ...
|
|
15560
|
+
annotations: { title: "Resize the canvas", ...SET6 },
|
|
14364
15561
|
handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
|
|
14365
15562
|
});
|
|
14366
15563
|
const EXTEND_SIDE = (side) => ({ type: "integer", minimum: 0, maximum: MAX_CANVAS_SIDE - 1, description: `Pixels to add on the ${side} (default 0).` });
|
|
@@ -14373,15 +15570,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14373
15570
|
properties: { left: EXTEND_SIDE("left"), top: EXTEND_SIDE("top"), right: EXTEND_SIDE("right"), bottom: EXTEND_SIDE("bottom") },
|
|
14374
15571
|
additionalProperties: false
|
|
14375
15572
|
},
|
|
14376
|
-
annotations: { title: "Extend the canvas", ...
|
|
15573
|
+
annotations: { title: "Extend the canvas", ...WRITE8 },
|
|
14377
15574
|
handler: (a) => c.extendCanvas({ left: a.left, top: a.top, right: a.right, bottom: a.bottom })
|
|
14378
15575
|
});
|
|
14379
15576
|
add({
|
|
14380
15577
|
name: "list_papers",
|
|
14381
15578
|
title: "List papers",
|
|
14382
15579
|
description: "Lists the built-in paper library (generated grain, no textures): id, name, family, grain pattern, colour, grain depth and the wet-media properties (absorption, permeability, roughness). Use an id with set_paper or new_canvas.",
|
|
14383
|
-
inputSchema:
|
|
14384
|
-
annotations: { title: "List papers", ...
|
|
15580
|
+
inputSchema: EMPTY4,
|
|
15581
|
+
annotations: { title: "List papers", ...READ9 },
|
|
14385
15582
|
handler: () => ({
|
|
14386
15583
|
papers: PAPER_LIBRARY.map((p) => ({ id: p.preset, name: p.name, family: p.family, pattern: p.pattern, color: p.color, depth: p.depth, absorption: p.absorption, permeability: p.permeability, roughness: p.roughness })),
|
|
14387
15584
|
families: [...PAPER_FAMILIES],
|
|
@@ -14392,8 +15589,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14392
15589
|
name: "get_paper",
|
|
14393
15590
|
title: "Get the paper",
|
|
14394
15591
|
description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness, and metallic, backlight and backlightColor when set).",
|
|
14395
|
-
inputSchema:
|
|
14396
|
-
annotations: { title: "Get the paper", ...
|
|
15592
|
+
inputSchema: EMPTY4,
|
|
15593
|
+
annotations: { title: "Get the paper", ...READ9 },
|
|
14397
15594
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
14398
15595
|
});
|
|
14399
15596
|
const tuning = {
|
|
@@ -14431,15 +15628,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14431
15628
|
},
|
|
14432
15629
|
additionalProperties: false
|
|
14433
15630
|
},
|
|
14434
|
-
annotations: { title: "Set the paper", ...
|
|
15631
|
+
annotations: { title: "Set the paper", ...SET6 },
|
|
14435
15632
|
handler: (a) => c.setPaper(a)
|
|
14436
15633
|
});
|
|
14437
15634
|
add({
|
|
14438
15635
|
name: "list_layers",
|
|
14439
15636
|
title: "List layers",
|
|
14440
15637
|
description: "Lists the layers top first: id, name, type (pixel or group), visibility, opacity, blend, clipping, locks, whether it is active, the painted bounds in document pixels (null when empty), parent and depth.",
|
|
14441
|
-
inputSchema:
|
|
14442
|
-
annotations: { title: "List layers", ...
|
|
15638
|
+
inputSchema: EMPTY4,
|
|
15639
|
+
annotations: { title: "List layers", ...READ9 },
|
|
14443
15640
|
coordinates: { out: ["layers[].bounds"] },
|
|
14444
15641
|
handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
|
|
14445
15642
|
});
|
|
@@ -14457,7 +15654,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14457
15654
|
},
|
|
14458
15655
|
additionalProperties: false
|
|
14459
15656
|
},
|
|
14460
|
-
annotations: { title: "Add a layer", ...
|
|
15657
|
+
annotations: { title: "Add a layer", ...WRITE8 },
|
|
14461
15658
|
coordinates: { out: ["bounds"] },
|
|
14462
15659
|
handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
|
|
14463
15660
|
});
|
|
@@ -14493,7 +15690,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14493
15690
|
required: ["layerId"],
|
|
14494
15691
|
additionalProperties: false
|
|
14495
15692
|
},
|
|
14496
|
-
annotations: { title: "Change a layer", ...
|
|
15693
|
+
annotations: { title: "Change a layer", ...SET6 },
|
|
14497
15694
|
coordinates: { out: ["bounds"] },
|
|
14498
15695
|
handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
|
|
14499
15696
|
});
|
|
@@ -14501,8 +15698,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14501
15698
|
name: "list_blend_modes",
|
|
14502
15699
|
title: "List blend modes",
|
|
14503
15700
|
description: "Lists every layer and brush blend mode: id, label, group (standard Photoshop modes, pigment, or painterly), what the pigment and painterly modes do, and the standard mode PSD export writes in their place. Changes nothing. Read it before choosing a painterly mode for update_layer or a brush tip.",
|
|
14504
|
-
inputSchema:
|
|
14505
|
-
annotations: { title: "List blend modes", ...
|
|
15701
|
+
inputSchema: EMPTY4,
|
|
15702
|
+
annotations: { title: "List blend modes", ...READ9 },
|
|
14506
15703
|
handler: () => ({
|
|
14507
15704
|
modes: BLEND_MODES.map((m) => ({
|
|
14508
15705
|
id: m.id,
|
|
@@ -14518,7 +15715,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14518
15715
|
title: "Choose the active layer",
|
|
14519
15716
|
description: "Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.",
|
|
14520
15717
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
14521
|
-
annotations: { title: "Choose the active layer", ...
|
|
15718
|
+
annotations: { title: "Choose the active layer", ...SET6 },
|
|
14522
15719
|
coordinates: { out: ["bounds"] },
|
|
14523
15720
|
handler: ({ layerId }) => {
|
|
14524
15721
|
c.setActiveLayer(layerId);
|
|
@@ -14538,7 +15735,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14538
15735
|
title: "Duplicate a layer",
|
|
14539
15736
|
description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
|
|
14540
15737
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
14541
|
-
annotations: { title: "Duplicate a layer", ...
|
|
15738
|
+
annotations: { title: "Duplicate a layer", ...WRITE8 },
|
|
14542
15739
|
coordinates: { out: ["bounds"] },
|
|
14543
15740
|
handler: ({ layerId }) => c.duplicateLayer(layerId)
|
|
14544
15741
|
});
|
|
@@ -14556,7 +15753,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14556
15753
|
required: ["layerId", "index"],
|
|
14557
15754
|
additionalProperties: false
|
|
14558
15755
|
},
|
|
14559
|
-
annotations: { title: "Reorder a layer", ...
|
|
15756
|
+
annotations: { title: "Reorder a layer", ...WRITE8 },
|
|
14560
15757
|
coordinates: { out: ["bounds"] },
|
|
14561
15758
|
handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
|
|
14562
15759
|
});
|
|
@@ -14576,7 +15773,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14576
15773
|
inputSchema: {
|
|
14577
15774
|
type: "object",
|
|
14578
15775
|
properties: {
|
|
14579
|
-
layerId:
|
|
15776
|
+
layerId: LAYER_ID5,
|
|
14580
15777
|
color: { ...COLOR3, description: "Fill colour #rrggbb (default the paint colour)." },
|
|
14581
15778
|
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
14582
15779
|
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
|
|
@@ -14585,7 +15782,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14585
15782
|
},
|
|
14586
15783
|
additionalProperties: false
|
|
14587
15784
|
},
|
|
14588
|
-
annotations: { title: "Fill a layer", ...
|
|
15785
|
+
annotations: { title: "Fill a layer", ...WRITE8 },
|
|
14589
15786
|
coordinates: { out: ["bounds"] },
|
|
14590
15787
|
handler: (a) => act({ kind: "fill", fill: a }, () => c.fillLayer(a))
|
|
14591
15788
|
});
|
|
@@ -14613,7 +15810,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14613
15810
|
additionalProperties: false
|
|
14614
15811
|
}
|
|
14615
15812
|
},
|
|
14616
|
-
layerId:
|
|
15813
|
+
layerId: LAYER_ID5,
|
|
14617
15814
|
reference: {
|
|
14618
15815
|
type: "string",
|
|
14619
15816
|
description: 'Which pixels stop the fill: "layer" (default, the target layer itself), "all" or "composite" (all visible layers without paper), or a layer id from list_layers (e.g. an ink line-art layer above the flats layer).'
|
|
@@ -14632,7 +15829,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14632
15829
|
},
|
|
14633
15830
|
additionalProperties: false
|
|
14634
15831
|
},
|
|
14635
|
-
annotations: { title: "Fill a region (bucket / flats)", ...
|
|
15832
|
+
annotations: { title: "Fill a region (bucket / flats)", ...WRITE8 },
|
|
14636
15833
|
coordinates: { in: ["", "points[]"], out: ["bounds", "regions[].point", "regions[].bounds"] },
|
|
14637
15834
|
handler: (a) => act({ kind: "fill-region", fill: a }, () => c.fillRegion(a))
|
|
14638
15835
|
});
|
|
@@ -14656,7 +15853,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14656
15853
|
},
|
|
14657
15854
|
additionalProperties: false
|
|
14658
15855
|
},
|
|
14659
|
-
annotations: { title: "Find enclosed unfilled regions", ...
|
|
15856
|
+
annotations: { title: "Find enclosed unfilled regions", ...READ9 },
|
|
14660
15857
|
coordinates: { out: ["regions[].point", "regions[].centroid", "regions[].bounds"] },
|
|
14661
15858
|
handler: (a) => c.findUnfilledRegions(a)
|
|
14662
15859
|
});
|
|
@@ -14664,7 +15861,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14664
15861
|
name: "clear_layer",
|
|
14665
15862
|
title: "Clear a layer",
|
|
14666
15863
|
description: "Removes every pixel of a pixel layer. One undo step. Returns the layer id and the bounds that were cleared (null if it was empty).",
|
|
14667
|
-
inputSchema: { type: "object", properties: { layerId:
|
|
15864
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, additionalProperties: false },
|
|
14668
15865
|
annotations: { title: "Clear a layer", ...DESTRUCTIVE3 },
|
|
14669
15866
|
coordinates: { out: ["bounds"] },
|
|
14670
15867
|
handler: ({ layerId }) => c.clearLayer(layerId)
|
|
@@ -14674,7 +15871,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14674
15871
|
title: "Get a brush",
|
|
14675
15872
|
description: "Reads a brush as facets (kind, medium, variety or tool, tip, marks, dynamics, behaviour, light, motion, colour: the brush library's form, as describe_brush returns it), with the engine it paints with and whether it is an unchanged built-in preset. Without id, the active brush as edited. Also returns strokePaint: what a stroke with it would paint now (its gradient colour, else the editor's paint from set_paint). Changes nothing.",
|
|
14676
15873
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." } }, additionalProperties: false },
|
|
14677
|
-
annotations: { title: "Get a brush", ...
|
|
15874
|
+
annotations: { title: "Get a brush", ...READ9 },
|
|
14678
15875
|
handler: ({ id }) => {
|
|
14679
15876
|
const st = c.getState();
|
|
14680
15877
|
const b = id === void 0 ? st.brush : c.getBrush(id);
|
|
@@ -14688,7 +15885,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14688
15885
|
title: "Set the brush",
|
|
14689
15886
|
description: "Chooses the active brush (id, from list_brush_presets) and / or changes it with facets laid over it (objects merge, lists and values replace, marks of another kind replace the marks; the brush library's create_brush fields); unset removes whole facets first (unset colour drops a brush's own gradient). Strokes without a brush use it. Editor state, not an undo step: the changes stay on the active brush until the library's create_brush or update_brush stores a brush. Returns the active brush as facets, the engine it paints with, and problems (values clamped or dropped).",
|
|
14690
15887
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets to start from (default the active brush)." }, name: { type: "string", minLength: 1, maxLength: 80, description: "Display name." }, ...BRUSH_PATCH_PROPERTIES, unset: BRUSH_UNSET_PROPERTY }, additionalProperties: false },
|
|
14691
|
-
annotations: { title: "Set the brush", ...
|
|
15888
|
+
annotations: { title: "Set the brush", ...SET6 },
|
|
14692
15889
|
handler: async ({ id, unset, ...patch }) => {
|
|
14693
15890
|
const stamps = patch.tip?.stamps;
|
|
14694
15891
|
if (stamps) await c.prepareStickers(stamps);
|
|
@@ -14699,24 +15896,33 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14699
15896
|
add({
|
|
14700
15897
|
name: "set_color",
|
|
14701
15898
|
title: "Set the paint colour",
|
|
14702
|
-
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.",
|
|
15899
|
+
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). dualColor sets a light / dark pair instead: strokes record the pair, and on a stroke layer each viewer sees them in the side of their own colour scheme (a pixel layer keeps the side painted). Editor state, not an undo step. Returns both colours and locked (and dualColor when given).",
|
|
14703
15900
|
inputSchema: {
|
|
14704
15901
|
type: "object",
|
|
14705
15902
|
properties: {
|
|
14706
15903
|
color: { ...COLOR3, description: "Paint colour #rrggbb." },
|
|
15904
|
+
dualColor: {
|
|
15905
|
+
type: "object",
|
|
15906
|
+
description: "A light / dark pair as the paint colour, instead of color: each side a colour #rrggbb.",
|
|
15907
|
+
properties: { light: { ...COLOR3, description: "The colour in the light scheme, #rrggbb." }, dark: { ...COLOR3, description: "The colour in the dark scheme, #rrggbb." } },
|
|
15908
|
+
required: ["light", "dark"],
|
|
15909
|
+
additionalProperties: false
|
|
15910
|
+
},
|
|
14707
15911
|
secondaryColor: { ...COLOR3, description: "Second colour #rrggbb." },
|
|
14708
15912
|
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)." }
|
|
14709
15913
|
},
|
|
14710
15914
|
additionalProperties: false
|
|
14711
15915
|
},
|
|
14712
|
-
annotations: { title: "Set the paint colour", ...
|
|
14713
|
-
handler: ({ color, secondaryColor, locked }) => {
|
|
14714
|
-
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
15916
|
+
annotations: { title: "Set the paint colour", ...SET6 },
|
|
15917
|
+
handler: ({ color, dualColor, secondaryColor, locked }) => {
|
|
15918
|
+
if (color === void 0 && dualColor === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, dualColor, secondaryColor or locked.");
|
|
15919
|
+
if (color !== void 0 && dualColor !== void 0) throw new Error("Pass color or dualColor, not both.");
|
|
14715
15920
|
if (color !== void 0) c.setColor(color);
|
|
15921
|
+
if (dualColor !== void 0) c.setColor(dualColor);
|
|
14716
15922
|
if (secondaryColor !== void 0) c.setSecondaryColor(secondaryColor);
|
|
14717
15923
|
if (locked !== void 0) c.setColorLocked(locked);
|
|
14718
15924
|
const s = c.getState();
|
|
14719
|
-
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked };
|
|
15925
|
+
return { color: s.color, secondaryColor: s.secondaryColor, locked: s.colorLocked, ...dualColor !== void 0 ? { dualColor: { light: dualColor.light.toLowerCase(), dark: dualColor.dark.toLowerCase() } } : {} };
|
|
14720
15926
|
}
|
|
14721
15927
|
});
|
|
14722
15928
|
add({
|
|
@@ -14724,7 +15930,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14724
15930
|
title: "Set the paint (colour or gradient)",
|
|
14725
15931
|
description: 'Sets what strokes paint with: one colour, or gradient paint that changes colour as the stroke goes. Gradient paint: 2..16 stops sorted by offset (#rrggbb, or "paint" / "secondary" for the two editor colours), space pigment (default: mixes like paint) / oklab / srgb, and map: distance (default: the colour changes as the stroke is dragged out, 300 px then mirrored back), pressure, tilt, velocity, direction, time, across-tip or random. Every stroking brush samples it per dab (dab, mixer, watercolour and gouache, oil and impasto, particle, bristle, sticker); erasers, blenders and smudges ignore it, and a brush with its own gradient (get_brush paint) paints that instead. It also ends effect paint (set_effect_paint); set_color goes back to one colour. Editor state, not an undo step. Returns paint, color and secondaryColor.',
|
|
14726
15932
|
inputSchema: { type: "object", properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ["paint"], additionalProperties: false },
|
|
14727
|
-
annotations: { title: "Set the paint (colour or gradient)", ...
|
|
15933
|
+
annotations: { title: "Set the paint (colour or gradient)", ...SET6 },
|
|
14728
15934
|
handler: ({ paint }) => {
|
|
14729
15935
|
const p = c.setPaint(paint);
|
|
14730
15936
|
const s = c.getState();
|
|
@@ -14745,7 +15951,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14745
15951
|
required: ["x", "y"],
|
|
14746
15952
|
additionalProperties: false
|
|
14747
15953
|
},
|
|
14748
|
-
annotations: { title: "Pick a colour", ...
|
|
15954
|
+
annotations: { title: "Pick a colour", ...READ9 },
|
|
14749
15955
|
coordinates: { in: [""] },
|
|
14750
15956
|
handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
|
|
14751
15957
|
});
|
|
@@ -14753,8 +15959,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14753
15959
|
name: "get_mixer_brush",
|
|
14754
15960
|
title: "Get the Mixer Brush load",
|
|
14755
15961
|
description: "Reads what the brush carries between strokes: reservoir #rrggbb or null (the Mixer Brush's loaded paint), amount 0..1 left, pickup #rrggbb or null (paint picked up from the canvas, kept by dirty brushes and a Mixer Brush that does not clean each stroke). Call before painting with mixer-brush when its loadEachStroke is false, or with a dirty brush.",
|
|
14756
|
-
inputSchema:
|
|
14757
|
-
annotations: { title: "Get the Mixer Brush load", ...
|
|
15962
|
+
inputSchema: EMPTY4,
|
|
15963
|
+
annotations: { title: "Get the Mixer Brush load", ...READ9 },
|
|
14758
15964
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
14759
15965
|
});
|
|
14760
15966
|
add({
|
|
@@ -14762,7 +15968,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14762
15968
|
title: "Load the Mixer Brush",
|
|
14763
15969
|
description: "Fills the Mixer Brush reservoir with a colour (amount 1), keeping what it picked up. Editor state, not an undo step. Returns the load.",
|
|
14764
15970
|
inputSchema: { type: "object", properties: { color: { ...COLOR3, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
|
|
14765
|
-
annotations: { title: "Load the Mixer Brush", ...
|
|
15971
|
+
annotations: { title: "Load the Mixer Brush", ...SET6 },
|
|
14766
15972
|
handler: ({ color }) => loadResult(c.loadMixerBrush(color))
|
|
14767
15973
|
});
|
|
14768
15974
|
add({
|
|
@@ -14770,7 +15976,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14770
15976
|
title: "Clean the Mixer Brush",
|
|
14771
15977
|
description: "Cleans the brush: empties the picked-up paint a dirty brush or the Mixer Brush carries (and with emptyReservoir the Mixer Brush paint too). Editor state, not an undo step. Returns the load.",
|
|
14772
15978
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
14773
|
-
annotations: { title: "Clean the Mixer Brush", ...
|
|
15979
|
+
annotations: { title: "Clean the Mixer Brush", ...SET6 },
|
|
14774
15980
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
14775
15981
|
});
|
|
14776
15982
|
add({
|
|
@@ -14781,7 +15987,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14781
15987
|
type: "object",
|
|
14782
15988
|
properties: {
|
|
14783
15989
|
points: POINTS2,
|
|
14784
|
-
layerId:
|
|
15990
|
+
layerId: LAYER_ID5,
|
|
14785
15991
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
14786
15992
|
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
14787
15993
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
@@ -14792,7 +15998,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14792
15998
|
required: ["points"],
|
|
14793
15999
|
additionalProperties: false
|
|
14794
16000
|
},
|
|
14795
|
-
annotations: { title: "Paint a brush stroke", ...
|
|
16001
|
+
annotations: { title: "Paint a brush stroke", ...WRITE8 },
|
|
14796
16002
|
coordinates: { in: ["points[]"], out: ["bounds"] },
|
|
14797
16003
|
handler: async (a) => {
|
|
14798
16004
|
await c.wetIdle();
|
|
@@ -14819,7 +16025,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14819
16025
|
spacing: { type: "number", minimum: 0.25, maximum: 100, description: "Distance between samples along the path in document pixels (default 2); the brush spaces its own dabs, so this only sets how closely curves are followed." },
|
|
14820
16026
|
pressure: { type: "array", minItems: 1, maxItems: 64, items: { type: "number", minimum: 0, maximum: 1 }, description: "Pen pressure along each subpath, spread evenly from start to end (default full pressure); [0.2, 1, 0.2] tapers both ends." },
|
|
14821
16027
|
speed: { type: "number", minimum: 0.01, maximum: 100, description: "Drawing speed in document pixels per millisecond (optional; gives each sample a time t so playback has a pace and speed-sensitive brushes respond)." },
|
|
14822
|
-
layerId:
|
|
16028
|
+
layerId: LAYER_ID5,
|
|
14823
16029
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
14824
16030
|
brush: { type: "object", description: "Facets to change for this stroke only, laid over brushId or the active brush (as set_brush takes them).", properties: BRUSH_PATCH_PROPERTIES, additionalProperties: false },
|
|
14825
16031
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
@@ -14829,7 +16035,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14829
16035
|
required: ["path"],
|
|
14830
16036
|
additionalProperties: false
|
|
14831
16037
|
},
|
|
14832
|
-
annotations: { title: "Brush along a path", ...
|
|
16038
|
+
annotations: { title: "Brush along a path", ...WRITE8 },
|
|
14833
16039
|
coordinates: { out: ["bounds"] },
|
|
14834
16040
|
handler: async (a) => {
|
|
14835
16041
|
await c.wetIdle();
|
|
@@ -14883,7 +16089,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14883
16089
|
properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
|
|
14884
16090
|
additionalProperties: false
|
|
14885
16091
|
},
|
|
14886
|
-
annotations: { title: "Get the stroke being painted", ...
|
|
16092
|
+
annotations: { title: "Get the stroke being painted", ...READ9 },
|
|
14887
16093
|
handler: (args) => {
|
|
14888
16094
|
const live = c.getLiveStroke();
|
|
14889
16095
|
if (!live) return { live: false };
|
|
@@ -14905,7 +16111,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14905
16111
|
},
|
|
14906
16112
|
additionalProperties: false
|
|
14907
16113
|
},
|
|
14908
|
-
annotations: { title: "List recorded strokes", ...
|
|
16114
|
+
annotations: { title: "List recorded strokes", ...READ9 },
|
|
14909
16115
|
coordinates: { out: ["strokes[].bounds", "strokes[].points[]"] },
|
|
14910
16116
|
handler: ({ ids, last, live, layerId }) => {
|
|
14911
16117
|
if (layerId !== void 0 && !c.getLayer(layerId)) throw new Error(`Unknown layer "${layerId}". Call list_layers for the ids.`);
|
|
@@ -14934,7 +16140,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14934
16140
|
required: ["x", "y"],
|
|
14935
16141
|
additionalProperties: false
|
|
14936
16142
|
},
|
|
14937
|
-
annotations: { title: "Find the stroke at a point", ...
|
|
16143
|
+
annotations: { title: "Find the stroke at a point", ...READ9 },
|
|
14938
16144
|
coordinates: { in: [""], out: ["stroke.bounds"] },
|
|
14939
16145
|
handler: ({ layerId, x, y }) => {
|
|
14940
16146
|
const s = c.liveStrokeAt(layerId, x, y);
|
|
@@ -14958,7 +16164,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14958
16164
|
required: ["id"],
|
|
14959
16165
|
additionalProperties: false
|
|
14960
16166
|
},
|
|
14961
|
-
annotations: { title: "Change a stroke", ...
|
|
16167
|
+
annotations: { title: "Change a stroke", ...WRITE8 },
|
|
14962
16168
|
coordinates: { out: ["bounds", "stroke.bounds"] },
|
|
14963
16169
|
handler: ({ id, ...update }) => {
|
|
14964
16170
|
if (!Object.keys(update).length) throw new Error("Pass at least one change: brushId, brush, color, paint or seed.");
|
|
@@ -14997,7 +16203,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14997
16203
|
required: ["id"],
|
|
14998
16204
|
additionalProperties: false
|
|
14999
16205
|
},
|
|
15000
|
-
annotations: { title: "Move or reshape a stroke", ...
|
|
16206
|
+
annotations: { title: "Move or reshape a stroke", ...WRITE8 },
|
|
15001
16207
|
coordinates: { in: ["points[]", "transform.origin"], out: ["bounds", "stroke.bounds", "stroke.points[]"] },
|
|
15002
16208
|
handler: async ({ id, points, transform }) => {
|
|
15003
16209
|
await c.prepareStrokeEdit(id, { points, transform });
|
|
@@ -15015,7 +16221,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15015
16221
|
required: ["id", "index"],
|
|
15016
16222
|
additionalProperties: false
|
|
15017
16223
|
},
|
|
15018
|
-
annotations: { title: "Move a stroke up or down", ...
|
|
16224
|
+
annotations: { title: "Move a stroke up or down", ...WRITE8 },
|
|
15019
16225
|
handler: ({ id, index }) => ({ order: c.reorderStroke(id, index) })
|
|
15020
16226
|
});
|
|
15021
16227
|
add({
|
|
@@ -15037,7 +16243,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15037
16243
|
title: "Make a layer a stroke layer",
|
|
15038
16244
|
description: 'Makes an empty pixel layer a stroke layer, so strokes painted on it stay editable. Refuses a layer with paint (add_layer {type: "strokes"} instead). One undo step. Returns the layer.',
|
|
15039
16245
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers (default the active layer)." } }, additionalProperties: false },
|
|
15040
|
-
annotations: { title: "Make a layer a stroke layer", ...
|
|
16246
|
+
annotations: { title: "Make a layer a stroke layer", ...WRITE8 },
|
|
15041
16247
|
coordinates: { out: ["bounds"] },
|
|
15042
16248
|
handler: ({ layerId }) => c.convertToLiveStrokes(layerId)
|
|
15043
16249
|
});
|
|
@@ -15079,7 +16285,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15079
16285
|
required: ["strokeIds"],
|
|
15080
16286
|
additionalProperties: false
|
|
15081
16287
|
},
|
|
15082
|
-
annotations: { title: "Replay strokes, varied", ...
|
|
16288
|
+
annotations: { title: "Replay strokes, varied", ...WRITE8 },
|
|
15083
16289
|
coordinates: { in: ["origin"], out: ["bounds"] },
|
|
15084
16290
|
handler: async (a) => {
|
|
15085
16291
|
const replay = a;
|
|
@@ -15101,7 +16307,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15101
16307
|
},
|
|
15102
16308
|
additionalProperties: false
|
|
15103
16309
|
},
|
|
15104
|
-
annotations: { title: "Rotate, mirror or zoom the view", ...
|
|
16310
|
+
annotations: { title: "Rotate, mirror or zoom the view", ...SET6 },
|
|
15105
16311
|
handler: ({ reset, ...v }) => {
|
|
15106
16312
|
if (reset) c.resetView();
|
|
15107
16313
|
return c.setView(v);
|
|
@@ -15112,15 +16318,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15112
16318
|
title: "Set the drawing hand",
|
|
15113
16319
|
description: "Says which hand holds the pen; palm rejection ignores touches on that side of the pen. Editor state, not an undo step. Returns the hand.",
|
|
15114
16320
|
inputSchema: { type: "object", properties: { hand: { type: "string", enum: ["right", "left"], description: "right or left." } }, required: ["hand"], additionalProperties: false },
|
|
15115
|
-
annotations: { title: "Set the drawing hand", ...
|
|
16321
|
+
annotations: { title: "Set the drawing hand", ...SET6 },
|
|
15116
16322
|
handler: ({ hand }) => ({ hand: c.setHand(hand) })
|
|
15117
16323
|
});
|
|
15118
16324
|
add({
|
|
15119
16325
|
name: "undo_paint",
|
|
15120
16326
|
title: "Undo",
|
|
15121
16327
|
description: "Undoes the last step (a stroke, a fill, a layer or paper change). Returns the label undone (null when there was nothing) and what can be undone and redone next.",
|
|
15122
|
-
inputSchema:
|
|
15123
|
-
annotations: { title: "Undo", ...
|
|
16328
|
+
inputSchema: EMPTY4,
|
|
16329
|
+
annotations: { title: "Undo", ...WRITE8 },
|
|
15124
16330
|
handler: () => {
|
|
15125
16331
|
const undone = c.undo();
|
|
15126
16332
|
const s = c.getState();
|
|
@@ -15131,8 +16337,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15131
16337
|
name: "redo_paint",
|
|
15132
16338
|
title: "Redo",
|
|
15133
16339
|
description: "Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.",
|
|
15134
|
-
inputSchema:
|
|
15135
|
-
annotations: { title: "Redo", ...
|
|
16340
|
+
inputSchema: EMPTY4,
|
|
16341
|
+
annotations: { title: "Redo", ...WRITE8 },
|
|
15136
16342
|
handler: () => {
|
|
15137
16343
|
const redone = c.redo();
|
|
15138
16344
|
const s = c.getState();
|
|
@@ -15152,7 +16358,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15152
16358
|
},
|
|
15153
16359
|
additionalProperties: false
|
|
15154
16360
|
},
|
|
15155
|
-
annotations: { title: "Render a preview", ...
|
|
16361
|
+
annotations: { title: "Render a preview", ...READ9 },
|
|
15156
16362
|
handler: ({ maxSide, layerIds, paper }) => {
|
|
15157
16363
|
if (layerIds) {
|
|
15158
16364
|
for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
|
|
@@ -15176,7 +16382,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15176
16382
|
},
|
|
15177
16383
|
additionalProperties: false
|
|
15178
16384
|
},
|
|
15179
|
-
annotations: { title: "Export the painting", ...
|
|
16385
|
+
annotations: { title: "Export the painting", ...READ9 },
|
|
15180
16386
|
handler: async ({ format, quality, scale, paper, time }) => {
|
|
15181
16387
|
const run = () => c.exportImage({ format, quality, scale, paper });
|
|
15182
16388
|
const bytes = time === void 0 ? await run() : await c.withEffectTime(time, run);
|
|
@@ -15188,7 +16394,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15188
16394
|
title: "Export as PSD",
|
|
15189
16395
|
description: 'Writes the painting as an 8-bit RGB PSD (PSB above 30000 px) through the shared PSD writer: every layer and group with its opacity, blend, mask and locks, the paper as a locked "Paper" pixel layer at the bottom, and the composite as the merged image. Changes nothing. Returns the file as base64, its size, whether it is a PSB, and warnings (pigment and painterly blend modes have no PSD equivalent: they are written as the nearest standard mode, see list_blend_modes).',
|
|
15190
16396
|
inputSchema: { type: "object", properties: { time: TIME }, additionalProperties: false },
|
|
15191
|
-
annotations: { title: "Export as PSD", ...
|
|
16397
|
+
annotations: { title: "Export as PSD", ...READ9 },
|
|
15192
16398
|
handler: async ({ time }) => {
|
|
15193
16399
|
const r = time === void 0 ? c.exportPsd() : await c.withEffectTime(time, () => c.exportPsd());
|
|
15194
16400
|
return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
|
|
@@ -15197,6 +16403,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15197
16403
|
tools.push(...createPaintGuideTools(c));
|
|
15198
16404
|
tools.push(...createPaintWetTools(c));
|
|
15199
16405
|
tools.push(...createPaintThickTools(c));
|
|
16406
|
+
tools.push(...createFerroTools(c));
|
|
15200
16407
|
tools.push(...createPaintReplayTools(c));
|
|
15201
16408
|
tools.push(...createPaintEvolutionTools(c));
|
|
15202
16409
|
tools.push(...createPaintStickerTools(c));
|
|
@@ -15920,6 +17127,10 @@ var EVOLUTION_SLIDERS = [
|
|
|
15920
17127
|
{ path: "brush", label: "Brush", min: 0, max: 1, step: 0.05 },
|
|
15921
17128
|
{ path: "seed", label: "Seed", min: 0, max: 1, step: 0.05 }
|
|
15922
17129
|
];
|
|
17130
|
+
var MAGNET_SLIDERS = [
|
|
17131
|
+
{ path: "strength", label: "Strength", min: -1, max: 1, step: 0.05 },
|
|
17132
|
+
{ path: "radius", label: "Radius", min: 4, max: 512, step: 1 }
|
|
17133
|
+
];
|
|
15923
17134
|
function quickStencil(kind, canvas) {
|
|
15924
17135
|
const { width: w, height: h } = canvas;
|
|
15925
17136
|
switch (kind) {
|
|
@@ -16205,8 +17416,8 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
16205
17416
|
}
|
|
16206
17417
|
|
|
16207
17418
|
// src/paint-editor/collaboration-tools.ts
|
|
16208
|
-
var
|
|
16209
|
-
var
|
|
17419
|
+
var READ10 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
17420
|
+
var WRITE9 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
16210
17421
|
var proposalIdArg = {
|
|
16211
17422
|
type: "object",
|
|
16212
17423
|
properties: { proposalId: { type: "string", minLength: 1, description: "Id of a pending paint proposal, from propose_paint or list_paint_proposals." } },
|
|
@@ -16284,7 +17495,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16284
17495
|
required: ["title", "ops"],
|
|
16285
17496
|
additionalProperties: false
|
|
16286
17497
|
},
|
|
16287
|
-
annotations: { title: "Propose paint", ...
|
|
17498
|
+
annotations: { title: "Propose paint", ...WRITE9 },
|
|
16288
17499
|
handler: (args, context) => {
|
|
16289
17500
|
const origin = context.actor?.id ? `agent:${context.actor.id}` : options.fallbackOrigin;
|
|
16290
17501
|
const proposal = proposals.propose({ ...args, ...origin ? { origin } : {} });
|
|
@@ -16297,7 +17508,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16297
17508
|
title: "List paint proposals",
|
|
16298
17509
|
description: "Lists the pending paint proposals (ghost paint not yet on the canvas): id, title, rationale, who proposed it, its op kinds, where it goes, where the ghost sits, the strokes it echoes and any of those since removed (missing: it can no longer be accepted), and whether accepting is allowed. Call before accept_paint_proposal or reject_paint_proposal.",
|
|
16299
17510
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
16300
|
-
annotations: { title: "List paint proposals", ...
|
|
17511
|
+
annotations: { title: "List paint proposals", ...READ10 },
|
|
16301
17512
|
handler: () => ({ proposals: proposals.getState().proposals.map(describe), readOnly: readOnlyReason() })
|
|
16302
17513
|
};
|
|
16303
17514
|
const acceptTool = {
|
|
@@ -16305,7 +17516,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16305
17516
|
title: "Accept a paint proposal",
|
|
16306
17517
|
description: "Paints a pending proposal on a new layer as one undo step, recorded as made by whoever proposed it, and drops it from the pending list; the user's active layer stays theirs. Refuses, changing nothing, when the painting is read-only or a stroke it echoes was removed. Returns the new layer id, the stroke ids painted and the undo step name.",
|
|
16307
17518
|
inputSchema: proposalIdArg,
|
|
16308
|
-
annotations: { title: "Accept a paint proposal", ...
|
|
17519
|
+
annotations: { title: "Accept a paint proposal", ...WRITE9 },
|
|
16309
17520
|
handler: (args) => {
|
|
16310
17521
|
const reason = readOnlyReason();
|
|
16311
17522
|
if (reason) throw new Error(reason);
|
|
@@ -16317,7 +17528,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
16317
17528
|
title: "Reject a paint proposal",
|
|
16318
17529
|
description: "Dismisses a pending paint proposal: its ghost disappears and the painting and its history are unchanged. Use it to withdraw a suggestion that no longer fits. Returns the dismissed proposal id and title.",
|
|
16319
17530
|
inputSchema: proposalIdArg,
|
|
16320
|
-
annotations: { title: "Reject a paint proposal", ...
|
|
17531
|
+
annotations: { title: "Reject a paint proposal", ...WRITE9 },
|
|
16321
17532
|
handler: (args) => {
|
|
16322
17533
|
const p = proposals.reject(args.proposalId);
|
|
16323
17534
|
return { proposalId: p.id, title: p.title, rejected: true };
|
|
@@ -16390,6 +17601,33 @@ var PAINT_DOCUMENT_JSON_SCHEMA = {
|
|
|
16390
17601
|
required: ["id", "layerId", "points", "brush"]
|
|
16391
17602
|
}
|
|
16392
17603
|
},
|
|
17604
|
+
ferro: { type: "object", additionalProperties: { type: "string" }, description: "Layer id \u2192 asset id of its liquid ferrofluid map." },
|
|
17605
|
+
ferroSettings: {
|
|
17606
|
+
type: "object",
|
|
17607
|
+
additionalProperties: {
|
|
17608
|
+
type: "object",
|
|
17609
|
+
properties: {
|
|
17610
|
+
wavelength: { type: "number", description: "Rosensweig spike spacing in pixels." },
|
|
17611
|
+
threshold: { type: "number", description: "Critical magnetic field threshold." }
|
|
17612
|
+
}
|
|
17613
|
+
},
|
|
17614
|
+
description: "Layer id \u2192 ferrofluid spike settings."
|
|
17615
|
+
},
|
|
17616
|
+
magnets: {
|
|
17617
|
+
type: "array",
|
|
17618
|
+
description: "Magnets on the canvas that pull liquid ferrofluid into spikes.",
|
|
17619
|
+
items: {
|
|
17620
|
+
type: "object",
|
|
17621
|
+
properties: {
|
|
17622
|
+
id: { type: "string", description: "Magnet id." },
|
|
17623
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
17624
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
17625
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Magnet dipole strength (-1..1)." },
|
|
17626
|
+
radius: { type: "number", minimum: 4, maximum: 512, description: "Magnet field falloff radius in pixels." }
|
|
17627
|
+
},
|
|
17628
|
+
required: ["id", "x", "y", "strength", "radius"]
|
|
17629
|
+
}
|
|
17630
|
+
},
|
|
16393
17631
|
fluor: {
|
|
16394
17632
|
type: "object",
|
|
16395
17633
|
description: "Per-layer UV-fluorescent ink channels (layerId \u2192 bounds + PNG data URL).",
|
|
@@ -16440,7 +17678,7 @@ function uiTables() {
|
|
|
16440
17678
|
brushOptions: BRUSH_OPTION_CONTROLS,
|
|
16441
17679
|
blendMenu: BLEND_MENU_GROUPS,
|
|
16442
17680
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
16443
|
-
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, uvLight: UV_LIGHT_SLIDERS, viewTilt: VIEW_TILT_SLIDERS, ghost: GHOST_SLIDERS, mutation: MUTATION_SLIDERS, livePatterns: LIVE_PATTERN_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
17681
|
+
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, uvLight: UV_LIGHT_SLIDERS, viewTilt: VIEW_TILT_SLIDERS, ghost: GHOST_SLIDERS, mutation: MUTATION_SLIDERS, livePatterns: LIVE_PATTERN_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS, magnet: MAGNET_SLIDERS },
|
|
16444
17682
|
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
16445
17683
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
16446
17684
|
wetGpu: WET_GPU_OPTIONS,
|
|
@@ -16517,6 +17755,7 @@ export {
|
|
|
16517
17755
|
DEFAULT_BRUSH_ID,
|
|
16518
17756
|
DEFAULT_DPI,
|
|
16519
17757
|
DEFAULT_DYNAMICS,
|
|
17758
|
+
DEFAULT_FERRO,
|
|
16520
17759
|
DEFAULT_FLOW,
|
|
16521
17760
|
DEFAULT_FLUORESCENCE,
|
|
16522
17761
|
DEFAULT_GHOST,
|
|
@@ -16525,6 +17764,8 @@ export {
|
|
|
16525
17764
|
DEFAULT_LIGHT,
|
|
16526
17765
|
DEFAULT_LIGHTING,
|
|
16527
17766
|
DEFAULT_LIVE_PATTERNS,
|
|
17767
|
+
DEFAULT_MAGNET_RADIUS,
|
|
17768
|
+
DEFAULT_MAGNET_STRENGTH,
|
|
16528
17769
|
DEFAULT_MULTI_COLOR,
|
|
16529
17770
|
DEFAULT_MUTATION,
|
|
16530
17771
|
DEFAULT_MUTATION_RATES,
|
|
@@ -16552,6 +17793,11 @@ export {
|
|
|
16552
17793
|
EVOLUTION_BRUSH_FACETS,
|
|
16553
17794
|
EVOLUTION_MUTATION_SCHEMA,
|
|
16554
17795
|
EVOLUTION_SLIDERS,
|
|
17796
|
+
EXTENSION_ASSET_REF,
|
|
17797
|
+
FERRO_CARRIER_RGB,
|
|
17798
|
+
FERRO_DEFAULT_GLOSS,
|
|
17799
|
+
FERRO_DEFAULT_METALLIC,
|
|
17800
|
+
FERRO_DEFAULT_RIM,
|
|
16555
17801
|
FilmBuffer,
|
|
16556
17802
|
GHOST_SLIDERS,
|
|
16557
17803
|
GHOST_TRIGGERS,
|
|
@@ -16576,6 +17822,7 @@ export {
|
|
|
16576
17822
|
LIVE_PATTERN_SLIDERS,
|
|
16577
17823
|
LIVE_STROKE_ENGINES,
|
|
16578
17824
|
LitCache,
|
|
17825
|
+
MAGNET_SLIDERS,
|
|
16579
17826
|
MAX_BRUSH_SIZE,
|
|
16580
17827
|
MAX_CANVAS_SIDE,
|
|
16581
17828
|
MAX_COORDINATE,
|
|
@@ -16688,9 +17935,11 @@ export {
|
|
|
16688
17935
|
addPaintGradientStop,
|
|
16689
17936
|
addPolygonArea,
|
|
16690
17937
|
advanceHeatField,
|
|
17938
|
+
advectFerro,
|
|
16691
17939
|
affineCopy,
|
|
16692
17940
|
applyAffine,
|
|
16693
17941
|
backlightOf,
|
|
17942
|
+
bakeFerroIntoThick,
|
|
16694
17943
|
brushEngineControls,
|
|
16695
17944
|
brushLoadColor,
|
|
16696
17945
|
brushLoadForStroke,
|
|
@@ -16707,6 +17956,7 @@ export {
|
|
|
16707
17956
|
computeUvSpill,
|
|
16708
17957
|
contactSheet,
|
|
16709
17958
|
createEvolutionTools,
|
|
17959
|
+
createFerroTools,
|
|
16710
17960
|
createHeatField,
|
|
16711
17961
|
createMutationTools,
|
|
16712
17962
|
createPaintAgentStrokes,
|
|
@@ -16751,11 +18001,20 @@ export {
|
|
|
16751
18001
|
effectPaintKey,
|
|
16752
18002
|
effectPaintSize,
|
|
16753
18003
|
emptiedLayers,
|
|
18004
|
+
emptyFerro,
|
|
16754
18005
|
emptyFluor,
|
|
16755
18006
|
emptyThermo,
|
|
16756
18007
|
emptyThickChannels,
|
|
16757
18008
|
engineBrushSourceIds,
|
|
16758
18009
|
exportProposalContactSheetPng,
|
|
18010
|
+
ferroCacheKey,
|
|
18011
|
+
ferroFieldAt,
|
|
18012
|
+
ferroFromJSON,
|
|
18013
|
+
ferroInfo,
|
|
18014
|
+
ferroSpikeHeightAt,
|
|
18015
|
+
ferroToJSON,
|
|
18016
|
+
ferroToShadow,
|
|
18017
|
+
ferroTwinBrush,
|
|
16759
18018
|
fillRegionMask,
|
|
16760
18019
|
finalizeThermoStroke,
|
|
16761
18020
|
findUnfilledRegionsMask,
|
|
@@ -16792,7 +18051,9 @@ export {
|
|
|
16792
18051
|
lightingKey,
|
|
16793
18052
|
liveStrokeBounds,
|
|
16794
18053
|
liveStrokeRefusal,
|
|
18054
|
+
magnetsFromJSON,
|
|
16795
18055
|
mapPenPoint,
|
|
18056
|
+
mergeFerro,
|
|
16796
18057
|
mergeFourChannel,
|
|
16797
18058
|
mix32,
|
|
16798
18059
|
mixColorOklch,
|
|
@@ -16814,6 +18075,8 @@ export {
|
|
|
16814
18075
|
normalizeBrushStroke,
|
|
16815
18076
|
normalizeCanvas,
|
|
16816
18077
|
normalizeDynamics,
|
|
18078
|
+
normalizeFerro,
|
|
18079
|
+
normalizeFerro as normalizeFerroSettings,
|
|
16817
18080
|
normalizeFlow,
|
|
16818
18081
|
normalizeFluorescence,
|
|
16819
18082
|
normalizeGhost,
|
|
@@ -16822,6 +18085,7 @@ export {
|
|
|
16822
18085
|
normalizeLight,
|
|
16823
18086
|
normalizeLighting,
|
|
16824
18087
|
normalizeLivePatterns,
|
|
18088
|
+
normalizeMagnet,
|
|
16825
18089
|
normalizeMultiColor,
|
|
16826
18090
|
normalizeMutation,
|
|
16827
18091
|
normalizeMutationRates,
|
|
@@ -16903,6 +18167,7 @@ export {
|
|
|
16903
18167
|
sampleHeatAt,
|
|
16904
18168
|
samplePath,
|
|
16905
18169
|
samplePressure,
|
|
18170
|
+
scaleFerro,
|
|
16906
18171
|
scaleFluor,
|
|
16907
18172
|
scalePaintGradient,
|
|
16908
18173
|
scaleStencil,
|
|
@@ -16911,6 +18176,7 @@ export {
|
|
|
16911
18176
|
seededStrokeVariation,
|
|
16912
18177
|
serializePaintDocument,
|
|
16913
18178
|
serializePaintDocumentExternal,
|
|
18179
|
+
shadowToFerro,
|
|
16914
18180
|
shiftHeatField,
|
|
16915
18181
|
solidPaint,
|
|
16916
18182
|
squaredDistanceTransform,
|