@pixerate/editor 0.271.0 → 0.272.1

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 (96) hide show
  1. package/dist/agent/index.cjs +34 -32
  2. package/dist/agent/index.js +10 -8
  3. package/dist/animation-editor/index.cjs +60 -60
  4. package/dist/animation-editor/index.js +4 -4
  5. package/dist/brushes/index.cjs +69 -66
  6. package/dist/brushes/index.js +6 -3
  7. package/dist/brushes/patch.d.cts +1 -1
  8. package/dist/brushes/patch.d.ts +1 -1
  9. package/dist/brushes/schema.d.cts +1 -0
  10. package/dist/brushes/schema.d.ts +1 -0
  11. package/dist/brushes/types.d.cts +8 -0
  12. package/dist/brushes/types.d.ts +8 -0
  13. package/dist/{brushstrokeRender-WR2N5HHY.cjs → brushstrokeRender-5YBCLFDQ.cjs} +15 -15
  14. package/dist/{brushstrokeRender-A727TTNQ.js → brushstrokeRender-DZGFU3J4.js} +3 -3
  15. package/dist/canvas/index.cjs +13 -13
  16. package/dist/canvas/index.js +1 -1
  17. package/dist/chart/contract.d.cts +95 -0
  18. package/dist/chart/contract.d.ts +95 -0
  19. package/dist/chart/controller.d.cts +4 -0
  20. package/dist/chart/controller.d.ts +4 -0
  21. package/dist/chart/index.cjs +492 -26
  22. package/dist/chart/index.d.cts +1 -0
  23. package/dist/chart/index.d.ts +1 -0
  24. package/dist/chart/index.js +476 -10
  25. package/dist/chart/tools.d.cts +3 -0
  26. package/dist/chart/tools.d.ts +3 -0
  27. package/dist/chart-maps/index.cjs +4 -64
  28. package/dist/chart-maps/index.js +3 -63
  29. package/dist/{chunk-YNFSH72F.js → chunk-2F3YDXCN.js} +1 -1
  30. package/dist/{chunk-QYXUE37Q.js → chunk-2XDNF73E.js} +1 -1
  31. package/dist/{chunk-3KNQ2W6X.cjs → chunk-3JKCREEU.cjs} +59 -53
  32. package/dist/{chunk-NMZQMPFZ.js → chunk-4LZQO5Y7.js} +2 -0
  33. package/dist/{chunk-B5W677L7.js → chunk-5HP2VSAS.js} +1 -1
  34. package/dist/chunk-5UGRMVNE.js +328 -0
  35. package/dist/{chunk-X4FTNFIA.js → chunk-7MGPW2OV.js} +1 -1
  36. package/dist/{chunk-BPJSI4NT.cjs → chunk-7W2EQQBP.cjs} +2 -2
  37. package/dist/{chunk-DGITFN73.cjs → chunk-BWJBZAUE.cjs} +8 -8
  38. package/dist/chunk-CHZYVNXZ.js +336 -0
  39. package/dist/{chunk-LETF75CA.js → chunk-CLB2I2FU.js} +1 -1
  40. package/dist/chunk-DRMZ5R3S.cjs +67 -0
  41. package/dist/{chunk-BAZMCFVL.js → chunk-DZVRH63Y.js} +1 -1
  42. package/dist/chunk-EI5GFTKG.js +67 -0
  43. package/dist/{chunk-U2ASNGO6.cjs → chunk-FXVCB6YG.cjs} +35 -35
  44. package/dist/{chunk-EU2ZZA6L.cjs → chunk-GHGBIAWQ.cjs} +126 -39
  45. package/dist/chunk-JV7DKLMX.js +1073 -0
  46. package/dist/{chunk-2WOGXEDX.cjs → chunk-M3RZOF3G.cjs} +1 -1
  47. package/dist/chunk-MFBUQC67.cjs +328 -0
  48. package/dist/{chunk-GSTSZPSR.js → chunk-N3SKPQEH.js} +225 -18
  49. package/dist/{chunk-J5VQYK7T.cjs → chunk-N5IMDAHM.cjs} +251 -44
  50. package/dist/{chunk-ININNHLM.js → chunk-NAHJV3O6.js} +11 -5
  51. package/dist/{chunk-IBXEQOGI.js → chunk-PYHB2EHE.js} +2 -324
  52. package/dist/chunk-QEUI6OGE.cjs +336 -0
  53. package/dist/{chunk-6ZSFRRJI.js → chunk-RSF6BT34.js} +1 -1
  54. package/dist/{chunk-JJ2H25JG.cjs → chunk-SNAFFCAX.cjs} +200 -174
  55. package/dist/chunk-T2HUQBRC.cjs +1073 -0
  56. package/dist/{chunk-M2ZVPU7L.cjs → chunk-TNLKFW7D.cjs} +23 -23
  57. package/dist/{chunk-KZI3YWKW.js → chunk-UXYCY3HP.js} +93 -6
  58. package/dist/{chunk-GSLOH2IN.cjs → chunk-VF6ODSYX.cjs} +2 -0
  59. package/dist/{chunk-NQUKTRKN.cjs → chunk-WZLNNGG3.cjs} +45 -45
  60. package/dist/{chunk-NRBGAEFI.cjs → chunk-Y3RUYOOW.cjs} +8 -330
  61. package/dist/{chunk-DDL3ZBHK.cjs → chunk-YJANF2BL.cjs} +86 -86
  62. package/dist/{chunk-TNHDCP4R.js → chunk-ZEI7ZEA4.js} +31 -5
  63. package/dist/commands/index.cjs +7 -323
  64. package/dist/commands/index.js +5 -321
  65. package/dist/extensions/index.cjs +4 -4
  66. package/dist/extensions/index.js +3 -3
  67. package/dist/image-editor/index.cjs +789 -789
  68. package/dist/image-editor/index.js +5 -5
  69. package/dist/index.cjs +9 -9
  70. package/dist/index.js +3 -3
  71. package/dist/paint/index.cjs +2 -2
  72. package/dist/paint/index.js +1 -1
  73. package/dist/paint/wet/world.d.cts +37 -1
  74. package/dist/paint/wet/world.d.ts +37 -1
  75. package/dist/paint-editor/brush-controls.d.cts +2 -0
  76. package/dist/paint-editor/brush-controls.d.ts +2 -0
  77. package/dist/paint-editor/brush-model.d.cts +14 -1
  78. package/dist/paint-editor/brush-model.d.ts +14 -1
  79. package/dist/paint-editor/index.cjs +343 -306
  80. package/dist/paint-editor/index.js +48 -11
  81. package/dist/paint-editor/ui.d.cts +1 -1
  82. package/dist/paint-editor/ui.d.ts +1 -1
  83. package/dist/paint-editor/wet.d.cts +8 -1
  84. package/dist/paint-editor/wet.d.ts +8 -1
  85. package/dist/persistence/index.cjs +24 -1042
  86. package/dist/persistence/index.js +22 -1040
  87. package/dist/tools/index.cjs +5 -3
  88. package/dist/tools/index.js +8 -6
  89. package/dist/vector/index.cjs +999 -999
  90. package/dist/vector/index.js +5 -5
  91. package/package.json +1 -1
  92. package/skills/pixerate-brushes/SKILL.md +1 -0
  93. package/skills/pixerate-paint-editor/SKILL.md +4 -11
  94. package/skills/pixerate-paint-editor/references/brush-facets.md +1 -0
  95. package/skills/pixerate-paint-editor/references/ghost.md +47 -0
  96. package/skills/pixerate-paint-watercolour/SKILL.md +1 -0
@@ -849,9 +849,13 @@ var SLOT_FLOATS = SLOT_PLANES * PLANE;
849
849
  var S2 = SLOT_SIDE;
850
850
  var SNAP_PLANES = SET_PLANES + MASS_CHANNELS;
851
851
  var noLight = () => new TiledSurface(1, /* @__PURE__ */ new Map(), 0);
852
- function newLane(id, index, params, flow, flowStrength, base, light) {
852
+ function cloneGhost(g) {
853
+ return g ? { pigment: [...g.pigment], remaining: g.remaining, total: g.total, linger: g.linger } : null;
854
+ }
855
+ function newLane(id, index, params, flow, flowStrength, base, light, ghost) {
853
856
  const l = light ?? noLight();
854
- return { id, index, params, flow, flowStrength, base, shaded: /* @__PURE__ */ new Map(), urgent: /* @__PURE__ */ new Set(), stamp: 1, display: null, shadedVersion: 0, displayKey: "", light: l, lit: l.tiles.size > 0, lightShaded: /* @__PURE__ */ new Map(), lightDisplay: null, lightKey: "" };
857
+ const g = cloneGhost(ghost);
858
+ return { id, index, params, flow, flowStrength, base, shaded: /* @__PURE__ */ new Map(), urgent: /* @__PURE__ */ new Set(), stamp: 1, display: null, shadedVersion: 0, displayKey: "", light: l, lit: !g && l.tiles.size > 0, ghost: g, lightShaded: /* @__PURE__ */ new Map(), lightDisplay: null, lightKey: "" };
855
859
  }
856
860
  var WetWorldError = class extends Error {
857
861
  constructor(message) {
@@ -1012,11 +1016,12 @@ var WetWorld = class {
1012
1016
  if (options.params) this.setParams(id, options.params);
1013
1017
  if (options.base) this.setBase(id, options.base);
1014
1018
  if (options.light) this.setLight(id, options.light);
1019
+ if (options.ghost !== void 0) this.setGhost(id, options.ghost);
1015
1020
  return;
1016
1021
  }
1017
1022
  const index = this.lanes.indexOf(null);
1018
1023
  if (index < 0) throw new WetWorldError(`At most ${MAX_WET_LANES} layers can hold wet paint at once; dry one (dry_paint) first.`);
1019
- this.lanes[index] = newLane(id, index, normalizeWetParams(options.params ?? {}, DEFAULT_WET_PARAMS), options.flow ?? null, options.flow ? options.flowStrength ?? 1 : 0, options.base ?? new TiledSurface(4, /* @__PURE__ */ new Map(), 0), options.light);
1024
+ this.lanes[index] = newLane(id, index, normalizeWetParams(options.params ?? {}, DEFAULT_WET_PARAMS), options.flow ?? null, options.flow ? options.flowStrength ?? 1 : 0, options.base ?? new TiledSurface(4, /* @__PURE__ */ new Map(), 0), options.light, options.ghost);
1020
1025
  this.pack(this.lanes[index]);
1021
1026
  }
1022
1027
  /** Drops a wet layer and its wet paint (nothing is baked). */
@@ -1089,17 +1094,46 @@ var WetWorld = class {
1089
1094
  }
1090
1095
  /** Whether a layer has light paint, under its wet paint or in it. */
1091
1096
  isLit(id) {
1092
- return this.laneOf(id).lit;
1097
+ const l = this.laneOf(id);
1098
+ return l.lit && !l.ghost;
1093
1099
  }
1094
1100
  /** Replaces the light paint under a layer's wet paint. */
1095
1101
  setLight(id, light) {
1096
1102
  const lane = this.laneOf(id);
1097
1103
  if (light === lane.light) return;
1098
1104
  lane.light = light;
1099
- if (light.tiles.size) lane.lit = true;
1105
+ if (light.tiles.size && !lane.ghost) lane.lit = true;
1100
1106
  lane.lightShaded.clear();
1101
1107
  lane.stamp++;
1102
1108
  }
1109
+ /** Active ghost paint state on the layer, or `null`. */
1110
+ getGhost(id) {
1111
+ return cloneGhost(this.laneOf(id).ghost);
1112
+ }
1113
+ /** Sets or clears a layer's active ghost paint state. */
1114
+ setGhost(id, ghost) {
1115
+ this.requireIdle();
1116
+ const lane = this.laneOf(id);
1117
+ lane.ghost = cloneGhost(ghost);
1118
+ if (lane.ghost) lane.lit = false;
1119
+ }
1120
+ /**
1121
+ * Starts a ghost paint fade on `id` with `pigment` per unit tracer and `linger` seconds. If an
1122
+ * earlier ghost of a different colour is still fading on the layer, it finishes at once first.
1123
+ */
1124
+ startGhost(id, pigment, linger, stepsPerSecond = 120) {
1125
+ this.requireIdle();
1126
+ const lane = this.laneOf(id);
1127
+ const p = [Math.fround(pigment[0]), Math.fround(pigment[1]), Math.fround(pigment[2]), Math.fround(pigment[3])];
1128
+ if (lane.ghost) {
1129
+ const g = lane.ghost.pigment;
1130
+ const same = Math.abs(g[0] - p[0]) <= 1e-6 && Math.abs(g[1] - p[1]) <= 1e-6 && Math.abs(g[2] - p[2]) <= 1e-6 && Math.abs(g[3] - p[3]) <= 1e-6;
1131
+ if (!same) this.fadeGhost(id, 1);
1132
+ }
1133
+ const total = Math.max(1, Math.round(linger * stepsPerSecond / FREE_INTERVAL));
1134
+ lane.ghost = { pigment: p, remaining: total, total, linger };
1135
+ lane.lit = false;
1136
+ }
1103
1137
  pack(lane) {
1104
1138
  packWetParams(lane.params, this.paper ?? DEFAULT_WET_PAPER, this.tilt, lane.flowStrength, this.prm);
1105
1139
  this.f32.set(this.prm, this.layout.params + lane.index * PARAM_FLOATS * 4 >> 2);
@@ -1332,6 +1366,13 @@ var WetWorld = class {
1332
1366
  let woke = false;
1333
1367
  for (const key of wakeups) woke = this.activate(key) || woke;
1334
1368
  if (this.clock % FREE_INTERVAL === 0) {
1369
+ for (const lane of this.lanes) {
1370
+ if (!lane?.ghost) continue;
1371
+ const g = lane.ghost;
1372
+ const amount = g.remaining <= 1 ? 1 : 1 / g.remaining;
1373
+ g.remaining = Math.max(0, g.remaining - 1);
1374
+ this.fadeGhost(lane.id, amount);
1375
+ }
1335
1376
  const dead = this.active.filter((key) => {
1336
1377
  if (this.u32[lastBase + key] & FLAG_ALIVE) return false;
1337
1378
  const laneBase = this.laneIndexOf(key) * perLane;
@@ -1531,7 +1572,10 @@ var WetWorld = class {
1531
1572
  paint(id, rect, cov, water, pigment, field = null, light = 0) {
1532
1573
  if (!(water >= 0)) throw new WetWorldError(`water ${water} must be 0 or more.`);
1533
1574
  if (!(light >= 0)) throw new WetWorldError(`light ${light} must be 0 or more.`);
1534
- if (light > 0) this.laneOf(id).lit = true;
1575
+ if (light > 0) {
1576
+ const l = this.laneOf(id);
1577
+ if (!l.ghost) l.lit = true;
1578
+ }
1535
1579
  this.footprint(id, rect, cov, (c, w, x, y) => {
1536
1580
  const dw = Math.fround(water * w);
1537
1581
  if (dw > 0) this.addWaterAt(c, dw);
@@ -1683,12 +1727,12 @@ var WetWorld = class {
1683
1727
  * the paper as settled pigment, so water can move it again, and the same share of its light
1684
1728
  * paint (`light`) as settled light. Only where there is surface water.
1685
1729
  */
1686
- rewet(id, rect, cov, amount, paper) {
1730
+ rewet(id, rect, cov, amount, paper, suspend = false) {
1687
1731
  const lane = this.laneOf(id);
1688
1732
  const pig = [0, 0, 0, 0];
1689
1733
  const tiles = new Map(lane.base.tiles);
1690
1734
  const written = /* @__PURE__ */ new Set();
1691
- const lightTiles = lane.lit ? new Map(lane.light.tiles) : null;
1735
+ const lightTiles = lane.lit && !lane.ghost ? new Map(lane.light.tiles) : null;
1692
1736
  const lightWritten = /* @__PURE__ */ new Set();
1693
1737
  const scratch2 = new Uint8ClampedArray(4);
1694
1738
  this.footprint(
@@ -1730,8 +1774,8 @@ var WetWorld = class {
1730
1774
  const clear = new Uint8ClampedArray([0, 0, 0, 0]);
1731
1775
  filmPixel(pig[0] * (1 - a), pig[1] * (1 - a), pig[2] * (1 - a), 0, clear, 0, paper, 0, scratch2, 0);
1732
1776
  tile.data.set(scratch2, i);
1733
- const d = this.single(c) + P_D0 * PLANE;
1734
- for (let q = 0; q < 3; q++) this.f32[d + q * PLANE] += Math.fround(pig[q] * a);
1777
+ const into = suspend ? c + P_P0 * PLANE : this.single(c) + P_D0 * PLANE;
1778
+ for (let q = 0; q < 3; q++) this.f32[into + q * PLANE] += Math.fround(pig[q] * a);
1735
1779
  },
1736
1780
  false
1737
1781
  );
@@ -1752,6 +1796,114 @@ var WetWorld = class {
1752
1796
  this.setLight(id, new TiledSurface(1, lightTiles, 0));
1753
1797
  }
1754
1798
  }
1799
+ /**
1800
+ * Subtracts `amount` (0..1) of a ghost lane's remaining tracer (`P_L`, `P_DL`) and proportional
1801
+ * ghost pigment (`lane.ghost.pigment`) across all live tiles of `id`. Speeds up in cells that
1802
+ * are nearly dry so drying paint never pops. Clears the lane's `ghost` state once no tracer remains.
1803
+ */
1804
+ fadeGhost(id, amount) {
1805
+ this.requireIdle();
1806
+ const lane = this.laneOf(id);
1807
+ if (!lane.ghost) return;
1808
+ const keys = this.active.filter((k) => this.laneIndexOf(k) === lane.index);
1809
+ let anyRemaining = false;
1810
+ for (const key of keys) {
1811
+ if (this.fadeGhostKey(key, lane, amount)) anyRemaining = true;
1812
+ }
1813
+ if (!anyRemaining || amount >= 1 - 1e-6) lane.ghost = null;
1814
+ this.refreshHalos();
1815
+ }
1816
+ /** Whether any live tile of `lane` still holds ghost tracer (`P_L > 0` or `P_DL > 0`). */
1817
+ hasGhostTracer(lane) {
1818
+ const f = this.f32;
1819
+ for (const key of this.active) {
1820
+ if (this.laneIndexOf(key) !== lane.index) continue;
1821
+ const b = this.slotBase(this.slotOf(key));
1822
+ const cur = b + this.parity * SET_PLANES * PLANE;
1823
+ for (let y = 1; y <= WET_TILE; y++) {
1824
+ for (let x = 1; x <= WET_TILE; x++) {
1825
+ const c = y * S2 + x;
1826
+ if (f[cur + P_L * PLANE + c] > 0 || f[b + P_DL * PLANE + c] > 0) return true;
1827
+ }
1828
+ }
1829
+ }
1830
+ return false;
1831
+ }
1832
+ /**
1833
+ * Fades ghost tracer and pigment in one slot by `amount` (0..1). Returns whether any tracer
1834
+ * remains in the slot.
1835
+ */
1836
+ fadeGhostKey(key, lane, amount) {
1837
+ if (!lane.ghost) return false;
1838
+ const pig = lane.ghost.pigment;
1839
+ const slot = this.slotOf(key);
1840
+ if (slot < 0) return false;
1841
+ const b = this.slotBase(slot);
1842
+ const cur = b + this.parity * SET_PLANES * PLANE;
1843
+ const f = this.f32;
1844
+ const baseAmt = Math.min(1, Math.max(0, amount));
1845
+ let remainingTracer = false;
1846
+ let yMin = WET_TILE + 1;
1847
+ let yMax = 0;
1848
+ for (let y = 1; y <= WET_TILE; y++) {
1849
+ for (let x = 1; x <= WET_TILE; x++) {
1850
+ const c = y * S2 + x;
1851
+ const tl = f[cur + P_L * PLANE + c];
1852
+ const dl = f[b + P_DL * PLANE + c];
1853
+ if (!(tl > 0 || dl > 0)) continue;
1854
+ const water = f[cur + P_RHO * PLANE + c] + f[cur + P_SW * PLANE + c];
1855
+ const dryFade = water <= DRY_EPS ? 1 : water < 0.02 ? (1 - water / 0.02) * 0.5 : 0;
1856
+ const a = baseAmt >= 1 - 1e-6 ? 1 : Math.min(1, Math.max(baseAmt, dryFade));
1857
+ if (!(a > 0)) {
1858
+ remainingTracer = true;
1859
+ continue;
1860
+ }
1861
+ if (y < yMin) yMin = y;
1862
+ if (y > yMax) yMax = y;
1863
+ if (tl > 0) {
1864
+ const done = a >= 1 || tl * (1 - a) <= 1e-7;
1865
+ const dt = done ? tl : Math.fround(tl * a);
1866
+ const tNew = done ? 0 : Math.fround(tl - dt);
1867
+ f[cur + P_L * PLANE + c] = tNew;
1868
+ for (let q = 0; q < 4; q++) {
1869
+ const idx = cur + (P_P0 + q) * PLANE + c;
1870
+ const eps = Math.max(1e-3, pig[q] * 2e-4);
1871
+ const v = Math.max(0, Math.fround(f[idx] - Math.fround(dt * pig[q])));
1872
+ f[idx] = tNew === 0 && v <= eps ? 0 : v;
1873
+ }
1874
+ if (tNew > 0) remainingTracer = true;
1875
+ }
1876
+ if (dl > 0) {
1877
+ const done = a >= 1 || dl * (1 - a) <= 1e-7;
1878
+ const dd = done ? dl : Math.fround(dl * a);
1879
+ const dNew = done ? 0 : Math.fround(dl - dd);
1880
+ f[b + P_DL * PLANE + c] = dNew;
1881
+ for (let q = 0; q < 4; q++) {
1882
+ const idx = b + (P_D0 + q) * PLANE + c;
1883
+ const eps = Math.max(1e-3, pig[q] * 2e-4);
1884
+ const v = Math.max(0, Math.fround(f[idx] - Math.fround(dd * pig[q])));
1885
+ f[idx] = dNew === 0 && v <= eps ? 0 : v;
1886
+ }
1887
+ if (dNew > 0) remainingTracer = true;
1888
+ }
1889
+ if (f[cur + P_L * PLANE + c] === 0 && f[b + P_DL * PLANE + c] === 0) {
1890
+ for (let q = 0; q < 4; q++) {
1891
+ const eps = Math.max(1e-3, pig[q] * 2e-4);
1892
+ const pi = cur + (P_P0 + q) * PLANE + c;
1893
+ if (f[pi] <= eps) f[pi] = 0;
1894
+ const di = b + (P_D0 + q) * PLANE + c;
1895
+ if (f[di] <= eps) f[di] = 0;
1896
+ }
1897
+ }
1898
+ }
1899
+ }
1900
+ if (yMax >= yMin) {
1901
+ lane.urgent.add(this.tileKeyOf(key));
1902
+ this.gpuDirtySlot(slot, yMin, yMax);
1903
+ lane.stamp++;
1904
+ }
1905
+ return remainingTracer;
1906
+ }
1755
1907
  /**
1756
1908
  * Dries wet paint at once (everything settles where it is, the water goes): in `rect`, or the
1757
1909
  * whole layer. Whole tiles that end up dry are baked into the base and freed. Returns the
@@ -1779,10 +1931,28 @@ var WetWorld = class {
1779
1931
  continue;
1780
1932
  }
1781
1933
  const f = this.f32;
1934
+ const pig = lane.ghost?.pigment ?? null;
1782
1935
  for (let y = y0; y < y1; y++) {
1783
1936
  for (let x = x0; x < x1; x++) {
1784
1937
  const c = this.cell(lane, x, y);
1785
1938
  const d = this.single(c) + P_D0 * PLANE;
1939
+ if (pig) {
1940
+ const tl = f[c + P_L * PLANE];
1941
+ const dl = f[d + 4 * PLANE];
1942
+ if (tl > 0 || dl > 0) {
1943
+ f[c + P_L * PLANE] = 0;
1944
+ f[d + 4 * PLANE] = 0;
1945
+ for (let q = 0; q < 4; q++) {
1946
+ const eps = Math.max(1e-3, pig[q] * 2e-4);
1947
+ const pi = c + (P_P0 + q) * PLANE;
1948
+ const pv = Math.max(0, Math.fround(f[pi] - Math.fround(tl * pig[q])));
1949
+ f[pi] = pv <= eps ? 0 : pv;
1950
+ const di = d + q * PLANE;
1951
+ const dv = Math.max(0, Math.fround(f[di] - Math.fround(dl * pig[q])));
1952
+ f[di] = dv <= eps ? 0 : dv;
1953
+ }
1954
+ }
1955
+ }
1786
1956
  for (let q = 0; q < MASS_CHANNELS; q++) {
1787
1957
  f[d + q * PLANE] += f[c + (P_P0 + q) * PLANE];
1788
1958
  f[c + (P_P0 + q) * PLANE] = 0;
@@ -1797,6 +1967,7 @@ var WetWorld = class {
1797
1967
  bounds = unionRect(bounds, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
1798
1968
  this.markEdited(lane, x0, y0, x1, y1);
1799
1969
  }
1970
+ if (lane.ghost && !this.hasGhostTracer(lane)) lane.ghost = null;
1800
1971
  this.syncList();
1801
1972
  this.refreshHalos();
1802
1973
  lane.stamp++;
@@ -1808,10 +1979,14 @@ var WetWorld = class {
1808
1979
  */
1809
1980
  bakeKey(key, all) {
1810
1981
  const lane = this.lanes[this.laneIndexOf(key)];
1982
+ if (lane.ghost) {
1983
+ this.fadeGhostKey(key, lane, 1);
1984
+ if (!this.hasGhostTracer(lane)) lane.ghost = null;
1985
+ }
1811
1986
  const [tx, ty] = this.tileXY(key);
1812
1987
  const tk = tileKey(tx, ty);
1813
1988
  let lightChanged = false;
1814
- if (lane.lit) {
1989
+ if (lane.lit && !lane.ghost) {
1815
1990
  const lout = this.shadeLightSlot(key, lane.light.tiles.get(tk) ?? null, all ? "bake-all" : "bake");
1816
1991
  if (lout) {
1817
1992
  const tiles2 = new Map(lane.light.tiles);
@@ -1857,7 +2032,8 @@ var WetWorld = class {
1857
2032
  const out = src ? new Uint8ClampedArray(src) : new Uint8ClampedArray(TILE * TILE * 4);
1858
2033
  const w = Math.min(WET_TILE, this.width - tx * WET_TILE);
1859
2034
  const h = Math.min(WET_TILE, this.height - ty * WET_TILE);
1860
- const lit = this.lanes[this.laneIndexOf(key)].lit;
2035
+ const lane = this.lanes[this.laneIndexOf(key)];
2036
+ const lit = lane.lit && !lane.ghost;
1861
2037
  const changed = shadeTile(
1862
2038
  this.f32,
1863
2039
  PLANE,
@@ -1885,7 +2061,38 @@ var WetWorld = class {
1885
2061
  for (const key of this.active) {
1886
2062
  if (this.laneIndexOf(key) !== lane.index) continue;
1887
2063
  const tk = this.tileKeyOf(key);
1888
- const out = this.shadeSlot(key, lane.base.tiles.get(tk)?.data ?? null, "bake-all");
2064
+ let out;
2065
+ if (lane.ghost) {
2066
+ const b = this.slotBase(this.slotOf(key));
2067
+ const cur = b + this.parity * SET_PLANES * PLANE;
2068
+ const savedP = this.f32.slice(cur + P_P0 * PLANE, cur + (P_P0 + MASS_CHANNELS) * PLANE);
2069
+ const savedD = this.f32.slice(b + P_D0 * PLANE, b + (P_D0 + MASS_CHANNELS) * PLANE);
2070
+ const pig = lane.ghost.pigment;
2071
+ for (let y = 1; y <= WET_TILE; y++) {
2072
+ for (let x = 1; x <= WET_TILE; x++) {
2073
+ const c = y * S2 + x;
2074
+ const tl = this.f32[cur + P_L * PLANE + c];
2075
+ const dl = this.f32[b + P_DL * PLANE + c];
2076
+ if (!(tl > 0 || dl > 0)) continue;
2077
+ this.f32[cur + P_L * PLANE + c] = 0;
2078
+ this.f32[b + P_DL * PLANE + c] = 0;
2079
+ for (let q = 0; q < 4; q++) {
2080
+ const eps = Math.max(1e-3, pig[q] * 2e-4);
2081
+ const pi = cur + (P_P0 + q) * PLANE + c;
2082
+ const pv = Math.max(0, Math.fround(this.f32[pi] - Math.fround(tl * pig[q])));
2083
+ this.f32[pi] = pv <= eps ? 0 : pv;
2084
+ const di = b + (P_D0 + q) * PLANE + c;
2085
+ const dv = Math.max(0, Math.fround(this.f32[di] - Math.fround(dl * pig[q])));
2086
+ this.f32[di] = dv <= eps ? 0 : dv;
2087
+ }
2088
+ }
2089
+ }
2090
+ out = this.shadeSlot(key, lane.base.tiles.get(tk)?.data ?? null, "bake-all");
2091
+ this.f32.set(savedP, cur + P_P0 * PLANE);
2092
+ this.f32.set(savedD, b + P_D0 * PLANE);
2093
+ } else {
2094
+ out = this.shadeSlot(key, lane.base.tiles.get(tk)?.data ?? null, "bake-all");
2095
+ }
1889
2096
  if (!out) continue;
1890
2097
  const t = finishTile(out, 4, 0);
1891
2098
  if (t) tiles.set(tk, t);
@@ -1897,7 +2104,7 @@ var WetWorld = class {
1897
2104
  bakedLight(id) {
1898
2105
  this.requireIdle();
1899
2106
  const lane = this.laneOf(id);
1900
- if (!lane.lit) return lane.light;
2107
+ if (!lane.lit || lane.ghost) return lane.light;
1901
2108
  const tiles = new Map(lane.light.tiles);
1902
2109
  for (const key of this.active) {
1903
2110
  if (this.laneIndexOf(key) !== lane.index) continue;
@@ -2045,7 +2252,7 @@ var WetWorld = class {
2045
2252
  */
2046
2253
  lightDisplay(id) {
2047
2254
  const lane = this.laneOf(id);
2048
- if (!lane.lit) return null;
2255
+ if (!lane.lit || lane.ghost) return null;
2049
2256
  const keys = this.active.filter((k) => this.laneIndexOf(k) === lane.index);
2050
2257
  let changed = false;
2051
2258
  const live = /* @__PURE__ */ new Set();
@@ -2291,7 +2498,7 @@ var WetWorld = class {
2291
2498
  slots.set(key, { planes, last: this.u32[(this.layout.last >> 2) + key] });
2292
2499
  bytes += planes.byteLength;
2293
2500
  }
2294
- const lanes = this.lanes.filter((l) => Boolean(l)).map((l) => ({ id: l.id, index: l.index, params: { ...l.params }, flow: l.flow, flowStrength: l.flowStrength, base: l.base, light: l.light, lit: l.lit }));
2501
+ const lanes = this.lanes.filter((l) => Boolean(l)).map((l) => ({ id: l.id, index: l.index, params: { ...l.params }, flow: l.flow, flowStrength: l.flowStrength, base: l.base, light: l.light, lit: l.lit, ghost: cloneGhost(l.ghost) }));
2295
2502
  return { clock: this.clock, tilt: { ...this.tilt }, lanes, slots, bytes };
2296
2503
  }
2297
2504
  /** Puts a snapshot back exactly (the simulation then continues as it would have). */
@@ -2301,8 +2508,8 @@ var WetWorld = class {
2301
2508
  this.lanes.fill(null);
2302
2509
  this.tilt = { ...snap.tilt };
2303
2510
  for (const l of snap.lanes) {
2304
- const lane = newLane(l.id, l.index, { ...l.params }, l.flow, l.flowStrength, l.base, l.light);
2305
- lane.lit = lane.lit || Boolean(l.lit);
2511
+ const lane = newLane(l.id, l.index, { ...l.params }, l.flow, l.flowStrength, l.base, l.light, l.ghost);
2512
+ lane.lit = !lane.ghost && (lane.lit || Boolean(l.lit));
2306
2513
  this.lanes[l.index] = lane;
2307
2514
  this.pack(this.lanes[l.index]);
2308
2515
  }