@pixerate/editor 0.281.0 → 0.282.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animation-editor/index.cjs +59 -59
- package/dist/animation-editor/index.js +3 -3
- package/dist/brushes/index.cjs +76 -71
- package/dist/brushes/index.js +13 -8
- package/dist/brushes/schema.d.cts +1 -0
- package/dist/brushes/schema.d.ts +1 -0
- package/dist/brushes/types.d.cts +29 -3
- package/dist/brushes/types.d.ts +29 -3
- package/dist/{brushstrokeRender-EIK3625T.js → brushstrokeRender-4MA5CBW3.js} +3 -3
- package/dist/{brushstrokeRender-7HOMKZNO.cjs → brushstrokeRender-JVKVKWDA.cjs} +15 -15
- package/dist/canvas/index.cjs +13 -13
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-NJDEKU5D.js → chunk-2YGYYVSG.js} +1 -1
- package/dist/{chunk-JVFGK3DK.cjs → chunk-3JUKN3RM.cjs} +76 -53
- package/dist/{chunk-HN57I7KT.js → chunk-CG3FBNXE.js} +1 -1
- package/dist/{chunk-WFSYZO5Z.cjs → chunk-DFZ4I7EZ.cjs} +1 -1
- package/dist/{chunk-LZE2QZCT.js → chunk-IHMMHZGP.js} +1 -1
- package/dist/{chunk-K4ANWBF7.cjs → chunk-IJQ2YH2M.cjs} +35 -35
- package/dist/{chunk-HRUK5TFW.cjs → chunk-IUKPQCYQ.cjs} +164 -71
- package/dist/{chunk-WCC4NXWY.cjs → chunk-JD63B33V.cjs} +86 -86
- package/dist/{chunk-NJLPW4MM.js → chunk-KV7QHPKL.js} +27 -4
- package/dist/{chunk-H3EME3S5.cjs → chunk-MRIAR5DY.cjs} +2 -2
- package/dist/{chunk-ROURHJYT.js → chunk-NEDS6DSC.js} +1 -1
- package/dist/{chunk-FKO3CY2C.js → chunk-ULPXJ3NB.js} +1 -1
- package/dist/{chunk-WZYRYYXN.cjs → chunk-UOM5S4W5.cjs} +12 -12
- package/dist/{chunk-YOM76H2V.js → chunk-VEX6YVNG.js} +1 -1
- package/dist/{chunk-MNHP3AQT.cjs → chunk-WEXWDBUF.cjs} +23 -23
- package/dist/{chunk-PPDUCZZV.js → chunk-YRGTQLDN.js} +122 -29
- package/dist/image-editor/index.cjs +788 -788
- package/dist/image-editor/index.js +4 -4
- package/dist/paint-editor/antimatter.d.cts +154 -0
- package/dist/paint-editor/antimatter.d.ts +154 -0
- package/dist/paint-editor/brush-controls.d.cts +1 -0
- package/dist/paint-editor/brush-controls.d.ts +1 -0
- package/dist/paint-editor/brush-model.d.cts +9 -5
- package/dist/paint-editor/brush-model.d.ts +9 -5
- package/dist/paint-editor/controller.d.cts +23 -0
- package/dist/paint-editor/controller.d.ts +23 -0
- package/dist/paint-editor/index.cjs +1931 -891
- package/dist/paint-editor/index.d.cts +1 -0
- package/dist/paint-editor/index.d.ts +1 -0
- package/dist/paint-editor/index.js +1098 -58
- package/dist/paint-editor/mark-settings.d.cts +30 -0
- package/dist/paint-editor/mark-settings.d.ts +30 -0
- package/dist/paint-editor/stroke.d.cts +16 -0
- package/dist/paint-editor/stroke.d.ts +16 -0
- package/dist/paint-editor/strokes.d.cts +2 -0
- package/dist/paint-editor/strokes.d.ts +2 -0
- package/dist/paint-editor/ui.d.cts +4 -2
- package/dist/paint-editor/ui.d.ts +4 -2
- package/dist/vector/index.cjs +999 -999
- package/dist/vector/index.js +4 -4
- package/package.json +1 -1
- package/skills/pixerate-brushes/SKILL.md +1 -1
- package/skills/pixerate-paint-editor/SKILL.md +8 -2
- package/skills/pixerate-paint-editor/references/antimatter.md +49 -0
- package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
editorStickers
|
|
4
4
|
} from "../chunk-NSETJI74.js";
|
|
5
5
|
import {
|
|
6
|
+
ANTIMATTER_SLIDERS,
|
|
6
7
|
BRUSH_PATCH_PROPERTIES,
|
|
7
8
|
BRUSH_SLIDERS,
|
|
8
9
|
BRUSH_UNSET_PROPERTY,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
mergeBrushPatch,
|
|
15
16
|
multiColorSliders,
|
|
16
17
|
patchBrushValue
|
|
17
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-KV7QHPKL.js";
|
|
18
19
|
import {
|
|
19
20
|
createStickerTools,
|
|
20
21
|
describeStickerRef
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
import {
|
|
23
24
|
BRUSH_PREVIEW_DARK_PAPER,
|
|
24
25
|
brushPreviewsOnDark
|
|
25
|
-
} from "../chunk-
|
|
26
|
+
} from "../chunk-NEDS6DSC.js";
|
|
26
27
|
import {
|
|
27
28
|
toVideoFfmpeg
|
|
28
29
|
} from "../chunk-S2XPXA47.js";
|
|
@@ -42,7 +43,7 @@ import {
|
|
|
42
43
|
resolveExternalAssets,
|
|
43
44
|
serializeLayers,
|
|
44
45
|
serializeLayersExternal
|
|
45
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-2YGYYVSG.js";
|
|
46
47
|
import "../chunk-ADMRNWR7.js";
|
|
47
48
|
import "../chunk-WJ4WQJCC.js";
|
|
48
49
|
import {
|
|
@@ -125,7 +126,7 @@ import {
|
|
|
125
126
|
wetPaperInput,
|
|
126
127
|
wetPigmentOf,
|
|
127
128
|
withStrokePath
|
|
128
|
-
} from "../chunk-
|
|
129
|
+
} from "../chunk-VEX6YVNG.js";
|
|
129
130
|
import {
|
|
130
131
|
BRUSH_ID,
|
|
131
132
|
BRUSH_PRESETS,
|
|
@@ -133,6 +134,7 @@ import {
|
|
|
133
134
|
BUILTIN_BRUSHES,
|
|
134
135
|
BrushLibraryError,
|
|
135
136
|
DAB_FLIPS,
|
|
137
|
+
DEFAULT_ANTIMATTER,
|
|
136
138
|
DEFAULT_BEHAVIOR,
|
|
137
139
|
DEFAULT_BRISTLE,
|
|
138
140
|
DEFAULT_BRUSH_ID,
|
|
@@ -155,6 +157,7 @@ import {
|
|
|
155
157
|
DOT_TEXTURE_ORDERS,
|
|
156
158
|
GHOST_TRIGGERS,
|
|
157
159
|
IMPASTO_TOOLS,
|
|
160
|
+
INVERT_MODES,
|
|
158
161
|
MAX_DOT_TEXTURES,
|
|
159
162
|
MAX_STICKER_STAMPS,
|
|
160
163
|
MULTI_COLOR_MODES,
|
|
@@ -179,6 +182,7 @@ import {
|
|
|
179
182
|
isWetCapable,
|
|
180
183
|
migrateBrushV1,
|
|
181
184
|
multiColorToPaint,
|
|
185
|
+
normalizeAntimatter,
|
|
182
186
|
normalizeBehavior,
|
|
183
187
|
normalizeBristle,
|
|
184
188
|
normalizeBrushSpec,
|
|
@@ -200,7 +204,7 @@ import {
|
|
|
200
204
|
readBrushSource,
|
|
201
205
|
resolveBrush,
|
|
202
206
|
usesPaintExchange
|
|
203
|
-
} from "../chunk-
|
|
207
|
+
} from "../chunk-YRGTQLDN.js";
|
|
204
208
|
import {
|
|
205
209
|
STAMP_KINDS,
|
|
206
210
|
StickersSlot,
|
|
@@ -417,6 +421,724 @@ function createPaintDocument(options = {}) {
|
|
|
417
421
|
return { canvas, paper: normalizePaper(options.paper ?? {}), layers: [layer], activeLayerId: layer.id, strokes: [] };
|
|
418
422
|
}
|
|
419
423
|
|
|
424
|
+
// src/paint-editor/antimatter.ts
|
|
425
|
+
var COVERAGE_STEPS = 4096;
|
|
426
|
+
function unionRect(a, b) {
|
|
427
|
+
if (!a) return { ...b };
|
|
428
|
+
const x = Math.min(a.x, b.x);
|
|
429
|
+
const y = Math.min(a.y, b.y);
|
|
430
|
+
return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };
|
|
431
|
+
}
|
|
432
|
+
function sameTile(a, b) {
|
|
433
|
+
if (!a || !b) return !a && !b;
|
|
434
|
+
if (a === b) return true;
|
|
435
|
+
if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;
|
|
436
|
+
for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
function blastNoise2D(ux, uy, salt) {
|
|
440
|
+
const x0 = Math.floor(ux);
|
|
441
|
+
const y0 = Math.floor(uy);
|
|
442
|
+
const fx = ux - x0;
|
|
443
|
+
const fy = uy - y0;
|
|
444
|
+
const tx = fx * fx * (3 - 2 * fx);
|
|
445
|
+
const ty = fy * fy * (3 - 2 * fy);
|
|
446
|
+
const n00 = hash2(x0, y0, salt);
|
|
447
|
+
const n10 = hash2(x0 + 1, y0, salt);
|
|
448
|
+
const n01 = hash2(x0, y0 + 1, salt);
|
|
449
|
+
const n11 = hash2(x0 + 1, y0 + 1, salt);
|
|
450
|
+
const a = n00 + (n10 - n00) * tx;
|
|
451
|
+
const b = n01 + (n11 - n01) * tx;
|
|
452
|
+
return a + (b - a) * ty;
|
|
453
|
+
}
|
|
454
|
+
function invertOklab8(r, g, b) {
|
|
455
|
+
const lab = rgbToOklab({ r: r / 255, g: g / 255, b: b / 255, a: 1 });
|
|
456
|
+
const rgb = oklabToRgb({ l: 1 - lab.l, a: -lab.a, b: -lab.b, alpha: 1 });
|
|
457
|
+
return [
|
|
458
|
+
Math.min(255, Math.max(0, Math.round(rgb.r * 255))),
|
|
459
|
+
Math.min(255, Math.max(0, Math.round(rgb.g * 255))),
|
|
460
|
+
Math.min(255, Math.max(0, Math.round(rgb.b * 255)))
|
|
461
|
+
];
|
|
462
|
+
}
|
|
463
|
+
function heatProfile(h) {
|
|
464
|
+
if (h <= 0) return { core: 0, ring: 0 };
|
|
465
|
+
const coreT = Math.min(1, Math.max(0, (h - 0.32) / 0.5));
|
|
466
|
+
const core = coreT * coreT * (3 - 2 * coreT);
|
|
467
|
+
const ringIn = Math.min(1, Math.max(0, h / 0.35));
|
|
468
|
+
const ringOut = Math.min(1, Math.max(0, (0.82 - h) / 0.47));
|
|
469
|
+
const ring = ringIn * ringOut * (3 - 2 * ringIn) * (3 - 2 * ringOut);
|
|
470
|
+
return { core, ring };
|
|
471
|
+
}
|
|
472
|
+
var AntimatterStrokeDriver = class {
|
|
473
|
+
constructor(init) {
|
|
474
|
+
this.init = init;
|
|
475
|
+
const { brush } = init;
|
|
476
|
+
this.smoother = new PenSmoother(brush.tip.smoothing, brush.pressureSmoothing);
|
|
477
|
+
const copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
478
|
+
this.copies = copies;
|
|
479
|
+
this.states = copies.map((c, i) => {
|
|
480
|
+
const seed = init.seed + i * 9973 | 0;
|
|
481
|
+
return {
|
|
482
|
+
walker: new PenDabWalker(brush, seed, c.linear),
|
|
483
|
+
seed,
|
|
484
|
+
blastCount: 0,
|
|
485
|
+
firedAny: false,
|
|
486
|
+
travel: 0,
|
|
487
|
+
fuelSum: 0,
|
|
488
|
+
fuelDabs: 0,
|
|
489
|
+
lastPos: null
|
|
490
|
+
};
|
|
491
|
+
});
|
|
492
|
+
const g = init.paint ?? null;
|
|
493
|
+
this.sampler = g ? new PaintGradientSampler(g.map.by === "across-tip" ? { ...g, map: { by: "distance", length: 300, repeat: "mirror" } } : g, init.seed) : null;
|
|
494
|
+
}
|
|
495
|
+
init;
|
|
496
|
+
dabCount = 0;
|
|
497
|
+
bounds = null;
|
|
498
|
+
blasts = [];
|
|
499
|
+
smoother;
|
|
500
|
+
copies;
|
|
501
|
+
states;
|
|
502
|
+
sampler;
|
|
503
|
+
sample = { rgb: [0, 0, 0], alpha: 1 };
|
|
504
|
+
effRgb = [0, 0, 0];
|
|
505
|
+
heat = /* @__PURE__ */ new Map();
|
|
506
|
+
heatCoords = /* @__PURE__ */ new Map();
|
|
507
|
+
tipBuf = new Float32Array(0);
|
|
508
|
+
lastRaw = null;
|
|
509
|
+
lastFed = null;
|
|
510
|
+
totalFuel = 0;
|
|
511
|
+
reachedSkipped = /* @__PURE__ */ new Set();
|
|
512
|
+
editedIds = /* @__PURE__ */ new Set();
|
|
513
|
+
outputs = /* @__PURE__ */ new Map();
|
|
514
|
+
/** Pushes raw pen samples and returns the bounding rectangles of any blasts that fired. */
|
|
515
|
+
push(points) {
|
|
516
|
+
const rects = [];
|
|
517
|
+
for (const raw of points) {
|
|
518
|
+
this.lastRaw = raw;
|
|
519
|
+
const p = this.smoother.push(raw);
|
|
520
|
+
this.lastFed = p;
|
|
521
|
+
const tNow = typeof raw.t === "number" && Number.isFinite(raw.t) ? raw.t : this.init.now ? this.init.now() : 0;
|
|
522
|
+
this.copies.forEach((c, i) => {
|
|
523
|
+
const dabs = [];
|
|
524
|
+
this.states[i].walker.push(c.map(p), dabs);
|
|
525
|
+
this.processDabs(i, dabs, tNow, rects);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (rects.length) this.recompositeAll();
|
|
529
|
+
return rects;
|
|
530
|
+
}
|
|
531
|
+
/** Flushes the smoother tail and finishes the stroke. */
|
|
532
|
+
finish() {
|
|
533
|
+
const rects = [];
|
|
534
|
+
const raw = this.lastRaw;
|
|
535
|
+
const fed = this.lastFed;
|
|
536
|
+
const tNow = raw && typeof raw.t === "number" && Number.isFinite(raw.t) ? raw.t : this.init.now ? this.init.now() : 0;
|
|
537
|
+
this.copies.forEach((c, i) => {
|
|
538
|
+
const dabs = [];
|
|
539
|
+
if (raw && fed && (raw.x !== fed.x || raw.y !== fed.y)) {
|
|
540
|
+
this.states[i].walker.push(c.map({ ...raw, pressure: fed.pressure }), dabs);
|
|
541
|
+
}
|
|
542
|
+
this.states[i].walker.finish(dabs);
|
|
543
|
+
this.processDabs(i, dabs, tNow, rects);
|
|
544
|
+
});
|
|
545
|
+
if (rects.length) this.recompositeAll();
|
|
546
|
+
return rects;
|
|
547
|
+
}
|
|
548
|
+
processDabs(copyIdx, dabs, tNow, rects) {
|
|
549
|
+
const st = this.states[copyIdx];
|
|
550
|
+
const settings = this.init.brush.antimatter;
|
|
551
|
+
for (const d of dabs) {
|
|
552
|
+
this.dabCount++;
|
|
553
|
+
const stepDist = st.lastPos ? Math.hypot(d.x - st.lastPos.x, d.y - st.lastPos.y) / Math.max(1, d.radius * 2) : 0;
|
|
554
|
+
st.lastPos = { x: d.x, y: d.y };
|
|
555
|
+
const dabFuel = this.measureFuel(d);
|
|
556
|
+
if (!st.firedAny) {
|
|
557
|
+
if (dabFuel >= settings.threshold && dabFuel > 0) {
|
|
558
|
+
st.firedAny = true;
|
|
559
|
+
st.travel = 0;
|
|
560
|
+
st.fuelSum = 0;
|
|
561
|
+
st.fuelDabs = 0;
|
|
562
|
+
rects.push(this.fireBlast(st, d, dabFuel, tNow));
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
st.travel += stepDist;
|
|
567
|
+
if (dabFuel > 0) {
|
|
568
|
+
st.fuelSum += dabFuel;
|
|
569
|
+
st.fuelDabs++;
|
|
570
|
+
}
|
|
571
|
+
if (st.travel >= settings.blastSpacing) {
|
|
572
|
+
const meanFuel = st.fuelDabs > 0 ? st.fuelSum / st.fuelDabs : dabFuel;
|
|
573
|
+
if (meanFuel >= settings.threshold && meanFuel > 0 && dabFuel > 0) {
|
|
574
|
+
st.travel = 0;
|
|
575
|
+
st.fuelSum = 0;
|
|
576
|
+
st.fuelDabs = 0;
|
|
577
|
+
rects.push(this.fireBlast(st, d, Math.min(1, meanFuel), tNow));
|
|
578
|
+
} else if (dabFuel < settings.threshold) {
|
|
579
|
+
st.fuelSum = 0;
|
|
580
|
+
st.fuelDabs = 0;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/** Measures fuel (`0..1`) under `d` from the pre-stroke pixels of every visible layer. */
|
|
586
|
+
measureFuel(d) {
|
|
587
|
+
const rect = dabRect(d, this.init.brush.tip.hardness);
|
|
588
|
+
if (rect.width <= 0 || rect.height <= 0) return 0;
|
|
589
|
+
const n = rect.width * rect.height;
|
|
590
|
+
if (this.tipBuf.length < n) this.tipBuf = new Float32Array(n);
|
|
591
|
+
const tip = this.tipBuf.subarray(0, n);
|
|
592
|
+
tip.fill(0);
|
|
593
|
+
rasterizeDab({ ...d, alpha: 1 }, this.init.brush.tip.hardness, rect, tip);
|
|
594
|
+
const mask = this.init.selection;
|
|
595
|
+
const targets = this.init.targets;
|
|
596
|
+
let sum = 0;
|
|
597
|
+
let activePixels = 0;
|
|
598
|
+
for (let yy = 0, k = 0; yy < rect.height; yy++) {
|
|
599
|
+
const py = rect.y + yy;
|
|
600
|
+
for (let xx = 0; xx < rect.width; xx++, k++) {
|
|
601
|
+
let rawC = tip[k];
|
|
602
|
+
if (rawC <= 0) continue;
|
|
603
|
+
const px = rect.x + xx;
|
|
604
|
+
if (mask) {
|
|
605
|
+
rawC *= px >= 0 && py >= 0 && px < mask.width && py < mask.height ? mask.data[py * mask.width + px] / 255 : 0;
|
|
606
|
+
}
|
|
607
|
+
const c = Math.round(rawC * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
608
|
+
if (c <= 0) continue;
|
|
609
|
+
activePixels++;
|
|
610
|
+
let prod = 1;
|
|
611
|
+
for (let i = 0; i < targets.length; i++) {
|
|
612
|
+
const t = targets[i];
|
|
613
|
+
if (t.opacity <= 0) continue;
|
|
614
|
+
const lx = px - t.offset.x;
|
|
615
|
+
const ly = py - t.offset.y;
|
|
616
|
+
const tx = Math.floor(lx / TILE);
|
|
617
|
+
const ty = Math.floor(ly / TILE);
|
|
618
|
+
const tile = t.base.tiles.get(tileKey(tx, ty));
|
|
619
|
+
if (!tile) continue;
|
|
620
|
+
const ox = lx - tx * TILE;
|
|
621
|
+
const oy = ly - ty * TILE;
|
|
622
|
+
const a = tile.data[(oy * TILE + ox) * 4 + 3] / 255;
|
|
623
|
+
if (a > 0) prod *= 1 - a * t.opacity;
|
|
624
|
+
}
|
|
625
|
+
const combinedAlpha = 1 - prod;
|
|
626
|
+
if (combinedAlpha > 0) sum += c * combinedAlpha;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (activePixels === 0 || sum <= 0) return 0;
|
|
630
|
+
return Math.round(sum / activePixels * COVERAGE_STEPS) / COVERAGE_STEPS;
|
|
631
|
+
}
|
|
632
|
+
fireBlast(st, d, fuel, tNow) {
|
|
633
|
+
const { brush, color, effect } = this.init;
|
|
634
|
+
const settings = brush.antimatter;
|
|
635
|
+
const blastIndex = st.blastCount++;
|
|
636
|
+
const tipRadius = Math.max(1, d.radius);
|
|
637
|
+
const fuelScale = 0.55 + 0.65 * Math.sqrt(fuel);
|
|
638
|
+
const jitter = 0.88 + 0.24 * rand01(st.seed, blastIndex, 17);
|
|
639
|
+
const radius = Math.max(2, tipRadius * settings.blastRadius * fuelScale * jitter);
|
|
640
|
+
let sparkRgb = [color[0], color[1], color[2]];
|
|
641
|
+
let flow = 1;
|
|
642
|
+
if (effect) {
|
|
643
|
+
const effA = sampleEffectAt(effect, d.x, d.y, this.effRgb, d);
|
|
644
|
+
sparkRgb = [Math.round(this.effRgb[0] * 255), Math.round(this.effRgb[1] * 255), Math.round(this.effRgb[2] * 255)];
|
|
645
|
+
flow = effA;
|
|
646
|
+
} else if (this.sampler) {
|
|
647
|
+
const g = this.sampler.dab(d, this.sample);
|
|
648
|
+
sparkRgb = [Math.round(g.rgb[0] * 255), Math.round(g.rgb[1] * 255), Math.round(g.rgb[2] * 255)];
|
|
649
|
+
flow = g.alpha;
|
|
650
|
+
}
|
|
651
|
+
const tipOpacity = Math.min(1, d.alpha * d.opacity * brush.tip.opacity * flow);
|
|
652
|
+
const lightAmount = brush.light.amount * (brush.light.pressure ? d.pressure : 1);
|
|
653
|
+
const sparkCount = settings.sparks > 0 ? Math.max(1, Math.round(settings.sparks * (6 + 10 * fuel))) : 0;
|
|
654
|
+
const blast = {
|
|
655
|
+
index: blastIndex,
|
|
656
|
+
x: d.x,
|
|
657
|
+
y: d.y,
|
|
658
|
+
radius,
|
|
659
|
+
tipSize: d.radius * 2,
|
|
660
|
+
tipOpacity,
|
|
661
|
+
fuel,
|
|
662
|
+
seed: st.seed,
|
|
663
|
+
color: sparkRgb,
|
|
664
|
+
lightAmount,
|
|
665
|
+
sparkCount,
|
|
666
|
+
t: tNow
|
|
667
|
+
};
|
|
668
|
+
this.blasts.push(blast);
|
|
669
|
+
this.totalFuel += fuel;
|
|
670
|
+
const rect = this.writeBlastToHeat(blast, this.heat, this.heatCoords, 1);
|
|
671
|
+
this.bounds = unionRect(this.bounds, rect);
|
|
672
|
+
return rect;
|
|
673
|
+
}
|
|
674
|
+
/** Writes one blast into `heatMap` (at `progress` 0..1) and returns the bounding rectangle including sparks. */
|
|
675
|
+
writeBlastToHeat(b, heatMap, coordMap, progress) {
|
|
676
|
+
const expand2 = 1 - (1 - progress) * (1 - progress);
|
|
677
|
+
const effRadius = b.radius * (0.2 + 0.8 * expand2);
|
|
678
|
+
const maxR = Math.ceil(effRadius * 1.22);
|
|
679
|
+
const x0 = Math.floor(b.x - maxR);
|
|
680
|
+
const y0 = Math.floor(b.y - maxR);
|
|
681
|
+
const x1 = Math.ceil(b.x + maxR);
|
|
682
|
+
const y1 = Math.ceil(b.y + maxR);
|
|
683
|
+
const salt = b.seed + b.index * 4099 | 0;
|
|
684
|
+
const mask = this.init.selection;
|
|
685
|
+
const targets = this.init.targets;
|
|
686
|
+
for (let py = y0; py <= y1; py++) {
|
|
687
|
+
const dy = py + 0.5 - b.y;
|
|
688
|
+
if (Math.abs(dy) > effRadius * 1.2) continue;
|
|
689
|
+
for (let px = x0; px <= x1; px++) {
|
|
690
|
+
const dx = px + 0.5 - b.x;
|
|
691
|
+
const dist = Math.hypot(dx, dy);
|
|
692
|
+
if (dist > effRadius * 1.2) continue;
|
|
693
|
+
let maskA = 1;
|
|
694
|
+
if (mask) {
|
|
695
|
+
if (px < 0 || py < 0 || px >= mask.width || py >= mask.height) continue;
|
|
696
|
+
maskA = mask.data[py * mask.width + px] / 255;
|
|
697
|
+
if (maskA <= 0) continue;
|
|
698
|
+
}
|
|
699
|
+
const rNorm = dist / effRadius;
|
|
700
|
+
const n = blastNoise2D(dx / effRadius * 2.5, dy / effRadius * 2.5, salt) - 0.5;
|
|
701
|
+
const rWarped = rNorm + n * 0.22 * Math.min(1, rNorm * 1.5);
|
|
702
|
+
if (rWarped >= 1) continue;
|
|
703
|
+
const t = 1 - rWarped;
|
|
704
|
+
const h = t * t * (3 - 2 * t) * b.tipOpacity * maskA;
|
|
705
|
+
if (h <= 0) continue;
|
|
706
|
+
const tx = Math.floor(px / TILE);
|
|
707
|
+
const ty = Math.floor(py / TILE);
|
|
708
|
+
const key = tileKey(tx, ty);
|
|
709
|
+
let tile = heatMap.get(key);
|
|
710
|
+
if (!tile) {
|
|
711
|
+
tile = new Float32Array(TILE * TILE);
|
|
712
|
+
heatMap.set(key, tile);
|
|
713
|
+
coordMap.set(key, { tx, ty });
|
|
714
|
+
}
|
|
715
|
+
const idx = (py - ty * TILE) * TILE + (px - tx * TILE);
|
|
716
|
+
if (h > tile[idx]) tile[idx] = h;
|
|
717
|
+
if (progress >= 1) {
|
|
718
|
+
for (let ti = 0; ti < targets.length; ti++) {
|
|
719
|
+
const tgt = targets[ti];
|
|
720
|
+
if (tgt.editable || this.reachedSkipped.has(tgt.id)) continue;
|
|
721
|
+
const lx = px - tgt.offset.x;
|
|
722
|
+
const ly = py - tgt.offset.y;
|
|
723
|
+
const ltx = Math.floor(lx / TILE);
|
|
724
|
+
const lty = Math.floor(ly / TILE);
|
|
725
|
+
const lt = tgt.base.tiles.get(tileKey(ltx, lty));
|
|
726
|
+
if (lt && lt.data[((ly - lty * TILE) * TILE + (lx - ltx * TILE)) * 4 + 3] > 0) {
|
|
727
|
+
this.reachedSkipped.add(tgt.id);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const sparkPad = b.sparkCount > 0 ? Math.ceil(b.tipSize * this.init.brush.antimatter.sparkReach * 1.3 + 4) : 0;
|
|
734
|
+
const reachR = Math.max(maxR, Math.ceil(b.radius * 0.6 + sparkPad));
|
|
735
|
+
return {
|
|
736
|
+
x: Math.floor(b.x - reachR),
|
|
737
|
+
y: Math.floor(b.y - reachR),
|
|
738
|
+
width: Math.ceil(reachR * 2) + 1,
|
|
739
|
+
height: Math.ceil(reachR * 2) + 1
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
/** Recomposites all editable target layers from their pre-stroke tiles using the current committed `heat` and `blasts`. */
|
|
743
|
+
recompositeAll() {
|
|
744
|
+
this.outputs.clear();
|
|
745
|
+
this.editedIds.clear();
|
|
746
|
+
const built = this.buildOutputs(this.heat, this.heatCoords, () => 1);
|
|
747
|
+
for (const out of built) {
|
|
748
|
+
this.outputs.set(out.id, out);
|
|
749
|
+
if (out.changed) this.editedIds.add(out.id);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Builds per-layer outputs for the given `heatMap` and blast progress function `progressOf(b)`.
|
|
754
|
+
* At `progressOf(b) === 1`, no flash or shock ring is drawn and sparks reach 100%, matching the
|
|
755
|
+
* committed pixels bit-for-bit.
|
|
756
|
+
*/
|
|
757
|
+
buildOutputs(heatMap, coordMap, progressOf) {
|
|
758
|
+
const settings = this.init.brush.antimatter;
|
|
759
|
+
const activeId = this.init.activeLayerId;
|
|
760
|
+
const outList = [];
|
|
761
|
+
for (const tgt of this.init.targets) {
|
|
762
|
+
if (!tgt.editable) continue;
|
|
763
|
+
const isActive = tgt.id === activeId;
|
|
764
|
+
const offX = tgt.offset.x;
|
|
765
|
+
const offY = tgt.offset.y;
|
|
766
|
+
const tiles = new Map(tgt.base.tiles);
|
|
767
|
+
const lightTiles = tgt.baseLight ? new Map(tgt.baseLight.tiles) : /* @__PURE__ */ new Map();
|
|
768
|
+
const heightTiles = tgt.baseThick ? new Map(tgt.baseThick.height.tiles) : null;
|
|
769
|
+
const touchedTiles = /* @__PURE__ */ new Map();
|
|
770
|
+
const touchedLight = /* @__PURE__ */ new Map();
|
|
771
|
+
const touchedHeight = /* @__PURE__ */ new Map();
|
|
772
|
+
const getLayerTile = (ltx, lty) => {
|
|
773
|
+
const lk = tileKey(ltx, lty);
|
|
774
|
+
let buf = touchedTiles.get(lk);
|
|
775
|
+
if (!buf) {
|
|
776
|
+
const src = tgt.base.tiles.get(lk)?.data;
|
|
777
|
+
buf = src ? new Uint8ClampedArray(src) : new Uint8ClampedArray(TILE * TILE * 4);
|
|
778
|
+
touchedTiles.set(lk, buf);
|
|
779
|
+
}
|
|
780
|
+
return buf;
|
|
781
|
+
};
|
|
782
|
+
const getLightTile = (ltx, lty) => {
|
|
783
|
+
const lk = tileKey(ltx, lty);
|
|
784
|
+
let buf = touchedLight.get(lk);
|
|
785
|
+
if (!buf) {
|
|
786
|
+
const src = tgt.baseLight?.tiles.get(lk)?.data;
|
|
787
|
+
buf = src ? new Uint8ClampedArray(src) : new Uint8ClampedArray(TILE * TILE);
|
|
788
|
+
touchedLight.set(lk, buf);
|
|
789
|
+
}
|
|
790
|
+
return buf;
|
|
791
|
+
};
|
|
792
|
+
const getHeightTile = (ltx, lty) => {
|
|
793
|
+
const lk = tileKey(ltx, lty);
|
|
794
|
+
let buf = touchedHeight.get(lk);
|
|
795
|
+
if (!buf) {
|
|
796
|
+
const src = tgt.baseThick?.height.tiles.get(lk)?.data;
|
|
797
|
+
buf = src ? new Uint8ClampedArray(src) : new Uint8ClampedArray(TILE * TILE);
|
|
798
|
+
touchedHeight.set(lk, buf);
|
|
799
|
+
}
|
|
800
|
+
return buf;
|
|
801
|
+
};
|
|
802
|
+
for (const [hk, hData] of heatMap) {
|
|
803
|
+
const { tx: dtx, ty: dty } = coordMap.get(hk);
|
|
804
|
+
const docOx = dtx * TILE;
|
|
805
|
+
const docOy = dty * TILE;
|
|
806
|
+
for (let oy = 0; oy < TILE; oy++) {
|
|
807
|
+
const py = docOy + oy;
|
|
808
|
+
const ly = py - offY;
|
|
809
|
+
const lty = Math.floor(ly / TILE);
|
|
810
|
+
const loy = ly - lty * TILE;
|
|
811
|
+
for (let ox = 0; ox < TILE; ox++) {
|
|
812
|
+
const h = hData[oy * TILE + ox];
|
|
813
|
+
if (h <= 0) continue;
|
|
814
|
+
const px = docOx + ox;
|
|
815
|
+
const lx = px - offX;
|
|
816
|
+
const ltx = Math.floor(lx / TILE);
|
|
817
|
+
const lox = lx - ltx * TILE;
|
|
818
|
+
const lk = tileKey(ltx, lty);
|
|
819
|
+
const lIdx = loy * TILE + lox;
|
|
820
|
+
const pIdx = lIdx * 4;
|
|
821
|
+
const srcTile = tgt.base.tiles.get(lk);
|
|
822
|
+
const a0 = srcTile ? srcTile.data[pIdx + 3] : 0;
|
|
823
|
+
const { core, ring } = heatProfile(h);
|
|
824
|
+
if (a0 > 0 || isActive && !tgt.lockTransparency && ring * settings.scorch > 0) {
|
|
825
|
+
const dst = getLayerTile(ltx, lty);
|
|
826
|
+
let r = srcTile ? srcTile.data[pIdx] : 0;
|
|
827
|
+
let g = srcTile ? srcTile.data[pIdx + 1] : 0;
|
|
828
|
+
let b = srcTile ? srcTile.data[pIdx + 2] : 0;
|
|
829
|
+
let a = a0;
|
|
830
|
+
const aNorm = a0 / 255;
|
|
831
|
+
if (a0 > 0) {
|
|
832
|
+
const invAmt = core * settings.invert;
|
|
833
|
+
if (invAmt > 0) {
|
|
834
|
+
let ir;
|
|
835
|
+
let ig;
|
|
836
|
+
let ib;
|
|
837
|
+
if (settings.invertMode === "oklab") {
|
|
838
|
+
[ir, ig, ib] = invertOklab8(r, g, b);
|
|
839
|
+
} else {
|
|
840
|
+
ir = 255 - r;
|
|
841
|
+
ig = 255 - g;
|
|
842
|
+
ib = 255 - b;
|
|
843
|
+
}
|
|
844
|
+
r = r + (ir - r) * invAmt;
|
|
845
|
+
g = g + (ig - g) * invAmt;
|
|
846
|
+
b = b + (ib - b) * invAmt;
|
|
847
|
+
}
|
|
848
|
+
const scorchAmt = ring * settings.scorch;
|
|
849
|
+
if (scorchAmt > 0) {
|
|
850
|
+
const lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
851
|
+
const desat = Math.min(1, scorchAmt * 0.85);
|
|
852
|
+
const sr = r + (lum * 0.85 + 2.7 - r) * desat;
|
|
853
|
+
const sg = g + (lum * 0.85 + 2.1 - g) * desat;
|
|
854
|
+
const sb = b + (lum * 0.85 + 1.8 - b) * desat;
|
|
855
|
+
const keep = 1 - scorchAmt * 0.78;
|
|
856
|
+
r = sr * keep;
|
|
857
|
+
g = sg * keep;
|
|
858
|
+
b = sb * keep;
|
|
859
|
+
}
|
|
860
|
+
if (!tgt.lockTransparency && settings.annihilate > 0 && core > 0) {
|
|
861
|
+
a = Math.round(a0 * (1 - core * settings.annihilate));
|
|
862
|
+
if (a <= 0) r = g = b = a = 0;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (isActive && !tgt.lockTransparency && ring * settings.scorch > 0 && a < 255) {
|
|
866
|
+
const sootA = Math.round(ring * settings.scorch * 96 * (1 - aNorm));
|
|
867
|
+
if (sootA > 0) {
|
|
868
|
+
const sa = sootA / 255;
|
|
869
|
+
const da = a / 255;
|
|
870
|
+
const outA = da + sa * (1 - da);
|
|
871
|
+
if (outA > 0) {
|
|
872
|
+
r = (r * da + 20 * sa * (1 - da)) / outA;
|
|
873
|
+
g = (g * da + 16 * sa * (1 - da)) / outA;
|
|
874
|
+
b = (b * da + 14 * sa * (1 - da)) / outA;
|
|
875
|
+
a = Math.round(outA * 255);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
dst[pIdx] = Math.min(255, Math.max(0, Math.round(r)));
|
|
880
|
+
dst[pIdx + 1] = Math.min(255, Math.max(0, Math.round(g)));
|
|
881
|
+
dst[pIdx + 2] = Math.min(255, Math.max(0, Math.round(b)));
|
|
882
|
+
dst[pIdx + 3] = Math.min(255, Math.max(0, a));
|
|
883
|
+
}
|
|
884
|
+
if (tgt.baseLight && core > 0) {
|
|
885
|
+
const lSrc = tgt.baseLight.tiles.get(lk);
|
|
886
|
+
const l0 = lSrc ? lSrc.data[lIdx] : 0;
|
|
887
|
+
if (l0 > 0) {
|
|
888
|
+
const lDst = getLightTile(ltx, lty);
|
|
889
|
+
lDst[lIdx] = Math.round(l0 * (1 - core));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (tgt.baseThick && settings.crater > 0 && (core > 0 || ring > 0)) {
|
|
893
|
+
const hSrc = tgt.baseThick.height.tiles.get(lk);
|
|
894
|
+
const h0 = hSrc ? hSrc.data[lIdx] : 0;
|
|
895
|
+
if (h0 > 0 || a0 > 0) {
|
|
896
|
+
const hDst = getHeightTile(ltx, lty);
|
|
897
|
+
const aNorm = a0 / 255;
|
|
898
|
+
const nextH = Math.round(
|
|
899
|
+
h0 * (1 - core * settings.crater * 0.85) - core * settings.crater * 40 + ring * settings.crater * 72 * aNorm
|
|
900
|
+
);
|
|
901
|
+
hDst[lIdx] = Math.min(255, Math.max(0, nextH));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (isActive) {
|
|
908
|
+
this.drawActiveExtras(tgt, getLayerTile, getLightTile, progressOf);
|
|
909
|
+
}
|
|
910
|
+
let changed = false;
|
|
911
|
+
for (const [lk, buf] of touchedTiles) {
|
|
912
|
+
const ft = finishTile(buf, 4, 0);
|
|
913
|
+
const prev = tgt.base.tiles.get(lk);
|
|
914
|
+
if (ft) tiles.set(lk, ft);
|
|
915
|
+
else tiles.delete(lk);
|
|
916
|
+
if (!sameTile(prev, ft)) changed = true;
|
|
917
|
+
}
|
|
918
|
+
let lightOut = null;
|
|
919
|
+
let lightChanged = false;
|
|
920
|
+
for (const [lk, buf] of touchedLight) {
|
|
921
|
+
const ft = finishTile(buf, 1, 0);
|
|
922
|
+
const prev = tgt.baseLight?.tiles.get(lk);
|
|
923
|
+
if (ft) lightTiles.set(lk, ft);
|
|
924
|
+
else lightTiles.delete(lk);
|
|
925
|
+
if (!sameTile(prev, ft)) lightChanged = true;
|
|
926
|
+
}
|
|
927
|
+
if (lightChanged) {
|
|
928
|
+
changed = true;
|
|
929
|
+
lightOut = new TiledSurface(1, lightTiles, 0);
|
|
930
|
+
}
|
|
931
|
+
let thickOut = null;
|
|
932
|
+
if (heightTiles && tgt.baseThick) {
|
|
933
|
+
let heightChanged = false;
|
|
934
|
+
for (const [lk, buf] of touchedHeight) {
|
|
935
|
+
const ft = finishTile(buf, 1, 0);
|
|
936
|
+
const prev = tgt.baseThick.height.tiles.get(lk);
|
|
937
|
+
if (ft) heightTiles.set(lk, ft);
|
|
938
|
+
else heightTiles.delete(lk);
|
|
939
|
+
if (!sameTile(prev, ft)) heightChanged = true;
|
|
940
|
+
}
|
|
941
|
+
if (heightChanged) {
|
|
942
|
+
changed = true;
|
|
943
|
+
thickOut = { ...tgt.baseThick, height: new TiledSurface(1, heightTiles, 0) };
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
outList.push({
|
|
947
|
+
id: tgt.id,
|
|
948
|
+
surface: changed ? new TiledSurface(4, tiles, 0) : tgt.base,
|
|
949
|
+
light: lightOut,
|
|
950
|
+
thick: thickOut,
|
|
951
|
+
changed
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return outList;
|
|
955
|
+
}
|
|
956
|
+
/** Draws sparks and (when `progress < 1`) the view-only flash and expanding shock ring on the active layer. */
|
|
957
|
+
drawActiveExtras(tgt, getLayerTile, getLightTile, progressOf) {
|
|
958
|
+
const { selection, brush } = this.init;
|
|
959
|
+
const reachFactor = brush.antimatter.sparkReach;
|
|
960
|
+
const offX = tgt.offset.x;
|
|
961
|
+
const offY = tgt.offset.y;
|
|
962
|
+
const blendPixel = (px, py, sr, sg, sb, sa, lightVal) => {
|
|
963
|
+
if (sa <= 0) return;
|
|
964
|
+
let maskA = 1;
|
|
965
|
+
if (selection) {
|
|
966
|
+
if (px < 0 || py < 0 || px >= selection.width || py >= selection.height) return;
|
|
967
|
+
maskA = selection.data[py * selection.width + px] / 255;
|
|
968
|
+
if (maskA <= 0) return;
|
|
969
|
+
}
|
|
970
|
+
const lx = px - offX;
|
|
971
|
+
const ly = py - offY;
|
|
972
|
+
const ltx = Math.floor(lx / TILE);
|
|
973
|
+
const lty = Math.floor(ly / TILE);
|
|
974
|
+
const lox = lx - ltx * TILE;
|
|
975
|
+
const loy = ly - lty * TILE;
|
|
976
|
+
const lIdx = loy * TILE + lox;
|
|
977
|
+
const pIdx = lIdx * 4;
|
|
978
|
+
const dst = getLayerTile(ltx, lty);
|
|
979
|
+
const da = dst[pIdx + 3] / 255;
|
|
980
|
+
if (tgt.lockTransparency && da <= 0) return;
|
|
981
|
+
const srcA = Math.min(1, sa * maskA);
|
|
982
|
+
const outA = tgt.lockTransparency ? da : da + srcA * (1 - da);
|
|
983
|
+
if (outA <= 0) return;
|
|
984
|
+
const w = tgt.lockTransparency ? srcA : srcA / outA;
|
|
985
|
+
dst[pIdx] = Math.min(255, Math.max(0, Math.round(dst[pIdx] * (1 - w) + sr * w)));
|
|
986
|
+
dst[pIdx + 1] = Math.min(255, Math.max(0, Math.round(dst[pIdx + 1] * (1 - w) + sg * w)));
|
|
987
|
+
dst[pIdx + 2] = Math.min(255, Math.max(0, Math.round(dst[pIdx + 2] * (1 - w) + sb * w)));
|
|
988
|
+
if (!tgt.lockTransparency) {
|
|
989
|
+
dst[pIdx + 3] = Math.min(255, Math.max(0, Math.round(outA * 255)));
|
|
990
|
+
}
|
|
991
|
+
if (lightVal > 0) {
|
|
992
|
+
const lDst = getLightTile(ltx, lty);
|
|
993
|
+
const nextL = Math.min(255, lDst[lIdx] + Math.round(lightVal * maskA * 255));
|
|
994
|
+
if (nextL > lDst[lIdx]) lDst[lIdx] = nextL;
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
for (const b of this.blasts) {
|
|
998
|
+
const progress = Math.max(0, Math.min(1, progressOf(b)));
|
|
999
|
+
if (progress <= 0) continue;
|
|
1000
|
+
const seed = b.seed;
|
|
1001
|
+
const bi = b.index;
|
|
1002
|
+
const [cr, cg, cb] = b.color;
|
|
1003
|
+
const baseAlpha = b.tipOpacity;
|
|
1004
|
+
for (let si = 0; si < b.sparkCount; si++) {
|
|
1005
|
+
const angle = rand01(seed, bi * 64 + si, 31) * Math.PI * 2;
|
|
1006
|
+
const speed = 0.45 + 0.85 * rand01(seed, bi * 64 + si, 37);
|
|
1007
|
+
const curve = (rand01(seed, bi * 64 + si, 41) - 0.5) * 0.45;
|
|
1008
|
+
const dist = b.tipSize * reachFactor * speed * progress;
|
|
1009
|
+
const startR = b.radius * (0.2 + 0.25 * rand01(seed, bi * 64 + si, 43));
|
|
1010
|
+
const steps = Math.max(2, Math.ceil(dist * 1.5));
|
|
1011
|
+
const ux = Math.round(Math.cos(angle) * 65536) / 65536;
|
|
1012
|
+
const uy = Math.round(Math.sin(angle) * 65536) / 65536;
|
|
1013
|
+
const nx = -uy;
|
|
1014
|
+
const ny = ux;
|
|
1015
|
+
for (let st = 0; st <= steps; st++) {
|
|
1016
|
+
const u = st / steps;
|
|
1017
|
+
const d = startR + u * dist;
|
|
1018
|
+
const bend = curve * u * u * dist * 0.35;
|
|
1019
|
+
const sx = Math.round(b.x + ux * d + nx * bend);
|
|
1020
|
+
const sy = Math.round(b.y + uy * d + ny * bend);
|
|
1021
|
+
const cool = Math.min(1, u * 1.15);
|
|
1022
|
+
const sr = 255 + (cr - 255) * cool;
|
|
1023
|
+
const sg = 252 + (cg - 252) * cool;
|
|
1024
|
+
const sb = 240 + (cb - 240) * cool;
|
|
1025
|
+
const fade = (1 - u * 0.72) * baseAlpha;
|
|
1026
|
+
const lightHit = b.lightAmount > 0 ? b.lightAmount * (1 - u * 0.65) : 0;
|
|
1027
|
+
blendPixel(sx, sy, sr, sg, sb, fade, lightHit);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (progress < 1) {
|
|
1031
|
+
const flashStrength = Math.max(0, 1 - progress * 2.2);
|
|
1032
|
+
const shockFade = (1 - progress) * (1 - progress);
|
|
1033
|
+
const shockR = b.radius * (0.25 + 0.75 * Math.sqrt(progress));
|
|
1034
|
+
const band = Math.max(1.5, b.radius * 0.14);
|
|
1035
|
+
const maxR = Math.ceil(shockR + band + 1);
|
|
1036
|
+
const minX = Math.floor(b.x - maxR);
|
|
1037
|
+
const maxX = Math.ceil(b.x + maxR);
|
|
1038
|
+
const minY = Math.floor(b.y - maxR);
|
|
1039
|
+
const maxY = Math.ceil(b.y + maxR);
|
|
1040
|
+
for (let py = minY; py <= maxY; py++) {
|
|
1041
|
+
const dy = py - b.y;
|
|
1042
|
+
for (let px = minX; px <= maxX; px++) {
|
|
1043
|
+
const dx = px - b.x;
|
|
1044
|
+
const d = Math.hypot(dx, dy);
|
|
1045
|
+
let a = 0;
|
|
1046
|
+
if (flashStrength > 0 && d < b.radius * 0.55) {
|
|
1047
|
+
const f = 1 - d / (b.radius * 0.55);
|
|
1048
|
+
a = Math.max(a, f * f * flashStrength * 0.9);
|
|
1049
|
+
}
|
|
1050
|
+
const ringDist = Math.abs(d - shockR);
|
|
1051
|
+
if (ringDist < band) {
|
|
1052
|
+
const rf = 1 - ringDist / band;
|
|
1053
|
+
a = Math.max(a, rf * rf * shockFade * 0.75);
|
|
1054
|
+
}
|
|
1055
|
+
if (a > 0) {
|
|
1056
|
+
blendPixel(px, py, 255, 250, 235, a, b.lightAmount * a);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Renders the blast state at `now` (ms) for view-only blast animation.
|
|
1065
|
+
* When `active` is `false` (all blasts have reached `progress === 1`), `outputs` match `layerOutputs()` bit-for-bit.
|
|
1066
|
+
*/
|
|
1067
|
+
renderAtTime(now) {
|
|
1068
|
+
const durationMs = this.init.brush.antimatter.duration * 1e3;
|
|
1069
|
+
if (durationMs <= 0 || this.blasts.length === 0) {
|
|
1070
|
+
return { outputs: this.layerOutputs(), active: false };
|
|
1071
|
+
}
|
|
1072
|
+
let anyActive = false;
|
|
1073
|
+
const progressOf = (b) => {
|
|
1074
|
+
if (now < b.t) return 0;
|
|
1075
|
+
const elapsed = now - b.t;
|
|
1076
|
+
if (elapsed >= durationMs) return 1;
|
|
1077
|
+
anyActive = true;
|
|
1078
|
+
return Math.min(1, 0.05 + 0.95 * (elapsed / durationMs));
|
|
1079
|
+
};
|
|
1080
|
+
for (const b of this.blasts) {
|
|
1081
|
+
if (now - b.t < durationMs) {
|
|
1082
|
+
anyActive = true;
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
if (!anyActive) {
|
|
1087
|
+
return { outputs: this.layerOutputs(), active: false };
|
|
1088
|
+
}
|
|
1089
|
+
const animHeat = /* @__PURE__ */ new Map();
|
|
1090
|
+
const animCoords = /* @__PURE__ */ new Map();
|
|
1091
|
+
for (const b of this.blasts) {
|
|
1092
|
+
const p = progressOf(b);
|
|
1093
|
+
if (p > 0) {
|
|
1094
|
+
this.writeBlastToHeat(b, animHeat, animCoords, p);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const outputs = this.buildOutputs(animHeat, animCoords, progressOf);
|
|
1098
|
+
return { outputs, active: true };
|
|
1099
|
+
}
|
|
1100
|
+
hasBlasts() {
|
|
1101
|
+
return this.blasts.length > 0;
|
|
1102
|
+
}
|
|
1103
|
+
/** Rebases blast birth times so the earliest blast starts at `now` ms. */
|
|
1104
|
+
rebaseTime(now) {
|
|
1105
|
+
if (!this.blasts.length) return;
|
|
1106
|
+
let minT = Infinity;
|
|
1107
|
+
for (const b of this.blasts) if (b.t < minT) minT = b.t;
|
|
1108
|
+
const shift = now - minT;
|
|
1109
|
+
for (const b of this.blasts) b.t += shift;
|
|
1110
|
+
}
|
|
1111
|
+
layerOutputs() {
|
|
1112
|
+
const list = [];
|
|
1113
|
+
for (const tgt of this.init.targets) {
|
|
1114
|
+
if (!tgt.editable) continue;
|
|
1115
|
+
const out = this.outputs.get(tgt.id);
|
|
1116
|
+
if (out) list.push(out);
|
|
1117
|
+
}
|
|
1118
|
+
return list;
|
|
1119
|
+
}
|
|
1120
|
+
stats() {
|
|
1121
|
+
const layers = [];
|
|
1122
|
+
const skippedLayers = [];
|
|
1123
|
+
for (const tgt of this.init.targets) {
|
|
1124
|
+
if (tgt.editable) {
|
|
1125
|
+
if (this.editedIds.has(tgt.id)) layers.push(tgt.id);
|
|
1126
|
+
} else if (this.reachedSkipped.has(tgt.id)) {
|
|
1127
|
+
skippedLayers.push(tgt.id);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
let sparks = 0;
|
|
1131
|
+
for (const b of this.blasts) sparks += b.sparkCount;
|
|
1132
|
+
return {
|
|
1133
|
+
blasts: this.blasts.length,
|
|
1134
|
+
sparks,
|
|
1135
|
+
fuel: Math.round(this.totalFuel * 1e4) / 1e4,
|
|
1136
|
+
layers,
|
|
1137
|
+
skippedLayers
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
420
1142
|
// src/paint-editor/ferro.ts
|
|
421
1143
|
var DEFAULT_MAGNET_STRENGTH = 0.75;
|
|
422
1144
|
var DEFAULT_MAGNET_RADIUS = 72;
|
|
@@ -3433,7 +4155,7 @@ function defaultTile(surface) {
|
|
|
3433
4155
|
if (surface.defaultValue) data.fill(surface.defaultValue);
|
|
3434
4156
|
return data;
|
|
3435
4157
|
}
|
|
3436
|
-
function
|
|
4158
|
+
function sameTile2(a, b) {
|
|
3437
4159
|
if (!a || !b) return !a && !b;
|
|
3438
4160
|
if (a === b) return true;
|
|
3439
4161
|
if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;
|
|
@@ -3456,11 +4178,12 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3456
4178
|
this.copies = init.copies.length ? init.copies : [IDENTITY_COPY];
|
|
3457
4179
|
const brush = init.brush;
|
|
3458
4180
|
this.invisibleDaylight = brush.fluorescence.amount > 0 && brush.fluorescence.invisible && brush.engine !== "eraser" && brush.engine !== "blender" && brush.engine !== "smudge";
|
|
4181
|
+
const isAntimatter = brush.engine === "antimatter";
|
|
3459
4182
|
if (init.wet) this.wetDriver = init.wet.driver;
|
|
3460
|
-
if (init.light && !init.wet && !init.sticker) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
3461
|
-
if (init.ferro && !init.wet && !init.sticker) this.ferroTwin = this.makeFerroTwin(init.ferro.surface);
|
|
3462
|
-
if (init.fluor && !init.wet && !init.sticker) this.fluorTwin = this.makeFluorTwin(init.fluor.surface);
|
|
3463
|
-
if (init.thermo && !init.wet && !init.sticker) this.thermoTwin = this.makeThermoTwin(init.thermo.surface);
|
|
4183
|
+
if (init.light && !init.wet && !init.sticker && !isAntimatter) this.lightTwin = this.makeLightTwin(init.light.surface);
|
|
4184
|
+
if (init.ferro && !init.wet && !init.sticker && !isAntimatter) this.ferroTwin = this.makeFerroTwin(init.ferro.surface);
|
|
4185
|
+
if (init.fluor && !init.wet && !init.sticker && !isAntimatter) this.fluorTwin = this.makeFluorTwin(init.fluor.surface);
|
|
4186
|
+
if (init.thermo && !init.wet && !init.sticker && !isAntimatter) this.thermoTwin = this.makeThermoTwin(init.thermo.surface);
|
|
3464
4187
|
this.apply = {
|
|
3465
4188
|
mode: brush.engine === "eraser" ? "erase" : "paint",
|
|
3466
4189
|
color: init.color,
|
|
@@ -3488,6 +4211,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3488
4211
|
thickDriver = null;
|
|
3489
4212
|
filmBuffer = null;
|
|
3490
4213
|
stickerSpray = null;
|
|
4214
|
+
antimatter = null;
|
|
3491
4215
|
/** Light paint: the stroke's twin over the light channel, and what it left there. */
|
|
3492
4216
|
lightTwin = null;
|
|
3493
4217
|
lightResult = null;
|
|
@@ -3515,8 +4239,21 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3515
4239
|
build(effect) {
|
|
3516
4240
|
const init = this.init;
|
|
3517
4241
|
const brush = init.brush;
|
|
3518
|
-
this.dry = this.wet = this.thickDriver = this.stickerSpray = this.filmBuffer = null;
|
|
3519
|
-
if (init.
|
|
4242
|
+
this.dry = this.wet = this.thickDriver = this.stickerSpray = this.filmBuffer = this.antimatter = null;
|
|
4243
|
+
if (brush.engine === "antimatter" && init.antimatter) {
|
|
4244
|
+
this.antimatter = new AntimatterStrokeDriver({
|
|
4245
|
+
activeLayerId: init.layerId,
|
|
4246
|
+
targets: init.antimatter.targets,
|
|
4247
|
+
brush,
|
|
4248
|
+
color: init.color,
|
|
4249
|
+
seed: init.seed,
|
|
4250
|
+
copies: this.copies,
|
|
4251
|
+
selection: init.selection ?? null,
|
|
4252
|
+
paint: init.paint ?? null,
|
|
4253
|
+
effect,
|
|
4254
|
+
now: init.antimatter.now
|
|
4255
|
+
});
|
|
4256
|
+
} else if (init.sticker) {
|
|
3520
4257
|
this.stickerSpray = new StickerSpray({ brush, target: this.tileTarget(), color: init.colorHex, seed: init.seed, copies: this.copies, nextId: init.sticker.nextId, paint: init.paint ?? null, effect, art: init.sticker.art, variant: init.sticker.variant });
|
|
3521
4258
|
} else if (brush.engine === "impasto" && init.thick) {
|
|
3522
4259
|
const rgb = (c) => [c[0] / 255, c[1] / 255, c[2] / 255];
|
|
@@ -3560,7 +4297,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3560
4297
|
this.fresh.clear();
|
|
3561
4298
|
this.bounds = null;
|
|
3562
4299
|
this.build(pixels);
|
|
3563
|
-
const rects = this.points.length ? this.dry ? this.dry.push(this.points) : this.thickDriver ? this.thickDriver.push(this.points) : this.stickerSpray ? this.stickerSpray.push(this.points) : this.wet.push(this.points) : [];
|
|
4300
|
+
const rects = this.points.length ? this.dry ? this.dry.push(this.points) : this.thickDriver ? this.thickDriver.push(this.points) : this.stickerSpray ? this.stickerSpray.push(this.points) : this.antimatter ? this.antimatter.push(this.points) : this.wet.push(this.points) : [];
|
|
3564
4301
|
this.update(was ? [...rects, was] : rects);
|
|
3565
4302
|
}
|
|
3566
4303
|
/** The stroke's twin over the light channel: the same marks in grey at the brush's light amount (see `light.ts`). */
|
|
@@ -3743,7 +4480,17 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3743
4480
|
this.wetUpdate(this.wetDriver.push(points));
|
|
3744
4481
|
return;
|
|
3745
4482
|
}
|
|
3746
|
-
this.update(
|
|
4483
|
+
this.update(
|
|
4484
|
+
this.dry ? this.dry.push(points) : this.thickDriver ? this.thickDriver.push(points) : this.stickerSpray ? this.stickerSpray.push(points) : this.antimatter ? this.antimatter.push(points) : this.wet.push(points)
|
|
4485
|
+
);
|
|
4486
|
+
}
|
|
4487
|
+
/** Antimatter strokes: per-layer outputs (pixel surface, light channel, thick channels) after the stroke so far. */
|
|
4488
|
+
antimatterOutputs() {
|
|
4489
|
+
return this.antimatter ? this.antimatter.layerOutputs() : null;
|
|
4490
|
+
}
|
|
4491
|
+
/** Antimatter strokes: the driver, used to play blast animations after commit. */
|
|
4492
|
+
antimatterDriver() {
|
|
4493
|
+
return this.antimatter;
|
|
3747
4494
|
}
|
|
3748
4495
|
/** Sticker strokes: the stickers sprayed so far. */
|
|
3749
4496
|
stickers() {
|
|
@@ -3752,6 +4499,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3752
4499
|
/** Impasto or iridescent strokes: the layer's thick-paint channels with the stroke so far. */
|
|
3753
4500
|
thickChannels() {
|
|
3754
4501
|
if (this.thickDriver) return this.thickDriver.channels();
|
|
4502
|
+
if (this.antimatter) return this.antimatter.layerOutputs().find((o) => o.id === this.layerId)?.thick ?? null;
|
|
3755
4503
|
const f = this.filmBuffer ?? this.wetDriver?.init.film ?? null;
|
|
3756
4504
|
if (f && this.init.thick) return { ...this.init.thick.channels, film: f.surface() };
|
|
3757
4505
|
return null;
|
|
@@ -3785,6 +4533,12 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3785
4533
|
update(rects) {
|
|
3786
4534
|
if (!rects.length) return;
|
|
3787
4535
|
for (const r of rects) this.bounds = union(this.bounds, r);
|
|
4536
|
+
if (this.antimatter) {
|
|
4537
|
+
for (const out of this.antimatter.layerOutputs()) {
|
|
4538
|
+
this.host.update(out.id, out.surface, rects);
|
|
4539
|
+
}
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
3788
4542
|
if (this.invisibleDaylight) {
|
|
3789
4543
|
this.host.update(this.layerId, this.base, rects);
|
|
3790
4544
|
return;
|
|
@@ -3827,6 +4581,24 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3827
4581
|
}
|
|
3828
4582
|
return this.result;
|
|
3829
4583
|
}
|
|
4584
|
+
if (this.antimatter) {
|
|
4585
|
+
this.update(this.antimatter.finish());
|
|
4586
|
+
this.closed = true;
|
|
4587
|
+
const stats = this.antimatter.stats();
|
|
4588
|
+
const changed2 = stats.blasts > 0 && stats.layers.length > 0;
|
|
4589
|
+
const activeOut = this.antimatter.layerOutputs().find((o) => o.id === this.layerId);
|
|
4590
|
+
const surface2 = changed2 ? activeOut?.surface ?? this.base : null;
|
|
4591
|
+
this.lightResult = activeOut?.light ?? null;
|
|
4592
|
+
this.result = {
|
|
4593
|
+
layerId: this.layerId,
|
|
4594
|
+
bounds: changed2 ? this.bounds : null,
|
|
4595
|
+
dabs: this.antimatter.dabCount,
|
|
4596
|
+
strokeId: null,
|
|
4597
|
+
antimatter: stats
|
|
4598
|
+
};
|
|
4599
|
+
this.host.commit(this, surface2, false);
|
|
4600
|
+
return this.result;
|
|
4601
|
+
}
|
|
3830
4602
|
this.update(this.dry ? this.dry.finish() : this.thickDriver ? this.thickDriver.finish() : this.stickerSpray ? this.stickerSpray.finish() : this.wet.finish());
|
|
3831
4603
|
this.closed = true;
|
|
3832
4604
|
if (this.lightTwin?.open) this.lightTwin.end();
|
|
@@ -3844,7 +4616,7 @@ var PaintStrokeSession = class _PaintStrokeSession {
|
|
|
3844
4616
|
const prev = this.base.tiles.get(key);
|
|
3845
4617
|
if (tile) tiles.set(key, tile);
|
|
3846
4618
|
else tiles.delete(key);
|
|
3847
|
-
if (!
|
|
4619
|
+
if (!sameTile2(prev, tile)) changed = true;
|
|
3848
4620
|
}
|
|
3849
4621
|
}
|
|
3850
4622
|
if (this.thickDriver && this.thickDriver.dabCount > 0) changed = true;
|
|
@@ -4196,6 +4968,10 @@ function normalizeBrushStroke(raw) {
|
|
|
4196
4968
|
if (effect) out.paint = effect;
|
|
4197
4969
|
else if (paint && paint.type === "gradient") out.paint = paint;
|
|
4198
4970
|
if (Array.isArray(r.steps) && r.steps.length === points.length && r.steps.every((v) => Number.isInteger(v) && v >= 0)) out.steps = [...r.steps];
|
|
4971
|
+
if (Array.isArray(r.targets)) {
|
|
4972
|
+
const targets = r.targets.filter((v) => typeof v === "string" && v.length > 0);
|
|
4973
|
+
if (targets.length) out.targets = targets;
|
|
4974
|
+
}
|
|
4199
4975
|
return out;
|
|
4200
4976
|
}
|
|
4201
4977
|
function strokeBounds(points) {
|
|
@@ -4740,8 +5516,21 @@ function paintTestStroke(surface, brush, paper, color, width, height, stampArt)
|
|
|
4740
5516
|
mixerLoad: brushLoadForStroke(brush, EMPTY_MIXER_LOAD, [r / 255, g / 255, b / 255]),
|
|
4741
5517
|
thick: brush.engine === "impasto" || brush.iridescence ? { channels: emptyThickChannels() } : void 0,
|
|
4742
5518
|
sticker: brush.engine === "sticker" ? { nextId: 1 } : void 0,
|
|
4743
|
-
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyLight() } : null,
|
|
4744
|
-
fluor: brush.fluorescence.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" ? { surface: emptyFluor() } : null,
|
|
5519
|
+
light: brush.light.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" && brush.engine !== "antimatter" ? { surface: emptyLight() } : null,
|
|
5520
|
+
fluor: brush.fluorescence.amount > 0 && brush.engine !== "wet" && brush.engine !== "sticker" && brush.engine !== "antimatter" ? { surface: emptyFluor() } : null,
|
|
5521
|
+
antimatter: brush.engine === "antimatter" ? {
|
|
5522
|
+
targets: [
|
|
5523
|
+
{
|
|
5524
|
+
id: "thumbnail",
|
|
5525
|
+
editable: true,
|
|
5526
|
+
lockTransparency: false,
|
|
5527
|
+
opacity: 1,
|
|
5528
|
+
offset: { x: 0, y: 0 },
|
|
5529
|
+
base: surface,
|
|
5530
|
+
baseLight: brush.light.amount > 0 ? emptyLight() : null
|
|
5531
|
+
}
|
|
5532
|
+
]
|
|
5533
|
+
} : null,
|
|
4745
5534
|
stampArt
|
|
4746
5535
|
}
|
|
4747
5536
|
);
|
|
@@ -4762,7 +5551,7 @@ function renderBrushThumbnail(brush, options) {
|
|
|
4762
5551
|
if (b.engine === "eraser") {
|
|
4763
5552
|
surface = fillRect(surface, { x: 0, y: 0, width, height }, rgba);
|
|
4764
5553
|
} else if (b.engine === "wet") {
|
|
4765
|
-
} else if (b.engine === "blender" || b.engine === "smudge" || b.engine === "mixer" && !b.loadsColor || b.behavior.pickup > 0) {
|
|
5554
|
+
} else if (b.engine === "blender" || b.engine === "smudge" || b.engine === "antimatter" || b.engine === "mixer" && !b.loadsColor || b.behavior.pickup > 0) {
|
|
4766
5555
|
const half = Math.ceil(height / 2);
|
|
4767
5556
|
surface = fillRect(surface, { x: 0, y: 0, width, height: half }, rgba);
|
|
4768
5557
|
surface = fillRect(surface, { x: 0, y: half, width, height: height - half }, [232, 196, 64, 255]);
|
|
@@ -5430,6 +6219,8 @@ function liveStrokeRefusal(brush) {
|
|
|
5430
6219
|
case "blender":
|
|
5431
6220
|
case "smudge":
|
|
5432
6221
|
return `The ${brush.name} brush moves the paint under it, so it can't be redrawn on its own; rasterize the layer first (rasterize_stroke_layer) or blend on a pixel layer.`;
|
|
6222
|
+
case "antimatter":
|
|
6223
|
+
return `The ${brush.name} brush detonates existing paint across visible layers, so it can't be redrawn on a stroke layer; paint it on a pixel layer (add_layer) or rasterize the layer first (rasterize_stroke_layer).`;
|
|
5433
6224
|
}
|
|
5434
6225
|
if (brush.mutation) return `The ${brush.name} brush paints mutation strokes (which fade once an agent reads them), so it goes on a pixel layer (add_layer).`;
|
|
5435
6226
|
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).`;
|
|
@@ -5808,7 +6599,7 @@ async function replayPainting(source, target, options = {}) {
|
|
|
5808
6599
|
strokes++;
|
|
5809
6600
|
if (!target.getLayer(st.layerId)) continue;
|
|
5810
6601
|
if (st.paint?.type === "effect" && st.paint.map?.by === "path") await target.prepareEffectPaints([st.paint]);
|
|
5811
|
-
const init = { layerId: st.layerId, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...st.paint ? { paint: st.paint } : {}, snapToPerspective: false, stencils: false, maskStencils: (st.stencils ?? []).map((x) => s === 1 ? x : scaleStencil(x, s)) };
|
|
6602
|
+
const init = { layerId: st.layerId, color: st.color, brush: st.brush, seed: st.seed, copies: st.copies ?? [], ...st.load ? { load: st.load } : {}, ...st.paint ? { paint: st.paint } : {}, ...st.targets ? { targets: st.targets } : {}, snapToPerspective: false, stencils: false, maskStencils: (st.stencils ?? []).map((x) => s === 1 ? x : scaleStencil(x, s)) };
|
|
5812
6603
|
if (st.brush.engine === "wet" && st.steps?.length === st.points.length) {
|
|
5813
6604
|
await target.wetIdle();
|
|
5814
6605
|
await advanceTo(st.steps[0]);
|
|
@@ -6926,7 +7717,7 @@ function rgbaOf(hex2) {
|
|
|
6926
7717
|
function rgbToHex(r, g, b) {
|
|
6927
7718
|
return "#" + [r, g, b].map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0")).join("");
|
|
6928
7719
|
}
|
|
6929
|
-
function
|
|
7720
|
+
function unionRect2(a, b) {
|
|
6930
7721
|
if (!a) return b ? { ...b } : null;
|
|
6931
7722
|
if (!b) return { ...a };
|
|
6932
7723
|
const x = Math.min(a.x, b.x);
|
|
@@ -6996,6 +7787,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
6996
7787
|
cached = null;
|
|
6997
7788
|
session = null;
|
|
6998
7789
|
live = null;
|
|
7790
|
+
liveMulti = null;
|
|
7791
|
+
liveThickMulti = null;
|
|
7792
|
+
liveLightMulti = null;
|
|
6999
7793
|
liveLayers = null;
|
|
7000
7794
|
dirty = "all";
|
|
7001
7795
|
seedCounter = 1;
|
|
@@ -7013,6 +7807,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7013
7807
|
liveEffectAt = null;
|
|
7014
7808
|
/** The view's stroke layers at `effectTime`, each with the document surface it was drawn from (stale once that changes). */
|
|
7015
7809
|
animatedLayers = /* @__PURE__ */ new Map();
|
|
7810
|
+
/** Blast animation frames (view only), kept apart from live effect frames so neither replaces the other. */
|
|
7811
|
+
blastLayers = /* @__PURE__ */ new Map();
|
|
7812
|
+
/** Blast animation light / thick channels (`null` light: none at this frame). */
|
|
7813
|
+
animatedLight = /* @__PURE__ */ new Map();
|
|
7814
|
+
animatedThick = /* @__PURE__ */ new Map();
|
|
7815
|
+
activeBlastAnim = null;
|
|
7816
|
+
playingClock = null;
|
|
7016
7817
|
/** `renderAnimationFrame` / `withEffectTime`: layer surfaces at another time, for one render. */
|
|
7017
7818
|
frameOverride = null;
|
|
7018
7819
|
/** The area each stroke painted (document pixels; `null`: nothing), learnt as it is painted or drawn, so a stroke layer edit redraws only where it changes (see `drawLiveStrokes`). */
|
|
@@ -7505,6 +8306,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7505
8306
|
}
|
|
7506
8307
|
undo() {
|
|
7507
8308
|
this.requireIdle("undoing");
|
|
8309
|
+
this.clearBlastAnimation();
|
|
7508
8310
|
const e = this.undoStack[this.undoStack.length - 1];
|
|
7509
8311
|
if (!e) return null;
|
|
7510
8312
|
if (e.wet) this.requireWetIdle("undoing");
|
|
@@ -7526,6 +8328,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7526
8328
|
}
|
|
7527
8329
|
redo() {
|
|
7528
8330
|
this.requireIdle("redoing");
|
|
8331
|
+
this.clearBlastAnimation();
|
|
7529
8332
|
const e = this.redoStack[this.redoStack.length - 1];
|
|
7530
8333
|
if (!e) return null;
|
|
7531
8334
|
if (e.wet) this.requireWetIdle("redoing");
|
|
@@ -7563,6 +8366,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7563
8366
|
/** Starts a new canvas: one empty layer over the paper (one undo step; undo brings the old one back). */
|
|
7564
8367
|
newCanvas(options = {}) {
|
|
7565
8368
|
this.requireIdle("starting a new canvas");
|
|
8369
|
+
this.clearBlastAnimation();
|
|
7566
8370
|
const next = createPaintDocument({
|
|
7567
8371
|
width: options.width ?? this.doc.canvas.width,
|
|
7568
8372
|
height: options.height ?? this.doc.canvas.height,
|
|
@@ -7728,9 +8532,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
7728
8532
|
describeLayer(l, parentId = findLayer(this.doc.layers, l.id)?.parent?.id ?? null, depth = findLayer(this.doc.layers, l.id)?.path.length ?? 0) {
|
|
7729
8533
|
let bounds = null;
|
|
7730
8534
|
if (l.type === "pixel") {
|
|
7731
|
-
const b =
|
|
8535
|
+
const b = unionRect2(unionRect2(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null), this.doc.ferro?.[l.id]?.bounds ?? null);
|
|
7732
8536
|
bounds = b ? { ...b, x: b.x + l.offset.x, y: b.y + l.offset.y } : null;
|
|
7733
|
-
} else for (const c of flattenLayers(l.children)) if (c.type === "pixel") bounds =
|
|
8537
|
+
} else for (const c of flattenLayers(l.children)) if (c.type === "pixel") bounds = unionRect2(bounds, this.describeLayer(c, l.id, depth + 1).bounds);
|
|
7734
8538
|
return {
|
|
7735
8539
|
id: l.id,
|
|
7736
8540
|
name: l.name,
|
|
@@ -8016,7 +8820,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
8016
8820
|
const upper = loc.layer;
|
|
8017
8821
|
if (!isPixelLayer(upper) || !isPixelLayer(below)) throw new PaintEditorError("Merge down needs a pixel layer directly above another pixel layer in the same group.");
|
|
8018
8822
|
if (isLayerLocked(below, "pixels")) throw new PaintEditorError(`Layer "${below.name}" has its pixels locked; unlock it to merge into it.`);
|
|
8019
|
-
const rect =
|
|
8823
|
+
const rect = unionRect2(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);
|
|
8020
8824
|
let surface = emptySurface(4);
|
|
8021
8825
|
if (rect) {
|
|
8022
8826
|
const base = { ...below, opacity: 1, blend: "normal", visible: true, clipped: void 0, mask: void 0 };
|
|
@@ -9480,6 +10284,15 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9480
10284
|
});
|
|
9481
10285
|
init = { ...init, seed: seedW, load };
|
|
9482
10286
|
wet = { driver, surface: () => world.display(id).surface };
|
|
10287
|
+
} else if (brush.engine === "antimatter") {
|
|
10288
|
+
const targetIds = new Set(this.antimatterTargetsOf(this.doc, init.targets).map((t) => t.id));
|
|
10289
|
+
targetIds.add(layer.id);
|
|
10290
|
+
const wetVisible = this.wet.wetLayerIds().filter((id) => targetIds.has(id));
|
|
10291
|
+
if (wetVisible.length) {
|
|
10292
|
+
this.dryLayersFirst(wetVisible);
|
|
10293
|
+
doc = this.doc;
|
|
10294
|
+
layer = findLayer(this.doc.layers, layer.id).layer;
|
|
10295
|
+
}
|
|
9483
10296
|
} else if (this.wet.wetLayerIds().includes(layer.id)) {
|
|
9484
10297
|
this.dryLayersFirst([layer.id]);
|
|
9485
10298
|
const dried = findLayer(this.doc.layers, layer.id).layer;
|
|
@@ -9524,7 +10337,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9524
10337
|
const seed = init.seed ?? mix32(this.seedCounter++ * 2654435761 >>> 0) & 2147483647;
|
|
9525
10338
|
const copies = (init.copies ?? this.strokeCopies()).filter((m) => m.some((v, i) => v !== IDENTITY_AFFINE[i]));
|
|
9526
10339
|
const hasThick = Boolean(doc.thick?.[layer.id]);
|
|
9527
|
-
const needThick = brush.engine === "impasto" || Boolean(brush.iridescence) || brush.engine === "eraser" && hasThick || Boolean(wet?.driver.init.film);
|
|
10340
|
+
const needThick = brush.engine !== "antimatter" && (brush.engine === "impasto" || Boolean(brush.iridescence) || brush.engine === "eraser" && hasThick || Boolean(wet?.driver.init.film));
|
|
10341
|
+
const isSpecialEngine = brush.engine === "sticker" || brush.engine === "antimatter";
|
|
9528
10342
|
const gradient = effect ? null : this.strokeGradient(init, brush, color);
|
|
9529
10343
|
const session = new PaintStrokeSession(host, {
|
|
9530
10344
|
layerId: layer.id,
|
|
@@ -9549,12 +10363,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9549
10363
|
wet,
|
|
9550
10364
|
thick: needThick ? { channels: doc.thick?.[layer.id] ?? emptyThickChannels() } : void 0,
|
|
9551
10365
|
// Light paint: a twin of the stroke paints the light channel (laying, dimming, moving light).
|
|
9552
|
-
light: !wet &&
|
|
10366
|
+
light: !wet && !isSpecialEngine && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
|
|
9553
10367
|
// Ferrofluid paint: a twin of the stroke paints the liquid ferrofluid channel.
|
|
9554
|
-
ferro: !wet &&
|
|
9555
|
-
fluor:
|
|
9556
|
-
thermo:
|
|
10368
|
+
ferro: !wet && !isSpecialEngine && (brush.ferro.amount > 0 || doc.ferro?.[layer.id]) ? { surface: doc.ferro?.[layer.id] ?? emptyFerro() } : null,
|
|
10369
|
+
fluor: !isSpecialEngine && (brush.fluorescence.amount > 0 || doc.fluor?.[layer.id]) ? { surface: doc.fluor?.[layer.id] ?? emptyFluor() } : null,
|
|
10370
|
+
thermo: !isSpecialEngine && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
|
|
9557
10371
|
sticker: brush.engine === "sticker" ? { nextId: this.nextStickerNumber(doc), art: this.stickerArt.get, variant: (ref2) => this.stickerArt.variantFor(ref2, this.colorScheme === "dark") } : void 0,
|
|
10372
|
+
antimatter: brush.engine === "antimatter" ? { targets: this.antimatterTargetsOf(doc, init.targets), now: () => (this.playingClock ?? defaultEffectClock).now() } : null,
|
|
9558
10373
|
stampArt: this.stampArt,
|
|
9559
10374
|
paint: gradient,
|
|
9560
10375
|
effect
|
|
@@ -9582,6 +10397,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9582
10397
|
else if (init.paint) stroke.paint = JSON.parse(JSON.stringify(init.paint));
|
|
9583
10398
|
if (init.mixerLoad.reservoir || init.mixerLoad.pickup) stroke.load = JSON.parse(JSON.stringify(init.mixerLoad));
|
|
9584
10399
|
if (init.wet) stroke.steps = [...init.wet.driver.steps];
|
|
10400
|
+
const fuel = init.antimatter?.targets;
|
|
10401
|
+
if (fuel?.length) stroke.targets = fuel.map((t) => t.id);
|
|
9585
10402
|
if (init.selection) {
|
|
9586
10403
|
const masks = (session.maskStencils ?? this.guides.stencils).filter((x) => x.enabled && x.mode === "mask");
|
|
9587
10404
|
if (masks.length) stroke.stencils = masks.map((x) => ({ ...x }));
|
|
@@ -9594,6 +10411,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9594
10411
|
*/
|
|
9595
10412
|
beginStroke(init = {}) {
|
|
9596
10413
|
this.requireIdle("starting another");
|
|
10414
|
+
this.clearBlastAnimation();
|
|
9597
10415
|
const before = this.doc;
|
|
9598
10416
|
const host = {
|
|
9599
10417
|
update: (layerId, surface, rects) => {
|
|
@@ -9603,6 +10421,23 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9603
10421
|
this.wet.syncDisplays();
|
|
9604
10422
|
return;
|
|
9605
10423
|
}
|
|
10424
|
+
const amOutputs = this.session?.antimatterOutputs();
|
|
10425
|
+
if (amOutputs) {
|
|
10426
|
+
const liveMulti = /* @__PURE__ */ new Map();
|
|
10427
|
+
const liveThickMulti = /* @__PURE__ */ new Map();
|
|
10428
|
+
const liveLightMulti = /* @__PURE__ */ new Map();
|
|
10429
|
+
for (const out of amOutputs) {
|
|
10430
|
+
liveMulti.set(out.id, out.surface);
|
|
10431
|
+
if (out.thick) liveThickMulti.set(out.id, out.thick);
|
|
10432
|
+
if (out.light) liveLightMulti.set(out.id, out.light);
|
|
10433
|
+
}
|
|
10434
|
+
this.liveMulti = liveMulti;
|
|
10435
|
+
this.liveThickMulti = liveThickMulti.size ? liveThickMulti : null;
|
|
10436
|
+
this.liveLightMulti = liveLightMulti.size ? liveLightMulti : null;
|
|
10437
|
+
this.live = { layerId: this.session.layerId, surface: liveMulti.get(this.session.layerId) ?? surface };
|
|
10438
|
+
this.emit(rects);
|
|
10439
|
+
return;
|
|
10440
|
+
}
|
|
9606
10441
|
this.live = { layerId, surface };
|
|
9607
10442
|
const ch = this.session?.thickChannels();
|
|
9608
10443
|
this.liveThick = ch ? { layerId, channels: ch } : null;
|
|
@@ -9629,6 +10464,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9629
10464
|
return;
|
|
9630
10465
|
}
|
|
9631
10466
|
this.live = null;
|
|
10467
|
+
this.liveMulti = null;
|
|
10468
|
+
this.liveThickMulti = null;
|
|
10469
|
+
this.liveLightMulti = null;
|
|
9632
10470
|
this.liveLayers = null;
|
|
9633
10471
|
this.liveThick = null;
|
|
9634
10472
|
this.liveFerro = null;
|
|
@@ -9643,8 +10481,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9643
10481
|
const snap = this.wetOp.before;
|
|
9644
10482
|
this.wetOp = null;
|
|
9645
10483
|
this.pendingWetEvents = [];
|
|
9646
|
-
|
|
9647
|
-
this.doc =
|
|
10484
|
+
this.wet.restore(snap);
|
|
10485
|
+
this.doc = before;
|
|
9648
10486
|
this.wet.syncDisplays();
|
|
9649
10487
|
} else if (this.doc.layers !== before.layers) {
|
|
9650
10488
|
this.doc = { ...this.doc, layers: before.layers };
|
|
@@ -9656,6 +10494,21 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9656
10494
|
const stroke = this.record(s);
|
|
9657
10495
|
s.result.strokeId = stroke.id;
|
|
9658
10496
|
this.paintedBounds.set(stroke, s.result?.bounds ?? null);
|
|
10497
|
+
const amOutputs = s.antimatterOutputs();
|
|
10498
|
+
if (amOutputs) {
|
|
10499
|
+
this.commit(
|
|
10500
|
+
`${brush.name} stroke`,
|
|
10501
|
+
this.withWetEvents(this.applyAntimatterOutputs(this.doc, amOutputs, stroke)),
|
|
10502
|
+
before
|
|
10503
|
+
);
|
|
10504
|
+
const amDriver = s.antimatterDriver();
|
|
10505
|
+
if (amDriver && brush.antimatter.duration > 0 && amDriver.hasBlasts()) {
|
|
10506
|
+
this.startBlastAnimation(amDriver, s.result?.bounds ?? null);
|
|
10507
|
+
}
|
|
10508
|
+
this.recordMutationStroke(stroke, s.result?.bounds ?? null);
|
|
10509
|
+
this.notifySample("end", s, [], stroke.id);
|
|
10510
|
+
return;
|
|
10511
|
+
}
|
|
9659
10512
|
if (wetStroke && this.wet.instantDry) {
|
|
9660
10513
|
this.wet.dry(s.layerId);
|
|
9661
10514
|
this.logWet({ kind: "dry", layerId: s.layerId, at: this.doc.strokes.length + 1 });
|
|
@@ -9837,6 +10690,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9837
10690
|
const stroke = this.record(s);
|
|
9838
10691
|
s.result.strokeId = stroke.id;
|
|
9839
10692
|
this.paintedBounds.set(stroke, s.result?.bounds ?? null);
|
|
10693
|
+
const amOutputs = s.antimatterOutputs();
|
|
10694
|
+
if (amOutputs) {
|
|
10695
|
+
out = this.applyAntimatterOutputs(out, amOutputs, stroke);
|
|
10696
|
+
return;
|
|
10697
|
+
}
|
|
9840
10698
|
const targetId = this.tempLayerTargets.get(s.layerId);
|
|
9841
10699
|
if (targetId && !findLayer(out.layers, s.layerId)) {
|
|
9842
10700
|
const temp = findLayer(this.doc.layers, s.layerId)?.layer;
|
|
@@ -9902,11 +10760,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9902
10760
|
}
|
|
9903
10761
|
result.wet = this.wet.isWet(session.layerId);
|
|
9904
10762
|
}
|
|
9905
|
-
return { doc: out, result, brush: session.init.brush };
|
|
10763
|
+
return { doc: out, result, brush: session.init.brush, antimatterDriver: session.antimatterDriver() };
|
|
9906
10764
|
}
|
|
9907
10765
|
/** Paints one stroke from data (one undo step). */
|
|
9908
10766
|
paintStroke(init) {
|
|
9909
10767
|
this.requireIdle("painting");
|
|
10768
|
+
this.clearBlastAnimation();
|
|
9910
10769
|
const points = this.checkPoints(init.points);
|
|
9911
10770
|
const before = this.doc;
|
|
9912
10771
|
let run;
|
|
@@ -9916,10 +10775,23 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9916
10775
|
this.wetOp = null;
|
|
9917
10776
|
throw e;
|
|
9918
10777
|
}
|
|
9919
|
-
const { doc, result, brush } = run;
|
|
10778
|
+
const { doc, result, brush, antimatterDriver } = run;
|
|
10779
|
+
if (!result.strokeId && this.wetOp) {
|
|
10780
|
+
const snap = this.wetOp.before;
|
|
10781
|
+
this.wetOp = null;
|
|
10782
|
+
this.pendingWetEvents = [];
|
|
10783
|
+
this.wet.restore(snap);
|
|
10784
|
+
this.doc = before;
|
|
10785
|
+
this.wet.syncDisplays();
|
|
10786
|
+
this.emit([]);
|
|
10787
|
+
return result;
|
|
10788
|
+
}
|
|
9920
10789
|
if (result.mixerLoad) this.keepLoad(brush, result.mixerLoad);
|
|
9921
10790
|
if (doc !== before || this.wetOp) {
|
|
9922
10791
|
this.commit(`${brush.name} stroke`, this.withWetEvents(doc), before);
|
|
10792
|
+
if (antimatterDriver && brush.antimatter.duration > 0 && antimatterDriver.hasBlasts()) {
|
|
10793
|
+
this.startBlastAnimation(antimatterDriver, result.bounds);
|
|
10794
|
+
}
|
|
9923
10795
|
const lastStroke = this.doc.strokes[this.doc.strokes.length - 1];
|
|
9924
10796
|
if (lastStroke && lastStroke.id === result.strokeId) this.recordMutationStroke(lastStroke, result.bounds);
|
|
9925
10797
|
} else this.emit([]);
|
|
@@ -9951,7 +10823,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
9951
10823
|
subpaths.forEach((points, i) => {
|
|
9952
10824
|
last = this.runStroke(doc, { ...stroke, ...stroke.seed !== void 0 ? { seed: stroke.seed + i } : {}, start: points[0] }, points);
|
|
9953
10825
|
doc = last.doc;
|
|
9954
|
-
bounds =
|
|
10826
|
+
bounds = unionRect2(bounds, last.result.bounds);
|
|
9955
10827
|
dabs += last.result.dabs;
|
|
9956
10828
|
if (last.result.strokeId) strokeIds.push(last.result.strokeId);
|
|
9957
10829
|
});
|
|
@@ -10039,11 +10911,15 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10039
10911
|
load = { ...load, reservoir: [r / 255, g / 255, b / 255] };
|
|
10040
10912
|
}
|
|
10041
10913
|
const paint = v.paint && options.color === void 0 ? { paint: v.paint } : {};
|
|
10042
|
-
const
|
|
10914
|
+
const targets = s.targets && options.layerId === void 0 ? { targets: s.targets } : {};
|
|
10915
|
+
const run = this.runStroke(doc, { layerId, color: options.color ?? s.color, brush, seed: s.seed, copies: v.copies ?? [], stencils: false, maskStencils: [], ...paint, ...targets, ...load && options.brushId === void 0 ? { load } : {} }, v.points);
|
|
10043
10916
|
doc = run.doc;
|
|
10044
|
-
bounds =
|
|
10917
|
+
bounds = unionRect2(bounds, run.result.bounds);
|
|
10045
10918
|
if (run.result.strokeId) ids.push(run.result.strokeId);
|
|
10046
10919
|
layerIds.add(layerId);
|
|
10920
|
+
if (run.result.antimatter) {
|
|
10921
|
+
for (const lid of run.result.antimatter.layers) layerIds.add(lid);
|
|
10922
|
+
}
|
|
10047
10923
|
}
|
|
10048
10924
|
}
|
|
10049
10925
|
if (doc !== this.doc) this.commit(strokes.length === 1 && repeat === 1 ? "Replay stroke" : "Replay strokes", doc);
|
|
@@ -10859,6 +11735,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10859
11735
|
currentLayers() {
|
|
10860
11736
|
const override = this.frameOverride;
|
|
10861
11737
|
if (override) return Array.from(override).reduce((ls, [id, surface]) => mapLayer(ls, id, (l) => ({ ...l, surface })), this.doc.layers);
|
|
11738
|
+
if (this.liveMulti) {
|
|
11739
|
+
let layers2 = this.doc.layers;
|
|
11740
|
+
for (const [id, surface] of this.liveMulti) {
|
|
11741
|
+
layers2 = mapLayer(layers2, id, (l) => ({ ...l, surface }));
|
|
11742
|
+
}
|
|
11743
|
+
return layers2;
|
|
11744
|
+
}
|
|
10862
11745
|
const live = this.live;
|
|
10863
11746
|
if (!live) return this.schemeLayers(this.animatedView());
|
|
10864
11747
|
if (this.liveLayers?.key === live.surface) return this.liveLayers.layers;
|
|
@@ -10956,13 +11839,17 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10956
11839
|
litLayers(layers) {
|
|
10957
11840
|
const thick = this.doc.thick;
|
|
10958
11841
|
const live = this.liveThick;
|
|
11842
|
+
const liveMulti = this.liveThickMulti;
|
|
11843
|
+
const animThick = this.animatedThick;
|
|
10959
11844
|
const ferro = this.doc.ferro;
|
|
10960
11845
|
const liveFerro = this.liveFerro;
|
|
10961
|
-
if (!thick && !live && !ferro && !liveFerro) return layers;
|
|
11846
|
+
if (!thick && !live && !liveMulti && !animThick.size && !ferro && !liveFerro) return layers;
|
|
10962
11847
|
let out = layers;
|
|
10963
11848
|
const ids = /* @__PURE__ */ new Set([
|
|
10964
11849
|
...Object.keys(thick ?? {}),
|
|
10965
11850
|
...live ? [live.layerId] : [],
|
|
11851
|
+
...liveMulti ? liveMulti.keys() : [],
|
|
11852
|
+
...animThick.keys(),
|
|
10966
11853
|
...Object.keys(ferro ?? {}),
|
|
10967
11854
|
...liveFerro ? [liveFerro.layerId] : []
|
|
10968
11855
|
]);
|
|
@@ -10970,7 +11857,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10970
11857
|
for (const id of ids) {
|
|
10971
11858
|
const l = findLayer(out, id)?.layer;
|
|
10972
11859
|
if (!isPixelLayer(l)) continue;
|
|
10973
|
-
const ch = (live?.layerId === id ? live.channels : thick?.[id]) ?? emptyThickChannels();
|
|
11860
|
+
const ch = liveMulti?.get(id) ?? (live?.layerId === id ? live.channels : animThick.get(id) ?? thick?.[id]) ?? emptyThickChannels();
|
|
10974
11861
|
const fSurf = liveFerro?.layerId === id ? liveFerro.surface : ferro?.[id];
|
|
10975
11862
|
const fSet = (liveFerro?.layerId === id ? liveFerro.settings : this.doc.ferroSettings?.[id]) ?? DEFAULT_FERRO;
|
|
10976
11863
|
const hasThick = ch.height.tiles.size > 0 || ch.material.tiles.size > 0 || ch.film.tiles.size > 0;
|
|
@@ -10990,7 +11877,16 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10990
11877
|
*/
|
|
10991
11878
|
lightLayers(layers) {
|
|
10992
11879
|
const light = this.doc.light ?? {};
|
|
10993
|
-
const
|
|
11880
|
+
const liveLight = this.liveLightMulti;
|
|
11881
|
+
const animLight = this.animatedLight;
|
|
11882
|
+
const ids = /* @__PURE__ */ new Set([
|
|
11883
|
+
...Object.keys(light),
|
|
11884
|
+
...liveLight ? liveLight.keys() : [],
|
|
11885
|
+
...animLight.keys(),
|
|
11886
|
+
...Object.keys(this.doc.thick ?? {}),
|
|
11887
|
+
...this.liveThickMulti ? this.liveThickMulti.keys() : [],
|
|
11888
|
+
...this.animatedThick.keys()
|
|
11889
|
+
]);
|
|
10994
11890
|
if (!ids.size) return layers;
|
|
10995
11891
|
let out = layers;
|
|
10996
11892
|
for (const id of ids) {
|
|
@@ -10998,10 +11894,12 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
10998
11894
|
if (!isPixelLayer(l)) continue;
|
|
10999
11895
|
let cache = this.lightCaches.get(id);
|
|
11000
11896
|
if (!cache) this.lightCaches.set(id, cache = new LightCache());
|
|
11001
|
-
const
|
|
11897
|
+
const layerLight = liveLight?.has(id) ? liveLight.get(id) ?? void 0 : animLight.has(id) ? animLight.get(id) ?? void 0 : light[id];
|
|
11898
|
+
const layerThick = this.liveThickMulti?.get(id) ?? (this.liveThick?.layerId === id ? this.liveThick.channels : this.animatedThick.get(id) ?? this.doc.thick?.[id]);
|
|
11899
|
+
const glowed = cache.glow(l.surface, layerLight, layerThick);
|
|
11002
11900
|
if (glowed !== l.surface) out = mapLayer(out, id, () => ({ ...l, surface: glowed }));
|
|
11003
11901
|
const committed = findLayer(this.doc.layers, id)?.layer;
|
|
11004
|
-
const spill =
|
|
11902
|
+
const spill = layerLight && isPixelLayer(committed) ? cache.spill(committed.surface, layerLight, this.lightSpillOf(id)) : null;
|
|
11005
11903
|
if (spill) out = insertAfter(out, id, { type: "pixel", id: `${id}${SPILL_SUFFIX}`, name: `${l.name} light`, visible: l.visible, opacity: l.opacity, blend: "linear-dodge", surface: spill, offset: { ...l.offset } });
|
|
11006
11904
|
}
|
|
11007
11905
|
return out;
|
|
@@ -11012,7 +11910,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11012
11910
|
if (!l.visible) continue;
|
|
11013
11911
|
if (l.type === "group") this.backlitLayers(l.children, committed, opacity * l.opacity, out);
|
|
11014
11912
|
else if (isPixelLayer(l)) {
|
|
11015
|
-
const thick = !committed
|
|
11913
|
+
const thick = !committed ? this.liveThickMulti?.get(l.id) ?? (this.liveThick?.layerId === l.id ? this.liveThick.channels : this.animatedThick.get(l.id) ?? this.doc.thick?.[l.id]) : this.doc.thick?.[l.id];
|
|
11016
11914
|
out.push({ surface: l.surface, offset: l.offset, opacity: opacity * l.opacity, ...thick ? { thick } : {} });
|
|
11017
11915
|
}
|
|
11018
11916
|
}
|
|
@@ -11057,7 +11955,8 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
11057
11955
|
*/
|
|
11058
11956
|
compositeTiles(rect, onTile, options = {}) {
|
|
11059
11957
|
const dirty = options.dirty === "all" ? void 0 : options.dirty;
|
|
11060
|
-
|
|
11958
|
+
const multiChanging = this.liveMulti && this.liveMulti.size > 1 || Boolean(this.activeBlastAnim);
|
|
11959
|
+
this.viewCompositor.compositeTiles(this.renderLayers({ paper: options.paper }, true), rect, this.sources(), onTile, { dirty, checkpointLayerId: multiChanging ? void 0 : this.live?.layerId ?? this.doc.activeLayerId });
|
|
11061
11960
|
}
|
|
11062
11961
|
/** The canvas as canvas-sized RGBA (paper included by default). */
|
|
11063
11962
|
compositePixels(options = {}) {
|
|
@@ -12058,7 +12957,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12058
12957
|
else delete next.load;
|
|
12059
12958
|
const list = strokes.map((x, i) => i === index ? next : x);
|
|
12060
12959
|
this.commitLiveStrokes("Edit stroke", layer, list, this.strokeAreas([was]));
|
|
12061
|
-
return { stroke: { ...next }, bounds:
|
|
12960
|
+
return { stroke: { ...next }, bounds: unionRect2(this.strokeArea(was), this.strokeArea(next)) ?? { x: 0, y: 0, width: 0, height: 0 } };
|
|
12062
12961
|
}
|
|
12063
12962
|
/**
|
|
12064
12963
|
* Replaces a live stroke's points, or moves, scales, rotates or flips them (`transform`, about
|
|
@@ -12083,7 +12982,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12083
12982
|
}
|
|
12084
12983
|
const list = strokes.map((x, i) => i === index ? next : x);
|
|
12085
12984
|
this.commitLiveStrokes("Edit stroke points", layer, list, this.strokeAreas([was]));
|
|
12086
|
-
return { stroke: { ...next }, bounds:
|
|
12985
|
+
return { stroke: { ...next }, bounds: unionRect2(this.strokeArea(was), this.strokeArea(next)) ?? { x: 0, y: 0, width: 0, height: 0 } };
|
|
12087
12986
|
}
|
|
12088
12987
|
/** Deletes live strokes (one undo step). Returns the ids removed and the area that changed. */
|
|
12089
12988
|
removeStrokes(ids) {
|
|
@@ -12096,7 +12995,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12096
12995
|
for (const f of found) layers.set(f.layer.id, f.layer);
|
|
12097
12996
|
for (const [layerId, layer] of layers) {
|
|
12098
12997
|
const gone = (doc.liveStrokes?.[layerId] ?? []).filter((x) => ids.includes(x.id));
|
|
12099
|
-
for (const x of gone) bounds =
|
|
12998
|
+
for (const x of gone) bounds = unionRect2(bounds, this.strokeArea(x));
|
|
12100
12999
|
doc = this.withLiveStrokes(doc, layer, (doc.liveStrokes?.[layerId] ?? []).filter((x) => !ids.includes(x.id)), this.strokeAreas(gone));
|
|
12101
13000
|
}
|
|
12102
13001
|
this.commit(ids.length === 1 ? "Remove stroke" : "Remove strokes", doc);
|
|
@@ -12226,7 +13125,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12226
13125
|
session.push(st.points);
|
|
12227
13126
|
const result = session.end();
|
|
12228
13127
|
const had = this.paintedBounds.get(st);
|
|
12229
|
-
this.paintedBounds.set(st, had && result.bounds ?
|
|
13128
|
+
this.paintedBounds.set(st, had && result.bounds ? unionRect2(had, result.bounds) : result.bounds ?? had ?? null);
|
|
12230
13129
|
return { surface: out, bounds: result.bounds, thick: outThick };
|
|
12231
13130
|
}
|
|
12232
13131
|
/**
|
|
@@ -12266,11 +13165,13 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12266
13165
|
// --- Live effect paint ------------------------------------------------------------
|
|
12267
13166
|
/** The view's layers: stroke layers playing live effects at `effectTime`, the rest as they are. */
|
|
12268
13167
|
animatedView() {
|
|
12269
|
-
if (!this.animatedLayers.size) return this.doc.layers;
|
|
13168
|
+
if (!this.animatedLayers.size && !this.blastLayers.size) return this.doc.layers;
|
|
12270
13169
|
let layers = this.doc.layers;
|
|
12271
|
-
for (const
|
|
12272
|
-
const
|
|
12273
|
-
|
|
13170
|
+
for (const frames of [this.animatedLayers, this.blastLayers]) {
|
|
13171
|
+
for (const [id, a] of frames) {
|
|
13172
|
+
const l = findLayer(layers, id)?.layer;
|
|
13173
|
+
if (isPixelLayer(l) && l.surface === a.base) layers = mapLayer(layers, id, () => ({ ...l, surface: a.surface }));
|
|
13174
|
+
}
|
|
12274
13175
|
}
|
|
12275
13176
|
return layers;
|
|
12276
13177
|
}
|
|
@@ -12352,6 +13253,11 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12352
13253
|
* stroke is painted, or when nothing is live. The bindings start it while the editor is shown.
|
|
12353
13254
|
*/
|
|
12354
13255
|
playEffects(clock = defaultEffectClock) {
|
|
13256
|
+
this.playingClock = clock;
|
|
13257
|
+
if (this.activeBlastAnim && !this.activeBlastAnim.ticked) {
|
|
13258
|
+
this.activeBlastAnim.driver.rebaseTime(clock.now());
|
|
13259
|
+
this.stepBlastAnimation(clock.now());
|
|
13260
|
+
}
|
|
12355
13261
|
let stopped = false;
|
|
12356
13262
|
let cancel = () => {
|
|
12357
13263
|
};
|
|
@@ -12361,6 +13267,9 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12361
13267
|
if (stopped) return;
|
|
12362
13268
|
const now = clock.now();
|
|
12363
13269
|
const { playing, fps } = this.effectPlaybackState;
|
|
13270
|
+
if (this.activeBlastAnim && !this.session) {
|
|
13271
|
+
this.stepBlastAnimation(now);
|
|
13272
|
+
}
|
|
12364
13273
|
if (!playing) started = now - this.effectTime;
|
|
12365
13274
|
if (!playing || this.session || now - last < 1e3 / fps - 1 || !this.liveEffects().layers.length) {
|
|
12366
13275
|
cancel = clock.frame(tick);
|
|
@@ -12377,13 +13286,133 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12377
13286
|
);
|
|
12378
13287
|
};
|
|
12379
13288
|
cancel = clock.frame(tick);
|
|
13289
|
+
const self = this;
|
|
12380
13290
|
return {
|
|
12381
13291
|
stop() {
|
|
12382
13292
|
stopped = true;
|
|
13293
|
+
if (self.playingClock === clock) self.playingClock = null;
|
|
12383
13294
|
cancel();
|
|
12384
13295
|
}
|
|
12385
13296
|
};
|
|
12386
13297
|
}
|
|
13298
|
+
/**
|
|
13299
|
+
* The fuel layers of an antimatter stroke: every visible, non-temporary pixel layer (group
|
|
13300
|
+
* opacity multiplied in), or on a replay exactly the recorded `targetIds` whatever their
|
|
13301
|
+
* visibility now. The editable ones (not locked, stroke, sticker or effect layers) change.
|
|
13302
|
+
*/
|
|
13303
|
+
antimatterTargetsOf(doc, targetIds) {
|
|
13304
|
+
const targetSet = targetIds?.length ? new Set(targetIds) : null;
|
|
13305
|
+
const out = [];
|
|
13306
|
+
const hasForcedDescendant = (list) => Boolean(targetSet && list.some((l) => l.type === "group" ? hasForcedDescendant(l.children) : targetSet.has(l.id)));
|
|
13307
|
+
const walk = (list, groupOpacity, groupVisible) => {
|
|
13308
|
+
for (const l of list) {
|
|
13309
|
+
if (l.type === "group") {
|
|
13310
|
+
const vis2 = groupVisible && l.visible;
|
|
13311
|
+
if (vis2 || hasForcedDescendant(l.children)) {
|
|
13312
|
+
walk(l.children, groupOpacity * (l.visible ? l.opacity : 1), vis2);
|
|
13313
|
+
}
|
|
13314
|
+
continue;
|
|
13315
|
+
}
|
|
13316
|
+
if (!isPixelLayer(l) || l.temporary) continue;
|
|
13317
|
+
const vis = groupVisible && l.visible;
|
|
13318
|
+
if (targetSet ? !targetSet.has(l.id) : !vis) continue;
|
|
13319
|
+
const canEdit = !isLayerLocked(l, "pixels") && !doc.liveStrokes?.[l.id] && !doc.stickers?.[l.id]?.length && !doc.effectLayers?.[l.id];
|
|
13320
|
+
out.push({
|
|
13321
|
+
id: l.id,
|
|
13322
|
+
editable: canEdit,
|
|
13323
|
+
lockTransparency: isLayerLocked(l, "transparency"),
|
|
13324
|
+
opacity: groupOpacity * (l.visible ? l.opacity : l.opacity || 1),
|
|
13325
|
+
offset: { ...l.offset },
|
|
13326
|
+
base: l.surface,
|
|
13327
|
+
baseLight: doc.light?.[l.id] ?? null,
|
|
13328
|
+
baseThick: doc.thick?.[l.id] ?? null
|
|
13329
|
+
});
|
|
13330
|
+
}
|
|
13331
|
+
};
|
|
13332
|
+
walk(doc.layers, 1, true);
|
|
13333
|
+
return out;
|
|
13334
|
+
}
|
|
13335
|
+
/** Applies every target layer's output from an antimatter stroke into a new document state. */
|
|
13336
|
+
applyAntimatterOutputs(doc, outputs, stroke) {
|
|
13337
|
+
let layers = doc.layers;
|
|
13338
|
+
let thick = doc.thick;
|
|
13339
|
+
let light = doc.light;
|
|
13340
|
+
for (const o of outputs) {
|
|
13341
|
+
if (!o.changed) continue;
|
|
13342
|
+
const l = findLayer(layers, o.id)?.layer;
|
|
13343
|
+
if (!isPixelLayer(l)) continue;
|
|
13344
|
+
layers = mapLayer(layers, o.id, () => ({ ...l, surface: o.surface }));
|
|
13345
|
+
if (o.thick !== null) thick = withLayerThick(thick, o.id, o.thick);
|
|
13346
|
+
if (o.light !== null) light = withLayerLight(light, o.id, o.light);
|
|
13347
|
+
}
|
|
13348
|
+
return withLiveStroke(
|
|
13349
|
+
{
|
|
13350
|
+
...doc,
|
|
13351
|
+
layers,
|
|
13352
|
+
strokes: [...doc.strokes, stroke],
|
|
13353
|
+
...thick && Object.keys(thick).length ? { thick } : { thick: void 0 },
|
|
13354
|
+
...light && Object.keys(light).length ? { light } : { light: void 0 }
|
|
13355
|
+
},
|
|
13356
|
+
stroke
|
|
13357
|
+
);
|
|
13358
|
+
}
|
|
13359
|
+
startBlastAnimation(driver, dirty) {
|
|
13360
|
+
driver.rebaseTime((this.playingClock ?? defaultEffectClock).now());
|
|
13361
|
+
this.activeBlastAnim = {
|
|
13362
|
+
driver,
|
|
13363
|
+
committedDoc: this.doc,
|
|
13364
|
+
dirty,
|
|
13365
|
+
ticked: false
|
|
13366
|
+
};
|
|
13367
|
+
if (this.playingClock) {
|
|
13368
|
+
this.stepBlastAnimation(this.playingClock.now());
|
|
13369
|
+
}
|
|
13370
|
+
}
|
|
13371
|
+
stepBlastAnimation(now) {
|
|
13372
|
+
const anim = this.activeBlastAnim;
|
|
13373
|
+
if (!anim) return false;
|
|
13374
|
+
if (this.doc !== anim.committedDoc || this.session) {
|
|
13375
|
+
this.clearBlastAnimation();
|
|
13376
|
+
return false;
|
|
13377
|
+
}
|
|
13378
|
+
anim.ticked = true;
|
|
13379
|
+
const frame = anim.driver.renderAtTime(now);
|
|
13380
|
+
if (!frame.active) {
|
|
13381
|
+
this.clearBlastAnimation(true);
|
|
13382
|
+
return true;
|
|
13383
|
+
}
|
|
13384
|
+
const nextLayers = /* @__PURE__ */ new Map();
|
|
13385
|
+
const nextLight = /* @__PURE__ */ new Map();
|
|
13386
|
+
const nextThick = /* @__PURE__ */ new Map();
|
|
13387
|
+
const targets = new Map(anim.driver.init.targets.map((t) => [t.id, t]));
|
|
13388
|
+
for (const out of frame.outputs) {
|
|
13389
|
+
const id = out.id;
|
|
13390
|
+
const committed = findLayer(this.doc.layers, id)?.layer;
|
|
13391
|
+
if (!isPixelLayer(committed)) continue;
|
|
13392
|
+
nextLayers.set(id, { base: committed.surface, surface: out.surface });
|
|
13393
|
+
const tgt = targets.get(id);
|
|
13394
|
+
const light = out.light ?? tgt?.baseLight ?? null;
|
|
13395
|
+
if (light || this.doc.light?.[id]) nextLight.set(id, light);
|
|
13396
|
+
const thick = out.thick ?? tgt?.baseThick ?? null;
|
|
13397
|
+
if (thick) nextThick.set(id, thick);
|
|
13398
|
+
}
|
|
13399
|
+
this.blastLayers = nextLayers;
|
|
13400
|
+
this.animatedLight = nextLight;
|
|
13401
|
+
this.animatedThick = nextThick;
|
|
13402
|
+
this.emit(anim.dirty ? [anim.dirty] : "all");
|
|
13403
|
+
return true;
|
|
13404
|
+
}
|
|
13405
|
+
clearBlastAnimation(emitDirty = false) {
|
|
13406
|
+
const anim = this.activeBlastAnim;
|
|
13407
|
+
if (!anim) return;
|
|
13408
|
+
this.activeBlastAnim = null;
|
|
13409
|
+
this.blastLayers.clear();
|
|
13410
|
+
this.animatedLight.clear();
|
|
13411
|
+
this.animatedThick.clear();
|
|
13412
|
+
if (emitDirty) {
|
|
13413
|
+
this.emit(anim.dirty ? [anim.dirty] : "all");
|
|
13414
|
+
}
|
|
13415
|
+
}
|
|
12387
13416
|
/**
|
|
12388
13417
|
* Runs `fn` with the painting as it is `time` ms into its live effects (see `getAnimation`): for
|
|
12389
13418
|
* exports at a moment. Nothing in the document changes.
|
|
@@ -12815,7 +13844,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12815
13844
|
let wet;
|
|
12816
13845
|
for (const s of cleanedStrokes) {
|
|
12817
13846
|
const r = this.paintStroke(s);
|
|
12818
|
-
bounds =
|
|
13847
|
+
bounds = unionRect2(bounds, r.bounds);
|
|
12819
13848
|
dabs += r.dabs;
|
|
12820
13849
|
if (r.strokeId) strokeIds.push(r.strokeId);
|
|
12821
13850
|
if (r.wet !== void 0) wet = r.wet;
|
|
@@ -12989,7 +14018,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
12989
14018
|
points: stroke.points.map((p) => ({ ...p })),
|
|
12990
14019
|
bounds: strokeRect ? { ...strokeRect } : null
|
|
12991
14020
|
});
|
|
12992
|
-
entry.bounds =
|
|
14021
|
+
entry.bounds = unionRect2(entry.bounds, strokeRect);
|
|
12993
14022
|
this.cached = null;
|
|
12994
14023
|
}
|
|
12995
14024
|
holdMutationEvaporation(layerId) {
|
|
@@ -13058,7 +14087,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
13058
14087
|
strokes: doc.strokes.filter((s) => s.layerId !== tempLayerId)
|
|
13059
14088
|
};
|
|
13060
14089
|
}
|
|
13061
|
-
const rect =
|
|
14090
|
+
const rect = unionRect2(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);
|
|
13062
14091
|
let surface = below.surface;
|
|
13063
14092
|
let offset = below.offset;
|
|
13064
14093
|
if (rect) {
|
|
@@ -13122,7 +14151,7 @@ var PaintEditorController = class _PaintEditorController {
|
|
|
13122
14151
|
this.releaseMutationEvaporation(m.layerId);
|
|
13123
14152
|
}
|
|
13124
14153
|
} else {
|
|
13125
|
-
m.bounds = m.strokes.reduce((acc, s) =>
|
|
14154
|
+
m.bounds = m.strokes.reduce((acc, s) => unionRect2(acc, s.bounds), null);
|
|
13126
14155
|
kept.push(m);
|
|
13127
14156
|
}
|
|
13128
14157
|
}
|
|
@@ -13715,7 +14744,7 @@ var PaintDirector = class {
|
|
|
13715
14744
|
if (key && this.cancelled.has(key)) throw new PaintEditorError("The mutation was cancelled.");
|
|
13716
14745
|
const r = await this.stroke(id, s, within, key);
|
|
13717
14746
|
layerId = r.layerId;
|
|
13718
|
-
bounds =
|
|
14747
|
+
bounds = unionRect3(bounds, r.bounds);
|
|
13719
14748
|
dabs += r.dabs;
|
|
13720
14749
|
if (r.strokeId) strokeIds.push(r.strokeId);
|
|
13721
14750
|
last = r;
|
|
@@ -13731,7 +14760,7 @@ var PaintDirector = class {
|
|
|
13731
14760
|
});
|
|
13732
14761
|
}
|
|
13733
14762
|
};
|
|
13734
|
-
function
|
|
14763
|
+
function unionRect3(a, b) {
|
|
13735
14764
|
if (!a) return b ? { ...b } : null;
|
|
13736
14765
|
if (!b) return { ...a };
|
|
13737
14766
|
const x = Math.min(a.x, b.x);
|
|
@@ -15982,7 +17011,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
|
|
|
15982
17011
|
add({
|
|
15983
17012
|
name: "paint_stroke",
|
|
15984
17013
|
title: "Paint a brush stroke",
|
|
15985
|
-
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer of the canvas with a library brush or the active one, optionally changed (by facets) for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer\u2019s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet; with light.amount, as glow-wash has, their light flows with the paint), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks,
|
|
17014
|
+
description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer of the canvas with a library brush or the active one, optionally changed (by facets) for this stroke. Grain-responsive brushes follow the paper; the mixer engine (mixer-brush) picks up and mixes paint already on the layer, the blender mixes nearby colours, the smudge drags paint, and dab brushes with behavior.pickup lift and carry paint, and wet brushes (engine wet: watercolour, gouache, add water, dry, blow, wet blend, wet smudge, blot) paint on the layer\u2019s wet layer, where the paint keeps moving until it dries (the result says wet: true while it is; see advance_wet_paint and dry_paint; a dry brush on wet paint dries it first; wet oil brushes blend into the wet oil they meet; with light.amount, as glow-wash has, their light flows with the paint), impasto brushes (engine impasto: impasto brush, paint tube, palette knife, roller, gloop pen, metallic paint) lay thick paint whose relief is lit by set_lighting, particle and bristle brushes draw procedural marks, sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers), and antimatter brushes (engine antimatter: antimatter, antimatter-flare, antimatter-charge) detonate where they touch paint across every visible layer, inverting colours in the core, removing alpha, scorching a soot ring, denting thick relief, clearing core light, and throwing sparks on the active layer (returning antimatter: { blasts, sparks, fuel, layers, skippedLayers }). Guide stencils make strokes that start near them follow them; mask stencils keep paint inside them. The symmetry guide adds its mirror copies; with a tracing image that samples colours, a stroke without color takes the image colour at its first point. With an agent cursor (a director), the stroke is painted live along the points at the pace of their times. One undo step. Returns the layer id, changed bounds, dab count, the recorded strokeId (for replay_strokes), for wet brushes wet (paint still wet), for antimatter brushes antimatter, and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
|
|
15986
17015
|
inputSchema: {
|
|
15987
17016
|
type: "object",
|
|
15988
17017
|
properties: {
|
|
@@ -17087,6 +18116,7 @@ var PARTICLE_RENDER_LABELS = { dot: "Dots", line: "Lines", sequence: "Beads", ri
|
|
|
17087
18116
|
var RIBBON_PAIR_LABELS = { adjacent: "Neighbours", "first-last": "Outermost" };
|
|
17088
18117
|
var DOT_TEXTURE_ORDER_LABELS = { sequence: "In turn", random: "Random" };
|
|
17089
18118
|
var DOT_ROTATION_LABELS = { follow: "Follow motion", random: "Random", fixed: "Tip angle" };
|
|
18119
|
+
var INVERT_MODE_LABELS = { rgb: "RGB", oklab: "OKLab" };
|
|
17090
18120
|
var STAMP_LABELS = { dot: "Dot", star: "Star", heart: "Heart", leaf: "Leaf", flower: "Flower", confetti: "Confetti", splat: "Splat", sparkle: "Sparkle", ring: "Ring" };
|
|
17091
18121
|
function toggleStickerRef(refs, ref2) {
|
|
17092
18122
|
if (refs.includes(ref2)) return refs.length > 1 ? refs.filter((r) => r !== ref2) : [...refs];
|
|
@@ -17678,7 +18708,7 @@ function uiTables() {
|
|
|
17678
18708
|
brushOptions: BRUSH_OPTION_CONTROLS,
|
|
17679
18709
|
blendMenu: BLEND_MENU_GROUPS,
|
|
17680
18710
|
layerEffects: LAYER_EFFECT_MENU.map(({ kind, label }) => ({ kind, label, controls: layerEffectControls(defaultLayerEffect(kind, kind)) })),
|
|
17681
|
-
sliders: { paper: PAPER_SLIDERS, wet: WET_PARAM_SLIDERS, wetTilt: WET_TILT_SLIDERS, wetSpeed: WET_SPEED_SLIDER, light: LIGHT_SLIDERS, uvLight: UV_LIGHT_SLIDERS, viewTilt: VIEW_TILT_SLIDERS, ghost: GHOST_SLIDERS, mutation: MUTATION_SLIDERS, livePatterns: LIVE_PATTERN_SLIDERS, stroke: STROKE_SLIDERS, sticker: STICKER_SLIDERS, magnet: MAGNET_SLIDERS },
|
|
18711
|
+
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, antimatter: ANTIMATTER_SLIDERS },
|
|
17682
18712
|
pathStrokeTapers: PATH_STROKE_TAPERS,
|
|
17683
18713
|
padSampleSizes: PAD_SAMPLE_SIZES,
|
|
17684
18714
|
wetGpu: WET_GPU_OPTIONS,
|
|
@@ -17692,6 +18722,7 @@ function uiTables() {
|
|
|
17692
18722
|
ribbonPairs: RIBBON_PAIR_LABELS,
|
|
17693
18723
|
dotTextureOrders: DOT_TEXTURE_ORDER_LABELS,
|
|
17694
18724
|
dotRotations: DOT_ROTATION_LABELS,
|
|
18725
|
+
invertModes: INVERT_MODE_LABELS,
|
|
17695
18726
|
stamps: STAMP_LABELS
|
|
17696
18727
|
}
|
|
17697
18728
|
};
|
|
@@ -17739,6 +18770,8 @@ function createPaintConformanceHost(setup, options) {
|
|
|
17739
18770
|
};
|
|
17740
18771
|
}
|
|
17741
18772
|
export {
|
|
18773
|
+
ANTIMATTER_SLIDERS,
|
|
18774
|
+
AntimatterStrokeDriver,
|
|
17742
18775
|
BACKLIGHT_SPILL,
|
|
17743
18776
|
BLEND_MENU_GROUPS,
|
|
17744
18777
|
BLEND_MODES,
|
|
@@ -17749,6 +18782,7 @@ export {
|
|
|
17749
18782
|
BristleStroke,
|
|
17750
18783
|
BrushSourceArtCache,
|
|
17751
18784
|
DAB_FLIPS,
|
|
18785
|
+
DEFAULT_ANTIMATTER,
|
|
17752
18786
|
DEFAULT_BACKLIGHT_COLOR,
|
|
17753
18787
|
DEFAULT_BEHAVIOR,
|
|
17754
18788
|
DEFAULT_BRISTLE,
|
|
@@ -17813,6 +18847,8 @@ export {
|
|
|
17813
18847
|
IDENTITY_COPY,
|
|
17814
18848
|
IMPASTO_TOOLS,
|
|
17815
18849
|
IMPASTO_TOOL_LABELS,
|
|
18850
|
+
INVERT_MODES,
|
|
18851
|
+
INVERT_MODE_LABELS,
|
|
17816
18852
|
IRIDESCENCE_SLIDERS,
|
|
17817
18853
|
LIGHT_SLIDERS,
|
|
17818
18854
|
LIVE_PATTERN_KINDS,
|
|
@@ -17940,6 +18976,7 @@ export {
|
|
|
17940
18976
|
applyAffine,
|
|
17941
18977
|
backlightOf,
|
|
17942
18978
|
bakeFerroIntoThick,
|
|
18979
|
+
blastNoise2D,
|
|
17943
18980
|
brushEngineControls,
|
|
17944
18981
|
brushLoadColor,
|
|
17945
18982
|
brushLoadForStroke,
|
|
@@ -18029,6 +19066,7 @@ export {
|
|
|
18029
19066
|
hasThickChannels,
|
|
18030
19067
|
hash2,
|
|
18031
19068
|
heatFromJSON,
|
|
19069
|
+
heatProfile,
|
|
18032
19070
|
heatStats,
|
|
18033
19071
|
heatToJSON,
|
|
18034
19072
|
hexToPaintRgb,
|
|
@@ -18039,6 +19077,7 @@ export {
|
|
|
18039
19077
|
hueToWheel,
|
|
18040
19078
|
inferEffectPathControl,
|
|
18041
19079
|
invertAffine,
|
|
19080
|
+
invertOklab8,
|
|
18042
19081
|
isBlendMode,
|
|
18043
19082
|
isGradientPaint,
|
|
18044
19083
|
isPaintEffectPaint,
|
|
@@ -18070,6 +19109,7 @@ export {
|
|
|
18070
19109
|
mutationBrushRefusal,
|
|
18071
19110
|
newPaintLayerId,
|
|
18072
19111
|
nextPaperEffectVersion,
|
|
19112
|
+
normalizeAntimatter,
|
|
18073
19113
|
normalizeBehavior,
|
|
18074
19114
|
normalizeBristle,
|
|
18075
19115
|
normalizeBrushStroke,
|