@pixerate/editor 0.280.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.
Files changed (66) hide show
  1. package/dist/animation-editor/index.cjs +59 -59
  2. package/dist/animation-editor/index.js +3 -3
  3. package/dist/brushes/index.cjs +76 -71
  4. package/dist/brushes/index.js +13 -8
  5. package/dist/brushes/schema.d.cts +1 -0
  6. package/dist/brushes/schema.d.ts +1 -0
  7. package/dist/brushes/types.d.cts +29 -3
  8. package/dist/brushes/types.d.ts +29 -3
  9. package/dist/{brushstrokeRender-EIK3625T.js → brushstrokeRender-4MA5CBW3.js} +3 -3
  10. package/dist/{brushstrokeRender-7HOMKZNO.cjs → brushstrokeRender-JVKVKWDA.cjs} +15 -15
  11. package/dist/canvas/edge-animation-tools.d.cts +115 -0
  12. package/dist/canvas/edge-animation-tools.d.ts +115 -0
  13. package/dist/canvas/edgeAnimation.d.cts +164 -0
  14. package/dist/canvas/edgeAnimation.d.ts +164 -0
  15. package/dist/canvas/edgePaths.d.cts +63 -0
  16. package/dist/canvas/edgePaths.d.ts +63 -0
  17. package/dist/canvas/index.cjs +691 -21
  18. package/dist/canvas/index.d.cts +3 -0
  19. package/dist/canvas/index.d.ts +3 -0
  20. package/dist/canvas/index.js +679 -9
  21. package/dist/{chunk-NJDEKU5D.js → chunk-2YGYYVSG.js} +1 -1
  22. package/dist/{chunk-JVFGK3DK.cjs → chunk-3JUKN3RM.cjs} +76 -53
  23. package/dist/{chunk-HN57I7KT.js → chunk-CG3FBNXE.js} +1 -1
  24. package/dist/{chunk-WFSYZO5Z.cjs → chunk-DFZ4I7EZ.cjs} +1 -1
  25. package/dist/{chunk-LZE2QZCT.js → chunk-IHMMHZGP.js} +1 -1
  26. package/dist/{chunk-K4ANWBF7.cjs → chunk-IJQ2YH2M.cjs} +35 -35
  27. package/dist/{chunk-HRUK5TFW.cjs → chunk-IUKPQCYQ.cjs} +164 -71
  28. package/dist/{chunk-WCC4NXWY.cjs → chunk-JD63B33V.cjs} +86 -86
  29. package/dist/{chunk-NJLPW4MM.js → chunk-KV7QHPKL.js} +27 -4
  30. package/dist/{chunk-H3EME3S5.cjs → chunk-MRIAR5DY.cjs} +2 -2
  31. package/dist/{chunk-ROURHJYT.js → chunk-NEDS6DSC.js} +1 -1
  32. package/dist/{chunk-FKO3CY2C.js → chunk-ULPXJ3NB.js} +1 -1
  33. package/dist/{chunk-WZYRYYXN.cjs → chunk-UOM5S4W5.cjs} +12 -12
  34. package/dist/{chunk-YOM76H2V.js → chunk-VEX6YVNG.js} +1 -1
  35. package/dist/{chunk-MNHP3AQT.cjs → chunk-WEXWDBUF.cjs} +23 -23
  36. package/dist/{chunk-PPDUCZZV.js → chunk-YRGTQLDN.js} +122 -29
  37. package/dist/image-editor/index.cjs +788 -788
  38. package/dist/image-editor/index.js +4 -4
  39. package/dist/paint-editor/antimatter.d.cts +154 -0
  40. package/dist/paint-editor/antimatter.d.ts +154 -0
  41. package/dist/paint-editor/brush-controls.d.cts +1 -0
  42. package/dist/paint-editor/brush-controls.d.ts +1 -0
  43. package/dist/paint-editor/brush-model.d.cts +9 -5
  44. package/dist/paint-editor/brush-model.d.ts +9 -5
  45. package/dist/paint-editor/controller.d.cts +23 -0
  46. package/dist/paint-editor/controller.d.ts +23 -0
  47. package/dist/paint-editor/index.cjs +1931 -891
  48. package/dist/paint-editor/index.d.cts +1 -0
  49. package/dist/paint-editor/index.d.ts +1 -0
  50. package/dist/paint-editor/index.js +1098 -58
  51. package/dist/paint-editor/mark-settings.d.cts +30 -0
  52. package/dist/paint-editor/mark-settings.d.ts +30 -0
  53. package/dist/paint-editor/stroke.d.cts +16 -0
  54. package/dist/paint-editor/stroke.d.ts +16 -0
  55. package/dist/paint-editor/strokes.d.cts +2 -0
  56. package/dist/paint-editor/strokes.d.ts +2 -0
  57. package/dist/paint-editor/ui.d.cts +4 -2
  58. package/dist/paint-editor/ui.d.ts +4 -2
  59. package/dist/vector/index.cjs +999 -999
  60. package/dist/vector/index.js +4 -4
  61. package/package.json +1 -1
  62. package/skills/pixerate-brushes/SKILL.md +1 -1
  63. package/skills/pixerate-canvas/SKILL.md +25 -2
  64. package/skills/pixerate-paint-editor/SKILL.md +8 -2
  65. package/skills/pixerate-paint-editor/references/antimatter.md +49 -0
  66. 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-NJLPW4MM.js";
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-ROURHJYT.js";
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-NJDEKU5D.js";
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-YOM76H2V.js";
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-PPDUCZZV.js";
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 sameTile(a, b) {
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.sticker) {
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(this.dry ? this.dry.push(points) : this.thickDriver ? this.thickDriver.push(points) : this.stickerSpray ? this.stickerSpray.push(points) : this.wet.push(points));
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 (!sameTile(prev, tile)) changed = true;
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 unionRect(a, b) {
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 = unionRect(unionRect(contentBounds(l.surface), this.doc.fluor?.[l.id] ? contentBounds(this.doc.fluor[l.id]) : null), this.doc.ferro?.[l.id]?.bounds ?? null);
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 = unionRect(bounds, this.describeLayer(c, l.id, depth + 1).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 = unionRect(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);
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 && brush.engine !== "sticker" && (brush.light.amount > 0 || doc.light?.[layer.id]) ? { surface: doc.light?.[layer.id] ?? emptyLight() } : null,
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 && brush.engine !== "sticker" && (brush.ferro.amount > 0 || doc.ferro?.[layer.id]) ? { surface: doc.ferro?.[layer.id] ?? emptyFerro() } : null,
9555
- fluor: brush.engine !== "sticker" && (brush.fluorescence.amount > 0 || doc.fluor?.[layer.id]) ? { surface: doc.fluor?.[layer.id] ?? emptyFluor() } : null,
9556
- thermo: brush.engine !== "sticker" && (brush.thermochromic.hotColor !== null || doc.thermo?.[layer.id]) ? { surface: doc.thermo?.[layer.id] ?? emptyThermo() } : null,
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
- if (wetStroke) this.wet.restore(snap);
9647
- this.doc = { ...this.doc, layers: before.layers };
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 = unionRect(bounds, last.result.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 run = this.runStroke(doc, { layerId, color: options.color ?? s.color, brush, seed: s.seed, copies: v.copies ?? [], stencils: false, maskStencils: [], ...paint, ...load && options.brushId === void 0 ? { load } : {} }, v.points);
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 = unionRect(bounds, run.result.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 ids = /* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(this.doc.thick ?? {})]);
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 glowed = cache.glow(l.surface, light[id], this.liveThick?.layerId === id ? this.liveThick.channels : this.doc.thick?.[id]);
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 = light[id] && isPixelLayer(committed) ? cache.spill(committed.surface, light[id], this.lightSpillOf(id)) : null;
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 && this.liveThick?.layerId === l.id ? this.liveThick.channels : this.doc.thick?.[l.id];
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
- this.viewCompositor.compositeTiles(this.renderLayers({ paper: options.paper }, true), rect, this.sources(), onTile, { dirty, checkpointLayerId: this.live?.layerId ?? this.doc.activeLayerId });
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: unionRect(this.strokeArea(was), this.strokeArea(next)) ?? { x: 0, y: 0, width: 0, height: 0 } };
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: unionRect(this.strokeArea(was), this.strokeArea(next)) ?? { x: 0, y: 0, width: 0, height: 0 } };
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 = unionRect(bounds, this.strokeArea(x));
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 ? unionRect(had, result.bounds) : result.bounds ?? had ?? null);
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 [id, a] of this.animatedLayers) {
12272
- const l = findLayer(layers, id)?.layer;
12273
- if (isPixelLayer(l) && l.surface === a.base) layers = mapLayer(layers, id, () => ({ ...l, surface: a.surface }));
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 = unionRect(bounds, r.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 = unionRect(entry.bounds, strokeRect);
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 = unionRect(this.describeLayer(below).bounds, this.describeLayer(upper).bounds);
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) => unionRect(acc, s.bounds), null);
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 = unionRect2(bounds, r.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 unionRect2(a, b) {
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, and sticker brushes spray editable stickers onto an empty or sticker layer (list_stickers). 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) and, for brushes that pick up paint, mixerLoad: what the brush carries after the stroke.",
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,