@pixerate/editor 0.275.1 → 0.277.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 +62 -62
- package/dist/animation-editor/index.js +6 -6
- package/dist/brushes/index.cjs +84 -71
- package/dist/brushes/index.js +20 -7
- package/dist/brushes/patch.d.cts +4 -4
- package/dist/brushes/patch.d.ts +4 -4
- package/dist/brushes/profiles.d.cts +4 -0
- package/dist/brushes/profiles.d.ts +4 -0
- package/dist/brushes/schema.d.cts +2 -0
- package/dist/brushes/schema.d.ts +2 -0
- package/dist/brushes/types.d.cts +32 -1
- package/dist/brushes/types.d.ts +32 -1
- package/dist/{brushstrokeRender-2RQL46WB.cjs → brushstrokeRender-7HOMKZNO.cjs} +21 -21
- package/dist/{brushstrokeRender-WGY4DZHF.js → brushstrokeRender-EIK3625T.js} +5 -5
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-T6KNJVBP.cjs → chunk-4NZTK7LM.cjs} +35 -35
- package/dist/{chunk-LMG57L5A.js → chunk-CTHWXW5M.js} +121 -8
- package/dist/{chunk-43XJC3PP.js → chunk-DWPFGWW5.js} +1 -1
- package/dist/{chunk-MSWLJI4G.js → chunk-FNIWHSLI.js} +2 -2
- package/dist/{chunk-JDXWLCXF.cjs → chunk-H3EME3S5.cjs} +2 -2
- package/dist/{chunk-Y2ISSVPH.js → chunk-HHX7ST6Z.js} +167 -9
- package/dist/{chunk-6QNDGMK7.cjs → chunk-HRUK5TFW.cjs} +187 -45
- package/dist/{chunk-NIBFKXMD.cjs → chunk-IFWLZZCW.cjs} +619 -2
- package/dist/{chunk-W6JDUXP3.js → chunk-J23JOHPR.js} +1 -1
- package/dist/{chunk-HVO6XCI4.js → chunk-JPB65KQ6.js} +1 -1
- package/dist/{chunk-Y354ETAE.cjs → chunk-JVFGK3DK.cjs} +69 -58
- package/dist/{chunk-ACTHSQOI.js → chunk-LDZOXESA.js} +618 -1
- package/dist/{chunk-IEEY5RJV.js → chunk-LZE2QZCT.js} +1 -1
- package/dist/{chunk-DHC4E2DU.cjs → chunk-MITQGHN3.cjs} +121 -8
- package/dist/{chunk-75SYKUOT.js → chunk-N4MCWQZ7.js} +1 -1
- package/dist/{chunk-G5TCMKX5.js → chunk-NJLPW4MM.js} +17 -6
- package/dist/{chunk-STLTOISW.cjs → chunk-NPPMKMCU.cjs} +17 -17
- package/dist/{chunk-OLZXXAER.cjs → chunk-PEQWARCP.cjs} +408 -250
- package/dist/{chunk-ZM7IKQ4X.js → chunk-PPDUCZZV.js} +152 -10
- package/dist/{chunk-RAPNRK4R.cjs → chunk-QBZEC2JJ.cjs} +88 -88
- package/dist/{chunk-RJGW3RQS.js → chunk-ROURHJYT.js} +1 -1
- package/dist/{chunk-62MFUHSI.cjs → chunk-UQXULAMQ.cjs} +16 -16
- package/dist/{chunk-YYZC2EQO.cjs → chunk-VNF7A2JA.cjs} +23 -23
- package/dist/{chunk-C6VB7CPY.cjs → chunk-WFSYZO5Z.cjs} +1 -1
- package/dist/image-editor/index.cjs +880 -852
- package/dist/image-editor/index.js +36 -8
- package/dist/paint/index.cjs +33 -5
- package/dist/paint/index.d.cts +1 -0
- package/dist/paint/index.d.ts +1 -0
- package/dist/paint/index.js +32 -4
- package/dist/paint/live-patterns.d.cts +149 -0
- package/dist/paint/live-patterns.d.ts +149 -0
- package/dist/paint/wet/world.d.cts +13 -0
- package/dist/paint/wet/world.d.ts +13 -0
- package/dist/paint-editor/brush-model.d.cts +21 -1
- package/dist/paint-editor/brush-model.d.ts +21 -1
- package/dist/paint-editor/controller.d.cts +102 -1
- package/dist/paint-editor/controller.d.ts +102 -1
- 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 +2291 -904
- package/dist/paint-editor/index.d.cts +2 -0
- package/dist/paint-editor/index.d.ts +2 -0
- package/dist/paint-editor/index.js +1533 -146
- package/dist/paint-editor/stroke.d.cts +14 -0
- package/dist/paint-editor/stroke.d.ts +14 -0
- 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 +7 -1
- package/dist/paint-editor/ui.d.ts +7 -1
- package/dist/paint-editor/wet-tools.d.cts +42 -0
- package/dist/paint-editor/wet-tools.d.ts +42 -0
- package/dist/paint-editor/wet.d.cts +22 -1
- package/dist/paint-editor/wet.d.ts +22 -1
- package/dist/vector/index.cjs +1002 -1002
- package/dist/vector/index.js +7 -7
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -0
- package/skills/pixerate-paint-editor/SKILL.md +11 -29
- 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 {
|
|
@@ -68,7 +68,7 @@ import "../chunk-ZIF4UJJ6.js";
|
|
|
68
68
|
import {
|
|
69
69
|
createPigmentTools,
|
|
70
70
|
pointerPressure
|
|
71
|
-
} from "../chunk-
|
|
71
|
+
} from "../chunk-JPB65KQ6.js";
|
|
72
72
|
import "../chunk-VWEWY4FY.js";
|
|
73
73
|
import "../chunk-HVQVHUTV.js";
|
|
74
74
|
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,
|
|
@@ -248,15 +250,18 @@ import {
|
|
|
248
250
|
normalizeWetParams,
|
|
249
251
|
pigmentOf,
|
|
250
252
|
wetSupport
|
|
251
|
-
} from "../chunk-
|
|
252
|
-
import "../chunk-
|
|
253
|
+
} from "../chunk-CTHWXW5M.js";
|
|
254
|
+
import "../chunk-J23JOHPR.js";
|
|
253
255
|
import {
|
|
254
256
|
CoverageMap,
|
|
257
|
+
DEFAULT_LIVE_PATTERNS,
|
|
255
258
|
DEFAULT_PAINT_GRADIENT,
|
|
256
259
|
DEFAULT_PAINT_GRADIENT_MAPPING,
|
|
257
260
|
EMPTY_MIXER_LOAD,
|
|
258
261
|
GRAIN_PATTERNS,
|
|
259
262
|
GRAIN_TILE,
|
|
263
|
+
LIVE_PATTERN_KINDS,
|
|
264
|
+
LIVE_PATTERN_PRESETS,
|
|
260
265
|
MAX_BRUSH_SIZE,
|
|
261
266
|
PAINT_COLOR_JSON_SCHEMA,
|
|
262
267
|
PAINT_GRADIENT_FULL_SPEED,
|
|
@@ -288,6 +293,7 @@ import {
|
|
|
288
293
|
isGradientPaint,
|
|
289
294
|
loadMixer,
|
|
290
295
|
normalizeHexColor,
|
|
296
|
+
normalizeLivePatterns,
|
|
291
297
|
paintGradientMappingFor,
|
|
292
298
|
paintSourceColor,
|
|
293
299
|
paintSourceCss,
|
|
@@ -300,7 +306,7 @@ import {
|
|
|
300
306
|
scalePaintGradient,
|
|
301
307
|
solidPaint,
|
|
302
308
|
updatePaintGradientStop
|
|
303
|
-
} from "../chunk-
|
|
309
|
+
} from "../chunk-LDZOXESA.js";
|
|
304
310
|
import {
|
|
305
311
|
TileableNoise,
|
|
306
312
|
hash2,
|
|
@@ -408,6 +414,442 @@ function createPaintDocument(options = {}) {
|
|
|
408
414
|
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
409
415
|
}
|
|
410
416
|
|
|
417
|
+
// src/paint-editor/ferro.ts
|
|
418
|
+
var DEFAULT_MAGNET_STRENGTH = 0.75;
|
|
419
|
+
var DEFAULT_MAGNET_RADIUS = 72;
|
|
420
|
+
var FERRO_CARRIER_RGB = [17, 24, 39];
|
|
421
|
+
var FERRO_DEFAULT_GLOSS = 224;
|
|
422
|
+
var FERRO_DEFAULT_METALLIC = 166;
|
|
423
|
+
var FERRO_DEFAULT_RIM = 64;
|
|
424
|
+
var SQRT3_HALF = 0.8660254037844386;
|
|
425
|
+
function normalizeMagnet(raw, base) {
|
|
426
|
+
const x = raw.x ?? base?.x;
|
|
427
|
+
const y = raw.y ?? base?.y;
|
|
428
|
+
const strength = raw.strength ?? base?.strength ?? DEFAULT_MAGNET_STRENGTH;
|
|
429
|
+
const radius = raw.radius ?? base?.radius ?? DEFAULT_MAGNET_RADIUS;
|
|
430
|
+
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
431
|
+
throw new Error(`x ${String(x)} is not a finite coordinate; pass a number in canvas pixels.`);
|
|
432
|
+
}
|
|
433
|
+
if (typeof y !== "number" || !Number.isFinite(y)) {
|
|
434
|
+
throw new Error(`y ${String(y)} is not a finite coordinate; pass a number in canvas pixels.`);
|
|
435
|
+
}
|
|
436
|
+
if (typeof strength !== "number" || !Number.isFinite(strength) || strength < -1 || strength > 1) {
|
|
437
|
+
throw new Error(`strength ${String(strength)} is out of range; use -1..1.`);
|
|
438
|
+
}
|
|
439
|
+
if (typeof radius !== "number" || !Number.isFinite(radius) || radius < 8 || radius > 2e3) {
|
|
440
|
+
throw new Error(`radius ${String(radius)} is out of range; use 8..2000 px.`);
|
|
441
|
+
}
|
|
442
|
+
return { id: raw.id, x, y, strength, radius };
|
|
443
|
+
}
|
|
444
|
+
function emptyFerro() {
|
|
445
|
+
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
446
|
+
}
|
|
447
|
+
var full = (t, channels) => {
|
|
448
|
+
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
449
|
+
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
450
|
+
const u = t.uniform ?? 0;
|
|
451
|
+
if (channels === 1) out.fill(u & 255);
|
|
452
|
+
else {
|
|
453
|
+
const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
|
|
454
|
+
for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
|
|
455
|
+
}
|
|
456
|
+
return out;
|
|
457
|
+
};
|
|
458
|
+
function ferroToShadow(ferro) {
|
|
459
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
460
|
+
for (const [key, tile] of ferro.tiles) {
|
|
461
|
+
const f = full(tile, 1);
|
|
462
|
+
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
463
|
+
for (let i = 0, j = 0; i < f.length; i++, j += 4) {
|
|
464
|
+
if (!f[i]) continue;
|
|
465
|
+
d[j] = d[j + 1] = d[j + 2] = 255;
|
|
466
|
+
d[j + 3] = f[i];
|
|
467
|
+
}
|
|
468
|
+
const t = finishTile(d, 4, 0);
|
|
469
|
+
if (t) tiles.set(key, t);
|
|
470
|
+
}
|
|
471
|
+
return new TiledSurface(4, tiles, 0);
|
|
472
|
+
}
|
|
473
|
+
function shadowToFerro(shadow) {
|
|
474
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
475
|
+
for (const [key, tile] of shadow.tiles) {
|
|
476
|
+
const s = full(tile, 4);
|
|
477
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
478
|
+
for (let i = 0, j = 0; i < d.length; i++, j += 4) {
|
|
479
|
+
if (s[j + 3]) d[i] = Math.round((s[j] + s[j + 1] + s[j + 2]) / 3 * s[j + 3] / 255);
|
|
480
|
+
}
|
|
481
|
+
const t = finishTile(d, 1, 0);
|
|
482
|
+
if (t) tiles.set(key, t);
|
|
483
|
+
}
|
|
484
|
+
return new TiledSurface(1, tiles, 0);
|
|
485
|
+
}
|
|
486
|
+
function ferroTwinBrush(brush) {
|
|
487
|
+
return normalizePaintBrush(
|
|
488
|
+
{
|
|
489
|
+
tip: { ...brush.tip, blend: "normal" },
|
|
490
|
+
behavior: { ...brush.behavior, mixing: "rgb" },
|
|
491
|
+
paint: null,
|
|
492
|
+
...brush.engine === "mixer" ? { mixer: { ...brush.mixer, mixing: "rgb" } } : {},
|
|
493
|
+
...brush.engine === "blender" ? { blender: { ...brush.blender, mixing: "rgb" } } : {},
|
|
494
|
+
...brush.engine === "smudge" ? { smudge: { ...brush.smudge, mixing: "rgb" } } : {}
|
|
495
|
+
},
|
|
496
|
+
brush
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function scaleFerro(ferro, k) {
|
|
500
|
+
if (k >= 1) return ferro;
|
|
501
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
502
|
+
if (k > 0) {
|
|
503
|
+
for (const [key, tile] of ferro.tiles) {
|
|
504
|
+
const d = Uint8ClampedArray.from(full(tile, 1), (v) => Math.round(v * k));
|
|
505
|
+
const t = finishTile(d, 1, 0);
|
|
506
|
+
if (t) tiles.set(key, t);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return new TiledSurface(1, tiles, 0);
|
|
510
|
+
}
|
|
511
|
+
function mergeFerro(lower, upper, upperColor) {
|
|
512
|
+
if (!upper?.tiles.size) return lower;
|
|
513
|
+
const keys = /* @__PURE__ */ new Set([...lower?.tiles.keys() ?? [], ...upper.tiles.keys()]);
|
|
514
|
+
const tiles = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const key of keys) {
|
|
516
|
+
const lo = lower?.tiles.get(key);
|
|
517
|
+
const up = upper.tiles.get(key);
|
|
518
|
+
const col = upperColor.tiles.get(key);
|
|
519
|
+
const l = lo ? full(lo, 1) : null;
|
|
520
|
+
const u = up ? full(up, 1) : null;
|
|
521
|
+
const c = col ? full(col, 4) : null;
|
|
522
|
+
const d = new Uint8ClampedArray(TILE * TILE);
|
|
523
|
+
for (let i = 0; i < d.length; i++) {
|
|
524
|
+
const uf = u ? u[i] : 0;
|
|
525
|
+
const ca = c ? c[i * 4 + 3] : 0;
|
|
526
|
+
const a = Math.max(ca, uf) / 255;
|
|
527
|
+
d[i] = Math.min(255, Math.round(uf + (l ? l[i] : 0) * (1 - a)));
|
|
528
|
+
}
|
|
529
|
+
const t = finishTile(d, 1, 0);
|
|
530
|
+
if (t) tiles.set(key, t);
|
|
531
|
+
}
|
|
532
|
+
return new TiledSurface(1, tiles, 0);
|
|
533
|
+
}
|
|
534
|
+
function ferroFieldAt(x, y, magnets) {
|
|
535
|
+
let bx = 0;
|
|
536
|
+
let by = 0;
|
|
537
|
+
let bz = 0;
|
|
538
|
+
for (let i = 0; i < magnets.length; i++) {
|
|
539
|
+
const m = magnets[i];
|
|
540
|
+
if (m.strength === 0) continue;
|
|
541
|
+
const invR = 1 / Math.max(8, m.radius);
|
|
542
|
+
const u = (x - m.x) * invR;
|
|
543
|
+
const v = (y - m.y) * invR;
|
|
544
|
+
const r2 = u * u + v * v;
|
|
545
|
+
const s = 1 + r2;
|
|
546
|
+
const d = 1 / (s * s * Math.sqrt(s));
|
|
547
|
+
bx += m.strength * 1.5 * u * d;
|
|
548
|
+
by += m.strength * 1.5 * v * d;
|
|
549
|
+
bz += m.strength * (1 - 0.5 * r2) * d;
|
|
550
|
+
}
|
|
551
|
+
const magnitude = Math.hypot(bx, by, bz);
|
|
552
|
+
return { bx, by, bz, magnitude };
|
|
553
|
+
}
|
|
554
|
+
function hexWave(px, py, k) {
|
|
555
|
+
const c0 = Math.cos(k * px);
|
|
556
|
+
const c1 = Math.cos(k * (-0.5 * px + SQRT3_HALF * py));
|
|
557
|
+
const c2 = Math.cos(k * (-0.5 * px - SQRT3_HALF * py));
|
|
558
|
+
return (c0 + c1 + c2 + 1.5) / 4.5;
|
|
559
|
+
}
|
|
560
|
+
function ferroSpikeHeightAt(x, y, ferroByte, magnets, settings = DEFAULT_FERRO) {
|
|
561
|
+
if (ferroByte <= 0 || magnets.length === 0) return 0;
|
|
562
|
+
const field = ferroFieldAt(x, y, magnets);
|
|
563
|
+
const bc = Math.max(0.01, settings.threshold);
|
|
564
|
+
if (field.magnitude <= bc) return 0;
|
|
565
|
+
const amp = Math.sqrt(1 - bc / field.magnitude) * (ferroByte / 255);
|
|
566
|
+
const wl = Math.max(4, settings.wavelength);
|
|
567
|
+
const k = 2 * Math.PI / wl;
|
|
568
|
+
const w0 = hexWave(x, y, k);
|
|
569
|
+
const sz = field.bz < 0 ? -1 : 1;
|
|
570
|
+
const tx = sz * field.bx / field.magnitude;
|
|
571
|
+
const ty = sz * field.by / field.magnitude;
|
|
572
|
+
const shift = 0.32 * wl * amp * w0;
|
|
573
|
+
const w = Math.max(0, hexWave(x - tx * shift, y - ty * shift, k));
|
|
574
|
+
const profile = 0.15 + 0.85 * w * Math.sqrt(w);
|
|
575
|
+
return Math.min(255, Math.max(1, Math.round(255 * amp * profile)));
|
|
576
|
+
}
|
|
577
|
+
function advectFerro(ferro, magnets, offset = { x: 0, y: 0 }, steps = 4) {
|
|
578
|
+
if (!ferro.tiles.size || !magnets.length || steps <= 0) return ferro;
|
|
579
|
+
const b = ferro.bounds;
|
|
580
|
+
if (!b) return ferro;
|
|
581
|
+
let active = false;
|
|
582
|
+
for (let i = 0; i < magnets.length; i++) {
|
|
583
|
+
if (magnets[i].strength !== 0) {
|
|
584
|
+
active = true;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (!active) return ferro;
|
|
589
|
+
const pad = Math.min(32, Math.max(8, steps * 4));
|
|
590
|
+
const rect = {
|
|
591
|
+
x: b.x - pad,
|
|
592
|
+
y: b.y - pad,
|
|
593
|
+
width: b.width + 2 * pad,
|
|
594
|
+
height: b.height + 2 * pad
|
|
595
|
+
};
|
|
596
|
+
const w = rect.width;
|
|
597
|
+
const h = rect.height;
|
|
598
|
+
const raw = readRegion(ferro, rect);
|
|
599
|
+
let cur = new Float64Array(w * h);
|
|
600
|
+
for (let i = 0; i < raw.length; i++) cur[i] = raw[i];
|
|
601
|
+
const vx = new Float64Array(w * h);
|
|
602
|
+
const vy = new Float64Array(w * h);
|
|
603
|
+
for (let gy = 0; gy < h; gy++) {
|
|
604
|
+
const docY = rect.y + gy + offset.y + 0.5;
|
|
605
|
+
for (let gx = 0; gx < w; gx++) {
|
|
606
|
+
const docX = rect.x + gx + offset.x + 0.5;
|
|
607
|
+
let fx = 0;
|
|
608
|
+
let fy = 0;
|
|
609
|
+
for (let mIdx = 0; mIdx < magnets.length; mIdx++) {
|
|
610
|
+
const m = magnets[mIdx];
|
|
611
|
+
if (m.strength === 0) continue;
|
|
612
|
+
const dx = m.x - docX;
|
|
613
|
+
const dy = m.y - docY;
|
|
614
|
+
const r = Math.hypot(dx, dy);
|
|
615
|
+
if (r <= 1e-6) continue;
|
|
616
|
+
const R = Math.max(8, m.radius);
|
|
617
|
+
const u = r / R;
|
|
618
|
+
const s = 1 + u * u;
|
|
619
|
+
const damp = Math.min(1, r * 0.5);
|
|
620
|
+
const pull = Math.abs(m.strength) * damp * 1.5 / (s * Math.sqrt(s));
|
|
621
|
+
fx += dx / r * pull;
|
|
622
|
+
fy += dy / r * pull;
|
|
623
|
+
}
|
|
624
|
+
const mag = Math.hypot(fx, fy);
|
|
625
|
+
if (mag > 2.5) {
|
|
626
|
+
const scale = 2.5 / mag;
|
|
627
|
+
fx *= scale;
|
|
628
|
+
fy *= scale;
|
|
629
|
+
}
|
|
630
|
+
const idx = gy * w + gx;
|
|
631
|
+
vx[idx] = fx;
|
|
632
|
+
vy[idx] = fy;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
for (let step = 0; step < steps; step++) {
|
|
636
|
+
const next = new Float64Array(w * h);
|
|
637
|
+
for (let gy = 0; gy < h; gy++) {
|
|
638
|
+
for (let gx = 0; gx < w; gx++) {
|
|
639
|
+
const idx = gy * w + gx;
|
|
640
|
+
const val = cur[idx];
|
|
641
|
+
if (val <= 0) continue;
|
|
642
|
+
const nx = Math.min(w - 1, Math.max(0, gx + vx[idx]));
|
|
643
|
+
const ny = Math.min(h - 1, Math.max(0, gy + vy[idx]));
|
|
644
|
+
const x0 = Math.floor(nx);
|
|
645
|
+
const y0 = Math.floor(ny);
|
|
646
|
+
const x1 = Math.min(w - 1, x0 + 1);
|
|
647
|
+
const y1 = Math.min(h - 1, y0 + 1);
|
|
648
|
+
const tx = nx - x0;
|
|
649
|
+
const ty = ny - y0;
|
|
650
|
+
next[y0 * w + x0] += val * (1 - tx) * (1 - ty);
|
|
651
|
+
next[y0 * w + x1] += val * tx * (1 - ty);
|
|
652
|
+
next[y1 * w + x0] += val * (1 - tx) * ty;
|
|
653
|
+
next[y1 * w + x1] += val * tx * ty;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (let i = 0; i < next.length; i++) {
|
|
657
|
+
cur[i] = Math.min(255, next[i]);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const out = new Uint8ClampedArray(w * h);
|
|
661
|
+
for (let i = 0; i < out.length; i++) {
|
|
662
|
+
out[i] = Math.min(255, Math.max(0, Math.round(cur[i])));
|
|
663
|
+
}
|
|
664
|
+
return surfaceFromPixels({ width: w, height: h, data: out }, 1, rect.x, rect.y);
|
|
665
|
+
}
|
|
666
|
+
function ferroInfo(ferro, magnets = [], settings = DEFAULT_FERRO, offset = { x: 0, y: 0 }) {
|
|
667
|
+
if (!ferro || !ferro.tiles.size) {
|
|
668
|
+
return {
|
|
669
|
+
bounds: null,
|
|
670
|
+
peak: 0,
|
|
671
|
+
liquidPixels: 0,
|
|
672
|
+
maxSpikeHeight: 0,
|
|
673
|
+
spikePixels: 0,
|
|
674
|
+
magnetCount: magnets.length
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
let peak = 0;
|
|
678
|
+
let liquidPixels = 0;
|
|
679
|
+
let maxSpikeHeight = 0;
|
|
680
|
+
let spikePixels = 0;
|
|
681
|
+
for (const [key, tile] of ferro.tiles) {
|
|
682
|
+
const tx = key % 65536 - 32768;
|
|
683
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
684
|
+
const d = full(tile, 1);
|
|
685
|
+
for (let y = 0, i = 0; y < TILE; y++) {
|
|
686
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
687
|
+
for (let x = 0; x < TILE; x++, i++) {
|
|
688
|
+
const fb = d[i];
|
|
689
|
+
if (!fb) continue;
|
|
690
|
+
liquidPixels++;
|
|
691
|
+
if (fb > peak) peak = fb;
|
|
692
|
+
if (magnets.length > 0) {
|
|
693
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
694
|
+
const hs = ferroSpikeHeightAt(docX, docY, fb, magnets, settings);
|
|
695
|
+
if (hs > 0) {
|
|
696
|
+
spikePixels++;
|
|
697
|
+
if (hs > maxSpikeHeight) maxSpikeHeight = hs;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
bounds: peak ? ferro.bounds : null,
|
|
705
|
+
peak,
|
|
706
|
+
liquidPixels,
|
|
707
|
+
maxSpikeHeight,
|
|
708
|
+
spikePixels,
|
|
709
|
+
magnetCount: magnets.length
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function ferroCacheKey(ferro) {
|
|
713
|
+
if (!ferro || !ferro.surface.tiles.size) return "";
|
|
714
|
+
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(";");
|
|
715
|
+
const s = ferro.settings;
|
|
716
|
+
return `${ferro.offset.x},${ferro.offset.y}|${s.wavelength.toFixed(2)},${s.threshold.toFixed(3)}|${mKey}`;
|
|
717
|
+
}
|
|
718
|
+
function bakeFerroIntoThick(color, ferro, thick, magnets, settings = DEFAULT_FERRO, offset = { x: 0, y: 0 }) {
|
|
719
|
+
const baseThick = thick ?? emptyThickChannels();
|
|
720
|
+
if (!ferro.tiles.size) {
|
|
721
|
+
return { color, thick: baseThick, maxSpikeHeight: 0, spikePixels: 0, curedPixels: 0 };
|
|
722
|
+
}
|
|
723
|
+
const colorTiles = new Map(color.tiles);
|
|
724
|
+
const heightTiles = new Map(baseThick.height.tiles);
|
|
725
|
+
const materialTiles = new Map(baseThick.material.tiles);
|
|
726
|
+
let maxSpikeHeight = 0;
|
|
727
|
+
let spikePixels = 0;
|
|
728
|
+
let curedPixels = 0;
|
|
729
|
+
for (const [key, fTile] of ferro.tiles) {
|
|
730
|
+
const tx = key % 65536 - 32768;
|
|
731
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
732
|
+
const fd = full(fTile, 1);
|
|
733
|
+
const cTile = colorTiles.get(key);
|
|
734
|
+
const hTile = heightTiles.get(key);
|
|
735
|
+
const mTile = materialTiles.get(key);
|
|
736
|
+
const cd = cTile ? new Uint8ClampedArray(full(cTile, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
737
|
+
const hd = hTile ? new Uint8ClampedArray(full(hTile, 1)) : new Uint8ClampedArray(TILE * TILE);
|
|
738
|
+
const md = mTile ? new Uint8ClampedArray(full(mTile, 4)) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
739
|
+
let changedColor = false;
|
|
740
|
+
let changedHeight = false;
|
|
741
|
+
let changedMaterial = false;
|
|
742
|
+
for (let y = 0, p = 0; y < TILE; y++) {
|
|
743
|
+
const docY = ty * TILE + y + offset.y + 0.5;
|
|
744
|
+
for (let x = 0; x < TILE; x++, p++) {
|
|
745
|
+
const fb = fd[p];
|
|
746
|
+
if (!fb) continue;
|
|
747
|
+
curedPixels++;
|
|
748
|
+
const i = p * 4;
|
|
749
|
+
if (cd[i + 3] < fb) {
|
|
750
|
+
const a0 = cd[i + 3] / fb;
|
|
751
|
+
cd[i] = Math.round(cd[i] * a0 + FERRO_CARRIER_RGB[0] * (1 - a0));
|
|
752
|
+
cd[i + 1] = Math.round(cd[i + 1] * a0 + FERRO_CARRIER_RGB[1] * (1 - a0));
|
|
753
|
+
cd[i + 2] = Math.round(cd[i + 2] * a0 + FERRO_CARRIER_RGB[2] * (1 - a0));
|
|
754
|
+
cd[i + 3] = fb;
|
|
755
|
+
changedColor = true;
|
|
756
|
+
}
|
|
757
|
+
const docX = tx * TILE + x + offset.x + 0.5;
|
|
758
|
+
const hs = magnets.length > 0 ? ferroSpikeHeightAt(docX, docY, fb, magnets, settings) : 0;
|
|
759
|
+
if (hs > 0) {
|
|
760
|
+
spikePixels++;
|
|
761
|
+
if (hs > maxSpikeHeight) maxSpikeHeight = hs;
|
|
762
|
+
}
|
|
763
|
+
const bead = Math.max(hs, Math.round(fb * 28 / 255));
|
|
764
|
+
const nh = Math.min(255, hd[p] + bead);
|
|
765
|
+
if (nh !== hd[p]) {
|
|
766
|
+
hd[p] = nh;
|
|
767
|
+
changedHeight = true;
|
|
768
|
+
}
|
|
769
|
+
const w = fb / 255;
|
|
770
|
+
const ng = Math.max(md[i], Math.round(FERRO_DEFAULT_GLOSS * w));
|
|
771
|
+
const nm = Math.max(md[i + 1], Math.round(FERRO_DEFAULT_METALLIC * w));
|
|
772
|
+
const nr = Math.max(md[i + 2], Math.round(FERRO_DEFAULT_RIM * w));
|
|
773
|
+
const na = md[i + 3] || 255;
|
|
774
|
+
if (ng !== md[i] || nm !== md[i + 1] || nr !== md[i + 2] || na !== md[i + 3]) {
|
|
775
|
+
md[i] = ng;
|
|
776
|
+
md[i + 1] = nm;
|
|
777
|
+
md[i + 2] = nr;
|
|
778
|
+
md[i + 3] = na;
|
|
779
|
+
changedMaterial = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (changedColor) {
|
|
784
|
+
const ct = finishTile(cd, 4, 0);
|
|
785
|
+
if (ct) colorTiles.set(key, ct);
|
|
786
|
+
}
|
|
787
|
+
if (changedHeight) {
|
|
788
|
+
const ht = finishTile(hd, 1, 0);
|
|
789
|
+
if (ht) heightTiles.set(key, ht);
|
|
790
|
+
}
|
|
791
|
+
if (changedMaterial) {
|
|
792
|
+
const mt = finishTile(md, 4, 0);
|
|
793
|
+
if (mt) materialTiles.set(key, mt);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
color: new TiledSurface(4, colorTiles, color.defaultValue),
|
|
798
|
+
thick: {
|
|
799
|
+
height: new TiledSurface(1, heightTiles, 0),
|
|
800
|
+
material: new TiledSurface(4, materialTiles, 0),
|
|
801
|
+
film: baseThick.film
|
|
802
|
+
},
|
|
803
|
+
maxSpikeHeight,
|
|
804
|
+
spikePixels,
|
|
805
|
+
curedPixels
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
function ferroToJSON(ferro, settings) {
|
|
809
|
+
const b = ferroInfo(ferro).bounds;
|
|
810
|
+
if (!b) return null;
|
|
811
|
+
const out = {
|
|
812
|
+
rect: { ...b },
|
|
813
|
+
ferro: pngDataUrl(b.width, b.height, readRegion(ferro, b), 1)
|
|
814
|
+
};
|
|
815
|
+
if (settings) {
|
|
816
|
+
if (settings.wavelength !== DEFAULT_FERRO.wavelength) out.wavelength = settings.wavelength;
|
|
817
|
+
if (settings.threshold !== DEFAULT_FERRO.threshold) out.threshold = settings.threshold;
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
}
|
|
821
|
+
function ferroFromJSON(raw) {
|
|
822
|
+
const r = raw;
|
|
823
|
+
if (!r || typeof r !== "object" || !r.rect || typeof r.ferro !== "string") {
|
|
824
|
+
throw new Error("not a saved ferrofluid channel");
|
|
825
|
+
}
|
|
826
|
+
const parsed = parseDataUrl(r.ferro);
|
|
827
|
+
if (!parsed) throw new Error("the ferrofluid channel is not a PNG data URL");
|
|
828
|
+
const img = decodePng(parsed.bytes, { keepGray: true });
|
|
829
|
+
const data = img.channels === 1 ? img.data : Uint8ClampedArray.from({ length: img.width * img.height }, (_, i) => img.data[i * 4]);
|
|
830
|
+
const surface = surfaceFromPixels({ width: img.width, height: img.height, data }, 1, r.rect.x, r.rect.y);
|
|
831
|
+
const settings = normalizeFerro({
|
|
832
|
+
amount: 0,
|
|
833
|
+
wavelength: r.wavelength,
|
|
834
|
+
threshold: r.threshold
|
|
835
|
+
});
|
|
836
|
+
return { surface, settings };
|
|
837
|
+
}
|
|
838
|
+
function magnetsFromJSON(raw) {
|
|
839
|
+
if (!Array.isArray(raw)) return [];
|
|
840
|
+
const out = [];
|
|
841
|
+
for (const item of raw) {
|
|
842
|
+
if (!item || typeof item !== "object") continue;
|
|
843
|
+
const m = item;
|
|
844
|
+
if (typeof m.id !== "string" || !m.id || typeof m.x !== "number" || typeof m.y !== "number") continue;
|
|
845
|
+
try {
|
|
846
|
+
out.push(normalizeMagnet({ id: m.id, x: m.x, y: m.y, strength: m.strength, radius: m.radius }));
|
|
847
|
+
} catch {
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
|
|
411
853
|
// src/paint-editor/thick.ts
|
|
412
854
|
var DEFAULT_LIGHTING = { azimuth: 315, elevation: 40, relief: 0.7 };
|
|
413
855
|
function normalizeLighting(raw = {}, base = DEFAULT_LIGHTING) {
|
|
@@ -547,8 +989,14 @@ function lightTile(color, height, heightAround, material, light, film = null, vi
|
|
|
547
989
|
}
|
|
548
990
|
var LitCache = class {
|
|
549
991
|
tiles = /* @__PURE__ */ new Map();
|
|
550
|
-
/**
|
|
551
|
-
|
|
992
|
+
/**
|
|
993
|
+
* The layer's pixels lit (tiles without relief, film or ferrofluid nearby are the colour tiles
|
|
994
|
+
* themselves). Without ferrofluid this is the thick-paint / thin-film pass; with it, the liquid
|
|
995
|
+
* ferrofluid adds its carrier colour, glossy metallic material and (near magnets) Rosensweig
|
|
996
|
+
* spike heights before the same lighting.
|
|
997
|
+
*/
|
|
998
|
+
lit(color, ch, light, viewTilt = DEFAULT_VIEW_TILT, ferro) {
|
|
999
|
+
if (ferro && ferro.surface.tiles.size > 0) return this.litFerro(color, ch, light, viewTilt, ferro);
|
|
552
1000
|
const hasRelief = ch.height.tiles.size > 0 && light.relief > 0;
|
|
553
1001
|
const hasFilm = ch.film.tiles.size > 0;
|
|
554
1002
|
if (!hasRelief && !hasFilm) return color;
|
|
@@ -597,6 +1045,101 @@ var LitCache = class {
|
|
|
597
1045
|
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
598
1046
|
return new TiledSurface(4, out, color.defaultValue);
|
|
599
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* The ferrofluid lighting pass: every tile with ferrofluid or thick relief nearby is lit, with the
|
|
1050
|
+
* liquid's carrier colour and gloss / metal material blended in and its spikes added to the
|
|
1051
|
+
* height field. Thick relief is lit even when `light.relief` is 0 here (the spikes need it), and
|
|
1052
|
+
* a thin film on the same layer still colours the lit tiles.
|
|
1053
|
+
*/
|
|
1054
|
+
litFerro(color, ch, light, viewTilt, ferro) {
|
|
1055
|
+
const hasFerroSpikes = ferro.magnets.length > 0;
|
|
1056
|
+
const lk = `${lightingKey(light)}|${viewTiltKey(viewTilt)}`;
|
|
1057
|
+
const fk = ferroCacheKey(ferro);
|
|
1058
|
+
const out = new Map(color.tiles);
|
|
1059
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1060
|
+
for (const key of color.tiles.keys()) keys.add(key);
|
|
1061
|
+
for (const key of ferro.surface.tiles.keys()) keys.add(key);
|
|
1062
|
+
for (const key of keys) {
|
|
1063
|
+
const tile = color.tiles.get(key) ?? null;
|
|
1064
|
+
const tx = key % 65536 - 32768;
|
|
1065
|
+
const ty = Math.floor(key / 65536) - 32768;
|
|
1066
|
+
const aroundH = (dx, dy) => ch.height.tiles.get(tileKey(tx + dx, ty + dy)) ?? null;
|
|
1067
|
+
const aroundF = (dx, dy) => ferro.surface.tiles.get(tileKey(tx + dx, ty + dy)) ?? null;
|
|
1068
|
+
const film = ch.film.tiles.get(key) ?? null;
|
|
1069
|
+
let any = !!film;
|
|
1070
|
+
for (let dy = -1; dy <= 1 && !any; dy++) {
|
|
1071
|
+
for (let dx = -1; dx <= 1 && !any; dx++) {
|
|
1072
|
+
if (aroundH(dx, dy) || aroundF(dx, dy)) any = true;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (!any) continue;
|
|
1076
|
+
const m = ch.material.tiles.get(key) ?? null;
|
|
1077
|
+
const f = ferro.surface.tiles.get(key) ?? null;
|
|
1078
|
+
if (!tile && !f) continue;
|
|
1079
|
+
const inputs = [tile, lk, m, fk, film];
|
|
1080
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(aroundH(dx, dy));
|
|
1081
|
+
for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) inputs.push(aroundF(dx, dy));
|
|
1082
|
+
const cached = this.tiles.get(key);
|
|
1083
|
+
if (cached && cached.inputs.length === inputs.length && cached.inputs.every((v, i) => v === inputs[i])) {
|
|
1084
|
+
out.set(key, cached.tile);
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
let cd = tile ? expand(tile, 4) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
1088
|
+
let md = m ? expand(m, 4) : null;
|
|
1089
|
+
if (f) {
|
|
1090
|
+
const fd = expand(f, 1);
|
|
1091
|
+
cd = new Uint8ClampedArray(cd);
|
|
1092
|
+
const mBuf = md ? new Uint8ClampedArray(md) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
1093
|
+
for (let p = 0; p < fd.length; p++) {
|
|
1094
|
+
const fb = fd[p];
|
|
1095
|
+
if (!fb) continue;
|
|
1096
|
+
const i = p * 4;
|
|
1097
|
+
if (cd[i + 3] < fb) {
|
|
1098
|
+
const a0 = cd[i + 3] / fb;
|
|
1099
|
+
cd[i] = Math.round(cd[i] * a0 + FERRO_CARRIER_RGB[0] * (1 - a0));
|
|
1100
|
+
cd[i + 1] = Math.round(cd[i + 1] * a0 + FERRO_CARRIER_RGB[1] * (1 - a0));
|
|
1101
|
+
cd[i + 2] = Math.round(cd[i + 2] * a0 + FERRO_CARRIER_RGB[2] * (1 - a0));
|
|
1102
|
+
cd[i + 3] = fb;
|
|
1103
|
+
}
|
|
1104
|
+
const w = fb / 255;
|
|
1105
|
+
mBuf[i] = Math.max(mBuf[i], Math.round(FERRO_DEFAULT_GLOSS * w));
|
|
1106
|
+
mBuf[i + 1] = Math.max(mBuf[i + 1], Math.round(FERRO_DEFAULT_METALLIC * w));
|
|
1107
|
+
mBuf[i + 2] = Math.max(mBuf[i + 2], Math.round(FERRO_DEFAULT_RIM * w));
|
|
1108
|
+
mBuf[i + 3] = mBuf[i + 3] || 255;
|
|
1109
|
+
}
|
|
1110
|
+
md = mBuf;
|
|
1111
|
+
}
|
|
1112
|
+
const buildHeightTile = (ntx, nty) => {
|
|
1113
|
+
const nkey = tileKey(ntx, nty);
|
|
1114
|
+
const ht = ch.height.tiles.get(nkey) ?? null;
|
|
1115
|
+
const ft = hasFerroSpikes ? ferro.surface.tiles.get(nkey) ?? null : null;
|
|
1116
|
+
if (!ht && !ft) return null;
|
|
1117
|
+
if (!ft) return expand(ht, 1);
|
|
1118
|
+
const hBase = ht ? expand(ht, 1) : null;
|
|
1119
|
+
const fData = expand(ft, 1);
|
|
1120
|
+
const outH = hBase ? new Uint8ClampedArray(hBase) : new Uint8ClampedArray(TILE * TILE);
|
|
1121
|
+
const ox = ntx * TILE + ferro.offset.x + 0.5;
|
|
1122
|
+
const oy = nty * TILE + ferro.offset.y + 0.5;
|
|
1123
|
+
for (let py = 0, p = 0; py < TILE; py++) {
|
|
1124
|
+
const docY = oy + py;
|
|
1125
|
+
for (let px = 0; px < TILE; px++, p++) {
|
|
1126
|
+
const fb = fData[p];
|
|
1127
|
+
if (!fb) continue;
|
|
1128
|
+
const hs = ferroSpikeHeightAt(ox + px, docY, fb, ferro.magnets, ferro.settings);
|
|
1129
|
+
if (hs > 0) outH[p] = Math.min(255, outH[p] + hs);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return outH;
|
|
1133
|
+
};
|
|
1134
|
+
const hd = buildHeightTile(tx, ty);
|
|
1135
|
+
const data = lightTile(cd, hd, (dx, dy) => buildHeightTile(tx + dx, ty + dy), md, light, film ? expand(film, 4) : null, viewTilt);
|
|
1136
|
+
const lit = { data, uniform: null };
|
|
1137
|
+
this.tiles.set(key, { inputs, tile: lit });
|
|
1138
|
+
out.set(key, lit);
|
|
1139
|
+
}
|
|
1140
|
+
for (const key of [...this.tiles.keys()]) if (!keys.has(key)) this.tiles.delete(key);
|
|
1141
|
+
return new TiledSurface(4, out, color.defaultValue);
|
|
1142
|
+
}
|
|
600
1143
|
};
|
|
601
1144
|
function expand(t, channels) {
|
|
602
1145
|
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
@@ -920,27 +1463,27 @@ var ThickStrokeDriver = class {
|
|
|
920
1463
|
const u = ((dx + 0.5 - d.x) * nxs + (dy + 0.5 - d.y) * nys) / r;
|
|
921
1464
|
const h0 = this.getH(lx, ly);
|
|
922
1465
|
let h = h0;
|
|
923
|
-
const
|
|
1466
|
+
const full3 = 255 * thick * k;
|
|
924
1467
|
switch (im.tool) {
|
|
925
1468
|
case "brush": {
|
|
926
1469
|
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);
|
|
927
|
-
const target =
|
|
1470
|
+
const target = full3 * c * ridge;
|
|
928
1471
|
h = Math.max(h0 * (1 - 0.25 * c * im.drag), Math.min(255, h0 * 0.35 + target));
|
|
929
1472
|
break;
|
|
930
1473
|
}
|
|
931
1474
|
case "tube": {
|
|
932
1475
|
const dome = Math.sqrt(Math.max(0, c));
|
|
933
|
-
h = Math.max(h0, Math.min(255,
|
|
1476
|
+
h = Math.max(h0, Math.min(255, full3 * dome * 1.1));
|
|
934
1477
|
break;
|
|
935
1478
|
}
|
|
936
1479
|
case "gloop": {
|
|
937
1480
|
const dome = Math.sqrt(Math.max(0, 1 - Math.min(1, Math.abs(u)) ** 2));
|
|
938
|
-
h = Math.max(h0, Math.min(255,
|
|
1481
|
+
h = Math.max(h0, Math.min(255, full3 * dome * (c > 0.2 ? 1 : c * 5)));
|
|
939
1482
|
break;
|
|
940
1483
|
}
|
|
941
1484
|
case "roller": {
|
|
942
1485
|
const stipple = 0.75 + 0.5 * hash2(lx >> 1, ly >> 1, seed);
|
|
943
|
-
h = h0 + (Math.min(255,
|
|
1486
|
+
h = h0 + (Math.min(255, full3 * 0.6 * stipple) - h0) * Math.min(1, c * 1.5);
|
|
944
1487
|
break;
|
|
945
1488
|
}
|
|
946
1489
|
}
|
|
@@ -1064,7 +1607,7 @@ function normalizeLightSpill(raw, base = DEFAULT_LIGHT_SPILL) {
|
|
|
1064
1607
|
function emptyLight() {
|
|
1065
1608
|
return new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
|
|
1066
1609
|
}
|
|
1067
|
-
var
|
|
1610
|
+
var full2 = (t, channels) => {
|
|
1068
1611
|
if (t.data.length === TILE * TILE * channels) return t.data;
|
|
1069
1612
|
const out = new Uint8ClampedArray(TILE * TILE * channels);
|
|
1070
1613
|
const u = t.uniform ?? 0;
|
|
@@ -1078,7 +1621,7 @@ var full = (t, channels) => {
|
|
|
1078
1621
|
function lightToShadow(light) {
|
|
1079
1622
|
const tiles = /* @__PURE__ */ new Map();
|
|
1080
1623
|
for (const [key, tile] of light.tiles) {
|
|
1081
|
-
const l =
|
|
1624
|
+
const l = full2(tile, 1);
|
|
1082
1625
|
const d = new Uint8ClampedArray(TILE * TILE * 4);
|
|
1083
1626
|
for (let i = 0, j = 0; i < l.length; i++, j += 4) {
|
|
1084
1627
|
if (!l[i]) continue;
|
|
@@ -1093,7 +1636,7 @@ function lightToShadow(light) {
|
|
|
1093
1636
|
function shadowToLight(shadow) {
|
|
1094
1637
|
const tiles = /* @__PURE__ */ new Map();
|
|
1095
1638
|
for (const [key, tile] of shadow.tiles) {
|
|
1096
|
-
const s =
|
|
1639
|
+
const s = full2(tile, 4);
|
|
1097
1640
|
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1098
1641
|
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);
|
|
1099
1642
|
const t = finishTile(d, 1, 0);
|
|
@@ -1128,7 +1671,7 @@ function scaleLight(light, k) {
|
|
|
1128
1671
|
const tiles = /* @__PURE__ */ new Map();
|
|
1129
1672
|
if (k > 0) {
|
|
1130
1673
|
for (const [key, tile] of light.tiles) {
|
|
1131
|
-
const d = Uint8ClampedArray.from(
|
|
1674
|
+
const d = Uint8ClampedArray.from(full2(tile, 1), (v) => Math.round(v * k));
|
|
1132
1675
|
const t = finishTile(d, 1, 0);
|
|
1133
1676
|
if (t) tiles.set(key, t);
|
|
1134
1677
|
}
|
|
@@ -1143,9 +1686,9 @@ function mergeLight(lower, upper, upperColor) {
|
|
|
1143
1686
|
const lo = lower?.tiles.get(key);
|
|
1144
1687
|
const up = upper.tiles.get(key);
|
|
1145
1688
|
const col = upperColor.tiles.get(key);
|
|
1146
|
-
const l = lo ?
|
|
1147
|
-
const u = up ?
|
|
1148
|
-
const c = col ?
|
|
1689
|
+
const l = lo ? full2(lo, 1) : null;
|
|
1690
|
+
const u = up ? full2(up, 1) : null;
|
|
1691
|
+
const c = col ? full2(col, 4) : null;
|
|
1149
1692
|
const d = new Uint8ClampedArray(TILE * TILE);
|
|
1150
1693
|
for (let i = 0; i < d.length; i++) {
|
|
1151
1694
|
const a = c ? c[i * 4 + 3] / 255 : 0;
|
|
@@ -1182,10 +1725,10 @@ var LightCache = class {
|
|
|
1182
1725
|
out.set(key, cached.tile);
|
|
1183
1726
|
continue;
|
|
1184
1727
|
}
|
|
1185
|
-
const c =
|
|
1186
|
-
const ld = l ?
|
|
1187
|
-
const md = m ?
|
|
1188
|
-
const hd = h ?
|
|
1728
|
+
const c = full2(tile, 4);
|
|
1729
|
+
const ld = l ? full2(l, 1) : null;
|
|
1730
|
+
const md = m ? full2(m, 4) : null;
|
|
1731
|
+
const hd = h ? full2(h, 1) : null;
|
|
1189
1732
|
const d = new Uint8ClampedArray(c);
|
|
1190
1733
|
for (let y = 0, i = 0; y < TILE; y++) {
|
|
1191
1734
|
for (let x = 0; x < TILE; x++, i++) {
|
|
@@ -2912,6 +3455,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2912
3455
|
this.invisibleDaylight = brush.fluorescence.amount > 0 && brush.fluorescence.invisible && brush.engine !== "eraser" && brush.engine !== "blender" && brush.engine !== "smudge";
|
|
2913
3456
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
2914
3457
|
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
3458
|
+
if (init.ferro && !init.wet && !init.sticker) this.ferroTwin = this.makeFerroTwin(init.ferro.surface);
|
|
2915
3459
|
if (init.fluor && !init.wet && !init.sticker) this.fluorTwin = this.makeFluorTwin(init.fluor.surface);
|
|
2916
3460
|
if (init.thermo && !init.wet && !init.sticker) this.thermoTwin = this.makeThermoTwin(init.thermo.surface);
|
|
2917
3461
|
this.apply = {
|
|
@@ -2944,6 +3488,9 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
2944
3488
|
/** Light paint: the stroke's twin over the light channel, and what it left there. */
|
|
2945
3489
|
lightTwin = null;
|
|
2946
3490
|
lightResult = null;
|
|
3491
|
+
/** Ferrofluid paint: the stroke's twin over the ferrofluid channel, and what it left there. */
|
|
3492
|
+
ferroTwin = null;
|
|
3493
|
+
ferroResult = null;
|
|
2947
3494
|
/** UV-fluorescent ink: the stroke's twin over the fluorescence channel, and what it left there. */
|
|
2948
3495
|
fluorTwin = null;
|
|
2949
3496
|
fluorResult = null;
|
|
@@ -3042,6 +3589,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3042
3589
|
paint,
|
|
3043
3590
|
effect,
|
|
3044
3591
|
light: null,
|
|
3592
|
+
ferro: null,
|
|
3045
3593
|
fluor: null,
|
|
3046
3594
|
thermo: null
|
|
3047
3595
|
});
|
|
@@ -3065,7 +3613,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3065
3613
|
inputFilter: void 0,
|
|
3066
3614
|
light: null,
|
|
3067
3615
|
fluor: null,
|
|
3068
|
-
thermo: null
|
|
3616
|
+
thermo: null,
|
|
3617
|
+
ferro: null
|
|
3069
3618
|
});
|
|
3070
3619
|
}
|
|
3071
3620
|
/** The stroke's twin over the 4-channel thermochromic surface (see `heat.ts`). */
|
|
@@ -3100,7 +3649,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3100
3649
|
effect: null,
|
|
3101
3650
|
light: null,
|
|
3102
3651
|
fluor: null,
|
|
3103
|
-
thermo: null
|
|
3652
|
+
thermo: null,
|
|
3653
|
+
ferro: null
|
|
3104
3654
|
});
|
|
3105
3655
|
}
|
|
3106
3656
|
/** Light paint: the layer's light channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
@@ -3115,6 +3665,44 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3115
3665
|
thermoChannel() {
|
|
3116
3666
|
return this.thermoResult;
|
|
3117
3667
|
}
|
|
3668
|
+
/** The stroke's twin over the ferrofluid channel: the same marks in grey at the brush's ferro amount (see `ferro.ts`). */
|
|
3669
|
+
makeFerroTwin(ferro) {
|
|
3670
|
+
const init = this.init;
|
|
3671
|
+
const grey = Math.round(init.brush.ferro.amount * 255);
|
|
3672
|
+
const g = grey / 255;
|
|
3673
|
+
const hex2 = "#" + grey.toString(16).padStart(2, "0").repeat(3);
|
|
3674
|
+
const host = {
|
|
3675
|
+
update: () => {
|
|
3676
|
+
},
|
|
3677
|
+
commit: (_s, surface) => {
|
|
3678
|
+
this.ferroResult = surface ? shadowToFerro(surface) : null;
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
const effect = init.effect ? { source: init.effect.source, pixels: { ...init.effect.pixels, data: greyPixels(init.effect.pixels.data, grey) } } : null;
|
|
3682
|
+
const paint = init.paint ? { ...init.paint, stops: init.paint.stops.map((st) => ({ ...st, color: hex2 })) } : null;
|
|
3683
|
+
return new _PaintStrokeSession(host, {
|
|
3684
|
+
...init,
|
|
3685
|
+
surface: ferroToShadow(ferro),
|
|
3686
|
+
lockTransparency: false,
|
|
3687
|
+
brush: ferroTwinBrush(init.brush),
|
|
3688
|
+
color: [grey, grey, grey, 255],
|
|
3689
|
+
colorHex: hex2,
|
|
3690
|
+
secondary: [grey, grey, grey, 255],
|
|
3691
|
+
inputFilter: void 0,
|
|
3692
|
+
mixerLoad: { reservoir: init.mixerLoad.reservoir ? [g, g, g] : null, amount: init.mixerLoad.amount, pickup: null },
|
|
3693
|
+
thick: init.thick,
|
|
3694
|
+
paint,
|
|
3695
|
+
effect,
|
|
3696
|
+
light: null,
|
|
3697
|
+
ferro: null,
|
|
3698
|
+
fluor: null,
|
|
3699
|
+
thermo: null
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
/** Ferrofluid paint: the layer's ferrofluid channel after the stroke, or `null` when the stroke has no twin or left it unchanged. */
|
|
3703
|
+
ferroChannel() {
|
|
3704
|
+
return this.ferroResult;
|
|
3705
|
+
}
|
|
3118
3706
|
tileTarget() {
|
|
3119
3707
|
return {
|
|
3120
3708
|
offset: this.init.offset,
|
|
@@ -3145,6 +3733,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3145
3733
|
this.lastRaw = points[points.length - 1];
|
|
3146
3734
|
this.host.sampled?.(this, points);
|
|
3147
3735
|
this.lightTwin?.push(points);
|
|
3736
|
+
this.ferroTwin?.push(points);
|
|
3148
3737
|
this.fluorTwin?.push(points);
|
|
3149
3738
|
this.thermoTwin?.push(points);
|
|
3150
3739
|
if (this.wetDriver) {
|
|
@@ -3238,6 +3827,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3238
3827
|
this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet.finish());
|
|
3239
3828
|
this.closed = true;
|
|
3240
3829
|
if (this.lightTwin?.open) this.lightTwin.end();
|
|
3830
|
+
if (this.ferroTwin?.open) this.ferroTwin.end();
|
|
3241
3831
|
if (this.fluorTwin?.open) this.fluorTwin.end();
|
|
3242
3832
|
if (this.thermoTwin?.open) this.thermoTwin.end();
|
|
3243
3833
|
const ch = this.base.channels;
|
|
@@ -3272,6 +3862,8 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3272
3862
|
if (this.fluorTwin?.open) this.fluorTwin.cancel();
|
|
3273
3863
|
if (this.thermoTwin?.open) this.thermoTwin.cancel();
|
|
3274
3864
|
this.lightResult = null;
|
|
3865
|
+
if (this.ferroTwin?.open) this.ferroTwin.cancel();
|
|
3866
|
+
this.ferroResult = null;
|
|
3275
3867
|
this.fluorResult = null;
|
|
3276
3868
|
this.thermoResult = null;
|
|
3277
3869
|
this.host.commit(this, null, true);
|
|
@@ -3388,8 +3980,8 @@ function nearestOnStencil(s, p) {
|
|
|
3388
3980
|
if (s.kind === "arc") {
|
|
3389
3981
|
const c = s.center;
|
|
3390
3982
|
const ang = Math.atan2(p.y - c.y, p.x - c.x);
|
|
3391
|
-
const
|
|
3392
|
-
if (
|
|
3983
|
+
const full3 = Math.abs(s.endAngle - s.startAngle) >= 360;
|
|
3984
|
+
if (full3) {
|
|
3393
3985
|
const q = { x: c.x + Math.cos(ang) * s.radius, y: c.y + Math.sin(ang) * s.radius };
|
|
3394
3986
|
return { point: q, distance: Math.hypot(p.x - q.x, p.y - q.y) };
|
|
3395
3987
|
}
|
|
@@ -3832,7 +4424,7 @@ function deserializePaintDocument(raw) {
|
|
|
3832
4424
|
const wetEvents = Array.isArray(r.wetEvents) ? r.wetEvents.filter((e) => e && typeof e === "object" && Number.isInteger(e.clock) && typeof e.kind === "string") : [];
|
|
3833
4425
|
const state = { canvas, paper, layers, activeLayerId, strokes: readStrokes(r.strokes, problems) };
|
|
3834
4426
|
if (wetEvents.length) state.wetEvents = wetEvents;
|
|
3835
|
-
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)) : [];
|
|
4427
|
+
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)) : [];
|
|
3836
4428
|
if (paintEvents.length) state.paintEvents = paintEvents;
|
|
3837
4429
|
const o = r.origin;
|
|
3838
4430
|
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) };
|
|
@@ -4764,6 +5356,7 @@ function liveStrokeRefusal(brush) {
|
|
|
4764
5356
|
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).`;
|
|
4765
5357
|
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).`;
|
|
4766
5358
|
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).`;
|
|
5359
|
+
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).`;
|
|
4767
5360
|
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.`;
|
|
4768
5361
|
return null;
|
|
4769
5362
|
}
|
|
@@ -5032,6 +5625,12 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5032
5625
|
if (strokes >= maxStrokes && item.kind === "stroke") break;
|
|
5033
5626
|
if (item.kind === "paint") {
|
|
5034
5627
|
const e = item.event;
|
|
5628
|
+
if (e.kind === "magnets") {
|
|
5629
|
+
const d = e.data;
|
|
5630
|
+
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)) });
|
|
5631
|
+
target.replaceMagnets(list, Boolean(d?.advect));
|
|
5632
|
+
continue;
|
|
5633
|
+
}
|
|
5035
5634
|
if (!target.getLayer(e.layerId)) continue;
|
|
5036
5635
|
if (e.kind === "fill") target.fillLayer({ layerId: e.layerId, ...e.data });
|
|
5037
5636
|
else if (e.kind === "fill-region") {
|
|
@@ -5046,6 +5645,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5046
5645
|
...d.expand !== void 0 ? { expand: Math.max(-16, Math.min(16, d.expand * s)) } : {}
|
|
5047
5646
|
});
|
|
5048
5647
|
} else if (e.kind === "clear") target.clearLayer(e.layerId);
|
|
5648
|
+
else if (e.kind === "cure-ferrofluid") target.cureFerrofluid(e.layerId);
|
|
5049
5649
|
else if (e.kind === "live-strokes") {
|
|
5050
5650
|
const d = e.data;
|
|
5051
5651
|
if (d?.rasterized) target.replaceLiveStrokes(e.layerId, null);
|
|
@@ -5091,6 +5691,26 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5091
5691
|
if (e.layerId && target.getLayer(e.layerId)) target.wetPaper({ layerId: e.layerId, rect: sr(d.rect), amount: d.amount });
|
|
5092
5692
|
break;
|
|
5093
5693
|
}
|
|
5694
|
+
case "live-patterns": {
|
|
5695
|
+
const d = e.data;
|
|
5696
|
+
if (e.layerId && target.getLayer(e.layerId)) {
|
|
5697
|
+
target.setLivePatterns(e.layerId, d ? { ...d, scale: Math.min(4, d.scale * s), enabled: true } : { enabled: false });
|
|
5698
|
+
}
|
|
5699
|
+
break;
|
|
5700
|
+
}
|
|
5701
|
+
case "seed-live-patterns": {
|
|
5702
|
+
const d = e.data;
|
|
5703
|
+
if (e.layerId && target.getLayer(e.layerId) && Array.isArray(d.points)) {
|
|
5704
|
+
target.seedLivePatterns({
|
|
5705
|
+
layerId: e.layerId,
|
|
5706
|
+
points: d.points.map((p) => ({ ...p, x: p.x * s, y: p.y * s })),
|
|
5707
|
+
radius: Math.min(512, d.radius * s),
|
|
5708
|
+
seed: d.seed,
|
|
5709
|
+
...d.settings ? { ...d.settings, ...d.settings.scale ? { scale: Math.min(4, d.settings.scale * s) } : {} } : {}
|
|
5710
|
+
});
|
|
5711
|
+
}
|
|
5712
|
+
break;
|
|
5713
|
+
}
|
|
5094
5714
|
case "ghost-release": {
|
|
5095
5715
|
const d = e.data;
|
|
5096
5716
|
if (e.layerId && target.getLayer(e.layerId)) target.wet.releaseGhost(e.layerId, d?.linger);
|
|
@@ -6339,6 +6959,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6339
6959
|
litCaches = /* @__PURE__ */ new Map();
|
|
6340
6960
|
/** The live impasto stroke's channels. */
|
|
6341
6961
|
liveThick = null;
|
|
6962
|
+
/** The live ferrofluid stroke's channel. */
|
|
6963
|
+
liveFerro = null;
|
|
6964
|
+
nextMagnetId = 1;
|
|
6342
6965
|
/** The UV / blacklight setting (saved with the document, not undo steps). */
|
|
6343
6966
|
uvLight = { ...DEFAULT_UV_LIGHT };
|
|
6344
6967
|
uvCaches = /* @__PURE__ */ new Map();
|
|
@@ -6410,6 +7033,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6410
7033
|
for (const [id, surface] of lights) if (findLayer(this.doc.layers, id)) light = withLayerLight(light, id, surface);
|
|
6411
7034
|
this.doc = { ...this.doc, ...light ? { light } : {} };
|
|
6412
7035
|
},
|
|
7036
|
+
layerHeight: (id) => this.doc.thick?.[id]?.height ?? null,
|
|
7037
|
+
updateHeights: (heights) => {
|
|
7038
|
+
let thick = this.doc.thick;
|
|
7039
|
+
for (const [id, height] of heights) {
|
|
7040
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
7041
|
+
const prev = thick?.[id] ?? emptyThickChannels();
|
|
7042
|
+
thick = { ...thick ?? {}, [id]: { ...prev, height } };
|
|
7043
|
+
}
|
|
7044
|
+
if (thick !== this.doc.thick) this.doc = { ...this.doc, ...thick ? { thick } : {} };
|
|
7045
|
+
},
|
|
6413
7046
|
changed: () => {
|
|
6414
7047
|
}
|
|
6415
7048
|
},
|
|
@@ -6858,9 +7491,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6858
7491
|
paper: options.paper === void 0 ? { ...this.doc.paper, offset: void 0 } : this.resolvePaper(options.paper)
|
|
6859
7492
|
});
|
|
6860
7493
|
this.extensions = {};
|
|
7494
|
+
this.nextMagnetId = 1;
|
|
6861
7495
|
this.guides = { ...this.guides, symmetry: { ...this.guides.symmetry, center: { x: next.canvas.width / 2, y: next.canvas.height / 2 } }, tracing: null };
|
|
6862
7496
|
this.tracingVersion++;
|
|
6863
|
-
if (this.wet.world) {
|
|
7497
|
+
if (this.wet.world || this.wet.patterns.size) {
|
|
6864
7498
|
this.beginWetOp();
|
|
6865
7499
|
this.wet.reset();
|
|
6866
7500
|
}
|
|
@@ -7015,7 +7649,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7015
7649
|
describeLayer(l, parentId = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0) {
|
|
7016
7650
|
let bounds = null;
|
|
7017
7651
|
if (l.type === "pixel") {
|
|
7018
|
-
const b = unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null);
|
|
7652
|
+
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);
|
|
7019
7653
|
bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;
|
|
7020
7654
|
} else for (const c of flattenLayers(l.children)) if (c.type === "pixel") bounds = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).bounds);
|
|
7021
7655
|
return {
|
|
@@ -7216,7 +7850,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7216
7850
|
...this.liveStrokesWithout(removedIds),
|
|
7217
7851
|
...this.lightWithout(removedIds),
|
|
7218
7852
|
...this.fluorWithout(removedIds),
|
|
7219
|
-
...this.thermoWithout(removedIds)
|
|
7853
|
+
...this.thermoWithout(removedIds),
|
|
7854
|
+
...this.ferroWithout(removedIds)
|
|
7220
7855
|
});
|
|
7221
7856
|
return { removed: id, activeLayerId: active };
|
|
7222
7857
|
}
|
|
@@ -7232,12 +7867,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7232
7867
|
const stickers = { ...this.doc.stickers ?? {} };
|
|
7233
7868
|
const liveStrokes = { ...this.doc.liveStrokes ?? {} };
|
|
7234
7869
|
const light = { ...this.doc.light ?? {} };
|
|
7870
|
+
const ferro = { ...this.doc.ferro ?? {} };
|
|
7871
|
+
const ferroSettings = { ...this.doc.ferroSettings ?? {} };
|
|
7235
7872
|
const fluor = { ...this.doc.fluor ?? {} };
|
|
7236
7873
|
const thermo = { ...this.doc.thermo ?? {} };
|
|
7237
7874
|
const copy = (l) => {
|
|
7238
7875
|
const id2 = newPaintLayerId();
|
|
7239
7876
|
if (light[l.id]) light[id2] = light[l.id];
|
|
7240
7877
|
if (this.lightSpills[l.id]) this.lightSpills[id2] = { ...this.lightSpills[l.id] };
|
|
7878
|
+
if (ferro[l.id]) ferro[id2] = ferro[l.id];
|
|
7879
|
+
if (ferroSettings[l.id]) ferroSettings[id2] = { ...ferroSettings[l.id] };
|
|
7241
7880
|
if (fluor[l.id]) fluor[id2] = fluor[l.id];
|
|
7242
7881
|
if (thermo[l.id]) thermo[id2] = thermo[l.id];
|
|
7243
7882
|
if (thick[l.id]) thick[id2] = thick[l.id];
|
|
@@ -7258,7 +7897,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7258
7897
|
...this.doc.liveStrokes ? { liveStrokes } : {},
|
|
7259
7898
|
...this.doc.light ? { light } : {},
|
|
7260
7899
|
...this.doc.fluor ? { fluor } : {},
|
|
7261
|
-
...this.doc.thermo ? { thermo } : {}
|
|
7900
|
+
...this.doc.thermo ? { thermo } : {},
|
|
7901
|
+
...this.doc.ferro ? { ferro } : {},
|
|
7902
|
+
...this.doc.ferroSettings ? { ferroSettings } : {}
|
|
7262
7903
|
})
|
|
7263
7904
|
);
|
|
7264
7905
|
return this.describeLayer(dup);
|
|
@@ -7311,6 +7952,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7311
7952
|
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;
|
|
7312
7953
|
const lightAfter = this.lightWithout([upper.id, below.id]).light ?? (this.doc.light ? { ...this.doc.light } : void 0);
|
|
7313
7954
|
const light = mergedLight ? { ...lightAfter ?? {}, [below.id]: mergedLight } : lightAfter;
|
|
7955
|
+
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;
|
|
7956
|
+
const ferroRemoved = this.ferroWithout([upper.id, below.id]);
|
|
7957
|
+
const ferroAfter = ferroRemoved.ferro ?? (this.doc.ferro ? { ...this.doc.ferro } : void 0);
|
|
7958
|
+
const ferro = mergedFerro?.tiles.size ? { ...ferroAfter ?? {}, [below.id]: mergedFerro } : ferroAfter;
|
|
7959
|
+
const ferroSettingsAfter = ferroRemoved.ferroSettings ?? (this.doc.ferroSettings ? { ...this.doc.ferroSettings } : void 0);
|
|
7960
|
+
const mergedFerroSettings = upper.visible && this.doc.ferroSettings?.[upper.id] ? this.doc.ferroSettings[upper.id] : this.doc.ferroSettings?.[below.id];
|
|
7961
|
+
const ferroSettings = mergedFerro?.tiles.size && mergedFerroSettings ? { ...ferroSettingsAfter ?? {}, [below.id]: mergedFerroSettings } : ferroSettingsAfter;
|
|
7314
7962
|
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;
|
|
7315
7963
|
const fluorAfter = this.fluorWithout([upper.id, below.id]).fluor ?? (this.doc.fluor ? { ...this.doc.fluor } : void 0);
|
|
7316
7964
|
const fluor = mergedFluor ? { ...fluorAfter ?? {}, [below.id]: mergedFluor } : fluorAfter;
|
|
@@ -7328,7 +7976,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7328
7976
|
...this.liveStrokesWithout([upper.id, below.id]),
|
|
7329
7977
|
...light ? { light } : {},
|
|
7330
7978
|
...fluor ? { fluor } : {},
|
|
7331
|
-
...thermo ? { thermo } : {}
|
|
7979
|
+
...thermo ? { thermo } : {},
|
|
7980
|
+
...ferro !== void 0 ? { ferro } : {},
|
|
7981
|
+
...ferroSettings !== void 0 ? { ferroSettings } : {}
|
|
7332
7982
|
})
|
|
7333
7983
|
);
|
|
7334
7984
|
return this.describeLayer(merged);
|
|
@@ -7353,7 +8003,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7353
8003
|
...this.doc.liveStrokes?.[l.id] ? { liveStrokes: { ...this.doc.liveStrokes, [l.id]: [] } } : {},
|
|
7354
8004
|
...this.lightWithout([l.id]),
|
|
7355
8005
|
...this.fluorWithout([l.id]),
|
|
7356
|
-
...this.thermoWithout([l.id])
|
|
8006
|
+
...this.thermoWithout([l.id]),
|
|
8007
|
+
...this.ferroWithout([l.id])
|
|
7357
8008
|
},
|
|
7358
8009
|
{ kind: "clear", layerId: l.id }
|
|
7359
8010
|
)
|
|
@@ -7403,6 +8054,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7403
8054
|
const surface = this.applyCoverage(l, coverage, rect, apply);
|
|
7404
8055
|
const lit = this.doc.light?.[l.id];
|
|
7405
8056
|
const light = lit ? withLayerLight(this.doc.light, l.id, scaleLight(lit, 1 - opacity)) : void 0;
|
|
8057
|
+
const fer = this.doc.ferro?.[l.id];
|
|
8058
|
+
const ferro = fer ? withLayerFerro(this.doc.ferro, l.id, scaleFerro(fer, 1 - opacity)) : void 0;
|
|
8059
|
+
const ferroSettings = fer ? withLayerFerroSettings(this.doc.ferroSettings, ferro, l.id, this.doc.ferroSettings?.[l.id] ?? null) : void 0;
|
|
7406
8060
|
const fl = this.doc.fluor?.[l.id];
|
|
7407
8061
|
const fluor = fl ? withLayerLight(this.doc.fluor, l.id, scaleFluor(fl, 1 - opacity)) : void 0;
|
|
7408
8062
|
const th = this.doc.thermo?.[l.id];
|
|
@@ -7415,7 +8069,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7415
8069
|
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })),
|
|
7416
8070
|
...light ? { light } : {},
|
|
7417
8071
|
...fluor ? { fluor } : {},
|
|
7418
|
-
...thermo ? { thermo } : {}
|
|
8072
|
+
...thermo ? { thermo } : {},
|
|
8073
|
+
...fer ? { ferro, ferroSettings } : {}
|
|
7419
8074
|
}),
|
|
7420
8075
|
{ kind: "fill", layerId: l.id, data: { color, opacity, grain: grainAmount, pressure, blend } }
|
|
7421
8076
|
)
|
|
@@ -8591,6 +9246,10 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8591
9246
|
if (brush.thermochromic.hotColor !== null && brush.engine === "sticker") {
|
|
8592
9247
|
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).`);
|
|
8593
9248
|
}
|
|
9249
|
+
if (brush.ferro.amount > 0) {
|
|
9250
|
+
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).`);
|
|
9251
|
+
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).`);
|
|
9252
|
+
}
|
|
8594
9253
|
if (doc.liveStrokes?.[layer.id]) {
|
|
8595
9254
|
const why = liveStrokeRefusal(brush);
|
|
8596
9255
|
if (why) throw new PaintEditorError(`Layer "${layer.name}" is a stroke layer. ${why}`);
|
|
@@ -8720,6 +9379,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8720
9379
|
thick: needThick ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
8721
9380
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
8722
9381
|
light: !wet && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
9382
|
+
// Ferrofluid paint: a twin of the stroke paints the liquid ferrofluid channel.
|
|
9383
|
+
ferro: !wet && brush.engine !== "sticker" && (brush.ferro.amount > 0 || doc.ferro?.[layer.id]) ? { surface: doc.ferro?.[layer.id] ?? emptyFerro() } : null,
|
|
8723
9384
|
fluor: brush.engine !== "sticker" && (brush.fluorescence.amount > 0 || doc.fluor?.[layer.id]) ? { surface: doc.fluor?.[layer.id] ?? emptyFluor() } : null,
|
|
8724
9385
|
thermo: brush.engine !== "sticker" && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
|
|
8725
9386
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
@@ -8770,6 +9431,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8770
9431
|
this.live = { layerId, surface };
|
|
8771
9432
|
const ch = this.session?.thickChannels();
|
|
8772
9433
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
9434
|
+
const fc = this.session?.ferroChannel();
|
|
9435
|
+
const fb = this.session?.init.brush.ferro;
|
|
9436
|
+
this.liveFerro = fc ? { layerId, surface: fc, settings: fb && fb.amount > 0 ? fb : this.doc.ferroSettings?.[layerId] ?? DEFAULT_FERRO } : null;
|
|
8773
9437
|
const fl = this.session?.fluorChannel();
|
|
8774
9438
|
this.liveFluor = fl ? { layerId, surface: fl } : null;
|
|
8775
9439
|
const th = this.session?.thermoChannel();
|
|
@@ -8792,6 +9456,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8792
9456
|
this.live = null;
|
|
8793
9457
|
this.liveLayers = null;
|
|
8794
9458
|
this.liveThick = null;
|
|
9459
|
+
this.liveFerro = null;
|
|
8795
9460
|
this.liveFluor = null;
|
|
8796
9461
|
this.liveThermo = null;
|
|
8797
9462
|
const wetStroke = Boolean(s.init.wet);
|
|
@@ -8823,11 +9488,26 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8823
9488
|
s.result.wet = this.wet.isWet(s.layerId);
|
|
8824
9489
|
}
|
|
8825
9490
|
const l = this.getLayer(s.layerId);
|
|
8826
|
-
|
|
9491
|
+
let ch = s.thickChannels();
|
|
9492
|
+
if (wetStroke) {
|
|
9493
|
+
const f = this.wet.patterns.get(l.id);
|
|
9494
|
+
if (f?.enabled) {
|
|
9495
|
+
this.wet.settingsOf(l.id).livePatterns = { ...f.settings };
|
|
9496
|
+
if (f.settings.relief > 0 && f.tiles.size > 0) {
|
|
9497
|
+
const prevCh = ch ?? this.doc.thick?.[l.id] ?? emptyThickChannels();
|
|
9498
|
+
const nextHeight = f.applyReliefHeight(prevCh.height);
|
|
9499
|
+
if (nextHeight !== prevCh.height) ch = { ...prevCh, height: nextHeight };
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
}
|
|
9503
|
+
const thick = withLayerThick(this.doc.thick, l.id, ch);
|
|
8827
9504
|
const sprayed = s.stickers();
|
|
8828
9505
|
const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
|
|
8829
9506
|
const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
|
|
8830
9507
|
const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
|
|
9508
|
+
const fc = s.ferroChannel();
|
|
9509
|
+
const ferro = fc ? withLayerFerro(this.doc.ferro, l.id, fc) : this.doc.ferro;
|
|
9510
|
+
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;
|
|
8831
9511
|
const fluor = withLayerLight(this.doc.fluor, l.id, s.fluorChannel());
|
|
8832
9512
|
const thermo = withLayerLight(this.doc.thermo, l.id, s.thermoChannel());
|
|
8833
9513
|
this.commit(
|
|
@@ -8842,7 +9522,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8842
9522
|
...stickers ? { stickers } : {},
|
|
8843
9523
|
...light ? { light } : {},
|
|
8844
9524
|
...fluor ? { fluor } : {},
|
|
8845
|
-
...thermo ? { thermo } : {}
|
|
9525
|
+
...thermo ? { thermo } : {},
|
|
9526
|
+
...ferro ? { ferro } : { ferro: void 0 },
|
|
9527
|
+
...ferroSettings ? { ferroSettings } : { ferroSettings: void 0 }
|
|
8846
9528
|
},
|
|
8847
9529
|
stroke
|
|
8848
9530
|
)
|
|
@@ -8927,6 +9609,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8927
9609
|
this.live = null;
|
|
8928
9610
|
this.liveLayers = null;
|
|
8929
9611
|
this.liveThick = null;
|
|
9612
|
+
this.liveFerro = null;
|
|
8930
9613
|
this.liveFluor = null;
|
|
8931
9614
|
this.liveThermo = null;
|
|
8932
9615
|
const masks = s.init.selection ? (s.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask") : [];
|
|
@@ -8947,6 +9630,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8947
9630
|
this.live = null;
|
|
8948
9631
|
this.liveLayers = null;
|
|
8949
9632
|
this.liveThick = null;
|
|
9633
|
+
this.liveFerro = null;
|
|
8950
9634
|
this.liveFluor = null;
|
|
8951
9635
|
this.liveThermo = null;
|
|
8952
9636
|
this.emit();
|
|
@@ -8976,9 +9660,29 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8976
9660
|
}
|
|
8977
9661
|
const l = findLayer(out.layers, s.layerId).layer;
|
|
8978
9662
|
const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;
|
|
8979
|
-
|
|
9663
|
+
let ch = s.thickChannels();
|
|
9664
|
+
if (s.init.wet) {
|
|
9665
|
+
const f = this.wet.patterns.get(l.id);
|
|
9666
|
+
if (f?.enabled) {
|
|
9667
|
+
this.wet.settingsOf(l.id).livePatterns = { ...f.settings };
|
|
9668
|
+
if (f.settings.relief > 0 && f.tiles.size > 0) {
|
|
9669
|
+
const prevCh = ch ?? out.thick?.[l.id] ?? emptyThickChannels();
|
|
9670
|
+
const nextHeight = f.applyReliefHeight(prevCh.height);
|
|
9671
|
+
if (nextHeight !== prevCh.height) ch = { ...prevCh, height: nextHeight };
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
}
|
|
9675
|
+
const thick = withLayerThick(out.thick, l.id, ch);
|
|
8980
9676
|
const sprayed = s.stickers();
|
|
8981
9677
|
const lit = s.lightChannel() ?? (s.init.wet && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null);
|
|
9678
|
+
const fc = s.ferroChannel();
|
|
9679
|
+
const nextFerro = fc ? withLayerFerro(out.ferro, l.id, fc) : out.ferro;
|
|
9680
|
+
const nextFerroSettings = fc ? withLayerFerroSettings(
|
|
9681
|
+
out.ferroSettings,
|
|
9682
|
+
nextFerro,
|
|
9683
|
+
l.id,
|
|
9684
|
+
s.init.brush.ferro.amount > 0 ? s.init.brush.ferro : out.ferroSettings?.[l.id] ?? DEFAULT_FERRO
|
|
9685
|
+
) : out.ferroSettings;
|
|
8982
9686
|
const fl = s.fluorChannel();
|
|
8983
9687
|
const th = s.thermoChannel();
|
|
8984
9688
|
out = withLiveStroke({
|
|
@@ -8988,6 +9692,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8988
9692
|
...thick ? { thick } : {},
|
|
8989
9693
|
...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
|
|
8990
9694
|
...lit ? { light: withLayerLight(out.light, l.id, lit) } : {},
|
|
9695
|
+
...fc ? { ferro: nextFerro, ferroSettings: nextFerroSettings } : {},
|
|
8991
9696
|
...fl ? { fluor: withLayerLight(out.fluor, l.id, fl) } : {},
|
|
8992
9697
|
...th ? { thermo: withLayerLight(out.thermo, l.id, th) } : {}
|
|
8993
9698
|
}, stroke);
|
|
@@ -9382,6 +10087,178 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9382
10087
|
this.commit("Flatten relief", this.withPaintEvent({ ...this.doc, ...t }, { kind: "flatten", layerId: l.id }));
|
|
9383
10088
|
return { layerId: l.id, flattened: true };
|
|
9384
10089
|
}
|
|
10090
|
+
// --- Ferrofluid paint and magnets -----------------------------------------------
|
|
10091
|
+
/** The document's `ferro` and `ferroSettings` without these layers. */
|
|
10092
|
+
ferroWithout(ids) {
|
|
10093
|
+
const out = {};
|
|
10094
|
+
const f = this.doc.ferro;
|
|
10095
|
+
if (f && ids.some((id) => f[id])) {
|
|
10096
|
+
const next = { ...f };
|
|
10097
|
+
for (const id of ids) delete next[id];
|
|
10098
|
+
out.ferro = Object.keys(next).length ? next : void 0;
|
|
10099
|
+
}
|
|
10100
|
+
const fs = this.doc.ferroSettings;
|
|
10101
|
+
if (fs && ids.some((id) => fs[id])) {
|
|
10102
|
+
const next = { ...fs };
|
|
10103
|
+
for (const id of ids) delete next[id];
|
|
10104
|
+
out.ferroSettings = Object.keys(next).length ? next : void 0;
|
|
10105
|
+
}
|
|
10106
|
+
return out;
|
|
10107
|
+
}
|
|
10108
|
+
/** Runs deterministic advection on all pixel layers with liquid ferrofluid toward `magnets`. */
|
|
10109
|
+
advectDocumentFerro(doc, magnets, steps = 4) {
|
|
10110
|
+
const f = doc.ferro;
|
|
10111
|
+
if (!f || !magnets.length) return f ? { ...f } : void 0;
|
|
10112
|
+
const out = {};
|
|
10113
|
+
for (const [id, surf] of Object.entries(f)) {
|
|
10114
|
+
const l = findLayer(doc.layers, id)?.layer;
|
|
10115
|
+
if (isPixelLayer(l) && surf.tiles.size > 0) {
|
|
10116
|
+
const next = advectFerro(surf, magnets, l.offset, steps);
|
|
10117
|
+
if (next.tiles.size > 0) out[id] = next;
|
|
10118
|
+
} else if (surf.tiles.size > 0) {
|
|
10119
|
+
out[id] = surf;
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10122
|
+
return Object.keys(out).length ? out : void 0;
|
|
10123
|
+
}
|
|
10124
|
+
/** Lists the magnets on the canvas. */
|
|
10125
|
+
listMagnets() {
|
|
10126
|
+
return (this.doc.magnets ?? []).map((m) => ({ ...m }));
|
|
10127
|
+
}
|
|
10128
|
+
/** Places a dipole magnet on the canvas and advects liquid ferrofluid toward it (one undo step). */
|
|
10129
|
+
addMagnet(input) {
|
|
10130
|
+
this.requireIdle("adding a magnet");
|
|
10131
|
+
const existing = this.doc.magnets ?? [];
|
|
10132
|
+
const usedIds = new Set(existing.map((m) => m.id));
|
|
10133
|
+
while (usedIds.has(`mag${this.nextMagnetId}`)) this.nextMagnetId++;
|
|
10134
|
+
const id = `mag${this.nextMagnetId++}`;
|
|
10135
|
+
let magnet;
|
|
10136
|
+
try {
|
|
10137
|
+
magnet = normalizeMagnet({ id, x: input.x, y: input.y, strength: input.strength, radius: input.radius });
|
|
10138
|
+
} catch (e) {
|
|
10139
|
+
throw new PaintEditorError(e.message);
|
|
10140
|
+
}
|
|
10141
|
+
const magnets = [...existing, magnet];
|
|
10142
|
+
const ferro = this.advectDocumentFerro(this.doc, magnets, 4);
|
|
10143
|
+
const nextDoc = this.withPaintEvent(
|
|
10144
|
+
{ ...this.doc, magnets, ...this.doc.ferro ? { ferro } : {} },
|
|
10145
|
+
{ kind: "magnets", layerId: "", data: { magnets: magnets.map((m) => ({ ...m })), advect: true } }
|
|
10146
|
+
);
|
|
10147
|
+
this.commit("Add magnet", nextDoc);
|
|
10148
|
+
return { ...magnet };
|
|
10149
|
+
}
|
|
10150
|
+
/** Moves or retunes a magnet and advects liquid ferrofluid toward the magnets (one undo step). */
|
|
10151
|
+
updateMagnet(id, update, commitToHistory = true) {
|
|
10152
|
+
this.requireIdle("updating a magnet");
|
|
10153
|
+
if (update.x === void 0 && update.y === void 0 && update.strength === void 0 && update.radius === void 0) {
|
|
10154
|
+
throw new PaintEditorError("Pass at least one change: x, y, strength or radius.");
|
|
10155
|
+
}
|
|
10156
|
+
const existing = this.doc.magnets ?? [];
|
|
10157
|
+
const idx = existing.findIndex((m) => m.id === id);
|
|
10158
|
+
if (idx < 0) throw new PaintEditorError(`Unknown magnet "${id}". Call list_magnets for the ids.`);
|
|
10159
|
+
const current = existing[idx];
|
|
10160
|
+
let magnet;
|
|
10161
|
+
try {
|
|
10162
|
+
magnet = normalizeMagnet(
|
|
10163
|
+
{
|
|
10164
|
+
id,
|
|
10165
|
+
x: update.x ?? current.x,
|
|
10166
|
+
y: update.y ?? current.y,
|
|
10167
|
+
strength: update.strength ?? current.strength,
|
|
10168
|
+
radius: update.radius ?? current.radius
|
|
10169
|
+
},
|
|
10170
|
+
current
|
|
10171
|
+
);
|
|
10172
|
+
} catch (e) {
|
|
10173
|
+
throw new PaintEditorError(e.message);
|
|
10174
|
+
}
|
|
10175
|
+
const magnets = existing.map((m, i) => i === idx ? magnet : m);
|
|
10176
|
+
const last = this.undoStack[this.undoStack.length - 1];
|
|
10177
|
+
const amending = !commitToHistory && Boolean(last && last.label === "Update magnet" && !last.wet && !this.wetOp && !this.grouping);
|
|
10178
|
+
const baseDoc = amending && last ? last.before : this.doc;
|
|
10179
|
+
const ferro = this.advectDocumentFerro(baseDoc, magnets, 4);
|
|
10180
|
+
const nextDoc = this.withPaintEvent(
|
|
10181
|
+
{ ...this.doc, paintEvents: baseDoc.paintEvents, magnets, ...baseDoc.ferro ? { ferro } : {} },
|
|
10182
|
+
{ kind: "magnets", layerId: "", data: { magnets: magnets.map((m) => ({ ...m })), advect: true } }
|
|
10183
|
+
);
|
|
10184
|
+
this.commitOrAmend("Update magnet", nextDoc, commitToHistory);
|
|
10185
|
+
return { ...magnet };
|
|
10186
|
+
}
|
|
10187
|
+
/** Removes magnets by id, or all magnets when `ids` is omitted or empty (one undo step). Returns the removed ids. */
|
|
10188
|
+
removeMagnets(ids) {
|
|
10189
|
+
this.requireIdle("removing magnets");
|
|
10190
|
+
const existing = this.doc.magnets ?? [];
|
|
10191
|
+
if (ids?.length) {
|
|
10192
|
+
for (const id of ids) {
|
|
10193
|
+
if (!existing.some((m) => m.id === id)) throw new PaintEditorError(`Unknown magnet "${id}". Call list_magnets for the ids.`);
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
const removed = ids?.length ? [...ids] : existing.map((m) => m.id);
|
|
10197
|
+
if (!removed.length) return [];
|
|
10198
|
+
const next = existing.filter((m) => !removed.includes(m.id));
|
|
10199
|
+
const nextDoc = this.withPaintEvent(
|
|
10200
|
+
{ ...this.doc, magnets: next.length ? next : void 0 },
|
|
10201
|
+
{ kind: "magnets", layerId: "", data: { magnets: next.map((m) => ({ ...m })), advect: false } }
|
|
10202
|
+
);
|
|
10203
|
+
this.commit("Remove magnets", nextDoc);
|
|
10204
|
+
return removed;
|
|
10205
|
+
}
|
|
10206
|
+
/** Replaces the canvas magnets (used by replay). */
|
|
10207
|
+
replaceMagnets(magnets, advect = false) {
|
|
10208
|
+
const next = magnets.map((m) => normalizeMagnet(m));
|
|
10209
|
+
const ferro = advect ? this.advectDocumentFerro(this.doc, next, 4) : this.doc.ferro;
|
|
10210
|
+
this.commit("Magnets", { ...this.doc, magnets: next.length ? next : void 0, ...this.doc.ferro ? { ferro } : {} });
|
|
10211
|
+
}
|
|
10212
|
+
/** Reads a layer's liquid ferrofluid coverage and active Rosensweig spike relief under the canvas magnets. */
|
|
10213
|
+
getFerrofluid(layerId) {
|
|
10214
|
+
const l = this.wetLayer(layerId, "read the ferrofluid of");
|
|
10215
|
+
const settings = this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO;
|
|
10216
|
+
const magnets = this.doc.magnets ?? [];
|
|
10217
|
+
const info = ferroInfo(this.doc.ferro?.[l.id], magnets, settings, l.offset);
|
|
10218
|
+
const bounds = info.bounds ? { ...info.bounds, x: info.bounds.x + l.offset.x, y: info.bounds.y + l.offset.y } : null;
|
|
10219
|
+
return {
|
|
10220
|
+
layerId: l.id,
|
|
10221
|
+
ferro: info.peak > 0,
|
|
10222
|
+
bounds,
|
|
10223
|
+
peak: info.peak,
|
|
10224
|
+
coverage: info.liquidPixels,
|
|
10225
|
+
activeSpikeArea: info.spikePixels,
|
|
10226
|
+
maxSpikeHeight: info.maxSpikeHeight,
|
|
10227
|
+
magnetCount: info.magnetCount,
|
|
10228
|
+
wavelength: settings.wavelength,
|
|
10229
|
+
threshold: settings.threshold
|
|
10230
|
+
};
|
|
10231
|
+
}
|
|
10232
|
+
/** 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). */
|
|
10233
|
+
cureFerrofluid(layerId) {
|
|
10234
|
+
this.requireIdle("curing ferrofluid");
|
|
10235
|
+
const l = this.paintTarget(layerId, "cure the ferrofluid of");
|
|
10236
|
+
const fSurf = this.doc.ferro?.[l.id];
|
|
10237
|
+
if (!fSurf || !fSurf.tiles.size) {
|
|
10238
|
+
return { layerId: l.id, cured: false, bounds: null, maxSpikeHeight: 0, activeSpikeArea: 0 };
|
|
10239
|
+
}
|
|
10240
|
+
const settings = this.doc.ferroSettings?.[l.id] ?? DEFAULT_FERRO;
|
|
10241
|
+
const magnets = this.doc.magnets ?? [];
|
|
10242
|
+
const fb = ferroInfo(fSurf, magnets, settings, l.offset).bounds;
|
|
10243
|
+
const bounds = fb ? { ...fb, x: fb.x + l.offset.x, y: fb.y + l.offset.y } : null;
|
|
10244
|
+
const baked = bakeFerroIntoThick(l.surface, fSurf, this.doc.thick?.[l.id], magnets, settings, l.offset);
|
|
10245
|
+
const removed = this.ferroWithout([l.id]);
|
|
10246
|
+
const nextDoc = {
|
|
10247
|
+
...this.doc,
|
|
10248
|
+
layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface: baked.color })),
|
|
10249
|
+
thick: { ...this.doc.thick ?? {}, [l.id]: baked.thick },
|
|
10250
|
+
ferro: removed.ferro,
|
|
10251
|
+
ferroSettings: removed.ferroSettings
|
|
10252
|
+
};
|
|
10253
|
+
this.commit("Cure ferrofluid", this.withPaintEvent(nextDoc, { kind: "cure-ferrofluid", layerId: l.id }));
|
|
10254
|
+
return {
|
|
10255
|
+
layerId: l.id,
|
|
10256
|
+
cured: true,
|
|
10257
|
+
bounds,
|
|
10258
|
+
maxSpikeHeight: baked.maxSpikeHeight,
|
|
10259
|
+
activeSpikeArea: baked.spikePixels
|
|
10260
|
+
};
|
|
10261
|
+
}
|
|
9385
10262
|
// --- Replay: time-lapse and high-resolution export ------------------------------
|
|
9386
10263
|
/**
|
|
9387
10264
|
* A fresh, headless editor for replaying this painting at `scale` (canvas, dpi and paper grain
|
|
@@ -9640,6 +10517,149 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9640
10517
|
if (this.wet.busy) return [];
|
|
9641
10518
|
return this.wet.waterOverlay(rect);
|
|
9642
10519
|
}
|
|
10520
|
+
layerLivePatternsInfo(layerId) {
|
|
10521
|
+
const info = this.wet.info(layerId);
|
|
10522
|
+
const stats = this.wet.livePatternStats(layerId);
|
|
10523
|
+
return {
|
|
10524
|
+
layerId,
|
|
10525
|
+
wet: info.wet,
|
|
10526
|
+
wetCells: info.wetCells,
|
|
10527
|
+
wetBounds: info.wetBounds,
|
|
10528
|
+
...stats
|
|
10529
|
+
};
|
|
10530
|
+
}
|
|
10531
|
+
checkLivePatternsInput(input) {
|
|
10532
|
+
if (!input) return {};
|
|
10533
|
+
const out = {};
|
|
10534
|
+
if (input.pattern !== void 0) {
|
|
10535
|
+
if (!LIVE_PATTERN_KINDS.includes(input.pattern)) {
|
|
10536
|
+
throw new PaintEditorError(`pattern must be one of ${LIVE_PATTERN_KINDS.join(", ")}, not ${JSON.stringify(input.pattern)}.`);
|
|
10537
|
+
}
|
|
10538
|
+
out.pattern = input.pattern;
|
|
10539
|
+
}
|
|
10540
|
+
if (input.feed !== void 0) {
|
|
10541
|
+
if (!(Number.isFinite(input.feed) && input.feed >= 5e-3 && input.feed <= 0.1)) {
|
|
10542
|
+
throw new PaintEditorError(`feed ${input.feed} is out of range; use 0.005..0.1.`);
|
|
10543
|
+
}
|
|
10544
|
+
out.feed = input.feed;
|
|
10545
|
+
}
|
|
10546
|
+
if (input.kill !== void 0) {
|
|
10547
|
+
if (!(Number.isFinite(input.kill) && input.kill >= 0.02 && input.kill <= 0.08)) {
|
|
10548
|
+
throw new PaintEditorError(`kill ${input.kill} is out of range; use 0.02..0.08.`);
|
|
10549
|
+
}
|
|
10550
|
+
out.kill = input.kill;
|
|
10551
|
+
}
|
|
10552
|
+
if (input.scale !== void 0) {
|
|
10553
|
+
if (!(Number.isFinite(input.scale) && input.scale >= 0.25 && input.scale <= 4)) {
|
|
10554
|
+
throw new PaintEditorError(`scale ${input.scale} is out of range; use 0.25..4.`);
|
|
10555
|
+
}
|
|
10556
|
+
out.scale = input.scale;
|
|
10557
|
+
}
|
|
10558
|
+
if (input.contrast !== void 0) {
|
|
10559
|
+
if (!(Number.isFinite(input.contrast) && input.contrast >= 0 && input.contrast <= 2)) {
|
|
10560
|
+
throw new PaintEditorError(`contrast ${input.contrast} is out of range; use 0..2.`);
|
|
10561
|
+
}
|
|
10562
|
+
out.contrast = input.contrast;
|
|
10563
|
+
}
|
|
10564
|
+
if (input.secondColor !== void 0) {
|
|
10565
|
+
out.secondColor = input.secondColor === null ? null : parseColor(input.secondColor, "secondColor");
|
|
10566
|
+
}
|
|
10567
|
+
if (input.relief !== void 0) {
|
|
10568
|
+
if (!(Number.isFinite(input.relief) && input.relief >= 0 && input.relief <= 1)) {
|
|
10569
|
+
throw new PaintEditorError(`relief ${input.relief} is out of range; use 0..1.`);
|
|
10570
|
+
}
|
|
10571
|
+
out.relief = input.relief;
|
|
10572
|
+
}
|
|
10573
|
+
return out;
|
|
10574
|
+
}
|
|
10575
|
+
/**
|
|
10576
|
+
* Reads a layer's Gray-Scott reaction-diffusion live patterns (`layerId`, default the active
|
|
10577
|
+
* layer), plus all layers with wet paint or live patterns and the built-in Pearson presets.
|
|
10578
|
+
*/
|
|
10579
|
+
getLivePatterns(layerId) {
|
|
10580
|
+
const l = this.wetLayer(layerId, "read the live patterns of");
|
|
10581
|
+
const primary = this.layerLivePatternsInfo(l.id);
|
|
10582
|
+
const ids = /* @__PURE__ */ new Set([l.id, ...this.wet.wetLayerIds()]);
|
|
10583
|
+
for (const [id, f] of this.wet.patterns) {
|
|
10584
|
+
if ((f.enabled || f.tiles.size > 0) && findLayer(this.doc.layers, id)) ids.add(id);
|
|
10585
|
+
}
|
|
10586
|
+
for (const [id, s] of this.wet.settings) {
|
|
10587
|
+
if (s.livePatterns && findLayer(this.doc.layers, id)) ids.add(id);
|
|
10588
|
+
}
|
|
10589
|
+
const layers = [...ids].map((id) => id === l.id ? primary : this.layerLivePatternsInfo(id));
|
|
10590
|
+
return {
|
|
10591
|
+
...primary,
|
|
10592
|
+
layers,
|
|
10593
|
+
presets: LIVE_PATTERN_PRESETS,
|
|
10594
|
+
brushPresets: ["turing-wash", "coral-ink"]
|
|
10595
|
+
};
|
|
10596
|
+
}
|
|
10597
|
+
/**
|
|
10598
|
+
* Configures a layer's Gray-Scott reaction-diffusion live patterns (or clears them with
|
|
10599
|
+
* `enabled: false`). One undo step.
|
|
10600
|
+
*/
|
|
10601
|
+
setLivePatterns(layerId, update) {
|
|
10602
|
+
this.requireIdle("changing live patterns");
|
|
10603
|
+
this.requireWetIdle("changing live patterns");
|
|
10604
|
+
const l = this.wetLayer(layerId, "set the live patterns of");
|
|
10605
|
+
if (isLayerLocked(l, "pixels")) throw new PaintEditorError(`Layer "${l.name}" has its pixels locked; unlock it first.`);
|
|
10606
|
+
if (update.enabled !== void 0 && typeof update.enabled !== "boolean") {
|
|
10607
|
+
throw new PaintEditorError("enabled must be true or false.");
|
|
10608
|
+
}
|
|
10609
|
+
const checked = this.checkLivePatternsInput(update);
|
|
10610
|
+
const before = this.doc;
|
|
10611
|
+
this.beginWetOp();
|
|
10612
|
+
const next = this.wet.setLivePatterns(l.id, update.enabled === false ? null : checked);
|
|
10613
|
+
this.logWet({ kind: "live-patterns", layerId: l.id, data: next });
|
|
10614
|
+
this.commit("Live patterns", this.withWetEvents(this.doc), before);
|
|
10615
|
+
return this.layerLivePatternsInfo(l.id);
|
|
10616
|
+
}
|
|
10617
|
+
/**
|
|
10618
|
+
* Seeds Gray-Scott reaction-diffusion activator (`V`) discs into a layer's live-pattern field so
|
|
10619
|
+
* patterns grow where the layer holds wet water and freeze where it dries. One undo step.
|
|
10620
|
+
*/
|
|
10621
|
+
seedLivePatterns(options) {
|
|
10622
|
+
this.requireIdle("seeding live patterns");
|
|
10623
|
+
this.requireWetIdle("seeding live patterns");
|
|
10624
|
+
const l = this.wetLayer(options.layerId, "seed live patterns on");
|
|
10625
|
+
if (isLayerLocked(l, "pixels")) throw new PaintEditorError(`Layer "${l.name}" has its pixels locked; unlock it first.`);
|
|
10626
|
+
if (!Array.isArray(options.points) || !options.points.length) {
|
|
10627
|
+
throw new PaintEditorError("points is empty; give at least one {x, y} in document pixels.");
|
|
10628
|
+
}
|
|
10629
|
+
const { width, height } = this.doc.canvas;
|
|
10630
|
+
const points = options.points.map((p, i) => {
|
|
10631
|
+
if (!p || !Number.isFinite(p.x) || !Number.isFinite(p.y)) {
|
|
10632
|
+
throw new PaintEditorError(`points[${i}] needs finite x and y in document pixels.`);
|
|
10633
|
+
}
|
|
10634
|
+
if (p.x < 0 || p.y < 0 || p.x >= width || p.y >= height) {
|
|
10635
|
+
throw new PaintEditorError(`points[${i}] (${p.x}, ${p.y}) is outside the ${width}\xD7${height} canvas.`);
|
|
10636
|
+
}
|
|
10637
|
+
if (p.strength !== void 0 && !(Number.isFinite(p.strength) && p.strength > 0 && p.strength <= 1)) {
|
|
10638
|
+
throw new PaintEditorError(`points[${i}].strength ${p.strength} is out of range; use above 0 up to 1.`);
|
|
10639
|
+
}
|
|
10640
|
+
return { x: p.x, y: p.y, ...p.strength !== void 0 ? { strength: p.strength } : {} };
|
|
10641
|
+
});
|
|
10642
|
+
const radius = options.radius ?? 16;
|
|
10643
|
+
if (!(Number.isFinite(radius) && radius >= 1 && radius <= 512)) {
|
|
10644
|
+
throw new PaintEditorError(`radius ${radius} is out of range; use 1..512 px.`);
|
|
10645
|
+
}
|
|
10646
|
+
const seed = options.seed ?? mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647;
|
|
10647
|
+
if (!Number.isInteger(seed) || seed < 0) {
|
|
10648
|
+
throw new PaintEditorError(`seed ${seed} must be a whole number of 0 or more.`);
|
|
10649
|
+
}
|
|
10650
|
+
const hasSettings = options.pattern !== void 0 || options.feed !== void 0 || options.kill !== void 0 || options.scale !== void 0 || options.contrast !== void 0 || options.secondColor !== void 0 || options.relief !== void 0;
|
|
10651
|
+
const settings = hasSettings ? this.checkLivePatternsInput(options) : void 0;
|
|
10652
|
+
const before = this.doc;
|
|
10653
|
+
this.beginWetOp();
|
|
10654
|
+
const { seededCells } = this.wet.seedLivePatterns(l.id, points, radius, seed, settings);
|
|
10655
|
+
this.logWet({
|
|
10656
|
+
kind: "seed-live-patterns",
|
|
10657
|
+
layerId: l.id,
|
|
10658
|
+
data: { points, radius, seed, ...settings ? { settings } : {} }
|
|
10659
|
+
});
|
|
10660
|
+
this.commit("Seed live patterns", this.withWetEvents(this.doc), before);
|
|
10661
|
+
return { ...this.layerLivePatternsInfo(l.id), seededCells };
|
|
10662
|
+
}
|
|
9643
10663
|
// --- Rendering -----------------------------------------------------------------
|
|
9644
10664
|
sources() {
|
|
9645
10665
|
return { width: this.doc.canvas.width, height: this.doc.canvas.height };
|
|
@@ -9741,20 +10761,33 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9741
10761
|
};
|
|
9742
10762
|
return visit(layers);
|
|
9743
10763
|
}
|
|
9744
|
-
/** The layers with thick paint lit (each thick layer's pixels replaced by its lit tiles). */
|
|
10764
|
+
/** The layers with thick paint and ferrofluid spikes lit (each thick or ferrofluid layer's pixels replaced by its lit tiles). */
|
|
9745
10765
|
litLayers(layers) {
|
|
9746
10766
|
const thick = this.doc.thick;
|
|
9747
10767
|
const live = this.liveThick;
|
|
9748
|
-
|
|
10768
|
+
const ferro = this.doc.ferro;
|
|
10769
|
+
const liveFerro = this.liveFerro;
|
|
10770
|
+
if (!thick && !live && !ferro && !liveFerro) return layers;
|
|
9749
10771
|
let out = layers;
|
|
9750
|
-
const ids = /* @__PURE__ */ new Set([
|
|
10772
|
+
const ids = /* @__PURE__ */ new Set([
|
|
10773
|
+
...Object.keys(thick ?? {}),
|
|
10774
|
+
...live ? [live.layerId] : [],
|
|
10775
|
+
...Object.keys(ferro ?? {}),
|
|
10776
|
+
...liveFerro ? [liveFerro.layerId] : []
|
|
10777
|
+
]);
|
|
10778
|
+
const magnets = this.doc.magnets ?? [];
|
|
9751
10779
|
for (const id of ids) {
|
|
9752
|
-
const
|
|
9753
|
-
|
|
9754
|
-
|
|
10780
|
+
const l = findLayer(out, id)?.layer;
|
|
10781
|
+
if (!isPixelLayer(l)) continue;
|
|
10782
|
+
const ch = (live?.layerId === id ? live.channels : thick?.[id]) ?? emptyThickChannels();
|
|
10783
|
+
const fSurf = liveFerro?.layerId === id ? liveFerro.surface : ferro?.[id];
|
|
10784
|
+
const fSet = (liveFerro?.layerId === id ? liveFerro.settings : this.doc.ferroSettings?.[id]) ?? DEFAULT_FERRO;
|
|
10785
|
+
const hasThick = ch.height.tiles.size > 0 || ch.material.tiles.size > 0 || ch.film.tiles.size > 0;
|
|
10786
|
+
const hasFerro = Boolean(fSurf && fSurf.tiles.size > 0);
|
|
10787
|
+
if (!hasThick && !hasFerro) continue;
|
|
9755
10788
|
let cache = this.litCaches.get(id);
|
|
9756
10789
|
if (!cache) this.litCaches.set(id, cache = new LitCache());
|
|
9757
|
-
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt);
|
|
10790
|
+
const lit = cache.lit(l.surface, ch, this.lighting, this.viewTilt, hasFerro ? { surface: fSurf, magnets, settings: fSet, offset: l.offset } : null);
|
|
9758
10791
|
if (lit !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: lit }));
|
|
9759
10792
|
}
|
|
9760
10793
|
return out;
|
|
@@ -9843,13 +10876,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9843
10876
|
}
|
|
9844
10877
|
/** The canvas (or some layers), scaled so the longest side is at most `maxSide`. */
|
|
9845
10878
|
renderPreview(options = {}) {
|
|
9846
|
-
const
|
|
10879
|
+
const full3 = this.compositePixels({ ...options, wet: options.wet ?? "display" });
|
|
9847
10880
|
const side = Math.max(1, Math.round(options.maxSide ?? 512));
|
|
9848
|
-
const k = Math.min(1, side / Math.max(
|
|
9849
|
-
if (k >= 1) return
|
|
9850
|
-
const w = Math.max(1, Math.round(
|
|
9851
|
-
const h = Math.max(1, Math.round(
|
|
9852
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
10881
|
+
const k = Math.min(1, side / Math.max(full3.width, full3.height));
|
|
10882
|
+
if (k >= 1) return full3;
|
|
10883
|
+
const w = Math.max(1, Math.round(full3.width * k));
|
|
10884
|
+
const h = Math.max(1, Math.round(full3.height * k));
|
|
10885
|
+
return { width: w, height: h, data: downsampleRgba(full3.data, full3.width, full3.height, w, h) };
|
|
9853
10886
|
}
|
|
9854
10887
|
// --- Save, load and export -------------------------------------------------------
|
|
9855
10888
|
/** The document as JSON with inline PNG assets. */
|
|
@@ -9895,7 +10928,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9895
10928
|
const savedGhost = ghost?.trigger === "agent" ? null : ghost;
|
|
9896
10929
|
const heldEvap = this.heldEvaporation.get(id);
|
|
9897
10930
|
const params = heldEvap !== void 0 ? { ...set.params, evaporation: heldEvap } : { ...set.params };
|
|
9898
|
-
layers[id] = {
|
|
10931
|
+
layers[id] = {
|
|
10932
|
+
params,
|
|
10933
|
+
flow: { ...set.flow },
|
|
10934
|
+
...savedGhost ? { ghost: savedGhost } : {},
|
|
10935
|
+
...set.livePatterns ? { livePatterns: { ...set.livePatterns } } : {}
|
|
10936
|
+
};
|
|
9899
10937
|
}
|
|
9900
10938
|
const flat = this.wet.tilt.angle === 0;
|
|
9901
10939
|
if (!Object.keys(layers).length && flat) return void 0;
|
|
@@ -9914,7 +10952,14 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9914
10952
|
}
|
|
9915
10953
|
const x = v;
|
|
9916
10954
|
const ghost = readWetGhost(x.ghost);
|
|
9917
|
-
|
|
10955
|
+
const livePatterns = x.livePatterns ? normalizeLivePatterns(x.livePatterns) : null;
|
|
10956
|
+
this.wet.settings.set(id, {
|
|
10957
|
+
params: normalizeWetParams(x.params ?? {}),
|
|
10958
|
+
flow: normalizeFlow(x.flow ?? {}),
|
|
10959
|
+
...ghost ? { ghost } : {},
|
|
10960
|
+
...livePatterns ? { livePatterns } : {}
|
|
10961
|
+
});
|
|
10962
|
+
if (livePatterns) this.wet.patternFieldOf(id).configure(livePatterns);
|
|
9918
10963
|
}
|
|
9919
10964
|
}
|
|
9920
10965
|
}
|
|
@@ -9953,6 +10998,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9953
10998
|
const spills = Object.fromEntries(Object.entries(this.lightSpills).filter(([id]) => findLayer(this.doc.layers, id)));
|
|
9954
10999
|
if (Object.keys(spills).length) ext.lightSpill = spills;
|
|
9955
11000
|
else delete ext.lightSpill;
|
|
11001
|
+
const ferro = {};
|
|
11002
|
+
for (const [id, f] of Object.entries(this.doc.ferro ?? {})) {
|
|
11003
|
+
if (!findLayer(this.doc.layers, id)) continue;
|
|
11004
|
+
const saved = ferroToJSON(f, this.doc.ferroSettings?.[id]);
|
|
11005
|
+
if (saved) ferro[id] = saved;
|
|
11006
|
+
}
|
|
11007
|
+
if (Object.keys(ferro).length) ext.ferro = ferro;
|
|
11008
|
+
else delete ext.ferro;
|
|
11009
|
+
if (this.doc.magnets?.length) ext.magnets = this.doc.magnets.map((m) => ({ ...m }));
|
|
11010
|
+
else delete ext.magnets;
|
|
9956
11011
|
const effectLayers = effectLayersToJSON(this.doc.effectLayers ?? {}, (id) => isPixelLayer(findLayer(this.doc.layers, id)?.layer));
|
|
9957
11012
|
if (effectLayers) ext.effectLayers = effectLayers;
|
|
9958
11013
|
else delete ext.effectLayers;
|
|
@@ -10090,6 +11145,25 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10090
11145
|
if (heat) state = { ...state, heat };
|
|
10091
11146
|
else problems.push("The heat field could not be read; the canvas loads cool.");
|
|
10092
11147
|
}
|
|
11148
|
+
if (ext.ferro && typeof ext.ferro === "object") {
|
|
11149
|
+
const ferro = {};
|
|
11150
|
+
const ferroSettings = {};
|
|
11151
|
+
for (const [id, v] of Object.entries(ext.ferro)) {
|
|
11152
|
+
if (!isPixelLayer(findLayer(state.layers, id)?.layer)) continue;
|
|
11153
|
+
try {
|
|
11154
|
+
const loaded = ferroFromJSON(v);
|
|
11155
|
+
ferro[id] = loaded.surface;
|
|
11156
|
+
ferroSettings[id] = loaded.settings;
|
|
11157
|
+
} catch (e) {
|
|
11158
|
+
problems.push(`The ferrofluid of layer "${id}" could not be read (${e.message}); it loads without ferrofluid.`);
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
if (Object.keys(ferro).length) state = { ...state, ferro, ferroSettings };
|
|
11162
|
+
}
|
|
11163
|
+
if (ext.magnets !== void 0) {
|
|
11164
|
+
const magnets = magnetsFromJSON(ext.magnets);
|
|
11165
|
+
if (magnets.length) state = { ...state, magnets };
|
|
11166
|
+
}
|
|
10093
11167
|
const raw = ext.layerChannels;
|
|
10094
11168
|
if (!raw || typeof raw !== "object") return state;
|
|
10095
11169
|
const thick = {};
|
|
@@ -10124,6 +11198,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10124
11198
|
this.redoStack = [];
|
|
10125
11199
|
this.resetMutations();
|
|
10126
11200
|
this.doc = this.readThick(loaded.state, loaded.extensions, loaded.problems);
|
|
11201
|
+
this.nextMagnetId = 1;
|
|
11202
|
+
for (const m of this.doc.magnets ?? []) {
|
|
11203
|
+
const n = Number(/^mag(\d+)$/.exec(m.id)?.[1] ?? 0);
|
|
11204
|
+
if (n >= this.nextMagnetId) this.nextMagnetId = n + 1;
|
|
11205
|
+
}
|
|
10127
11206
|
for (const list of Object.values(this.doc.liveStrokes ?? {})) {
|
|
10128
11207
|
for (const s of list) {
|
|
10129
11208
|
const n = Number(/^s(\d+)$/.exec(s.id)?.[1] ?? 0);
|
|
@@ -10134,6 +11213,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10134
11213
|
delete this.extensions.layerChannels;
|
|
10135
11214
|
delete this.extensions.light;
|
|
10136
11215
|
delete this.extensions.lightSpill;
|
|
11216
|
+
delete this.extensions.ferro;
|
|
11217
|
+
delete this.extensions.magnets;
|
|
10137
11218
|
delete this.extensions.lighting;
|
|
10138
11219
|
delete this.extensions.stickers;
|
|
10139
11220
|
delete this.extensions.liveStrokes;
|
|
@@ -10174,13 +11255,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10174
11255
|
return encodePng(img.width, img.height, img.data);
|
|
10175
11256
|
}
|
|
10176
11257
|
scaled(options) {
|
|
10177
|
-
const
|
|
11258
|
+
const full3 = this.compositePixels(options);
|
|
10178
11259
|
const s = options.scale ?? 1;
|
|
10179
11260
|
if (!(s > 0 && s <= 1)) throw new PaintEditorError(`scale ${s} is out of range; use above 0 up to 1 (exports never upscale pixels).`);
|
|
10180
|
-
if (s === 1) return
|
|
10181
|
-
const w = Math.max(1, Math.round(
|
|
10182
|
-
const h = Math.max(1, Math.round(
|
|
10183
|
-
return { width: w, height: h, data: downsampleRgba(
|
|
11261
|
+
if (s === 1) return full3;
|
|
11262
|
+
const w = Math.max(1, Math.round(full3.width * s));
|
|
11263
|
+
const h = Math.max(1, Math.round(full3.height * s));
|
|
11264
|
+
return { width: w, height: h, data: downsampleRgba(full3.data, full3.width, full3.height, w, h) };
|
|
10184
11265
|
}
|
|
10185
11266
|
/**
|
|
10186
11267
|
* The canvas as PNG, JPEG or WebP bytes. JPEG has no alpha, so it always includes the paper.
|
|
@@ -11383,11 +12464,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11383
12464
|
width,
|
|
11384
12465
|
height
|
|
11385
12466
|
) ?? { x: 0, y: 0, width, height };
|
|
11386
|
-
const
|
|
12467
|
+
const full3 = this.compositePixels({ wet: "display" });
|
|
11387
12468
|
const cropData = new Uint8ClampedArray(cropRect.width * cropRect.height * 4);
|
|
11388
12469
|
for (let y = 0; y < cropRect.height; y++) {
|
|
11389
12470
|
const srcRow = ((cropRect.y + y) * width + cropRect.x) * 4;
|
|
11390
|
-
cropData.set(
|
|
12471
|
+
cropData.set(full3.data.subarray(srcRow, srcRow + cropRect.width * 4), y * cropRect.width * 4);
|
|
11391
12472
|
}
|
|
11392
12473
|
const cropPngBase64 = bytesToBase64(encodePng(cropRect.width, cropRect.height, cropData));
|
|
11393
12474
|
const preview = this.renderPreview({ wet: "display", maxSide: options.maxSide ?? 512 });
|
|
@@ -12018,6 +13099,19 @@ function withLayerThick(thick, layerId, next) {
|
|
|
12018
13099
|
else delete out[layerId];
|
|
12019
13100
|
return out;
|
|
12020
13101
|
}
|
|
13102
|
+
function withLayerFerro(ferro, layerId, next) {
|
|
13103
|
+
if (!next) return ferro ? { ...ferro } : void 0;
|
|
13104
|
+
const out = { ...ferro ?? {} };
|
|
13105
|
+
if (next.tiles.size) out[layerId] = next;
|
|
13106
|
+
else delete out[layerId];
|
|
13107
|
+
return Object.keys(out).length ? out : void 0;
|
|
13108
|
+
}
|
|
13109
|
+
function withLayerFerroSettings(settings, ferro, layerId, next) {
|
|
13110
|
+
const out = { ...settings ?? {} };
|
|
13111
|
+
if (next && ferro?.[layerId]?.tiles.size) out[layerId] = { amount: next.amount, wavelength: next.wavelength, threshold: next.threshold };
|
|
13112
|
+
else if (!ferro?.[layerId]?.tiles.size) delete out[layerId];
|
|
13113
|
+
return Object.keys(out).length ? out : void 0;
|
|
13114
|
+
}
|
|
12021
13115
|
var BACKLIGHT_LAYER_ID = "__backlight";
|
|
12022
13116
|
function insertAfter(layers, id, node) {
|
|
12023
13117
|
const i = layers.findIndex((l) => l.id === id);
|
|
@@ -12059,6 +13153,7 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12059
13153
|
for (const l of flattenLayers(doc.layers)) if (l.type === "pixel") layers = mapLayer(layers, l.id, () => ({ ...l, offset: movePoint(l.offset, dx, dy) }));
|
|
12060
13154
|
const m = [1, 0, 0, 1, dx, dy];
|
|
12061
13155
|
const moveStickers = (list) => list.map((s) => ({ ...s, x: Math.round((s.x + dx) * 100) / 100, y: Math.round((s.y + dy) * 100) / 100 }));
|
|
13156
|
+
const moveMagnets = (list) => list.map((mag) => ({ ...mag, x: Math.round((mag.x + dx) * 100) / 100, y: Math.round((mag.y + dy) * 100) / 100 }));
|
|
12062
13157
|
const out = {
|
|
12063
13158
|
...doc,
|
|
12064
13159
|
layers,
|
|
@@ -12074,11 +13169,14 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12074
13169
|
return s.stencils ? { ...moved, stencils: s.stencils.map((x) => translateStencil(x, dx, dy)) } : moved;
|
|
12075
13170
|
};
|
|
12076
13171
|
if (doc.stickers) out.stickers = Object.fromEntries(Object.entries(doc.stickers).map(([id, list]) => [id, moveStickers(list)]));
|
|
13172
|
+
if (doc.magnets) out.magnets = moveMagnets(doc.magnets);
|
|
12077
13173
|
if (doc.liveStrokes) out.liveStrokes = Object.fromEntries(Object.entries(doc.liveStrokes).map(([id, list]) => [id, list.map(moveStroke)]));
|
|
12078
13174
|
if (doc.paintEvents) {
|
|
12079
13175
|
out.paintEvents = doc.paintEvents.map((e) => {
|
|
12080
13176
|
const list = e.kind === "stickers" ? e.data?.stickers : void 0;
|
|
12081
13177
|
if (list) return { ...e, data: { ...e.data, stickers: moveStickers(list) } };
|
|
13178
|
+
const magnets = e.kind === "magnets" ? e.data?.magnets : void 0;
|
|
13179
|
+
if (magnets) return { ...e, data: { ...e.data, magnets: moveMagnets(magnets) } };
|
|
12082
13180
|
const strokes = e.kind === "live-strokes" ? e.data?.strokes : void 0;
|
|
12083
13181
|
if (strokes) return { ...e, data: { ...e.data, strokes: strokes.map(moveStroke) } };
|
|
12084
13182
|
if (e.kind === "fill-region" && e.data) {
|
|
@@ -12102,6 +13200,9 @@ function shiftDocument(doc, dx, dy) {
|
|
|
12102
13200
|
if (e.kind === "dry" && typeof d.x === "number") return { ...e, data: moveRect(d, dx, dy) };
|
|
12103
13201
|
if (e.kind === "wet-paper" && d.rect) return { ...e, data: { ...d, rect: moveRect(d.rect, dx, dy) } };
|
|
12104
13202
|
if (e.kind === "flow" && d.center) return { ...e, data: { ...d, center: movePoint(d.center, dx, dy) } };
|
|
13203
|
+
if (e.kind === "seed-live-patterns" && Array.isArray(d.points)) {
|
|
13204
|
+
return { ...e, data: { ...d, points: d.points.map((p) => ({ ...p, x: p.x + dx, y: p.y + dy })) } };
|
|
13205
|
+
}
|
|
12105
13206
|
return e;
|
|
12106
13207
|
});
|
|
12107
13208
|
}
|
|
@@ -12431,7 +13532,7 @@ function unionRect2(a, b) {
|
|
|
12431
13532
|
|
|
12432
13533
|
// src/paint-editor/playback.ts
|
|
12433
13534
|
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)$/;
|
|
12434
|
-
var PAPER = /^(set_paper|set_lighting|flatten_relief|set_instant_dry)$/;
|
|
13535
|
+
var PAPER = /^(set_paper|set_lighting|flatten_relief|set_instant_dry|add_magnet|update_magnet|remove_magnets|cure_ferrofluid)$/;
|
|
12435
13536
|
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)$/;
|
|
12436
13537
|
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)$/;
|
|
12437
13538
|
var COLOR = /^(set_color|set_paint|color_harmony|add_swatch|remove_swatch|load_mixer_brush|clean_mixer_brush)$/;
|
|
@@ -13139,6 +14240,43 @@ var WET_PARAM_SCHEMA = {
|
|
|
13139
14240
|
bloom: UNIT("Blooms: water running into a drying wash pushes its pigment into cauliflower edges (default 0.4)."),
|
|
13140
14241
|
bleed: UNIT("Colour bleed between touching wet areas (default 0.3).")
|
|
13141
14242
|
};
|
|
14243
|
+
var LIVE_PATTERN_SCHEMA = {
|
|
14244
|
+
pattern: {
|
|
14245
|
+
type: "string",
|
|
14246
|
+
enum: [...LIVE_PATTERN_KINDS],
|
|
14247
|
+
description: "Pearson Gray-Scott preset: spots, stripes, coral, maze or worms (default spots). Sets feed and kill unless explicit feed/kill are also passed."
|
|
14248
|
+
},
|
|
14249
|
+
feed: {
|
|
14250
|
+
type: "number",
|
|
14251
|
+
minimum: 5e-3,
|
|
14252
|
+
maximum: 0.1,
|
|
14253
|
+
description: "Gray-Scott feed rate F, 0.005..0.1 (default: the pattern preset\u2019s feed rate)."
|
|
14254
|
+
},
|
|
14255
|
+
kill: {
|
|
14256
|
+
type: "number",
|
|
14257
|
+
minimum: 0.02,
|
|
14258
|
+
maximum: 0.08,
|
|
14259
|
+
description: "Gray-Scott kill rate k, 0.02..0.08 (default: the pattern preset\u2019s kill rate)."
|
|
14260
|
+
},
|
|
14261
|
+
scale: {
|
|
14262
|
+
type: "number",
|
|
14263
|
+
minimum: 0.25,
|
|
14264
|
+
maximum: 4,
|
|
14265
|
+
description: "Diffusion scale 0.25..4 (larger values grow broader pattern features; default 1)."
|
|
14266
|
+
},
|
|
14267
|
+
contrast: {
|
|
14268
|
+
type: "number",
|
|
14269
|
+
minimum: 0,
|
|
14270
|
+
maximum: 2,
|
|
14271
|
+
description: "How strongly V modulates the wet pigment or blends toward secondColor, 0..2 (default 0.85)."
|
|
14272
|
+
},
|
|
14273
|
+
secondColor: {
|
|
14274
|
+
type: ["string", "null"],
|
|
14275
|
+
pattern: "^#[0-9a-fA-F]{6}$",
|
|
14276
|
+
description: "Optional #rrggbb colour that high-V pattern ridges blend toward, or null to modulate pigment contrast (default null)."
|
|
14277
|
+
},
|
|
14278
|
+
relief: UNIT("Impasto ridge height 0..1 written from V into the layer\u2019s thick height map so light catches the pattern (default 0).")
|
|
14279
|
+
};
|
|
13142
14280
|
function createPaintWetTools(c) {
|
|
13143
14281
|
const stillWet = () => c.wetState().wet;
|
|
13144
14282
|
return [
|
|
@@ -13302,6 +14440,73 @@ function createPaintWetTools(c) {
|
|
|
13302
14440
|
inputSchema: { type: "object", properties: { visible: { type: "boolean", description: "Show (true) or hide (false) the overlay." } }, required: ["visible"], additionalProperties: false },
|
|
13303
14441
|
annotations: { title: "Show the water", ...SET2 },
|
|
13304
14442
|
handler: ({ visible }) => ({ visible: c.setWaterOverlay(visible), stillWet: stillWet() })
|
|
14443
|
+
},
|
|
14444
|
+
{
|
|
14445
|
+
name: "get_live_patterns",
|
|
14446
|
+
title: "Get live patterns",
|
|
14447
|
+
description: "Reads the Gray-Scott reaction-diffusion live patterns on a layer (layerId, default the active layer): whether live patterns are enabled, the active settings (pattern, feed, kill, scale, contrast, secondColor, relief), activeTiles, patternCells, wetPatternCells (pattern cells still holding water where the pattern can grow), meanV, maxV, bounds, wet coverage (wet, wetCells, wetBounds), layers (every layer with wet paint or live patterns), and presets (built-in Pearson feed/kill presets: spots, stripes, coral, maze, worms). Call it before or after advance_wet_paint and dry_paint to inspect pattern growth and freezing.",
|
|
14448
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID2 }, additionalProperties: false },
|
|
14449
|
+
annotations: { title: "Get live patterns", ...READ3 },
|
|
14450
|
+
coordinates: { out: ["wetBounds", "bounds", "layers[].wetBounds", "layers[].bounds"] },
|
|
14451
|
+
handler: ({ layerId }) => c.getLivePatterns(layerId)
|
|
14452
|
+
},
|
|
14453
|
+
{
|
|
14454
|
+
name: "set_live_patterns",
|
|
14455
|
+
title: "Set live patterns",
|
|
14456
|
+
description: "Configures a layer\u2019s Gray-Scott reaction-diffusion live patterns (or clears them with enabled: false). Patterns grow inside wet paint during advance_wet_paint and freeze when the paint dries (dry_paint). Choose a Pearson preset (spots, stripes, coral, maze, worms) or explicit feed and kill rates, scale, contrast, secondColor (#rrggbb or null to modulate pigment contrast) and relief (0..1, writes ridge height into thick paint so lighting catches the pattern). One undo step. Returns the layer\u2019s live-pattern stats and stillWet.",
|
|
14457
|
+
inputSchema: {
|
|
14458
|
+
type: "object",
|
|
14459
|
+
properties: {
|
|
14460
|
+
layerId: LAYER_ID2,
|
|
14461
|
+
enabled: { type: "boolean", description: "true (default) configures live patterns on the layer; false clears them." },
|
|
14462
|
+
...LIVE_PATTERN_SCHEMA
|
|
14463
|
+
},
|
|
14464
|
+
additionalProperties: false
|
|
14465
|
+
},
|
|
14466
|
+
annotations: { title: "Set live patterns", ...SET2 },
|
|
14467
|
+
coordinates: { out: ["wetBounds", "bounds"] },
|
|
14468
|
+
handler: async ({ layerId, ...update }) => {
|
|
14469
|
+
await c.wetIdle();
|
|
14470
|
+
return { ...c.setLivePatterns(layerId, update), stillWet: stillWet() };
|
|
14471
|
+
}
|
|
14472
|
+
},
|
|
14473
|
+
{
|
|
14474
|
+
name: "seed_live_patterns",
|
|
14475
|
+
title: "Seed live patterns",
|
|
14476
|
+
description: 'Seeds Gray-Scott reaction-diffusion activator (V) discs at points on a layer so reaction-diffusion patterns (spots, stripes, coral, maze, worms) grow where the layer holds wet water (advance_wet_paint) and freeze where it dries (dry_paint). Painting with a wet brush that has a livePatterns facet (such as preset "turing-wash" or "coral-ink") also seeds along the stroke automatically. One undo step. Returns seededCells, the layer\u2019s live-pattern stats (patternCells, wetPatternCells, meanV, maxV, bounds) and stillWet.',
|
|
14477
|
+
inputSchema: {
|
|
14478
|
+
type: "object",
|
|
14479
|
+
properties: {
|
|
14480
|
+
layerId: LAYER_ID2,
|
|
14481
|
+
points: {
|
|
14482
|
+
type: "array",
|
|
14483
|
+
minItems: 1,
|
|
14484
|
+
maxItems: 1024,
|
|
14485
|
+
description: "Seed centres in document pixels where reaction-diffusion activator V is nucleated.",
|
|
14486
|
+
items: {
|
|
14487
|
+
type: "object",
|
|
14488
|
+
properties: {
|
|
14489
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
14490
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
14491
|
+
strength: { type: "number", exclusiveMinimum: 0, maximum: 1, description: "Seed strength 0..1 (default 1)." }
|
|
14492
|
+
},
|
|
14493
|
+
required: ["x", "y"],
|
|
14494
|
+
additionalProperties: false
|
|
14495
|
+
}
|
|
14496
|
+
},
|
|
14497
|
+
radius: { type: "number", minimum: 1, maximum: 512, description: "Seed disc radius in document pixels (default 16)." },
|
|
14498
|
+
seed: { type: "integer", minimum: 0, description: "Deterministic noise seed for the nucleated discs (default: next editor seed)." },
|
|
14499
|
+
...LIVE_PATTERN_SCHEMA
|
|
14500
|
+
},
|
|
14501
|
+
required: ["points"],
|
|
14502
|
+
additionalProperties: false
|
|
14503
|
+
},
|
|
14504
|
+
annotations: { title: "Seed live patterns", ...WRITE3 },
|
|
14505
|
+
coordinates: { in: ["points"], out: ["wetBounds", "bounds"] },
|
|
14506
|
+
handler: async (args) => {
|
|
14507
|
+
await c.wetIdle();
|
|
14508
|
+
return { ...c.seedLivePatterns(args), stillWet: stillWet() };
|
|
14509
|
+
}
|
|
13305
14510
|
}
|
|
13306
14511
|
];
|
|
13307
14512
|
}
|
|
@@ -13518,8 +14723,111 @@ function createPaintThickTools(c) {
|
|
|
13518
14723
|
];
|
|
13519
14724
|
}
|
|
13520
14725
|
|
|
13521
|
-
// src/paint-editor/
|
|
14726
|
+
// src/paint-editor/ferro-tools.ts
|
|
13522
14727
|
var READ5 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14728
|
+
var SET4 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14729
|
+
var WRITE5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14730
|
+
var EMPTY3 = { type: "object", properties: {}, additionalProperties: false };
|
|
14731
|
+
var LAYER_ID4 = { type: "string", description: "Pixel layer id from list_layers (default the active layer)." };
|
|
14732
|
+
function createFerroTools(c) {
|
|
14733
|
+
return [
|
|
14734
|
+
{
|
|
14735
|
+
name: "add_magnet",
|
|
14736
|
+
title: "Place a magnet",
|
|
14737
|
+
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).",
|
|
14738
|
+
inputSchema: {
|
|
14739
|
+
type: "object",
|
|
14740
|
+
properties: {
|
|
14741
|
+
x: { type: "number", description: "Magnet centre x in document pixels." },
|
|
14742
|
+
y: { type: "number", description: "Magnet centre y in document pixels." },
|
|
14743
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Dipole strength -1..1 (default 0.8; negative flips the pole)." },
|
|
14744
|
+
radius: { type: "number", minimum: 4, maximum: 2048, description: "Dipole falloff radius in pixels (default 80)." }
|
|
14745
|
+
},
|
|
14746
|
+
required: ["x", "y"],
|
|
14747
|
+
additionalProperties: false
|
|
14748
|
+
},
|
|
14749
|
+
annotations: { title: "Place a magnet", ...WRITE5 },
|
|
14750
|
+
coordinates: { in: [""], out: [""] },
|
|
14751
|
+
handler: (a) => c.addMagnet(a)
|
|
14752
|
+
},
|
|
14753
|
+
{
|
|
14754
|
+
name: "update_magnet",
|
|
14755
|
+
title: "Move or tune a magnet",
|
|
14756
|
+
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.",
|
|
14757
|
+
inputSchema: {
|
|
14758
|
+
type: "object",
|
|
14759
|
+
properties: {
|
|
14760
|
+
id: { type: "string", description: "Magnet id from list_magnets." },
|
|
14761
|
+
x: { type: "number", description: "Magnet centre x in document pixels." },
|
|
14762
|
+
y: { type: "number", description: "Magnet centre y in document pixels." },
|
|
14763
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Dipole strength -1..1." },
|
|
14764
|
+
radius: { type: "number", minimum: 4, maximum: 2048, description: "Dipole falloff radius in pixels (4..2048)." }
|
|
14765
|
+
},
|
|
14766
|
+
required: ["id"],
|
|
14767
|
+
additionalProperties: false
|
|
14768
|
+
},
|
|
14769
|
+
annotations: { title: "Move or tune a magnet", ...SET4 },
|
|
14770
|
+
coordinates: { in: [""], out: [""] },
|
|
14771
|
+
handler: ({ id, ...u }) => c.updateMagnet(id, u)
|
|
14772
|
+
},
|
|
14773
|
+
{
|
|
14774
|
+
name: "remove_magnets",
|
|
14775
|
+
title: "Remove magnets",
|
|
14776
|
+
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.",
|
|
14777
|
+
inputSchema: {
|
|
14778
|
+
type: "object",
|
|
14779
|
+
properties: {
|
|
14780
|
+
ids: {
|
|
14781
|
+
type: "array",
|
|
14782
|
+
items: { type: "string", description: "Magnet id from list_magnets." },
|
|
14783
|
+
description: "Magnet ids to remove (default: remove all magnets)."
|
|
14784
|
+
}
|
|
14785
|
+
},
|
|
14786
|
+
additionalProperties: false
|
|
14787
|
+
},
|
|
14788
|
+
annotations: { title: "Remove magnets", ...WRITE5 },
|
|
14789
|
+
handler: ({ ids }) => ({ removed: c.removeMagnets(ids) })
|
|
14790
|
+
},
|
|
14791
|
+
{
|
|
14792
|
+
name: "list_magnets",
|
|
14793
|
+
title: "List magnets",
|
|
14794
|
+
description: "Lists the magnets placed on the canvas in order: id, x, y (document pixels), strength (-1..1) and radius (px). Changes nothing.",
|
|
14795
|
+
inputSchema: EMPTY3,
|
|
14796
|
+
annotations: { title: "List magnets", ...READ5 },
|
|
14797
|
+
coordinates: { out: ["magnets[]"] },
|
|
14798
|
+
handler: () => ({ magnets: c.listMagnets() })
|
|
14799
|
+
},
|
|
14800
|
+
{
|
|
14801
|
+
name: "get_ferrofluid",
|
|
14802
|
+
title: "Read a layer\u2019s ferrofluid and spikes",
|
|
14803
|
+
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.",
|
|
14804
|
+
inputSchema: {
|
|
14805
|
+
type: "object",
|
|
14806
|
+
properties: { layerId: LAYER_ID4 },
|
|
14807
|
+
additionalProperties: false
|
|
14808
|
+
},
|
|
14809
|
+
annotations: { title: "Read a layer\u2019s ferrofluid and spikes", ...READ5 },
|
|
14810
|
+
coordinates: { out: ["bounds"] },
|
|
14811
|
+
handler: ({ layerId }) => c.getFerrofluid(layerId)
|
|
14812
|
+
},
|
|
14813
|
+
{
|
|
14814
|
+
name: "cure_ferrofluid",
|
|
14815
|
+
title: "Cure ferrofluid into solid relief",
|
|
14816
|
+
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.",
|
|
14817
|
+
inputSchema: {
|
|
14818
|
+
type: "object",
|
|
14819
|
+
properties: { layerId: LAYER_ID4 },
|
|
14820
|
+
additionalProperties: false
|
|
14821
|
+
},
|
|
14822
|
+
annotations: { title: "Cure ferrofluid into solid relief", ...WRITE5 },
|
|
14823
|
+
coordinates: { out: ["bounds"] },
|
|
14824
|
+
handler: ({ layerId }) => c.cureFerrofluid(layerId)
|
|
14825
|
+
}
|
|
14826
|
+
];
|
|
14827
|
+
}
|
|
14828
|
+
|
|
14829
|
+
// src/paint-editor/replay-tools.ts
|
|
14830
|
+
var READ6 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13523
14831
|
function createPaintReplayTools(c) {
|
|
13524
14832
|
return [
|
|
13525
14833
|
{
|
|
@@ -13538,7 +14846,7 @@ function createPaintReplayTools(c) {
|
|
|
13538
14846
|
},
|
|
13539
14847
|
additionalProperties: false
|
|
13540
14848
|
},
|
|
13541
|
-
annotations: { title: "Export a time-lapse", ...
|
|
14849
|
+
annotations: { title: "Export a time-lapse", ...READ6 },
|
|
13542
14850
|
handler: async (a) => {
|
|
13543
14851
|
await c.wetIdle();
|
|
13544
14852
|
const tl = await c.timeLapse({ frames: a.frames, fps: a.fps, maxSide: a.maxSide, wetStepsPerFrame: a.wetStepsPerFrame, holdMs: a.holdMs });
|
|
@@ -13570,7 +14878,7 @@ function createPaintReplayTools(c) {
|
|
|
13570
14878
|
required: ["scale"],
|
|
13571
14879
|
additionalProperties: false
|
|
13572
14880
|
},
|
|
13573
|
-
annotations: { title: "Export at a higher resolution", ...
|
|
14881
|
+
annotations: { title: "Export at a higher resolution", ...READ6 },
|
|
13574
14882
|
handler: async ({ scale, paper }) => {
|
|
13575
14883
|
await c.wetIdle();
|
|
13576
14884
|
const img = await c.renderHighRes({ scale, paper });
|
|
@@ -13582,9 +14890,9 @@ function createPaintReplayTools(c) {
|
|
|
13582
14890
|
}
|
|
13583
14891
|
|
|
13584
14892
|
// src/paint-editor/mutation-tools.ts
|
|
13585
|
-
var
|
|
13586
|
-
var
|
|
13587
|
-
var
|
|
14893
|
+
var READ7 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14894
|
+
var WRITE6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
14895
|
+
var SET5 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13588
14896
|
var POINTS = {
|
|
13589
14897
|
type: "array",
|
|
13590
14898
|
minItems: 1,
|
|
@@ -13623,7 +14931,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13623
14931
|
},
|
|
13624
14932
|
additionalProperties: false
|
|
13625
14933
|
},
|
|
13626
|
-
annotations: { title: "List pending mutations", ...
|
|
14934
|
+
annotations: { title: "List pending mutations", ...READ7 },
|
|
13627
14935
|
coordinates: { out: ["mutations[].bounds"] },
|
|
13628
14936
|
handler: (a) => ({ mutations: c.listMutations({ includeAll: Boolean(a.includeAll) }) })
|
|
13629
14937
|
},
|
|
@@ -13654,7 +14962,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13654
14962
|
required: ["id"],
|
|
13655
14963
|
additionalProperties: false
|
|
13656
14964
|
},
|
|
13657
|
-
annotations: { title: "Read and claim a mutation", ...
|
|
14965
|
+
annotations: { title: "Read and claim a mutation", ...WRITE6 },
|
|
13658
14966
|
coordinates: { out: ["bounds", "strokes[].bounds", "strokes[].points[]", "cropPng.rect"] },
|
|
13659
14967
|
handler: (a) => c.readMutation(a.id, {
|
|
13660
14968
|
actor: options.actor?.id,
|
|
@@ -13731,7 +15039,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13731
15039
|
required: ["id", "strokes"],
|
|
13732
15040
|
additionalProperties: false
|
|
13733
15041
|
},
|
|
13734
|
-
annotations: { title: "Answer a mutation with strokes", ...
|
|
15042
|
+
annotations: { title: "Answer a mutation with strokes", ...WRITE6 },
|
|
13735
15043
|
coordinates: { in: ["strokes[].points[]"], out: ["bounds"] },
|
|
13736
15044
|
handler: async (a) => {
|
|
13737
15045
|
await c.wetIdle();
|
|
@@ -13785,7 +15093,7 @@ function createMutationTools(c, options = {}) {
|
|
|
13785
15093
|
required: ["id"],
|
|
13786
15094
|
additionalProperties: false
|
|
13787
15095
|
},
|
|
13788
|
-
annotations: { title: "Decline a mutation", ...
|
|
15096
|
+
annotations: { title: "Decline a mutation", ...SET5 },
|
|
13789
15097
|
coordinates: { out: ["bounds"] },
|
|
13790
15098
|
handler: (a) => c.declineMutation(a.id, a.reason)
|
|
13791
15099
|
}
|
|
@@ -13793,8 +15101,8 @@ function createMutationTools(c, options = {}) {
|
|
|
13793
15101
|
}
|
|
13794
15102
|
|
|
13795
15103
|
// src/paint-editor/evolution-tools.ts
|
|
13796
|
-
var
|
|
13797
|
-
var
|
|
15104
|
+
var READ8 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15105
|
+
var WRITE7 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
13798
15106
|
var UNIT2 = (description) => ({ type: "number", minimum: 0, maximum: 1, description });
|
|
13799
15107
|
var EVOLUTION_MUTATION_SCHEMA = {
|
|
13800
15108
|
type: "object",
|
|
@@ -13839,7 +15147,7 @@ function createPaintEvolutionTools(c) {
|
|
|
13839
15147
|
required: ["strokeIds"],
|
|
13840
15148
|
additionalProperties: false
|
|
13841
15149
|
},
|
|
13842
|
-
annotations: { title: "Breed stroke variants", ...
|
|
15150
|
+
annotations: { title: "Breed stroke variants", ...WRITE7 },
|
|
13843
15151
|
coordinates: { out: ["proposals[].bounds"] },
|
|
13844
15152
|
handler: ({ strokeIds, count, mutation, seed }, context) => {
|
|
13845
15153
|
const actor = context?.actor?.id;
|
|
@@ -13882,7 +15190,7 @@ function createPaintEvolutionTools(c) {
|
|
|
13882
15190
|
required: ["proposalIds"],
|
|
13883
15191
|
additionalProperties: false
|
|
13884
15192
|
},
|
|
13885
|
-
annotations: { title: "Breed from paint proposals", ...
|
|
15193
|
+
annotations: { title: "Breed from paint proposals", ...WRITE7 },
|
|
13886
15194
|
coordinates: { out: ["proposals[].bounds"] },
|
|
13887
15195
|
handler: ({ proposalIds, count, mutation, seed }, context) => {
|
|
13888
15196
|
const actor = context?.actor?.id;
|
|
@@ -13910,7 +15218,7 @@ function createPaintEvolutionTools(c) {
|
|
|
13910
15218
|
required: ["proposalId"],
|
|
13911
15219
|
additionalProperties: false
|
|
13912
15220
|
},
|
|
13913
|
-
annotations: { title: "Get a proposal\u2019s stroke genome", ...
|
|
15221
|
+
annotations: { title: "Get a proposal\u2019s stroke genome", ...READ8 },
|
|
13914
15222
|
coordinates: { out: ["bounds", "strokes[].points[]"] },
|
|
13915
15223
|
handler: ({ proposalId }) => c.getProposalGenome(proposalId)
|
|
13916
15224
|
}
|
|
@@ -13929,13 +15237,13 @@ function createPaintEvolutionToolProvider(controller, options = {}) {
|
|
|
13929
15237
|
}
|
|
13930
15238
|
|
|
13931
15239
|
// src/paint-editor/tools.ts
|
|
13932
|
-
var
|
|
13933
|
-
var
|
|
13934
|
-
var
|
|
15240
|
+
var READ9 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
15241
|
+
var WRITE8 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
15242
|
+
var SET6 = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
13935
15243
|
var DESTRUCTIVE3 = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
|
|
13936
|
-
var
|
|
15244
|
+
var EMPTY4 = { type: "object", properties: {}, additionalProperties: false };
|
|
13937
15245
|
var COLOR3 = { type: "string", pattern: "^#[0-9a-fA-F]{6}$", description: 'Colour #rrggbb, e.g. "#2a4b8d".' };
|
|
13938
|
-
var
|
|
15246
|
+
var LAYER_ID5 = { type: "string", description: "Layer id from list_layers (default the active layer)." };
|
|
13939
15247
|
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)." };
|
|
13940
15248
|
var POINTS2 = {
|
|
13941
15249
|
type: "array",
|
|
@@ -13978,8 +15286,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
13978
15286
|
name: "get_canvas_info",
|
|
13979
15287
|
title: "Get the canvas",
|
|
13980
15288
|
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.",
|
|
13981
|
-
inputSchema:
|
|
13982
|
-
annotations: { title: "Get the canvas", ...
|
|
15289
|
+
inputSchema: EMPTY4,
|
|
15290
|
+
annotations: { title: "Get the canvas", ...READ9 },
|
|
13983
15291
|
coordinates: { out: ["bounds"] },
|
|
13984
15292
|
handler: () => {
|
|
13985
15293
|
const s = c.getState();
|
|
@@ -14040,7 +15348,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14040
15348
|
},
|
|
14041
15349
|
additionalProperties: false
|
|
14042
15350
|
},
|
|
14043
|
-
annotations: { title: "Resize the canvas", ...
|
|
15351
|
+
annotations: { title: "Resize the canvas", ...SET6 },
|
|
14044
15352
|
handler: (a) => c.setCanvas({ width: a.width, height: a.height, dpi: a.dpi })
|
|
14045
15353
|
});
|
|
14046
15354
|
const EXTEND_SIDE = (side) => ({ type: "integer", minimum: 0, maximum: MAX_CANVAS_SIDE - 1, description: `Pixels to add on the ${side} (default 0).` });
|
|
@@ -14053,15 +15361,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14053
15361
|
properties: { left: EXTEND_SIDE("left"), top: EXTEND_SIDE("top"), right: EXTEND_SIDE("right"), bottom: EXTEND_SIDE("bottom") },
|
|
14054
15362
|
additionalProperties: false
|
|
14055
15363
|
},
|
|
14056
|
-
annotations: { title: "Extend the canvas", ...
|
|
15364
|
+
annotations: { title: "Extend the canvas", ...WRITE8 },
|
|
14057
15365
|
handler: (a) => c.extendCanvas({ left: a.left, top: a.top, right: a.right, bottom: a.bottom })
|
|
14058
15366
|
});
|
|
14059
15367
|
add({
|
|
14060
15368
|
name: "list_papers",
|
|
14061
15369
|
title: "List papers",
|
|
14062
15370
|
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.",
|
|
14063
|
-
inputSchema:
|
|
14064
|
-
annotations: { title: "List papers", ...
|
|
15371
|
+
inputSchema: EMPTY4,
|
|
15372
|
+
annotations: { title: "List papers", ...READ9 },
|
|
14065
15373
|
handler: () => ({
|
|
14066
15374
|
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 })),
|
|
14067
15375
|
families: [...PAPER_FAMILIES],
|
|
@@ -14072,8 +15380,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14072
15380
|
name: "get_paper",
|
|
14073
15381
|
title: "Get the paper",
|
|
14074
15382
|
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).",
|
|
14075
|
-
inputSchema:
|
|
14076
|
-
annotations: { title: "Get the paper", ...
|
|
15383
|
+
inputSchema: EMPTY4,
|
|
15384
|
+
annotations: { title: "Get the paper", ...READ9 },
|
|
14077
15385
|
handler: () => JSON.parse(JSON.stringify(c.document.paper))
|
|
14078
15386
|
});
|
|
14079
15387
|
const tuning = {
|
|
@@ -14111,15 +15419,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14111
15419
|
},
|
|
14112
15420
|
additionalProperties: false
|
|
14113
15421
|
},
|
|
14114
|
-
annotations: { title: "Set the paper", ...
|
|
15422
|
+
annotations: { title: "Set the paper", ...SET6 },
|
|
14115
15423
|
handler: (a) => c.setPaper(a)
|
|
14116
15424
|
});
|
|
14117
15425
|
add({
|
|
14118
15426
|
name: "list_layers",
|
|
14119
15427
|
title: "List layers",
|
|
14120
15428
|
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.",
|
|
14121
|
-
inputSchema:
|
|
14122
|
-
annotations: { title: "List layers", ...
|
|
15429
|
+
inputSchema: EMPTY4,
|
|
15430
|
+
annotations: { title: "List layers", ...READ9 },
|
|
14123
15431
|
coordinates: { out: ["layers[].bounds"] },
|
|
14124
15432
|
handler: () => ({ layers: c.listLayers(), activeLayerId: c.document.activeLayerId })
|
|
14125
15433
|
});
|
|
@@ -14137,7 +15445,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14137
15445
|
},
|
|
14138
15446
|
additionalProperties: false
|
|
14139
15447
|
},
|
|
14140
|
-
annotations: { title: "Add a layer", ...
|
|
15448
|
+
annotations: { title: "Add a layer", ...WRITE8 },
|
|
14141
15449
|
coordinates: { out: ["bounds"] },
|
|
14142
15450
|
handler: (a) => c.addLayer({ name: a.name, type: a.type, aboveId: a.aboveId, parentId: a.parentId })
|
|
14143
15451
|
});
|
|
@@ -14173,7 +15481,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14173
15481
|
required: ["layerId"],
|
|
14174
15482
|
additionalProperties: false
|
|
14175
15483
|
},
|
|
14176
|
-
annotations: { title: "Change a layer", ...
|
|
15484
|
+
annotations: { title: "Change a layer", ...SET6 },
|
|
14177
15485
|
coordinates: { out: ["bounds"] },
|
|
14178
15486
|
handler: ({ layerId, ...u }) => c.updateLayer(layerId, u)
|
|
14179
15487
|
});
|
|
@@ -14181,8 +15489,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14181
15489
|
name: "list_blend_modes",
|
|
14182
15490
|
title: "List blend modes",
|
|
14183
15491
|
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.",
|
|
14184
|
-
inputSchema:
|
|
14185
|
-
annotations: { title: "List blend modes", ...
|
|
15492
|
+
inputSchema: EMPTY4,
|
|
15493
|
+
annotations: { title: "List blend modes", ...READ9 },
|
|
14186
15494
|
handler: () => ({
|
|
14187
15495
|
modes: BLEND_MODES.map((m) => ({
|
|
14188
15496
|
id: m.id,
|
|
@@ -14198,7 +15506,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14198
15506
|
title: "Choose the active layer",
|
|
14199
15507
|
description: "Makes a layer active: strokes, fills and clears without a layerId go to it. Editor state, not an undo step. Returns the layer.",
|
|
14200
15508
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
14201
|
-
annotations: { title: "Choose the active layer", ...
|
|
15509
|
+
annotations: { title: "Choose the active layer", ...SET6 },
|
|
14202
15510
|
coordinates: { out: ["bounds"] },
|
|
14203
15511
|
handler: ({ layerId }) => {
|
|
14204
15512
|
c.setActiveLayer(layerId);
|
|
@@ -14218,7 +15526,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14218
15526
|
title: "Duplicate a layer",
|
|
14219
15527
|
description: "Copies a layer (a group with its children) directly above it; the copy becomes active. One undo step. Returns the copy.",
|
|
14220
15528
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers." } }, required: ["layerId"], additionalProperties: false },
|
|
14221
|
-
annotations: { title: "Duplicate a layer", ...
|
|
15529
|
+
annotations: { title: "Duplicate a layer", ...WRITE8 },
|
|
14222
15530
|
coordinates: { out: ["bounds"] },
|
|
14223
15531
|
handler: ({ layerId }) => c.duplicateLayer(layerId)
|
|
14224
15532
|
});
|
|
@@ -14236,7 +15544,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14236
15544
|
required: ["layerId", "index"],
|
|
14237
15545
|
additionalProperties: false
|
|
14238
15546
|
},
|
|
14239
|
-
annotations: { title: "Reorder a layer", ...
|
|
15547
|
+
annotations: { title: "Reorder a layer", ...WRITE8 },
|
|
14240
15548
|
coordinates: { out: ["bounds"] },
|
|
14241
15549
|
handler: ({ layerId, index, parentId }) => c.moveLayer(layerId, index, parentId)
|
|
14242
15550
|
});
|
|
@@ -14256,7 +15564,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14256
15564
|
inputSchema: {
|
|
14257
15565
|
type: "object",
|
|
14258
15566
|
properties: {
|
|
14259
|
-
layerId:
|
|
15567
|
+
layerId: LAYER_ID5,
|
|
14260
15568
|
color: { ...COLOR3, description: "Fill colour #rrggbb (default the paint colour)." },
|
|
14261
15569
|
opacity: { type: "number", minimum: 0, maximum: 1, description: "Opacity 0..1 (default 1)." },
|
|
14262
15570
|
grain: { type: "number", minimum: 0, maximum: 1, description: "Paper grain response 0..1 (default 0: flat)." },
|
|
@@ -14265,7 +15573,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14265
15573
|
},
|
|
14266
15574
|
additionalProperties: false
|
|
14267
15575
|
},
|
|
14268
|
-
annotations: { title: "Fill a layer", ...
|
|
15576
|
+
annotations: { title: "Fill a layer", ...WRITE8 },
|
|
14269
15577
|
coordinates: { out: ["bounds"] },
|
|
14270
15578
|
handler: (a) => act({ kind: "fill", fill: a }, () => c.fillLayer(a))
|
|
14271
15579
|
});
|
|
@@ -14293,7 +15601,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14293
15601
|
additionalProperties: false
|
|
14294
15602
|
}
|
|
14295
15603
|
},
|
|
14296
|
-
layerId:
|
|
15604
|
+
layerId: LAYER_ID5,
|
|
14297
15605
|
reference: {
|
|
14298
15606
|
type: "string",
|
|
14299
15607
|
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).'
|
|
@@ -14312,7 +15620,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14312
15620
|
},
|
|
14313
15621
|
additionalProperties: false
|
|
14314
15622
|
},
|
|
14315
|
-
annotations: { title: "Fill a region (bucket / flats)", ...
|
|
15623
|
+
annotations: { title: "Fill a region (bucket / flats)", ...WRITE8 },
|
|
14316
15624
|
coordinates: { in: ["", "points[]"], out: ["bounds", "regions[].point", "regions[].bounds"] },
|
|
14317
15625
|
handler: (a) => act({ kind: "fill-region", fill: a }, () => c.fillRegion(a))
|
|
14318
15626
|
});
|
|
@@ -14336,7 +15644,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14336
15644
|
},
|
|
14337
15645
|
additionalProperties: false
|
|
14338
15646
|
},
|
|
14339
|
-
annotations: { title: "Find enclosed unfilled regions", ...
|
|
15647
|
+
annotations: { title: "Find enclosed unfilled regions", ...READ9 },
|
|
14340
15648
|
coordinates: { out: ["regions[].point", "regions[].centroid", "regions[].bounds"] },
|
|
14341
15649
|
handler: (a) => c.findUnfilledRegions(a)
|
|
14342
15650
|
});
|
|
@@ -14344,7 +15652,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14344
15652
|
name: "clear_layer",
|
|
14345
15653
|
title: "Clear a layer",
|
|
14346
15654
|
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).",
|
|
14347
|
-
inputSchema: { type: "object", properties: { layerId:
|
|
15655
|
+
inputSchema: { type: "object", properties: { layerId: LAYER_ID5 }, additionalProperties: false },
|
|
14348
15656
|
annotations: { title: "Clear a layer", ...DESTRUCTIVE3 },
|
|
14349
15657
|
coordinates: { out: ["bounds"] },
|
|
14350
15658
|
handler: ({ layerId }) => c.clearLayer(layerId)
|
|
@@ -14354,7 +15662,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14354
15662
|
title: "Get a brush",
|
|
14355
15663
|
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.",
|
|
14356
15664
|
inputSchema: { type: "object", properties: { id: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." } }, additionalProperties: false },
|
|
14357
|
-
annotations: { title: "Get a brush", ...
|
|
15665
|
+
annotations: { title: "Get a brush", ...READ9 },
|
|
14358
15666
|
handler: ({ id }) => {
|
|
14359
15667
|
const st = c.getState();
|
|
14360
15668
|
const b = id === void 0 ? st.brush : c.getBrush(id);
|
|
@@ -14368,7 +15676,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14368
15676
|
title: "Set the brush",
|
|
14369
15677
|
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).",
|
|
14370
15678
|
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 },
|
|
14371
|
-
annotations: { title: "Set the brush", ...
|
|
15679
|
+
annotations: { title: "Set the brush", ...SET6 },
|
|
14372
15680
|
handler: async ({ id, unset, ...patch }) => {
|
|
14373
15681
|
const stamps = patch.tip?.stamps;
|
|
14374
15682
|
if (stamps) await c.prepareStickers(stamps);
|
|
@@ -14389,7 +15697,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14389
15697
|
},
|
|
14390
15698
|
additionalProperties: false
|
|
14391
15699
|
},
|
|
14392
|
-
annotations: { title: "Set the paint colour", ...
|
|
15700
|
+
annotations: { title: "Set the paint colour", ...SET6 },
|
|
14393
15701
|
handler: ({ color, secondaryColor, locked }) => {
|
|
14394
15702
|
if (color === void 0 && secondaryColor === void 0 && locked === void 0) throw new Error("Pass color, secondaryColor or locked.");
|
|
14395
15703
|
if (color !== void 0) c.setColor(color);
|
|
@@ -14404,7 +15712,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14404
15712
|
title: "Set the paint (colour or gradient)",
|
|
14405
15713
|
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.',
|
|
14406
15714
|
inputSchema: { type: "object", properties: { paint: PAINT_SOURCE_JSON_SCHEMA }, required: ["paint"], additionalProperties: false },
|
|
14407
|
-
annotations: { title: "Set the paint (colour or gradient)", ...
|
|
15715
|
+
annotations: { title: "Set the paint (colour or gradient)", ...SET6 },
|
|
14408
15716
|
handler: ({ paint }) => {
|
|
14409
15717
|
const p = c.setPaint(paint);
|
|
14410
15718
|
const s = c.getState();
|
|
@@ -14425,7 +15733,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14425
15733
|
required: ["x", "y"],
|
|
14426
15734
|
additionalProperties: false
|
|
14427
15735
|
},
|
|
14428
|
-
annotations: { title: "Pick a colour", ...
|
|
15736
|
+
annotations: { title: "Pick a colour", ...READ9 },
|
|
14429
15737
|
coordinates: { in: [""] },
|
|
14430
15738
|
handler: ({ x, y, layerId }) => c.pickColor(x, y, { layerId })
|
|
14431
15739
|
});
|
|
@@ -14433,8 +15741,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14433
15741
|
name: "get_mixer_brush",
|
|
14434
15742
|
title: "Get the Mixer Brush load",
|
|
14435
15743
|
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.",
|
|
14436
|
-
inputSchema:
|
|
14437
|
-
annotations: { title: "Get the Mixer Brush load", ...
|
|
15744
|
+
inputSchema: EMPTY4,
|
|
15745
|
+
annotations: { title: "Get the Mixer Brush load", ...READ9 },
|
|
14438
15746
|
handler: () => loadResult(c.getState().mixerLoad)
|
|
14439
15747
|
});
|
|
14440
15748
|
add({
|
|
@@ -14442,7 +15750,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14442
15750
|
title: "Load the Mixer Brush",
|
|
14443
15751
|
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.",
|
|
14444
15752
|
inputSchema: { type: "object", properties: { color: { ...COLOR3, description: "Colour #rrggbb (default the paint colour)." } }, additionalProperties: false },
|
|
14445
|
-
annotations: { title: "Load the Mixer Brush", ...
|
|
15753
|
+
annotations: { title: "Load the Mixer Brush", ...SET6 },
|
|
14446
15754
|
handler: ({ color }) => loadResult(c.loadMixerBrush(color))
|
|
14447
15755
|
});
|
|
14448
15756
|
add({
|
|
@@ -14450,7 +15758,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14450
15758
|
title: "Clean the Mixer Brush",
|
|
14451
15759
|
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.",
|
|
14452
15760
|
inputSchema: { type: "object", properties: { emptyReservoir: { type: "boolean", description: "Also drop the loaded paint (default false)." } }, additionalProperties: false },
|
|
14453
|
-
annotations: { title: "Clean the Mixer Brush", ...
|
|
15761
|
+
annotations: { title: "Clean the Mixer Brush", ...SET6 },
|
|
14454
15762
|
handler: ({ emptyReservoir }) => loadResult(c.cleanMixerBrush(emptyReservoir === true))
|
|
14455
15763
|
});
|
|
14456
15764
|
add({
|
|
@@ -14461,7 +15769,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14461
15769
|
type: "object",
|
|
14462
15770
|
properties: {
|
|
14463
15771
|
points: POINTS2,
|
|
14464
|
-
layerId:
|
|
15772
|
+
layerId: LAYER_ID5,
|
|
14465
15773
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
14466
15774
|
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 },
|
|
14467
15775
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
@@ -14472,7 +15780,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14472
15780
|
required: ["points"],
|
|
14473
15781
|
additionalProperties: false
|
|
14474
15782
|
},
|
|
14475
|
-
annotations: { title: "Paint a brush stroke", ...
|
|
15783
|
+
annotations: { title: "Paint a brush stroke", ...WRITE8 },
|
|
14476
15784
|
coordinates: { in: ["points[]"], out: ["bounds"] },
|
|
14477
15785
|
handler: async (a) => {
|
|
14478
15786
|
await c.wetIdle();
|
|
@@ -14499,7 +15807,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14499
15807
|
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." },
|
|
14500
15808
|
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." },
|
|
14501
15809
|
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)." },
|
|
14502
|
-
layerId:
|
|
15810
|
+
layerId: LAYER_ID5,
|
|
14503
15811
|
brushId: { type: "string", description: "Brush id from list_brush_presets (default the active brush)." },
|
|
14504
15812
|
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 },
|
|
14505
15813
|
color: { ...COLOR3, description: "Paint colour #rrggbb (default the paint colour)." },
|
|
@@ -14509,7 +15817,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14509
15817
|
required: ["path"],
|
|
14510
15818
|
additionalProperties: false
|
|
14511
15819
|
},
|
|
14512
|
-
annotations: { title: "Brush along a path", ...
|
|
15820
|
+
annotations: { title: "Brush along a path", ...WRITE8 },
|
|
14513
15821
|
coordinates: { out: ["bounds"] },
|
|
14514
15822
|
handler: async (a) => {
|
|
14515
15823
|
await c.wetIdle();
|
|
@@ -14563,7 +15871,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14563
15871
|
properties: { limit: { type: "integer", minimum: 1, maximum: 5e3, description: "Most recent samples to return (default 500)." } },
|
|
14564
15872
|
additionalProperties: false
|
|
14565
15873
|
},
|
|
14566
|
-
annotations: { title: "Get the stroke being painted", ...
|
|
15874
|
+
annotations: { title: "Get the stroke being painted", ...READ9 },
|
|
14567
15875
|
handler: (args) => {
|
|
14568
15876
|
const live = c.getLiveStroke();
|
|
14569
15877
|
if (!live) return { live: false };
|
|
@@ -14585,7 +15893,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14585
15893
|
},
|
|
14586
15894
|
additionalProperties: false
|
|
14587
15895
|
},
|
|
14588
|
-
annotations: { title: "List recorded strokes", ...
|
|
15896
|
+
annotations: { title: "List recorded strokes", ...READ9 },
|
|
14589
15897
|
coordinates: { out: ["strokes[].bounds", "strokes[].points[]"] },
|
|
14590
15898
|
handler: ({ ids, last, live, layerId }) => {
|
|
14591
15899
|
if (layerId !== void 0 && !c.getLayer(layerId)) throw new Error(`Unknown layer "${layerId}". Call list_layers for the ids.`);
|
|
@@ -14614,7 +15922,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14614
15922
|
required: ["x", "y"],
|
|
14615
15923
|
additionalProperties: false
|
|
14616
15924
|
},
|
|
14617
|
-
annotations: { title: "Find the stroke at a point", ...
|
|
15925
|
+
annotations: { title: "Find the stroke at a point", ...READ9 },
|
|
14618
15926
|
coordinates: { in: [""], out: ["stroke.bounds"] },
|
|
14619
15927
|
handler: ({ layerId, x, y }) => {
|
|
14620
15928
|
const s = c.liveStrokeAt(layerId, x, y);
|
|
@@ -14638,7 +15946,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14638
15946
|
required: ["id"],
|
|
14639
15947
|
additionalProperties: false
|
|
14640
15948
|
},
|
|
14641
|
-
annotations: { title: "Change a stroke", ...
|
|
15949
|
+
annotations: { title: "Change a stroke", ...WRITE8 },
|
|
14642
15950
|
coordinates: { out: ["bounds", "stroke.bounds"] },
|
|
14643
15951
|
handler: ({ id, ...update }) => {
|
|
14644
15952
|
if (!Object.keys(update).length) throw new Error("Pass at least one change: brushId, brush, color, paint or seed.");
|
|
@@ -14677,7 +15985,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14677
15985
|
required: ["id"],
|
|
14678
15986
|
additionalProperties: false
|
|
14679
15987
|
},
|
|
14680
|
-
annotations: { title: "Move or reshape a stroke", ...
|
|
15988
|
+
annotations: { title: "Move or reshape a stroke", ...WRITE8 },
|
|
14681
15989
|
coordinates: { in: ["points[]", "transform.origin"], out: ["bounds", "stroke.bounds", "stroke.points[]"] },
|
|
14682
15990
|
handler: async ({ id, points, transform }) => {
|
|
14683
15991
|
await c.prepareStrokeEdit(id, { points, transform });
|
|
@@ -14695,7 +16003,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14695
16003
|
required: ["id", "index"],
|
|
14696
16004
|
additionalProperties: false
|
|
14697
16005
|
},
|
|
14698
|
-
annotations: { title: "Move a stroke up or down", ...
|
|
16006
|
+
annotations: { title: "Move a stroke up or down", ...WRITE8 },
|
|
14699
16007
|
handler: ({ id, index }) => ({ order: c.reorderStroke(id, index) })
|
|
14700
16008
|
});
|
|
14701
16009
|
add({
|
|
@@ -14717,7 +16025,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14717
16025
|
title: "Make a layer a stroke layer",
|
|
14718
16026
|
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.',
|
|
14719
16027
|
inputSchema: { type: "object", properties: { layerId: { type: "string", description: "Layer id from list_layers (default the active layer)." } }, additionalProperties: false },
|
|
14720
|
-
annotations: { title: "Make a layer a stroke layer", ...
|
|
16028
|
+
annotations: { title: "Make a layer a stroke layer", ...WRITE8 },
|
|
14721
16029
|
coordinates: { out: ["bounds"] },
|
|
14722
16030
|
handler: ({ layerId }) => c.convertToLiveStrokes(layerId)
|
|
14723
16031
|
});
|
|
@@ -14759,7 +16067,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14759
16067
|
required: ["strokeIds"],
|
|
14760
16068
|
additionalProperties: false
|
|
14761
16069
|
},
|
|
14762
|
-
annotations: { title: "Replay strokes, varied", ...
|
|
16070
|
+
annotations: { title: "Replay strokes, varied", ...WRITE8 },
|
|
14763
16071
|
coordinates: { in: ["origin"], out: ["bounds"] },
|
|
14764
16072
|
handler: async (a) => {
|
|
14765
16073
|
const replay = a;
|
|
@@ -14781,7 +16089,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14781
16089
|
},
|
|
14782
16090
|
additionalProperties: false
|
|
14783
16091
|
},
|
|
14784
|
-
annotations: { title: "Rotate, mirror or zoom the view", ...
|
|
16092
|
+
annotations: { title: "Rotate, mirror or zoom the view", ...SET6 },
|
|
14785
16093
|
handler: ({ reset, ...v }) => {
|
|
14786
16094
|
if (reset) c.resetView();
|
|
14787
16095
|
return c.setView(v);
|
|
@@ -14792,15 +16100,15 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14792
16100
|
title: "Set the drawing hand",
|
|
14793
16101
|
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.",
|
|
14794
16102
|
inputSchema: { type: "object", properties: { hand: { type: "string", enum: ["right", "left"], description: "right or left." } }, required: ["hand"], additionalProperties: false },
|
|
14795
|
-
annotations: { title: "Set the drawing hand", ...
|
|
16103
|
+
annotations: { title: "Set the drawing hand", ...SET6 },
|
|
14796
16104
|
handler: ({ hand }) => ({ hand: c.setHand(hand) })
|
|
14797
16105
|
});
|
|
14798
16106
|
add({
|
|
14799
16107
|
name: "undo_paint",
|
|
14800
16108
|
title: "Undo",
|
|
14801
16109
|
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.",
|
|
14802
|
-
inputSchema:
|
|
14803
|
-
annotations: { title: "Undo", ...
|
|
16110
|
+
inputSchema: EMPTY4,
|
|
16111
|
+
annotations: { title: "Undo", ...WRITE8 },
|
|
14804
16112
|
handler: () => {
|
|
14805
16113
|
const undone = c.undo();
|
|
14806
16114
|
const s = c.getState();
|
|
@@ -14811,8 +16119,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14811
16119
|
name: "redo_paint",
|
|
14812
16120
|
title: "Redo",
|
|
14813
16121
|
description: "Redoes the last undone step. Returns the label redone (null when there was nothing) and what can be undone and redone next.",
|
|
14814
|
-
inputSchema:
|
|
14815
|
-
annotations: { title: "Redo", ...
|
|
16122
|
+
inputSchema: EMPTY4,
|
|
16123
|
+
annotations: { title: "Redo", ...WRITE8 },
|
|
14816
16124
|
handler: () => {
|
|
14817
16125
|
const redone = c.redo();
|
|
14818
16126
|
const s = c.getState();
|
|
@@ -14832,7 +16140,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14832
16140
|
},
|
|
14833
16141
|
additionalProperties: false
|
|
14834
16142
|
},
|
|
14835
|
-
annotations: { title: "Render a preview", ...
|
|
16143
|
+
annotations: { title: "Render a preview", ...READ9 },
|
|
14836
16144
|
handler: ({ maxSide, layerIds, paper }) => {
|
|
14837
16145
|
if (layerIds) {
|
|
14838
16146
|
for (const id of layerIds) if (!c.getLayer(id)) throw new Error(`Unknown layer "${id}". Call list_layers for the ids.`);
|
|
@@ -14856,7 +16164,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14856
16164
|
},
|
|
14857
16165
|
additionalProperties: false
|
|
14858
16166
|
},
|
|
14859
|
-
annotations: { title: "Export the painting", ...
|
|
16167
|
+
annotations: { title: "Export the painting", ...READ9 },
|
|
14860
16168
|
handler: async ({ format, quality, scale, paper, time }) => {
|
|
14861
16169
|
const run = () => c.exportImage({ format, quality, scale, paper });
|
|
14862
16170
|
const bytes = time === void 0 ? await run() : await c.withEffectTime(time, run);
|
|
@@ -14868,7 +16176,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14868
16176
|
title: "Export as PSD",
|
|
14869
16177
|
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).',
|
|
14870
16178
|
inputSchema: { type: "object", properties: { time: TIME }, additionalProperties: false },
|
|
14871
|
-
annotations: { title: "Export as PSD", ...
|
|
16179
|
+
annotations: { title: "Export as PSD", ...READ9 },
|
|
14872
16180
|
handler: async ({ time }) => {
|
|
14873
16181
|
const r = time === void 0 ? c.exportPsd() : await c.withEffectTime(time, () => c.exportPsd());
|
|
14874
16182
|
return { data: bytesToBase64(r.bytes), bytes: r.bytes.length, psb: r.psb, warnings: r.warnings };
|
|
@@ -14877,6 +16185,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
14877
16185
|
tools.push(...createPaintGuideTools(c));
|
|
14878
16186
|
tools.push(...createPaintWetTools(c));
|
|
14879
16187
|
tools.push(...createPaintThickTools(c));
|
|
16188
|
+
tools.push(...createFerroTools(c));
|
|
14880
16189
|
tools.push(...createPaintReplayTools(c));
|
|
14881
16190
|
tools.push(...createPaintEvolutionTools(c));
|
|
14882
16191
|
tools.push(...createPaintStickerTools(c));
|
|
@@ -15435,6 +16744,18 @@ var WET_TILT_SLIDERS = [
|
|
|
15435
16744
|
{ path: "direction", label: "Downhill", min: 0, max: 359, step: 1 }
|
|
15436
16745
|
];
|
|
15437
16746
|
var WET_SPEED_SLIDER = { path: "speed", label: "Speed", min: 15, max: 480, step: 15 };
|
|
16747
|
+
var LIVE_PATTERN_LABELS = {
|
|
16748
|
+
spots: "Spots",
|
|
16749
|
+
stripes: "Stripes",
|
|
16750
|
+
coral: "Coral",
|
|
16751
|
+
maze: "Maze",
|
|
16752
|
+
worms: "Worms"
|
|
16753
|
+
};
|
|
16754
|
+
var LIVE_PATTERN_SLIDERS = [
|
|
16755
|
+
{ path: "scale", label: "Scale", min: 0.25, max: 4, step: 0.05 },
|
|
16756
|
+
{ path: "contrast", label: "Contrast", min: 0, max: 2, step: 0.05 },
|
|
16757
|
+
{ path: "relief", label: "Relief", min: 0, max: 1, step: 0.01 }
|
|
16758
|
+
];
|
|
15438
16759
|
var WET_GPU_OPTIONS = [
|
|
15439
16760
|
{ value: "auto", label: "GPU for large washes" },
|
|
15440
16761
|
{ value: "off", label: "CPU only" }
|
|
@@ -15588,6 +16909,10 @@ var EVOLUTION_SLIDERS = [
|
|
|
15588
16909
|
{ path: "brush", label: "Brush", min: 0, max: 1, step: 0.05 },
|
|
15589
16910
|
{ path: "seed", label: "Seed", min: 0, max: 1, step: 0.05 }
|
|
15590
16911
|
];
|
|
16912
|
+
var MAGNET_SLIDERS = [
|
|
16913
|
+
{ path: "strength", label: "Strength", min: -1, max: 1, step: 0.05 },
|
|
16914
|
+
{ path: "radius", label: "Radius", min: 4, max: 512, step: 1 }
|
|
16915
|
+
];
|
|
15591
16916
|
function quickStencil(kind, canvas) {
|
|
15592
16917
|
const { width: w, height: h } = canvas;
|
|
15593
16918
|
switch (kind) {
|
|
@@ -15873,8 +17198,8 @@ function createPaintEditorDocument(controller, options = {}) {
|
|
|
15873
17198
|
}
|
|
15874
17199
|
|
|
15875
17200
|
// src/paint-editor/collaboration-tools.ts
|
|
15876
|
-
var
|
|
15877
|
-
var
|
|
17201
|
+
var READ10 = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
17202
|
+
var WRITE9 = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
15878
17203
|
var proposalIdArg = {
|
|
15879
17204
|
type: "object",
|
|
15880
17205
|
properties: { proposalId: { type: "string", minLength: 1, description: "Id of a pending paint proposal, from propose_paint or list_paint_proposals." } },
|
|
@@ -15952,7 +17277,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
15952
17277
|
required: ["title", "ops"],
|
|
15953
17278
|
additionalProperties: false
|
|
15954
17279
|
},
|
|
15955
|
-
annotations: { title: "Propose paint", ...
|
|
17280
|
+
annotations: { title: "Propose paint", ...WRITE9 },
|
|
15956
17281
|
handler: (args, context) => {
|
|
15957
17282
|
const origin = context.actor?.id ? `agent:${context.actor.id}` : options.fallbackOrigin;
|
|
15958
17283
|
const proposal = proposals.propose({ ...args, ...origin ? { origin } : {} });
|
|
@@ -15965,7 +17290,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
15965
17290
|
title: "List paint proposals",
|
|
15966
17291
|
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.",
|
|
15967
17292
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
15968
|
-
annotations: { title: "List paint proposals", ...
|
|
17293
|
+
annotations: { title: "List paint proposals", ...READ10 },
|
|
15969
17294
|
handler: () => ({ proposals: proposals.getState().proposals.map(describe), readOnly: readOnlyReason() })
|
|
15970
17295
|
};
|
|
15971
17296
|
const acceptTool = {
|
|
@@ -15973,7 +17298,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
15973
17298
|
title: "Accept a paint proposal",
|
|
15974
17299
|
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.",
|
|
15975
17300
|
inputSchema: proposalIdArg,
|
|
15976
|
-
annotations: { title: "Accept a paint proposal", ...
|
|
17301
|
+
annotations: { title: "Accept a paint proposal", ...WRITE9 },
|
|
15977
17302
|
handler: (args) => {
|
|
15978
17303
|
const reason = readOnlyReason();
|
|
15979
17304
|
if (reason) throw new Error(reason);
|
|
@@ -15985,7 +17310,7 @@ function createPaintProposalToolProvider(proposals, options = {}) {
|
|
|
15985
17310
|
title: "Reject a paint proposal",
|
|
15986
17311
|
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.",
|
|
15987
17312
|
inputSchema: proposalIdArg,
|
|
15988
|
-
annotations: { title: "Reject a paint proposal", ...
|
|
17313
|
+
annotations: { title: "Reject a paint proposal", ...WRITE9 },
|
|
15989
17314
|
handler: (args) => {
|
|
15990
17315
|
const p = proposals.reject(args.proposalId);
|
|
15991
17316
|
return { proposalId: p.id, title: p.title, rejected: true };
|
|
@@ -16058,6 +17383,33 @@ var PAINT_DOCUMENT_JSON_SCHEMA = {
|
|
|
16058
17383
|
required: ["id", "layerId", "points", "brush"]
|
|
16059
17384
|
}
|
|
16060
17385
|
},
|
|
17386
|
+
ferro: { type: "object", additionalProperties: { type: "string" }, description: "Layer id \u2192 asset id of its liquid ferrofluid map." },
|
|
17387
|
+
ferroSettings: {
|
|
17388
|
+
type: "object",
|
|
17389
|
+
additionalProperties: {
|
|
17390
|
+
type: "object",
|
|
17391
|
+
properties: {
|
|
17392
|
+
wavelength: { type: "number", description: "Rosensweig spike spacing in pixels." },
|
|
17393
|
+
threshold: { type: "number", description: "Critical magnetic field threshold." }
|
|
17394
|
+
}
|
|
17395
|
+
},
|
|
17396
|
+
description: "Layer id \u2192 ferrofluid spike settings."
|
|
17397
|
+
},
|
|
17398
|
+
magnets: {
|
|
17399
|
+
type: "array",
|
|
17400
|
+
description: "Magnets on the canvas that pull liquid ferrofluid into spikes.",
|
|
17401
|
+
items: {
|
|
17402
|
+
type: "object",
|
|
17403
|
+
properties: {
|
|
17404
|
+
id: { type: "string", description: "Magnet id." },
|
|
17405
|
+
x: { type: "number", description: "Document x in pixels." },
|
|
17406
|
+
y: { type: "number", description: "Document y in pixels." },
|
|
17407
|
+
strength: { type: "number", minimum: -1, maximum: 1, description: "Magnet dipole strength (-1..1)." },
|
|
17408
|
+
radius: { type: "number", minimum: 4, maximum: 512, description: "Magnet field falloff radius in pixels." }
|
|
17409
|
+
},
|
|
17410
|
+
required: ["id", "x", "y", "strength", "radius"]
|
|
17411
|
+
}
|
|
17412
|
+
},
|
|
16061
17413
|
fluor: {
|
|
16062
17414
|
type: "object",
|
|
16063
17415
|
description: "Per-layer UV-fluorescent ink channels (layerId \u2192 bounds + PNG data URL).",
|
|
@@ -16108,13 +17460,14 @@ function uiTables() {
|
|
|
16108
17460
|
brushOptions: BRUSH_OPTION_CONTROLS,
|
|
16109
17461
|
blendMenu: BLEND_MENU_GROUPS,
|
|
16110
17462
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
16111
|
-
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, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS },
|
|
17463
|
+
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 },
|
|
16112
17464
|
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
16113
17465
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
16114
17466
|
wetGpu: WET_GPU_OPTIONS,
|
|
16115
17467
|
labels: {
|
|
16116
17468
|
wetTools: WET_TOOL_LABELS,
|
|
16117
17469
|
wetMedia: WET_MEDIUM_LABELS,
|
|
17470
|
+
livePatterns: LIVE_PATTERN_LABELS,
|
|
16118
17471
|
impastoTools: IMPASTO_TOOL_LABELS,
|
|
16119
17472
|
particleKinds: PARTICLE_KIND_LABELS,
|
|
16120
17473
|
particleRenders: PARTICLE_RENDER_LABELS,
|
|
@@ -16184,6 +17537,7 @@ export {
|
|
|
16184
17537
|
DEFAULT_BRUSH_ID,
|
|
16185
17538
|
DEFAULT_DPI,
|
|
16186
17539
|
DEFAULT_DYNAMICS,
|
|
17540
|
+
DEFAULT_FERRO,
|
|
16187
17541
|
DEFAULT_FLOW,
|
|
16188
17542
|
DEFAULT_FLUORESCENCE,
|
|
16189
17543
|
DEFAULT_GHOST,
|
|
@@ -16191,6 +17545,9 @@ export {
|
|
|
16191
17545
|
DEFAULT_IRIDESCENCE,
|
|
16192
17546
|
DEFAULT_LIGHT,
|
|
16193
17547
|
DEFAULT_LIGHTING,
|
|
17548
|
+
DEFAULT_LIVE_PATTERNS,
|
|
17549
|
+
DEFAULT_MAGNET_RADIUS,
|
|
17550
|
+
DEFAULT_MAGNET_STRENGTH,
|
|
16194
17551
|
DEFAULT_MULTI_COLOR,
|
|
16195
17552
|
DEFAULT_MUTATION,
|
|
16196
17553
|
DEFAULT_MUTATION_RATES,
|
|
@@ -16218,6 +17575,10 @@ export {
|
|
|
16218
17575
|
EVOLUTION_BRUSH_FACETS,
|
|
16219
17576
|
EVOLUTION_MUTATION_SCHEMA,
|
|
16220
17577
|
EVOLUTION_SLIDERS,
|
|
17578
|
+
FERRO_CARRIER_RGB,
|
|
17579
|
+
FERRO_DEFAULT_GLOSS,
|
|
17580
|
+
FERRO_DEFAULT_METALLIC,
|
|
17581
|
+
FERRO_DEFAULT_RIM,
|
|
16221
17582
|
FilmBuffer,
|
|
16222
17583
|
GHOST_SLIDERS,
|
|
16223
17584
|
GHOST_TRIGGERS,
|
|
@@ -16235,8 +17596,14 @@ export {
|
|
|
16235
17596
|
IMPASTO_TOOL_LABELS,
|
|
16236
17597
|
IRIDESCENCE_SLIDERS,
|
|
16237
17598
|
LIGHT_SLIDERS,
|
|
17599
|
+
LIVE_PATTERN_KINDS,
|
|
17600
|
+
LIVE_PATTERN_LABELS,
|
|
17601
|
+
LIVE_PATTERN_PRESETS,
|
|
17602
|
+
LIVE_PATTERN_SCHEMA,
|
|
17603
|
+
LIVE_PATTERN_SLIDERS,
|
|
16238
17604
|
LIVE_STROKE_ENGINES,
|
|
16239
17605
|
LitCache,
|
|
17606
|
+
MAGNET_SLIDERS,
|
|
16240
17607
|
MAX_BRUSH_SIZE,
|
|
16241
17608
|
MAX_CANVAS_SIDE,
|
|
16242
17609
|
MAX_COORDINATE,
|
|
@@ -16349,9 +17716,11 @@ export {
|
|
|
16349
17716
|
addPaintGradientStop,
|
|
16350
17717
|
addPolygonArea,
|
|
16351
17718
|
advanceHeatField,
|
|
17719
|
+
advectFerro,
|
|
16352
17720
|
affineCopy,
|
|
16353
17721
|
applyAffine,
|
|
16354
17722
|
backlightOf,
|
|
17723
|
+
bakeFerroIntoThick,
|
|
16355
17724
|
brushEngineControls,
|
|
16356
17725
|
brushLoadColor,
|
|
16357
17726
|
brushLoadForStroke,
|
|
@@ -16368,6 +17737,7 @@ export {
|
|
|
16368
17737
|
computeUvSpill,
|
|
16369
17738
|
contactSheet,
|
|
16370
17739
|
createEvolutionTools,
|
|
17740
|
+
createFerroTools,
|
|
16371
17741
|
createHeatField,
|
|
16372
17742
|
createMutationTools,
|
|
16373
17743
|
createPaintAgentStrokes,
|
|
@@ -16412,11 +17782,20 @@ export {
|
|
|
16412
17782
|
effectPaintKey,
|
|
16413
17783
|
effectPaintSize,
|
|
16414
17784
|
emptiedLayers,
|
|
17785
|
+
emptyFerro,
|
|
16415
17786
|
emptyFluor,
|
|
16416
17787
|
emptyThermo,
|
|
16417
17788
|
emptyThickChannels,
|
|
16418
17789
|
engineBrushSourceIds,
|
|
16419
17790
|
exportProposalContactSheetPng,
|
|
17791
|
+
ferroCacheKey,
|
|
17792
|
+
ferroFieldAt,
|
|
17793
|
+
ferroFromJSON,
|
|
17794
|
+
ferroInfo,
|
|
17795
|
+
ferroSpikeHeightAt,
|
|
17796
|
+
ferroToJSON,
|
|
17797
|
+
ferroToShadow,
|
|
17798
|
+
ferroTwinBrush,
|
|
16420
17799
|
fillRegionMask,
|
|
16421
17800
|
finalizeThermoStroke,
|
|
16422
17801
|
findUnfilledRegionsMask,
|
|
@@ -16453,7 +17832,9 @@ export {
|
|
|
16453
17832
|
lightingKey,
|
|
16454
17833
|
liveStrokeBounds,
|
|
16455
17834
|
liveStrokeRefusal,
|
|
17835
|
+
magnetsFromJSON,
|
|
16456
17836
|
mapPenPoint,
|
|
17837
|
+
mergeFerro,
|
|
16457
17838
|
mergeFourChannel,
|
|
16458
17839
|
mix32,
|
|
16459
17840
|
mixColorOklch,
|
|
@@ -16475,6 +17856,8 @@ export {
|
|
|
16475
17856
|
normalizeBrushStroke,
|
|
16476
17857
|
normalizeCanvas,
|
|
16477
17858
|
normalizeDynamics,
|
|
17859
|
+
normalizeFerro,
|
|
17860
|
+
normalizeFerro as normalizeFerroSettings,
|
|
16478
17861
|
normalizeFlow,
|
|
16479
17862
|
normalizeFluorescence,
|
|
16480
17863
|
normalizeGhost,
|
|
@@ -16482,6 +17865,8 @@ export {
|
|
|
16482
17865
|
normalizeImpasto,
|
|
16483
17866
|
normalizeLight,
|
|
16484
17867
|
normalizeLighting,
|
|
17868
|
+
normalizeLivePatterns,
|
|
17869
|
+
normalizeMagnet,
|
|
16485
17870
|
normalizeMultiColor,
|
|
16486
17871
|
normalizeMutation,
|
|
16487
17872
|
normalizeMutationRates,
|
|
@@ -16563,6 +17948,7 @@ export {
|
|
|
16563
17948
|
sampleHeatAt,
|
|
16564
17949
|
samplePath,
|
|
16565
17950
|
samplePressure,
|
|
17951
|
+
scaleFerro,
|
|
16566
17952
|
scaleFluor,
|
|
16567
17953
|
scalePaintGradient,
|
|
16568
17954
|
scaleStencil,
|
|
@@ -16571,6 +17957,7 @@ export {
|
|
|
16571
17957
|
seededStrokeVariation,
|
|
16572
17958
|
serializePaintDocument,
|
|
16573
17959
|
serializePaintDocumentExternal,
|
|
17960
|
+
shadowToFerro,
|
|
16574
17961
|
shiftHeatField,
|
|
16575
17962
|
solidPaint,
|
|
16576
17963
|
squaredDistanceTransform,
|