@pixerate/editor 0.243.0 → 0.244.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 (62) hide show
  1. package/dist/animation-editor/index.cjs +59 -59
  2. package/dist/animation-editor/index.js +3 -3
  3. package/dist/{brushstrokeRender-274HGNZR.js → brushstrokeRender-4542T5HP.js} +2 -2
  4. package/dist/{brushstrokeRender-C7RJZPMC.cjs → brushstrokeRender-QA5WVIDT.cjs} +5 -5
  5. package/dist/canvas/index.cjs +8 -8
  6. package/dist/canvas/index.js +1 -1
  7. package/dist/{chunk-6L3WWIDW.js → chunk-4EQY4TZJ.js} +1 -1
  8. package/dist/{chunk-5FZS7J7C.cjs → chunk-4T47Z4J3.cjs} +18 -2
  9. package/dist/{chunk-4ZHXLFXS.js → chunk-5RFENYO5.js} +1 -1
  10. package/dist/{chunk-SRXWDOUI.js → chunk-7YQNKSQ7.js} +1 -1
  11. package/dist/{chunk-OONFIFEU.cjs → chunk-BDIH4DYR.cjs} +23 -23
  12. package/dist/{chunk-KK2FWMA2.cjs → chunk-HZ547SGQ.cjs} +1 -1
  13. package/dist/{chunk-3WOZEBXZ.cjs → chunk-IZL5V5RK.cjs} +2 -2
  14. package/dist/{chunk-IXI5KATU.js → chunk-L5PB7EG3.js} +1 -1
  15. package/dist/{chunk-R2FVAKFH.cjs → chunk-M6JORA5Z.cjs} +86 -86
  16. package/dist/{chunk-H4Z72Z4Z.js → chunk-OSTKS3YN.js} +1 -1
  17. package/dist/{chunk-HLBTRSQJ.js → chunk-P3NIPIKS.js} +263 -63
  18. package/dist/{chunk-UV7OOZJQ.cjs → chunk-SD23XMVJ.cjs} +35 -35
  19. package/dist/{chunk-HQZHOPNE.js → chunk-TBJEMELS.js} +18 -2
  20. package/dist/{chunk-XFYLA3ZO.cjs → chunk-VMROIWLL.cjs} +290 -90
  21. package/dist/image-editor/index.cjs +785 -777
  22. package/dist/image-editor/index.js +13 -5
  23. package/dist/kernel-wasm-4KVONDAU.js +9 -0
  24. package/dist/kernel-wasm-TTYRBGAK.cjs +9 -0
  25. package/dist/paint/index.cjs +10 -2
  26. package/dist/paint/index.js +9 -1
  27. package/dist/paint/wet/film.d.cts +19 -1
  28. package/dist/paint/wet/film.d.ts +19 -1
  29. package/dist/paint/wet/gpu-kernels.d.cts +2 -2
  30. package/dist/paint/wet/gpu-kernels.d.ts +2 -2
  31. package/dist/paint/wet/kernel-wasm.d.cts +5 -5
  32. package/dist/paint/wet/kernel-wasm.d.ts +5 -5
  33. package/dist/paint/wet/kernel.d.cts +1 -1
  34. package/dist/paint/wet/kernel.d.ts +1 -1
  35. package/dist/paint/wet/medium.d.cts +3 -1
  36. package/dist/paint/wet/medium.d.ts +3 -1
  37. package/dist/paint/wet/model.d.cts +19 -4
  38. package/dist/paint/wet/model.d.ts +19 -4
  39. package/dist/paint/wet/world.d.cts +34 -8
  40. package/dist/paint/wet/world.d.ts +34 -8
  41. package/dist/paint-editor/backlight.d.cts +77 -0
  42. package/dist/paint-editor/backlight.d.ts +77 -0
  43. package/dist/paint-editor/controller.d.cts +11 -0
  44. package/dist/paint-editor/controller.d.ts +11 -0
  45. package/dist/paint-editor/index.cjs +685 -397
  46. package/dist/paint-editor/index.d.cts +1 -0
  47. package/dist/paint-editor/index.d.ts +1 -0
  48. package/dist/paint-editor/index.js +308 -20
  49. package/dist/paint-editor/paper.d.cts +7 -0
  50. package/dist/paint-editor/paper.d.ts +7 -0
  51. package/dist/paint-editor/wet.d.cts +9 -0
  52. package/dist/paint-editor/wet.d.ts +9 -0
  53. package/dist/vector/index.cjs +962 -962
  54. package/dist/vector/index.js +4 -4
  55. package/package.json +1 -1
  56. package/skills/pixerate-paint-editor/SKILL.md +3 -3
  57. package/skills/pixerate-paint-editor/references/light-paint.md +24 -7
  58. package/skills/pixerate-paint-watercolour/SKILL.md +10 -1
  59. package/skills/pixerate-paint-watercolour/evals/glowing-wash.json +22 -0
  60. package/skills/pixerate-paint-watercolour/evals/stained-glass.json +16 -0
  61. package/dist/kernel-wasm-6FPOLVUZ.cjs +0 -9
  62. package/dist/kernel-wasm-WIMMJUF2.js +0 -9
@@ -32,6 +32,7 @@ export { BLEND_MODES, PAINTERLY_BLEND_MODES, isBlendMode, isPainterlyBlendMode,
32
32
  export * from './wet.cjs';
33
33
  export * from './wet-tools.cjs';
34
34
  export * from './thick.cjs';
35
+ export { BACKLIGHT_SPILL, DEFAULT_BACKLIGHT_COLOR, backlightOf } from './backlight.cjs';
35
36
  export * from './thick-tools.cjs';
36
37
  export * from './replay.cjs';
37
38
  export * from './replay-tools.cjs';
@@ -32,6 +32,7 @@ export { BLEND_MODES, PAINTERLY_BLEND_MODES, isBlendMode, isPainterlyBlendMode,
32
32
  export * from './wet.js';
33
33
  export * from './wet-tools.js';
34
34
  export * from './thick.js';
35
+ export { BACKLIGHT_SPILL, DEFAULT_BACKLIGHT_COLOR, backlightOf } from './backlight.js';
35
36
  export * from './thick-tools.js';
36
37
  export * from './replay.js';
37
38
  export * from './replay-tools.js';
@@ -7,7 +7,7 @@ import {
7
7
  resolveExternalAssets,
8
8
  serializeLayers,
9
9
  serializeLayersExternal
10
- } from "../chunk-SRXWDOUI.js";
10
+ } from "../chunk-7YQNKSQ7.js";
11
11
  import "../chunk-3CI7FRMK.js";
12
12
  import "../chunk-ZNA3IQZF.js";
13
13
  import {
@@ -44,7 +44,7 @@ import {
44
44
  pigmentOf,
45
45
  pointerPressure,
46
46
  wetSupport
47
- } from "../chunk-HLBTRSQJ.js";
47
+ } from "../chunk-P3NIPIKS.js";
48
48
  import "../chunk-HVQVHUTV.js";
49
49
  import {
50
50
  writeZip
@@ -55,7 +55,7 @@ import {
55
55
  realPresenceClock,
56
56
  tweenPresenceCursor
57
57
  } from "../chunk-IVSWRVB4.js";
58
- import "../chunk-6L3WWIDW.js";
58
+ import "../chunk-4EQY4TZJ.js";
59
59
  import {
60
60
  BRUSH_ID,
61
61
  BRUSH_ROTATIONS,
@@ -133,7 +133,7 @@ import {
133
133
  tiltToOrientation,
134
134
  toStrokePoint,
135
135
  usesPaintExchange
136
- } from "../chunk-HQZHOPNE.js";
136
+ } from "../chunk-TBJEMELS.js";
137
137
  import "../chunk-M6DNZNVY.js";
138
138
  import "../chunk-WEIK3UYB.js";
139
139
  import "../chunk-PWBZH3FK.js";
@@ -2214,6 +2214,7 @@ function downloadBytes(bytes, fileName, mimeType) {
2214
2214
  // src/paint-editor/wet.ts
2215
2215
  var STROKE_WATER = 0.9;
2216
2216
  var STROKE_MASS = PIGMENT_UNIT * 2.5;
2217
+ var STROKE_LIGHT = 1.5;
2217
2218
  var COVERAGE_STEPS = 4096;
2218
2219
  function union2(a, b) {
2219
2220
  if (!a) return { ...b };
@@ -2245,8 +2246,10 @@ var WetStrokeDriver = class {
2245
2246
  this.medium = new WetMedium(init.world, init.laneId, {
2246
2247
  water: STROKE_WATER * Math.min(1, wet.water * (1 + thin)) * perDab,
2247
2248
  mass: STROKE_MASS * wet.pigment * (1 - 0.6 * thin) * perDab,
2248
- body: wet.body * (1 - thin)
2249
+ body: wet.body * (1 - thin),
2250
+ light: 0
2249
2251
  });
2252
+ this.lightBase = wet.tool === "paint" ? STROKE_LIGHT * brush.light.amount * (1 - 0.6 * thin) * perDab : 0;
2250
2253
  const rates = exchangeRates(brush.behavior.pickup, brush.behavior.mix, wet.load, brush.behavior.mixing);
2251
2254
  this.states = copies.map((c, i) => ({
2252
2255
  walker: new PenDabWalker(brush, init.seed + i * 7919 | 0, c.linear),
@@ -2276,6 +2279,7 @@ var WetStrokeDriver = class {
2276
2279
  paint = [0, 0, 0];
2277
2280
  centre = [0, 0, 0];
2278
2281
  depth;
2282
+ lightBase;
2279
2283
  sampler;
2280
2284
  sample = { rgb: [0, 0, 0], alpha: 1 };
2281
2285
  copies;
@@ -2374,6 +2378,7 @@ var WetStrokeDriver = class {
2374
2378
  switch (wet.tool) {
2375
2379
  case "paint": {
2376
2380
  const ex = st.exchange;
2381
+ this.medium.options.light = this.lightBase * (brush.light.pressure ? d.pressure : 1);
2377
2382
  let flow = 1;
2378
2383
  const effect = this.init.effect;
2379
2384
  if (effect) {
@@ -2562,6 +2567,7 @@ var PaintWetLayers = class {
2562
2567
  const flow = this.settings.get(l.id)?.flow;
2563
2568
  return {
2564
2569
  base: translateSurface(l.base, left, top),
2570
+ ...l.light ? { light: translateSurface(l.light, left, top) } : {},
2565
2571
  flow: l.flow && flow && flow.kind !== "none" ? flowField(flow, width, height) : l.flow
2566
2572
  };
2567
2573
  });
@@ -2601,7 +2607,7 @@ var PaintWetLayers = class {
2601
2607
  if (!w.hasLane(layerId)) {
2602
2608
  const base = this.host.layerSurface(layerId) ?? new TiledSurface(4, /* @__PURE__ */ new Map(), 0);
2603
2609
  const s = this.settingsOf(layerId, medium);
2604
- w.ensureLane(layerId, { params: s.params, base });
2610
+ w.ensureLane(layerId, { params: s.params, base, light: this.host.layerLight(layerId) });
2605
2611
  const { width, height } = this.host.canvas();
2606
2612
  if (s.flow.kind !== "none") w.setFlow(layerId, flowField(s.flow, width, height), s.flow.strength);
2607
2613
  }
@@ -2659,6 +2665,7 @@ var PaintWetLayers = class {
2659
2665
  if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
2660
2666
  rects.push(...d.changed);
2661
2667
  }
2668
+ this.syncLights();
2662
2669
  if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
2663
2670
  return rects;
2664
2671
  }
@@ -2674,9 +2681,21 @@ var PaintWetLayers = class {
2674
2681
  if (this.host.layerSurface(id) !== d.surface) surfaces.set(id, d.surface);
2675
2682
  rects.push(...d.changed);
2676
2683
  }
2684
+ this.syncLights();
2677
2685
  if (surfaces.size) this.host.updateSurfaces(surfaces, rects);
2678
2686
  return rects;
2679
2687
  }
2688
+ /** Shows each wet layer's live light paint (the light under it with the wet light shaded in). */
2689
+ syncLights() {
2690
+ const w = this.world;
2691
+ if (!w) return;
2692
+ const lights = /* @__PURE__ */ new Map();
2693
+ for (const id of w.laneIds()) {
2694
+ const l = w.lightDisplay(id);
2695
+ if (l && this.host.layerLight(id) !== l) lights.set(id, l);
2696
+ }
2697
+ if (lights.size) this.host.updateLights(lights);
2698
+ }
2680
2699
  /** Drops the wet layers whose paint has all dried (they become ordinary pixel layers). */
2681
2700
  retireDry() {
2682
2701
  const w = this.world;
@@ -2684,7 +2703,9 @@ var PaintWetLayers = class {
2684
2703
  for (const id of w.laneIds()) {
2685
2704
  if (w.isActive(id) || this.driver?.init.laneId === id) continue;
2686
2705
  const base = w.getBase(id);
2706
+ const light = w.isLit(id) ? w.getLight(id) : null;
2687
2707
  w.removeLane(id);
2708
+ if (light && this.host.layerLight(id) !== light) this.host.updateLights(/* @__PURE__ */ new Map([[id, light]]));
2688
2709
  this.host.updateSurfaces(/* @__PURE__ */ new Map([[id, base]]), []);
2689
2710
  }
2690
2711
  }
@@ -2803,6 +2824,12 @@ var PaintWetLayers = class {
2803
2824
  this.retireDry();
2804
2825
  return out;
2805
2826
  }
2827
+ /** The layer's light paint with all its wet paint dried (`null` without a wet layer or light), without changing anything. */
2828
+ bakedLight(layerId) {
2829
+ const w = this.world;
2830
+ if (!w?.hasLane(layerId) || !w.isLit(layerId)) return null;
2831
+ return w.bakedLight(layerId);
2832
+ }
2806
2833
  /** The layer's pixels with all its wet paint dried (deterministic), without changing anything. */
2807
2834
  bakedSurface(layerId) {
2808
2835
  const w = this.world;
@@ -3477,6 +3504,212 @@ function liveStrokeBounds(stroke) {
3477
3504
  return { x: Math.floor(x0 - pad), y: Math.floor(y0 - pad), width: Math.ceil(x1 - x0 + 2 * pad), height: Math.ceil(y1 - y0 + 2 * pad) };
3478
3505
  }
3479
3506
 
3507
+ // src/paint-editor/backlight.ts
3508
+ var DEFAULT_BACKLIGHT_COLOR = "#fff1d6";
3509
+ var BACKLIGHT_SPILL = { radius: 24, strength: 1.5 };
3510
+ var BLOCK_HEIGHT = 96;
3511
+ var VALLEY_GAIN = 0.35;
3512
+ function backlightOf(paper) {
3513
+ return paper.backlight ?? 0;
3514
+ }
3515
+ var LIN = new Float64Array(256);
3516
+ for (let i = 0; i < 256; i++) LIN[i] = srgbToLinear(i / 255);
3517
+ function needAlpha(e, p) {
3518
+ if (e < p) return 1 - e / p;
3519
+ if (e > p) return (e - p) / (255 - p);
3520
+ return 0;
3521
+ }
3522
+ function tilesUnder(surface, rect, offset, out) {
3523
+ if (!surface) {
3524
+ out.push(null);
3525
+ return;
3526
+ }
3527
+ const x0 = Math.floor((rect.x - offset.x) / TILE);
3528
+ const y0 = Math.floor((rect.y - offset.y) / TILE);
3529
+ const x1 = Math.floor((rect.x - offset.x + rect.width - 1) / TILE);
3530
+ const y1 = Math.floor((rect.y - offset.y + rect.height - 1) / TILE);
3531
+ for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) out.push(surface.tiles.get(tileKey(tx, ty)) ?? null);
3532
+ }
3533
+ var sameRefs = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
3534
+ var BacklightCache = class {
3535
+ tiles = /* @__PURE__ */ new Map();
3536
+ key = "";
3537
+ spillOf = null;
3538
+ out = null;
3539
+ /**
3540
+ * The backlight layer's pixels for `layers` (bottom first) on `paper` over a `width` × `height`
3541
+ * canvas, or `null` when the paper has no backlight. `spillFrom` is the committed painting (the
3542
+ * spill is drawn from it, so a stroke in progress does not redo the blur every frame).
3543
+ */
3544
+ surface(paper, grain, width, height, layers, spillFrom) {
3545
+ const amount = backlightOf(paper);
3546
+ if (!(amount > 0)) return null;
3547
+ const key = JSON.stringify([amount, paper.backlightColor ?? DEFAULT_BACKLIGHT_COLOR, paper.color, paper.depth, grain.flat, width, height, layers.length, layers.map((l) => [l.opacity, l.offset.x, l.offset.y, l.surface.channels])]);
3548
+ if (key !== this.key) {
3549
+ this.tiles.clear();
3550
+ this.key = key;
3551
+ }
3552
+ const light = /* @__PURE__ */ new Map();
3553
+ const color = /* @__PURE__ */ new Map();
3554
+ const painted = /* @__PURE__ */ new Map();
3555
+ const tilesX = Math.ceil(width / TILE);
3556
+ const tilesY = Math.ceil(height / TILE);
3557
+ const refs = [];
3558
+ for (let ty = 0; ty < tilesY; ty++) {
3559
+ for (let tx = 0; tx < tilesX; tx++) {
3560
+ const rect = { x: tx * TILE, y: ty * TILE, width: Math.min(TILE, width - tx * TILE), height: Math.min(TILE, height - ty * TILE) };
3561
+ refs.length = 0;
3562
+ for (const l of layers) {
3563
+ tilesUnder(l.surface, rect, l.offset, refs);
3564
+ tilesUnder(l.thick?.height, rect, l.offset, refs);
3565
+ tilesUnder(l.thick?.material, rect, l.offset, refs);
3566
+ }
3567
+ const k = tileKey(tx, ty);
3568
+ let r = this.tiles.get(k);
3569
+ if (!r || !sameRefs(r.refs, refs)) {
3570
+ r = { refs: [...refs], ...computeTile(paper, grain, amount, rect, layers) };
3571
+ this.tiles.set(k, r);
3572
+ }
3573
+ if (r.light) light.set(k, r.light);
3574
+ if (r.color) color.set(k, r.color);
3575
+ if (r.painted) painted.set(k, r.painted);
3576
+ }
3577
+ }
3578
+ return { light: new TiledSurface(4, light, 0), color: new TiledSurface(4, color, 0), painted: new TiledSurface(1, painted, 0) };
3579
+ }
3580
+ /**
3581
+ * The full Backlight layer: the light through the paint (`current`) plus the spill of the light
3582
+ * through the committed paint (`committed`), both Linear Dodge. Cached on their tiles.
3583
+ */
3584
+ layer(current, committed, amount) {
3585
+ let spill = null;
3586
+ if (committed) {
3587
+ const s = this.spillOf;
3588
+ if (s && s.color.id === committed.color.id && s.painted.id === committed.painted.id) spill = s.out;
3589
+ else {
3590
+ spill = computeSpill(committed.color, committed.painted, { radius: BACKLIGHT_SPILL.radius, strength: BACKLIGHT_SPILL.strength * amount });
3591
+ this.spillOf = { color: committed.color, painted: committed.painted, out: spill };
3592
+ }
3593
+ }
3594
+ if (!spill) return current.light;
3595
+ const parts = [...current.light.tiles.values(), ...spill.tiles.values()];
3596
+ if (this.out && sameRefs(this.out.parts, parts)) return this.out.surface;
3597
+ const keys = /* @__PURE__ */ new Set([...current.light.tiles.keys(), ...spill.tiles.keys()]);
3598
+ const tiles = /* @__PURE__ */ new Map();
3599
+ for (const key of keys) {
3600
+ const a = current.light.tiles.get(key);
3601
+ const b = spill.tiles.get(key);
3602
+ const t = !a ? b : !b ? a : addTiles(a, b);
3603
+ if (t) tiles.set(key, t);
3604
+ }
3605
+ const surface = new TiledSurface(4, tiles, 0);
3606
+ this.out = { parts, surface };
3607
+ return surface;
3608
+ }
3609
+ };
3610
+ var full4 = (t) => {
3611
+ if (t.data.length === TILE * TILE * 4) return t.data;
3612
+ const out = new Uint8ClampedArray(TILE * TILE * 4);
3613
+ const u = t.uniform ?? 0;
3614
+ const bytes = [u >>> 24 & 255, u >>> 16 & 255, u >>> 8 & 255, u & 255];
3615
+ for (let i = 0; i < out.length; i += 4) out.set(bytes, i);
3616
+ return out;
3617
+ };
3618
+ function addTiles(a, b) {
3619
+ const x = full4(a);
3620
+ const y = full4(b);
3621
+ const out = new Uint8ClampedArray(TILE * TILE * 4);
3622
+ for (let i = 0; i < out.length; i += 4) {
3623
+ const ay = y[i + 3] / 255;
3624
+ const r = x[i] + y[i] * ay;
3625
+ const g = x[i + 1] + y[i + 1] * ay;
3626
+ const bl = x[i + 2] + y[i + 2] * ay;
3627
+ if (r < 0.5 && g < 0.5 && bl < 0.5) continue;
3628
+ out[i] = Math.round(r);
3629
+ out[i + 1] = Math.round(g);
3630
+ out[i + 2] = Math.round(bl);
3631
+ out[i + 3] = 255;
3632
+ }
3633
+ return finishTile(out, 4, 0);
3634
+ }
3635
+ function computeTile(paper, grain, amount, rect, layers) {
3636
+ const n = rect.width * rect.height;
3637
+ const [p0, p1, p2] = hexToRgb8(paper.color);
3638
+ const pb = [p0, p1, p2];
3639
+ const pl = [Math.max(1e-4, LIN[p0]), Math.max(1e-4, LIN[p1]), Math.max(1e-4, LIN[p2])];
3640
+ const [c0, c1, c2] = hexToRgb8(paper.backlightColor ?? DEFAULT_BACKLIGHT_COLOR);
3641
+ const t0 = new Float32Array(n).fill(amount * LIN[c0]);
3642
+ const t1 = new Float32Array(n).fill(amount * LIN[c1]);
3643
+ const t2 = new Float32Array(n).fill(amount * LIN[c2]);
3644
+ const depth = grain.flat ? 0 : paper.depth;
3645
+ if (depth > 0) {
3646
+ for (let y = 0, i = 0; y < rect.height; y++)
3647
+ for (let x = 0; x < rect.width; x++, i++) {
3648
+ const g = 1 + VALLEY_GAIN * depth * (0.5 - grain.at(rect.x + x + 0.5, rect.y + y + 0.5));
3649
+ t0[i] *= g;
3650
+ t1[i] *= g;
3651
+ t2[i] *= g;
3652
+ }
3653
+ }
3654
+ const clear = new Float32Array(n).fill(1);
3655
+ for (const l of layers) {
3656
+ const lr = { x: rect.x - l.offset.x, y: rect.y - l.offset.y, width: rect.width, height: rect.height };
3657
+ if (!l.surface.tiles.size) continue;
3658
+ const px = readRegion(l.surface, lr);
3659
+ const h = l.thick?.height.tiles.size ? readRegion(l.thick.height, lr) : null;
3660
+ const m = h && l.thick?.material.tiles.size ? readRegion(l.thick.material, lr) : null;
3661
+ const op = l.opacity;
3662
+ for (let i = 0; i < n; i++) {
3663
+ const a8 = px[i * 4 + 3];
3664
+ if (!a8 && !(h && h[i])) continue;
3665
+ const a = a8 / 255;
3666
+ let aMin = 0;
3667
+ const tr = [1, 1, 1];
3668
+ for (let c = 0; c < 3; c++) {
3669
+ const e = a * px[i * 4 + c] + (1 - a) * pb[c];
3670
+ aMin = Math.max(aMin, needAlpha(e, pb[c]));
3671
+ tr[c] = Math.min(1, LIN[Math.round(e)] / pl[c]);
3672
+ }
3673
+ const hide = aMin < 1 && a > aMin ? (a - aMin) / (1 - aMin) : 0;
3674
+ let k = 1 - hide;
3675
+ if (h && h[i]) {
3676
+ const ma = m ? m[i * 4 + 3] : 0;
3677
+ const clarity = ma ? (255 - ma) / 254 : 0;
3678
+ k *= 1 - Math.min(1, h[i] / BLOCK_HEIGHT) * (1 - clarity);
3679
+ }
3680
+ t0[i] *= 1 - op + op * tr[0] * k;
3681
+ t1[i] *= 1 - op + op * tr[1] * k;
3682
+ t2[i] *= 1 - op + op * tr[2] * k;
3683
+ clear[i] *= 1 - op * Math.max(a, h && h[i] ? 1 : 0);
3684
+ }
3685
+ }
3686
+ const light = new Uint8ClampedArray(TILE * TILE * 4);
3687
+ const color = new Uint8ClampedArray(TILE * TILE * 4);
3688
+ const painted = new Uint8ClampedArray(TILE * TILE);
3689
+ for (let y = 0, i = 0; y < rect.height; y++) {
3690
+ for (let x = 0; x < rect.width; x++, i++) {
3691
+ const o = (y * TILE + x) * 4;
3692
+ const add = [Math.min(1, t0[i]), Math.min(1, t1[i]), Math.min(1, t2[i])];
3693
+ const mx = Math.max(add[0], add[1], add[2]);
3694
+ if (mx > 0.5 / 255) {
3695
+ light[o] = Math.round(add[0] * 255);
3696
+ light[o + 1] = Math.round(add[1] * 255);
3697
+ light[o + 2] = Math.round(add[2] * 255);
3698
+ light[o + 3] = 255;
3699
+ }
3700
+ const pv = Math.round((1 - clear[i]) * 255);
3701
+ if (pv && mx > 0.5 / 255) {
3702
+ painted[y * TILE + x] = pv;
3703
+ color[o] = Math.round(linearToSrgb(add[0]) * 255);
3704
+ color[o + 1] = Math.round(linearToSrgb(add[1]) * 255);
3705
+ color[o + 2] = Math.round(linearToSrgb(add[2]) * 255);
3706
+ color[o + 3] = 255;
3707
+ }
3708
+ }
3709
+ }
3710
+ return { light: finishTile(light, 4, 0), color: finishTile(color, 4, 0), painted: finishTile(painted, 1, 0) };
3711
+ }
3712
+
3480
3713
  // src/paint-editor/replay.ts
3481
3714
  function replayItems(doc) {
3482
3715
  const items = [];
@@ -3867,6 +4100,9 @@ var PaintEditorController = class _PaintEditorController {
3867
4100
  /** Light paint: each layer's spill settings (saved with the document, not undo steps; missing = the defaults). */
3868
4101
  lightSpills = {};
3869
4102
  lightCaches = /* @__PURE__ */ new Map();
4103
+ /** Backlit paper: the light through the paint as drawn (live strokes too), and as committed (its spill). */
4104
+ backlightView = new BacklightCache();
4105
+ backlightCommitted = new BacklightCache();
3870
4106
  /** Live effect paint: playing and the frame rate cap (editor state), and the time the view shows. */
3871
4107
  effectPlaybackState = { playing: true, fps: 30 };
3872
4108
  effectTime = 0;
@@ -3928,6 +4164,12 @@ var PaintEditorController = class _PaintEditorController {
3928
4164
  this.doc = { ...this.doc, layers };
3929
4165
  this.emit(rects.length ? rects : "all");
3930
4166
  },
4167
+ layerLight: (id) => this.doc.light?.[id] ?? null,
4168
+ updateLights: (lights) => {
4169
+ let light = this.doc.light;
4170
+ for (const [id, surface] of lights) if (findLayer(this.doc.layers, id)) light = withLayerLight(light, id, surface);
4171
+ this.doc = { ...this.doc, ...light ? { light } : {} };
4172
+ },
3931
4173
  changed: () => {
3932
4174
  }
3933
4175
  },
@@ -5193,7 +5435,6 @@ var PaintEditorController = class _PaintEditorController {
5193
5435
  const brush = this.strokeBrush(init);
5194
5436
  const effect = this.strokeEffect(init, brush);
5195
5437
  if (brush.light.amount > 0) {
5196
- if (brush.engine === "wet") throw new PaintEditorError(`The ${brush.name} brush paints wet media, which doesn't take light yet (glowing watercolour is planned in #494); paint light with a dab or impasto light brush (list_brushes category light), or set light.amount to 0 (set_brush).`);
5197
5438
  if (brush.engine === "sticker") throw new PaintEditorError(`The ${brush.name} brush sprays stickers, which don't take light yet; paint light with a dab or particle light brush (list_brushes category light), or set light.amount to 0 (set_brush).`);
5198
5439
  }
5199
5440
  if (doc.liveStrokes?.[layer.id]) {
@@ -5376,7 +5617,8 @@ var PaintEditorController = class _PaintEditorController {
5376
5617
  const thick = ch ? { ...this.doc.thick ?? {}, [l.id]: ch } : this.doc.thick;
5377
5618
  const sprayed = s.stickers();
5378
5619
  const stickers = sprayed ? { ...this.doc.stickers ?? {}, [l.id]: [...this.doc.stickers?.[l.id] ?? [], ...sprayed] } : this.doc.stickers;
5379
- const light = withLayerLight(this.doc.light, l.id, s.lightChannel());
5620
+ const wetLight = wetStroke && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null;
5621
+ const light = withLayerLight(this.doc.light, l.id, s.lightChannel() ?? wetLight);
5380
5622
  this.commit(`${brush.name} stroke`, this.withWetEvents(withLiveStroke({ ...this.doc, layers: mapLayer(this.doc.layers, l.id, () => ({ ...l, surface })), strokes: [...this.doc.strokes, stroke], ...thick ? { thick } : {}, ...stickers ? { stickers } : {}, ...light ? { light } : {} }, stroke)), wetStroke ? before : this.doc);
5381
5623
  }
5382
5624
  };
@@ -5410,13 +5652,14 @@ var PaintEditorController = class _PaintEditorController {
5410
5652
  const offset = s.init.wet ? { x: 0, y: 0 } : l.offset;
5411
5653
  const ch = s.thickChannels();
5412
5654
  const sprayed = s.stickers();
5655
+ const lit = s.lightChannel() ?? (s.init.wet && this.wet.world?.hasLane(l.id) ? this.wet.world.lightDisplay(l.id) : null);
5413
5656
  out = withLiveStroke({
5414
5657
  ...out,
5415
5658
  layers: mapLayer(out.layers, l.id, () => ({ ...l, surface, offset })),
5416
5659
  strokes: [...out.strokes, stroke],
5417
5660
  ...ch ? { thick: { ...out.thick ?? {}, [l.id]: ch } } : {},
5418
5661
  ...sprayed ? { stickers: { ...out.stickers ?? {}, [l.id]: [...out.stickers?.[l.id] ?? [], ...sprayed] } } : {},
5419
- ...s.lightChannel() ? { light: withLayerLight(out.light, l.id, s.lightChannel()) } : {}
5662
+ ...lit ? { light: withLayerLight(out.light, l.id, lit) } : {}
5420
5663
  }, stroke);
5421
5664
  }
5422
5665
  };
@@ -5565,6 +5808,7 @@ var PaintEditorController = class _PaintEditorController {
5565
5808
  clearLight(layerId) {
5566
5809
  this.requireIdle("clearing light");
5567
5810
  const l = this.paintTarget(layerId, "clear the light of");
5811
+ if (this.wet.bakedLight(l.id)) this.dryLayersFirst([l.id]);
5568
5812
  const t = this.lightWithout([l.id]);
5569
5813
  if (!t.light) return { layerId: l.id, cleared: false };
5570
5814
  this.commit("Clear light", { ...this.doc, light: t.light });
@@ -5941,8 +6185,39 @@ var PaintEditorController = class _PaintEditorController {
5941
6185
  }
5942
6186
  return out;
5943
6187
  }
6188
+ /** Visible pixel layers bottom first, as the backlight reads them (group opacities multiplied in). */
6189
+ backlitLayers(layers, committed, opacity = 1, out = []) {
6190
+ for (const l of layers) {
6191
+ if (!l.visible) continue;
6192
+ if (l.type === "group") this.backlitLayers(l.children, committed, opacity * l.opacity, out);
6193
+ else if (isPixelLayer(l)) {
6194
+ const thick = !committed && this.liveThick?.layerId === l.id ? this.liveThick.channels : this.doc.thick?.[l.id];
6195
+ out.push({ surface: l.surface, offset: l.offset, opacity: opacity * l.opacity, ...thick ? { thick } : {} });
6196
+ }
6197
+ }
6198
+ return out;
6199
+ }
6200
+ /**
6201
+ * Backlit paper: the light that makes it through `base` (the layers as drawn), with the spill of
6202
+ * the light through the committed painting, as a Linear Dodge (Add) layer for the top of the
6203
+ * stack; `null` when the paper has no backlight.
6204
+ */
6205
+ backlightLayer(base) {
6206
+ const paper = this.doc.paper;
6207
+ const amount = backlightOf(paper);
6208
+ if (!(amount > 0)) return null;
6209
+ const { width, height } = this.doc.canvas;
6210
+ const grain = paperGrain(paper);
6211
+ const current = this.backlightView.surface(paper, grain, width, height, this.backlitLayers(base, false), null);
6212
+ const committed = base === this.doc.layers ? current : this.backlightCommitted.surface(paper, grain, width, height, this.backlitLayers(this.doc.layers, true), null);
6213
+ const surface = this.backlightView.layer(current, committed, amount);
6214
+ return { type: "pixel", id: BACKLIGHT_LAYER_ID, name: "Backlight", visible: true, opacity: 1, blend: "linear-dodge", surface, offset: { x: 0, y: 0 } };
6215
+ }
5944
6216
  renderLayers(options, view = false) {
5945
- let layers = this.lightLayers(this.litLayers((options.wet ?? "display") === "dried" ? this.driedLayers() : this.currentLayers()));
6217
+ const base = (options.wet ?? "display") === "dried" ? this.driedLayers() : this.currentLayers();
6218
+ let layers = this.lightLayers(this.litLayers(base));
6219
+ const backlight = options.paper !== false && !options.layerIds ? this.backlightLayer(base) : null;
6220
+ if (backlight) layers = [...layers, backlight];
5946
6221
  if (options.layerIds) {
5947
6222
  const picked = [];
5948
6223
  for (const id of options.layerIds) picked.push(findLayer(layers, id)?.layer ?? this.requireLayer(id));
@@ -6032,7 +6307,7 @@ var PaintEditorController = class _PaintEditorController {
6032
6307
  const light = {};
6033
6308
  for (const [id, t] of Object.entries(this.doc.light ?? {})) {
6034
6309
  if (!findLayer(this.doc.layers, id)) continue;
6035
- const saved = lightToJSON(t);
6310
+ const saved = lightToJSON(this.wet.bakedLight(id) ?? t);
6036
6311
  if (saved) light[id] = saved;
6037
6312
  }
6038
6313
  if (Object.keys(light).length) ext.light = light;
@@ -6218,7 +6493,9 @@ var PaintEditorController = class _PaintEditorController {
6218
6493
  * pixels), and the composite as the merged image.
6219
6494
  */
6220
6495
  exportPsd() {
6221
- const dried = this.lightLayers(this.litLayers(this.driedLayers(this.doc.layers)));
6496
+ const driedBase = this.driedLayers(this.doc.layers);
6497
+ const backlight = this.backlightLayer(driedBase);
6498
+ const dried = [...this.lightLayers(this.litLayers(driedBase)), ...backlight ? [backlight] : []];
6222
6499
  const { width, height, dpi } = this.doc.canvas;
6223
6500
  const paperPixels = this.compositePixels({ layerIds: [], paper: true });
6224
6501
  const paperLayer = {
@@ -6249,6 +6526,9 @@ var PaintEditorController = class _PaintEditorController {
6249
6526
  const one = names.length === 1;
6250
6527
  result.warnings.push(`${label} blending has no PSD equivalent; ${list} ${one ? "was" : "were"} written as ${std}, the nearest Photoshop mode, so ${one ? "it looks" : "they look"} different in Photoshop (the merged image is exact).`);
6251
6528
  }
6529
+ if (backlight) {
6530
+ result.warnings.push('Backlit paper is written as pixels: the light that comes through the paint is a "Backlight" Linear Dodge (Add) layer at the top. Opening the PSD again gives a plain pixel layer, not a backlight.');
6531
+ }
6252
6532
  if (Object.values(this.doc.light ?? {}).some((l) => l.tiles.size)) {
6253
6533
  result.warnings.push('Light paint is written as pixels: each layer with its glow, and its spill as a "\u2039layer\u203A light" Linear Dodge (Add) layer above it. Opening the PSD again gives plain pixel layers, not editable light.');
6254
6534
  }
@@ -7265,6 +7545,7 @@ function withLayerLight(light, layerId, next) {
7265
7545
  else delete out[layerId];
7266
7546
  return out;
7267
7547
  }
7548
+ var BACKLIGHT_LAYER_ID = "__backlight";
7268
7549
  function insertAfter(layers, id, node) {
7269
7550
  const i = layers.findIndex((l) => l.id === id);
7270
7551
  if (i >= 0) return [...layers.slice(0, i + 1), node, ...layers.slice(i + 1)];
@@ -8259,7 +8540,7 @@ function createPaintWetTools(c) {
8259
8540
  {
8260
8541
  name: "advance_wet_paint",
8261
8542
  title: "Let the wet paint flow",
8262
- description: "Lets time pass for wet paint: the water spreads, runs downhill on a tilted canvas, granulates, blooms and evaporates, and pigment settles. Give steps (simulation steps; at the default 120 per second, 120 steps is one second) or seconds (default 1). Not an undo step, as time passing never is. Returns the steps run, the clock, stillWet and each wet layer\u2019s state. Paint dries by itself after a few thousand steps; dry_paint dries it at once.",
8543
+ description: "Lets time pass for wet paint: the water spreads, runs downhill on a tilted canvas, granulates, blooms and evaporates, and pigment settles. Give steps (simulation steps; at the default 120 per second, 120 steps is one second) or seconds (default 1). Not an undo step, as time passing never is. Returns the steps run, the clock, stillWet and each wet layer\u2019s state. Paint dries by itself after a few thousand steps; dry_paint dries it at once. Wet light paint (wet brushes with light.amount) flows with the pigment and settles where it dries into the layer\u2019s light (get_light).",
8263
8544
  inputSchema: {
8264
8545
  type: "object",
8265
8546
  properties: {
@@ -8709,7 +8990,7 @@ var IMPASTO_BRUSH = {
8709
8990
  };
8710
8991
  var LIGHT_BRUSH = {
8711
8992
  type: "object",
8712
- description: "Light paint: the light the brush lays with its colour, glowing in the paint's own colour and spilling onto its surroundings (set_light_spill). Dab, particle, bristle, Mixer Brush and impasto brushes take it; wet and sticker brushes refuse light above 0. Missing fields keep their current values.",
8993
+ description: "Light paint: the light the brush lays with its colour, glowing in the paint's own colour and spilling onto its surroundings (set_light_spill). Dab, particle, bristle, Mixer Brush, impasto and wet paint brushes take it (wet light flows, pools, granulates and blooms with the pigment until the paint dries, then is ordinary light paint); wet tools other than paint lay none, and sticker brushes refuse light above 0. Missing fields keep their current values.",
8713
8994
  properties: {
8714
8995
  amount: { type: "number", minimum: 0, maximum: 1, description: "Light each dab lays, scaled by its coverage and flow (0 none, default; 1 full)." },
8715
8996
  pressure: { type: "boolean", description: "Pressure scales the light (default false)." }
@@ -8940,7 +9221,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
8940
9221
  add({
8941
9222
  name: "get_paper",
8942
9223
  title: "Get the paper",
8943
- description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness).",
9224
+ description: "Reads every setting of the current paper (preset, pattern, seed, colour, scale, tiling, depth, shading, dry and wet tuning, absorption, permeability, roughness, and metallic, backlight and backlightColor when set).",
8944
9225
  inputSchema: EMPTY3,
8945
9226
  annotations: { title: "Get the paper", ...READ6 },
8946
9227
  handler: () => JSON.parse(JSON.stringify(c.document.paper))
@@ -8956,7 +9237,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
8956
9237
  add({
8957
9238
  name: "set_paper",
8958
9239
  title: "Set the paper",
8959
- description: "Changes the paper under the whole painting: a preset from list_papers and / or its settings. Grain depth decides how much grain-responsive brushes (pencil, pastel, charcoal) skip the valleys; changing it re-renders the paper but not strokes already painted. One undo step. Returns the paper.",
9240
+ description: "Changes the paper under the whole painting: a preset from list_papers and / or its settings. Grain depth decides how much grain-responsive brushes (pencil, pastel, charcoal) skip the valleys; changing it re-renders the paper but not strokes already painted. backlight lights the paper from behind (the stained-glass preset has it on). One undo step. Returns the paper.",
8960
9241
  inputSchema: {
8961
9242
  type: "object",
8962
9243
  properties: {
@@ -8974,6 +9255,8 @@ function createPaintEditorToolProvider(controller, options = {}) {
8974
9255
  permeability: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how freely water flows over the fibres (low: more ragged, slower spreading)." },
8975
9256
  roughness: { type: "number", minimum: 0, maximum: 1, description: "Wet media: how much the grain pins wet edges and catches granulating pigment." },
8976
9257
  metallic: { type: "number", minimum: 0, maximum: 1, description: "Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light (see set_lighting). 0 by default." },
9258
+ backlight: { type: "number", minimum: 0, maximum: 1, description: "Backlit paper (a lightbox, stained glass): light from behind the paper, 0 (none, the default) .. 1. Transparent glazes (watercolour, airbrush, thin marker) glow in their own colour; opaque paint (gouache, oil, solid ink) and thick paint block it; the light through paint spills softly around it. Nothing is repainted." },
9259
+ backlightColor: { ...COLOR3, description: "The backlight\u2019s colour #rrggbb (default #fff1d6, warm white)." },
8977
9260
  name: { type: "string", minLength: 1, description: "Display name." }
8978
9261
  },
8979
9262
  additionalProperties: false
@@ -9148,7 +9431,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
9148
9431
  add({
9149
9432
  name: "list_brushes",
9150
9433
  title: "List brushes",
9151
- description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
9434
+ description: "Lists the brush library: built-in dry media (pencils, pastels, charcoal, airbrush, marker, ink, erasers), paint brushes (dirty round, multi-colour flat, palette dabber), blending tools (blender, smudge, pigment smudge, Mixer Brush), light paint (category light: glow gel, neon marker, light airbrush, spark trail, glow wash; light is the amount they lay) and saved user brushes, with id, name, category, engine, size, grain response, rotation mode, whether it can pick up paint (wetCapable), its pick-up ratio, dirty and multi-colour mode, plus the active brush id.",
9152
9435
  inputSchema: EMPTY3,
9153
9436
  annotations: { title: "List brushes", ...READ6 },
9154
9437
  handler: () => {
@@ -9332,7 +9615,7 @@ function createPaintEditorToolProvider(controller, options = {}) {
9332
9615
  add({
9333
9616
  name: "paint_stroke",
9334
9617
  title: "Paint a brush stroke",
9335
- description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed 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), 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.",
9618
+ description: "Paints one stroke through points (document pixels, with optional pressure, time, tilt and barrel rotation) on a pixel layer with a library brush or the active one, optionally changed 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.",
9336
9619
  inputSchema: {
9337
9620
  type: "object",
9338
9621
  properties: {
@@ -10054,7 +10337,8 @@ var PAPER_SLIDERS = [
10054
10337
  { path: "absorption", label: "Absorption", min: 0, max: 1, step: 0.01 },
10055
10338
  { path: "permeability", label: "Permeability", min: 0, max: 1, step: 0.01 },
10056
10339
  { path: "roughness", label: "Roughness", min: 0, max: 1, step: 0.01 },
10057
- { path: "metallic", label: "Metallic", min: 0, max: 1, step: 0.01 }
10340
+ { path: "metallic", label: "Metallic", min: 0, max: 1, step: 0.01 },
10341
+ { path: "backlight", label: "Backlight", min: 0, max: 1, step: 0.01 }
10058
10342
  ];
10059
10343
  function readPath(obj, path) {
10060
10344
  let v = obj;
@@ -10110,7 +10394,8 @@ var PICKUP_SLIDERS = [
10110
10394
  ];
10111
10395
  function brushEngineControls(brush) {
10112
10396
  const c = engineControls(brush);
10113
- if (!c || !["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine)) return c;
10397
+ const wetPaint = brush.engine === "wet" && brush.wet?.tool === "paint";
10398
+ if (!c || !(wetPaint || ["dab", "mixer", "particle", "bristle", "impasto"].includes(brush.engine))) return c;
10114
10399
  const extra = brush.engine === "impasto" ? [{ path: "impasto.rim", label: "Rim", min: 0, max: 1, step: 0.01 }, { path: "impasto.clarity", label: "Clarity", min: 0, max: 1, step: 0.01 }] : [];
10115
10400
  return { ...c, sliders: [...c.sliders, LIGHT_SLIDER, ...extra], light: true };
10116
10401
  }
@@ -10644,6 +10929,7 @@ function createPaintEditorDocument(controller, options = {}) {
10644
10929
  });
10645
10930
  }
10646
10931
  export {
10932
+ BACKLIGHT_SPILL,
10647
10933
  BLEND_MENU_GROUPS,
10648
10934
  BLEND_MODES,
10649
10935
  BRUSH_ID,
@@ -10651,6 +10937,7 @@ export {
10651
10937
  BRUSH_SLIDERS,
10652
10938
  BUILTIN_BRUSHES,
10653
10939
  BristleStroke,
10940
+ DEFAULT_BACKLIGHT_COLOR,
10654
10941
  DEFAULT_BEHAVIOR,
10655
10942
  DEFAULT_BRISTLE,
10656
10943
  DEFAULT_BRUSH_ID,
@@ -10776,6 +11063,7 @@ export {
10776
11063
  addPaintGradientStop,
10777
11064
  affineCopy,
10778
11065
  applyAffine,
11066
+ backlightOf,
10779
11067
  brushEngineControls,
10780
11068
  brushLoadColor,
10781
11069
  brushLoadForStroke,
@@ -40,6 +40,13 @@ export interface PaperSettings {
40
40
  roughness: number;
41
41
  /** Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light, 0..1 (default 0). */
42
42
  metallic?: number;
43
+ /**
44
+ * Backlit paper (a lightbox, stained glass): light from behind the paper, 0..1 (default 0: none).
45
+ * Transparent glazes glow in their own colour; opaque and thick paint block it (`backlight.ts`).
46
+ */
47
+ backlight?: number;
48
+ /** The backlight's colour #rrggbb (default warm white, `#fff1d6`). */
49
+ backlightColor?: string;
43
50
  /**
44
51
  * Where the grain's (0, 0) sits in document pixels (missing = the top-left). `extendCanvas` moves
45
52
  * it with the paint, so the texture stays under what was painted on it.
@@ -40,6 +40,13 @@ export interface PaperSettings {
40
40
  roughness: number;
41
41
  /** Metallic paper (gold or silver leaf, pearlescent card): the grain catches the light, 0..1 (default 0). */
42
42
  metallic?: number;
43
+ /**
44
+ * Backlit paper (a lightbox, stained glass): light from behind the paper, 0..1 (default 0: none).
45
+ * Transparent glazes glow in their own colour; opaque and thick paint block it (`backlight.ts`).
46
+ */
47
+ backlight?: number;
48
+ /** The backlight's colour #rrggbb (default warm white, `#fff1d6`). */
49
+ backlightColor?: string;
43
50
  /**
44
51
  * Where the grain's (0, 0) sits in document pixels (missing = the top-left). `extendCanvas` moves
45
52
  * it with the paint, so the texture stays under what was painted on it.