paperlab 0.6.0 → 0.7.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.
@@ -1,3 +1,8 @@
1
+ import {
2
+ DAMAGE_CHANNELS,
3
+ DAMAGE_LOOK_DEFAULTS
4
+ } from "./chunk-7TLIWPGM.js";
5
+
1
6
  // src/config/merge.ts
2
7
  function mergeConfig(base, override) {
3
8
  if (override === void 0) return base;
@@ -4399,10 +4404,10 @@ function usePaperStates(config, enabled, instant, onAction, onStateChange) {
4399
4404
  }
4400
4405
 
4401
4406
  // src/PaperMesh.tsx
4402
- import * as THREE7 from "three";
4407
+ import * as THREE8 from "three";
4403
4408
  import { gsap as gsap2 } from "gsap";
4404
- import { useFrame, useThree } from "@react-three/fiber";
4405
- import { forwardRef, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef3 } from "react";
4409
+ import { useFrame as useFrame3, useThree } from "@react-three/fiber";
4410
+ import { forwardRef, useEffect as useEffect6, useImperativeHandle, useMemo as useMemo4, useRef as useRef3 } from "react";
4406
4411
 
4407
4412
  // src/core/normals.ts
4408
4413
  function computeSheetNormals(geometry) {
@@ -4498,6 +4503,27 @@ function spreadByAspect(sheet2, target) {
4498
4503
  if (!(long > 0)) return [target, target];
4499
4504
  return [Math.round(sheet2.width / long * target), Math.round(sheet2.height / long * target)];
4500
4505
  }
4506
+ function surfacePointAt(positions, cols, rows, u, v, out) {
4507
+ const fx = Math.min(1, Math.max(0, u)) * (cols - 1);
4508
+ const fy = (1 - Math.min(1, Math.max(0, v))) * (rows - 1);
4509
+ const ix = Math.min(Math.floor(fx), cols - 2);
4510
+ const iy = Math.min(Math.floor(fy), rows - 2);
4511
+ const tx = fx - ix;
4512
+ const ty = fy - iy;
4513
+ const a = (iy * cols + ix) * 3;
4514
+ const b = a + cols * 3;
4515
+ const c = b + 3;
4516
+ const d = a + 3;
4517
+ for (let axis = 0; axis < 3; axis++) {
4518
+ const pa = positions[a + axis];
4519
+ const pb = positions[b + axis];
4520
+ const pc = positions[c + axis];
4521
+ const pd = positions[d + axis];
4522
+ const value = tx + ty <= 1 ? pa + tx * (pd - pa) + ty * (pb - pa) : pc + (1 - tx) * (pb - pc) + (1 - ty) * (pd - pc);
4523
+ out.setComponent(axis, value);
4524
+ }
4525
+ return out;
4526
+ }
4501
4527
  function createSheetGeometry(sheet2, minSegments = 2, autoSegments = LEGACY_FLAT_SEGMENTS) {
4502
4528
  const [sx, sy] = resolveSegments(sheet2, minSegments, autoSegments);
4503
4529
  return new THREE.PlaneGeometry(sheet2.width, sheet2.height, sx, sy);
@@ -4910,9 +4936,86 @@ var ClothSim = class {
4910
4936
  height;
4911
4937
  positions;
4912
4938
  prev;
4939
+ /** Where the constructor laid every particle out — what {@link restore} puts back. */
4940
+ laidOut;
4913
4941
  pinned;
4914
4942
  pinTargets;
4915
- constraints = [];
4943
+ /**
4944
+ * The constraints, one typed array per field rather than one object each.
4945
+ *
4946
+ * Because the effects layer has to vary them across the sheet, and an
4947
+ * object with a fixed `rest` cannot be varied cheaply. Paper that chars
4948
+ * SHRINKS — that is what curls it toward the flame, not softening — which
4949
+ * is a shorter rest length on exactly the constraints the heat reached.
4950
+ * Burnt-through paper has to stop pulling on what is left, which is a
4951
+ * broken flag. A wet corner is floppier than the dry sheet above it, which
4952
+ * is a per-constraint stiffness. None of that is in this file; this file
4953
+ * only makes it possible, and at the defaults below the solve is
4954
+ * bit-identical to the object version it replaces.
4955
+ */
4956
+ constraintCount;
4957
+ constraintA;
4958
+ constraintB;
4959
+ /** 0 = structural, 1 = shear, 2 = bend (scaled by `stiffness` at solve time). */
4960
+ constraintKind;
4961
+ /**
4962
+ * The length each constraint is laid out at. Never changes; the reference
4963
+ * to shrink from.
4964
+ *
4965
+ * Float64, not Float32, and that is measured rather than cautious: with
4966
+ * single-precision rest lengths every one of them rounded, and the first
4967
+ * typed-array version of this solve disagreed with the object version it
4968
+ * replaced in 476–711 of 714 coordinates after five seconds. The object
4969
+ * version held a JS number, which is a double.
4970
+ */
4971
+ naturalLength;
4972
+ /** The length it pulls toward now. `naturalLength` until something shrinks or stretches it. */
4973
+ restLength;
4974
+ /** A multiplier on the constraint's own strength. 1 is the paper as built. */
4975
+ constraintStiffness;
4976
+ /** Non-zero where the paper between two particles is gone. Skipped by the solve. */
4977
+ broken;
4978
+ /**
4979
+ * How easily each particle is moved, 0..1: one over its mass, relative to
4980
+ * dry paper.
4981
+ *
4982
+ * Mass matters in exactly two places here, and both are wired, because a
4983
+ * "heavier" that touched neither would be another knob that does nothing.
4984
+ * Gravity is an acceleration, so weight alone does not make wet paper fall
4985
+ * differently. What does: the air pushes a heavy particle less (the aero
4986
+ * term is divided by mass), and the constraint solve moves a heavy particle
4987
+ * less than a light one it is tied to. Zero is infinite mass: static, moved by nothing — not the air, not the solve, and not gravity either.
4988
+ */
4989
+ invMass;
4990
+ /**
4991
+ * How far the middle of each BEND spring sits off the line between its ends
4992
+ * when relaxed, along the sheet's normal — a signed rest curvature, as a
4993
+ * sagitta in world units. Ignored on every other kind of constraint.
4994
+ *
4995
+ * Positive curls the paper toward its FRONT, the face a flat sheet turns to
4996
+ * +z — which puts the middle of each span that far BEHIND its ends. Paper
4997
+ * curling toward you is hollow on the side you see; the first cut of this
4998
+ * had the middle in front, which is a dome, and a dome's rim goes the other
4999
+ * way.
5000
+ *
5001
+ * The only lever here with a sign, and the sign is why it exists. A bend
5002
+ * spring is a distance, and a distance cannot tell paper curled toward you
5003
+ * from paper curled away: shorten it and the sheet buckles, but which way
5004
+ * is a coin toss, and gravity can flip it through later. Measured on the
5005
+ * first version, which pushed hot paper toward the front along its normal
5006
+ * and left the char to hold it — the curl held while the heat did and
5007
+ * snapped through to the BACK the moment the front had passed. Charred
5008
+ * paper stays curled toward the side that burnt because that side shrank
5009
+ * more: a curvature built into the material, which is what this is.
5010
+ *
5011
+ * Solved as a couple — the middle moves one way, the two ends half as far
5012
+ * the other — so it bends the sheet and never pushes it anywhere. A push
5013
+ * cannot do this job: it moves paper, and a uniformly curled sheet in still
5014
+ * air has no business going anywhere.
5015
+ */
5016
+ restBend;
5017
+ /** For each bend spring, the particle between its ends; -1 for every other kind. */
5018
+ constraintMiddle;
4916
5019
  params;
4917
5020
  time = 0;
4918
5021
  accumulator = 0;
@@ -4968,11 +5071,11 @@ var ClothSim = class {
4968
5071
  }
4969
5072
  }
4970
5073
  this.prev.set(this.positions);
5074
+ this.laidOut = this.positions.slice();
4971
5075
  const idx = (r, c) => r * cols + c;
5076
+ const links = [];
4972
5077
  const link = (a, b, kind) => {
4973
- const dx = this.positions[a * 3] - this.positions[b * 3];
4974
- const dy = this.positions[a * 3 + 1] - this.positions[b * 3 + 1];
4975
- this.constraints.push({ a, b, rest: Math.hypot(dx, dy), kind });
5078
+ links.push(a, b, kind);
4976
5079
  };
4977
5080
  for (let r = 0; r < rows; r++) {
4978
5081
  for (let c = 0; c < cols; c++) {
@@ -4986,6 +5089,31 @@ var ClothSim = class {
4986
5089
  if (r + 2 < rows) link(idx(r, c), idx(r + 2, c), 2);
4987
5090
  }
4988
5091
  }
5092
+ const m = links.length / 3;
5093
+ this.constraintCount = m;
5094
+ this.constraintA = new Uint32Array(m);
5095
+ this.constraintB = new Uint32Array(m);
5096
+ this.constraintKind = new Uint8Array(m);
5097
+ this.naturalLength = new Float64Array(m);
5098
+ this.restLength = new Float64Array(m);
5099
+ this.constraintStiffness = new Float64Array(m).fill(1);
5100
+ this.broken = new Uint8Array(m);
5101
+ this.restBend = new Float64Array(m);
5102
+ this.constraintMiddle = new Int32Array(m).fill(-1);
5103
+ for (let k = 0; k < m; k++) {
5104
+ const a = links[k * 3];
5105
+ const b = links[k * 3 + 1];
5106
+ this.constraintA[k] = a;
5107
+ this.constraintB[k] = b;
5108
+ this.constraintKind[k] = links[k * 3 + 2];
5109
+ if (this.constraintKind[k] === 2) this.constraintMiddle[k] = (a + b) / 2;
5110
+ const dx = this.positions[a * 3] - this.positions[b * 3];
5111
+ const dy = this.positions[a * 3 + 1] - this.positions[b * 3 + 1];
5112
+ const rest = Math.hypot(dx, dy);
5113
+ this.naturalLength[k] = rest;
5114
+ this.restLength[k] = rest;
5115
+ }
5116
+ this.invMass = new Float32Array(this.count).fill(1);
4989
5117
  const pin = (r, c) => {
4990
5118
  const i = idx(r, c);
4991
5119
  this.pinned[i] = 1;
@@ -5061,6 +5189,22 @@ var ClothSim = class {
5061
5189
  this.asleep = false;
5062
5190
  this.stillFrames = 0;
5063
5191
  }
5192
+ /**
5193
+ * Back to the sheet as it was laid out: flat, still, every particle where
5194
+ * the constructor put it. Pins, a grab and every lever are left alone.
5195
+ *
5196
+ * For a history that has been rewound — a burn played back from an earlier
5197
+ * moment, say, which hands back paper that had burnt away and paper that
5198
+ * had FALLEN, lying on the floor with its springs to the sheet suddenly
5199
+ * whole again. Solved from there, the solve hauls it up through the sheet
5200
+ * in a tangle. Laid out afresh, it is simply the sheet it is again.
5201
+ */
5202
+ restore() {
5203
+ this.positions.set(this.laidOut);
5204
+ this.prev.set(this.laidOut);
5205
+ this.accumulator = 0;
5206
+ this.wake();
5207
+ }
5064
5208
  /**
5065
5209
  * Take hold of the sheet at one particle.
5066
5210
  *
@@ -5103,7 +5247,7 @@ var ClothSim = class {
5103
5247
  for (let c = centreCol - spanCols; c <= centreCol + spanCols; c++) {
5104
5248
  if (c < 0 || c >= this.cols) continue;
5105
5249
  const i = r * this.cols + c;
5106
- if (this.pinned[i]) continue;
5250
+ if (this.pinned[i] || this.invMass[i] <= 0) continue;
5107
5251
  const distance = Math.hypot((c - centreCol) * cellX, (r - centreRow) * cellY);
5108
5252
  const weight = i === this.grabbedIndex ? 1 : grabFalloff(distance, radius);
5109
5253
  if (weight <= 0) continue;
@@ -5117,6 +5261,12 @@ var ClothSim = class {
5117
5261
  );
5118
5262
  }
5119
5263
  }
5264
+ if (this.grabbed.length === 0) {
5265
+ this.grabbedIndex = -1;
5266
+ this.grabOffsets = new Float32Array(0);
5267
+ this.wake();
5268
+ return -1;
5269
+ }
5120
5270
  this.grabOffsets = Float32Array.from(offsets);
5121
5271
  this.grabAt.set(this.positions.subarray(anchor, anchor + 3));
5122
5272
  this.grabWas.set(this.grabAt);
@@ -5277,12 +5427,19 @@ var ClothSim = class {
5277
5427
  const ax = nx * facing * (1 - AERO_TURBULENCE) + gust2 * 0.25 * AERO_TURBULENCE;
5278
5428
  const ay = ny * facing * (1 - AERO_TURBULENCE);
5279
5429
  const az = nz * facing * (1 - AERO_TURBULENCE) + gust2 * AERO_TURBULENCE;
5430
+ const im = this.invMass[i];
5431
+ if (im <= 0) {
5432
+ this.prev[i3] = x;
5433
+ this.prev[i3 + 1] = y;
5434
+ this.prev[i3 + 2] = z25;
5435
+ continue;
5436
+ }
5280
5437
  this.prev[i3] = x;
5281
5438
  this.prev[i3 + 1] = y;
5282
5439
  this.prev[i3 + 2] = z25;
5283
- p[i3] = x + vx + ax * dt2;
5284
- p[i3 + 1] = y + vy + (ay - gravity * 3.2) * dt2;
5285
- p[i3 + 2] = z25 + vz + az * dt2;
5440
+ p[i3] = x + vx + ax * im * dt2;
5441
+ p[i3 + 1] = y + vy + (ay * im - gravity * 3.2) * dt2;
5442
+ p[i3 + 2] = z25 + vz + az * im * dt2;
5286
5443
  maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
5287
5444
  }
5288
5445
  for (let k = 0; k < this.grabbed.length; k++) {
@@ -5295,21 +5452,38 @@ var ClothSim = class {
5295
5452
  p[i3 + axis] = p[i3 + axis] + (target - p[i3 + axis]) * held;
5296
5453
  }
5297
5454
  }
5455
+ const {
5456
+ constraintA,
5457
+ constraintB,
5458
+ constraintKind,
5459
+ restLength,
5460
+ constraintStiffness,
5461
+ broken,
5462
+ invMass,
5463
+ restBend,
5464
+ constraintMiddle,
5465
+ normals
5466
+ } = this;
5467
+ const bend2 = 0.25 + stiffness * 0.7;
5298
5468
  for (let iter = 0; iter < SOLVER_ITERATIONS2; iter++) {
5299
- for (const c of this.constraints) {
5300
- const k = c.kind === 2 ? 0.25 + stiffness * 0.7 : c.kind === 1 ? 0.85 : 1;
5301
- const a3 = c.a * 3;
5302
- const b3 = c.b * 3;
5469
+ for (let c = 0; c < this.constraintCount; c++) {
5470
+ if (broken[c]) continue;
5471
+ const kind = constraintKind[c];
5472
+ const k = (kind === 2 ? bend2 : kind === 1 ? 0.85 : 1) * constraintStiffness[c];
5473
+ const ia = constraintA[c];
5474
+ const ib = constraintB[c];
5475
+ const a3 = ia * 3;
5476
+ const b3 = ib * 3;
5303
5477
  const dx = p[b3] - p[a3];
5304
5478
  const dy = p[b3 + 1] - p[a3 + 1];
5305
5479
  const dz = p[b3 + 2] - p[a3 + 2];
5306
5480
  const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
5307
5481
  if (dist === 0) continue;
5308
- const ma = this.pinned[c.a] ? 0 : 1 - this.grabWeights[c.a];
5309
- const mb = this.pinned[c.b] ? 0 : 1 - this.grabWeights[c.b];
5482
+ const ma = this.pinned[ia] ? 0 : invMass[ia] * (1 - this.grabWeights[ia]);
5483
+ const mb = this.pinned[ib] ? 0 : invMass[ib] * (1 - this.grabWeights[ib]);
5310
5484
  const total = ma + mb;
5311
5485
  if (total <= 0) continue;
5312
- const diff = (dist - c.rest) / dist / total * k;
5486
+ const diff = (dist - restLength[c]) / dist / total * k;
5313
5487
  const aw = ma;
5314
5488
  const bw = mb;
5315
5489
  p[a3] = p[a3] + dx * diff * aw;
@@ -5318,6 +5492,31 @@ var ClothSim = class {
5318
5492
  p[b3] = p[b3] - dx * diff * bw;
5319
5493
  p[b3 + 1] = p[b3 + 1] - dy * diff * bw;
5320
5494
  p[b3 + 2] = p[b3 + 2] - dz * diff * bw;
5495
+ const sag = restBend[c];
5496
+ if (sag !== 0) {
5497
+ const im = constraintMiddle[c];
5498
+ const m3 = im * 3;
5499
+ const nx = normals[m3];
5500
+ const ny = normals[m3 + 1];
5501
+ const nz = normals[m3 + 2];
5502
+ const offset = (p[m3] - (p[a3] + p[b3]) / 2) * nx + (p[m3 + 1] - (p[a3 + 1] + p[b3 + 1]) / 2) * ny + (p[m3 + 2] - (p[a3 + 2] + p[b3 + 2]) / 2) * nz;
5503
+ const mm = this.pinned[im] ? 0 : invMass[im] * (1 - this.grabWeights[im]);
5504
+ const weight = mm + (ma + mb) / 4;
5505
+ if (weight > 0) {
5506
+ const lambda = (-sag - offset) / weight * k;
5507
+ const along = lambda * mm;
5508
+ const back = lambda / 2;
5509
+ p[m3] = p[m3] + nx * along;
5510
+ p[m3 + 1] = p[m3 + 1] + ny * along;
5511
+ p[m3 + 2] = p[m3 + 2] + nz * along;
5512
+ p[a3] = p[a3] - nx * back * ma;
5513
+ p[a3 + 1] = p[a3 + 1] - ny * back * ma;
5514
+ p[a3 + 2] = p[a3 + 2] - nz * back * ma;
5515
+ p[b3] = p[b3] - nx * back * mb;
5516
+ p[b3 + 1] = p[b3 + 1] - ny * back * mb;
5517
+ p[b3 + 2] = p[b3 + 2] - nz * back * mb;
5518
+ }
5519
+ }
5321
5520
  }
5322
5521
  }
5323
5522
  for (let i = 0; i < this.count; i++) {
@@ -5338,9 +5537,392 @@ var ClothSim = class {
5338
5537
  }
5339
5538
  };
5340
5539
 
5540
+ // src/physics/damage.ts
5541
+ var { char: CHAR, saturation: SATURATION, presence: PRESENCE } = DAMAGE_CHANNELS;
5542
+ var GONE_BELOW = 128;
5543
+ var CHAR_SHRINK = 0.12;
5544
+ var CHAR_CURL = 6;
5545
+ var FRONT_GRADIENT = 16 * 16;
5546
+ var WET_MASS = 2;
5547
+ var TEAR_STRETCH = 1.5;
5548
+ var DamageCoupling = class {
5549
+ sim;
5550
+ source = null;
5551
+ version = -1;
5552
+ size = 0;
5553
+ /** Each particle's texel, laid out for `size`. */
5554
+ particleTexel = new Int32Array(0);
5555
+ /** Gone, per particle, as of the last read. */
5556
+ gone;
5557
+ /** Whether any particle has ever been gone since the last reset — nothing can tear before. */
5558
+ holed = false;
5559
+ /** The gone particles with live paper beside them. See {@link follow}. */
5560
+ followers = [];
5561
+ /**
5562
+ * Which piece of paper each particle belongs to, named by the lowest index
5563
+ * in it — or -1 in the middle of a hole.
5564
+ *
5565
+ * A piece is whatever the sim's unbroken springs still hold together, which
5566
+ * is exactly what moves as one: two pieces are two things the solve lets
5567
+ * part. A gone particle on a cut takes the piece it borders MOST — a cut a
5568
+ * single particle wide borders two, and a particle averaged between them
5569
+ * would be dragged across the gap as one piece fell, stretching both edges
5570
+ * into it.
5571
+ */
5572
+ piece;
5573
+ /** Union-find over the particles, reused on every read. */
5574
+ parent;
5575
+ /**
5576
+ * Per bend spring, how squarely it lies ACROSS the burn front, 0..1 — and
5577
+ * remembered once it has been.
5578
+ *
5579
+ * A burnt edge rolls about an axis along the front, like a scroll; it does
5580
+ * not dish into a bowl. Measured with the curl in every bend spring alike: a
5581
+ * burnt band settled into a saddle, its corners forward and its middle BACK
5582
+ * (+0.07, −0.06), because a surface cannot curve two ways at once without
5583
+ * stretching and paper does not stretch. Curled across the front only, the
5584
+ * same band rolled toward the viewer along its whole edge (+0.13).
5585
+ *
5586
+ * "Across the front" is the direction the char is changing fastest in, at
5587
+ * the moment the front passes. It has to be remembered: once the front has
5588
+ * gone by, the char behind it is uniform and says nothing about direction,
5589
+ * but the paper stays rolled the way it rolled.
5590
+ */
5591
+ curlWeight;
5592
+ /** The mesh's index as it was built — what {@link tear} hides triangles from and mends them back to. */
5593
+ original = null;
5594
+ /** Per triangle of that index: torn, and hidden until the history is rewound. */
5595
+ torn = new Uint8Array(0);
5596
+ /** The triangles with a gone corner — the only ones that can tear. */
5597
+ rim = [];
5598
+ rimStale = true;
5599
+ /** Every torn triangle has to be put back: the history was rewound, or the source went. */
5600
+ mend = false;
5601
+ constructor(sim) {
5602
+ this.sim = sim;
5603
+ this.gone = new Uint8Array(sim.count);
5604
+ this.piece = new Int32Array(sim.count);
5605
+ this.parent = new Int32Array(sim.count);
5606
+ this.curlWeight = new Float32Array(sim.constraintCount);
5607
+ }
5608
+ /**
5609
+ * Bring the sim's levers up to date with `source`. Call before stepping.
5610
+ *
5611
+ * Returns whether anything was read. Free when the source has not changed,
5612
+ * and a null source hands every lever back at its default.
5613
+ */
5614
+ update(source) {
5615
+ const next = source ?? null;
5616
+ if (next === this.source && (next === null || next.version === this.version)) return false;
5617
+ if (next !== this.source) {
5618
+ this.curlWeight.fill(0);
5619
+ if (next && this.holed) {
5620
+ this.rewind();
5621
+ this.gone.fill(0);
5622
+ }
5623
+ }
5624
+ this.source = next;
5625
+ if (!next) {
5626
+ this.reset();
5627
+ return true;
5628
+ }
5629
+ if (next.size !== this.size) this.layout(next.size);
5630
+ this.version = next.version;
5631
+ this.read(next.pixels);
5632
+ this.sim.wake();
5633
+ return true;
5634
+ }
5635
+ /**
5636
+ * Carry the gone particles at the edge of a hole along with the paper beside
5637
+ * them. Call after stepping.
5638
+ *
5639
+ * Each one goes where its live neighbours say it would be if the grid were
5640
+ * laid out flat around them — their positions plus its rest offset from
5641
+ * each, averaged. The triangles that straddle the burnt edge then keep
5642
+ * roughly their shape instead of stretching back to wherever the particle
5643
+ * was when it burnt. Only the neighbours in its own {@link piece} count, so
5644
+ * on a cut between two pieces it leaves with one of them.
5645
+ */
5646
+ follow() {
5647
+ if (this.followers.length === 0) return;
5648
+ const { gone, sim } = this;
5649
+ const piece = this.piece;
5650
+ const p = sim.positions;
5651
+ const { cols, rows } = sim;
5652
+ const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
5653
+ const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
5654
+ for (const g of this.followers) {
5655
+ const r = g / cols | 0;
5656
+ const c = g % cols;
5657
+ const own = piece[g];
5658
+ let sx = 0;
5659
+ let sy = 0;
5660
+ let sz = 0;
5661
+ let n = 0;
5662
+ for (let dr = -1; dr <= 1; dr++) {
5663
+ const nr = r + dr;
5664
+ if (nr < 0 || nr >= rows) continue;
5665
+ for (let dc = -1; dc <= 1; dc++) {
5666
+ const nc = c + dc;
5667
+ if (dr === 0 && dc === 0 || nc < 0 || nc >= cols) continue;
5668
+ const j = nr * cols + nc;
5669
+ if (gone[j] || piece[j] !== own) continue;
5670
+ const j3 = j * 3;
5671
+ sx += p[j3] - dc * cellX;
5672
+ sy += p[j3 + 1] + dr * cellY;
5673
+ sz += p[j3 + 2];
5674
+ n++;
5675
+ }
5676
+ }
5677
+ if (n === 0) continue;
5678
+ const g3 = g * 3;
5679
+ p[g3] = sx / n;
5680
+ p[g3 + 1] = sy / n;
5681
+ p[g3 + 2] = sz / n;
5682
+ }
5683
+ }
5684
+ /**
5685
+ * Hide the triangles a piece has torn away from. Call after {@link follow},
5686
+ * with the sheet's index buffer: it is rewritten in place, and the return
5687
+ * says whether it changed, so the caller knows to upload it.
5688
+ *
5689
+ * A triangle tears when one of its edges has stretched past
5690
+ * {@link TEAR_STRETCH} — which, among triangles with a burnt-away corner,
5691
+ * only ever happens across a gap that is opening. It is then collapsed to a
5692
+ * point, which draws nothing, casts no shadow and adds nothing to its
5693
+ * corners' normals, and it stays hidden: torn paper does not mend because
5694
+ * the two sides swing close again. Only a rewound history puts it back.
5695
+ *
5696
+ * A sheet in one piece, held, never stretches one that far — so a burn that
5697
+ * cuts nothing loose leaves this index exactly as it was built.
5698
+ */
5699
+ tear(index) {
5700
+ if (!this.holed && !this.mend) return false;
5701
+ if (!this.original || this.original.length !== index.length) {
5702
+ this.original = index.slice();
5703
+ this.torn = new Uint8Array(index.length / 3);
5704
+ this.rimStale = true;
5705
+ }
5706
+ const { original, torn, gone, sim } = this;
5707
+ let changed = false;
5708
+ if (this.mend) {
5709
+ this.mend = false;
5710
+ index.set(original);
5711
+ changed = true;
5712
+ }
5713
+ if (this.rimStale) {
5714
+ this.rimStale = false;
5715
+ const rim = [];
5716
+ for (let t = 0; t < torn.length; t++) {
5717
+ if (gone[original[t * 3]] || gone[original[t * 3 + 1]] || gone[original[t * 3 + 2]]) rim.push(t);
5718
+ }
5719
+ this.rim = rim;
5720
+ }
5721
+ const p = sim.positions;
5722
+ const { cols, rows } = sim;
5723
+ const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
5724
+ const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
5725
+ const limit = TEAR_STRETCH * TEAR_STRETCH;
5726
+ const stretched = (i, j) => {
5727
+ const dc = i % cols - j % cols;
5728
+ const dr = (i / cols | 0) - (j / cols | 0);
5729
+ const rest = (dc * cellX) ** 2 + (dr * cellY) ** 2;
5730
+ const i3 = i * 3;
5731
+ const j3 = j * 3;
5732
+ const now = (p[i3] - p[j3]) ** 2 + (p[i3 + 1] - p[j3 + 1]) ** 2 + (p[i3 + 2] - p[j3 + 2]) ** 2;
5733
+ return now > rest * limit;
5734
+ };
5735
+ for (const t of this.rim) {
5736
+ if (torn[t]) continue;
5737
+ const a = original[t * 3];
5738
+ const b = original[t * 3 + 1];
5739
+ const c = original[t * 3 + 2];
5740
+ if (!stretched(a, b) && !stretched(b, c) && !stretched(c, a)) continue;
5741
+ torn[t] = 1;
5742
+ index[t * 3 + 1] = a;
5743
+ index[t * 3 + 2] = a;
5744
+ changed = true;
5745
+ }
5746
+ return changed;
5747
+ }
5748
+ /** Map each particle to its texel. The damage grid's row 0 is v = 0, the sheet's BOTTOM. */
5749
+ layout(size) {
5750
+ const { cols, rows, count } = this.sim;
5751
+ const last = size - 1;
5752
+ this.size = size;
5753
+ this.particleTexel = new Int32Array(count);
5754
+ for (let r = 0; r < rows; r++) {
5755
+ for (let c = 0; c < cols; c++) {
5756
+ const u = cols > 1 ? c / (cols - 1) : 0.5;
5757
+ const v = rows > 1 ? 1 - r / (rows - 1) : 0.5;
5758
+ this.particleTexel[r * cols + c] = Math.round(v * last) * size + Math.round(u * last);
5759
+ }
5760
+ }
5761
+ }
5762
+ read(pixels) {
5763
+ const { sim, gone, particleTexel, curlWeight, size } = this;
5764
+ const {
5765
+ invMass,
5766
+ restLength,
5767
+ naturalLength,
5768
+ restBend,
5769
+ broken,
5770
+ constraintA,
5771
+ constraintB,
5772
+ constraintKind,
5773
+ constraintMiddle
5774
+ } = sim;
5775
+ let rewound = false;
5776
+ let holed = false;
5777
+ for (let i = 0; i < sim.count; i++) {
5778
+ const t = particleTexel[i] * 4;
5779
+ const isGone = pixels[t + PRESENCE] < GONE_BELOW;
5780
+ if (gone[i] && !isGone) rewound = true;
5781
+ if (isGone) holed = true;
5782
+ gone[i] = isGone ? 1 : 0;
5783
+ invMass[i] = isGone ? 0 : 1 / (1 + WET_MASS * pixels[t + SATURATION] / 255);
5784
+ }
5785
+ if (rewound) this.rewind();
5786
+ this.holed = holed;
5787
+ this.rimStale = true;
5788
+ for (let k = 0; k < sim.constraintCount; k++) {
5789
+ const a = constraintA[k];
5790
+ const b = constraintB[k];
5791
+ const m = constraintMiddle[k];
5792
+ if (gone[a] || gone[b] || m >= 0 && gone[m]) {
5793
+ broken[k] = 1;
5794
+ continue;
5795
+ }
5796
+ broken[k] = 0;
5797
+ let charSum = pixels[particleTexel[a] * 4 + CHAR] + pixels[particleTexel[b] * 4 + CHAR];
5798
+ let samples = 2;
5799
+ if (m >= 0) {
5800
+ charSum += pixels[particleTexel[m] * 4 + CHAR];
5801
+ samples = 3;
5802
+ }
5803
+ const char = charSum / (samples * 255);
5804
+ restLength[k] = naturalLength[k] * (1 - CHAR_SHRINK * char);
5805
+ if (constraintKind[k] === 2) {
5806
+ const t = particleTexel[m];
5807
+ const x = t % size;
5808
+ const y = t / size | 0;
5809
+ const gx = pixels[(y * size + Math.min(size - 1, x + 2)) * 4 + CHAR] - pixels[(y * size + Math.max(0, x - 2)) * 4 + CHAR];
5810
+ const gy = pixels[(Math.min(size - 1, y + 2) * size + x) * 4 + CHAR] - pixels[(Math.max(0, y - 2) * size + x) * 4 + CHAR];
5811
+ const steep = gx * gx + gy * gy;
5812
+ if (steep > FRONT_GRADIENT) {
5813
+ const across = (b - a === 2 ? gx * gx : gy * gy) / steep;
5814
+ if (across > curlWeight[k]) curlWeight[k] = across;
5815
+ }
5816
+ const span = naturalLength[k];
5817
+ restBend[k] = CHAR_CURL * char * curlWeight[k] * span * span / 8;
5818
+ }
5819
+ }
5820
+ this.findPieces();
5821
+ }
5822
+ /**
5823
+ * Which piece each particle is in ({@link piece}), and which gone ones
5824
+ * follow a piece at all ({@link followers}).
5825
+ *
5826
+ * Pieces by union-find over the springs that still hold, rooted at the
5827
+ * lowest index so the same burn names the same pieces every time. Then each
5828
+ * gone particle with live paper beside it joins the piece it has the most
5829
+ * neighbours in, the lower-named on a tie.
5830
+ */
5831
+ findPieces() {
5832
+ const { sim, gone } = this;
5833
+ const parent = this.parent;
5834
+ const piece = this.piece;
5835
+ const { broken, constraintA, constraintB } = sim;
5836
+ for (let i = 0; i < sim.count; i++) parent[i] = i;
5837
+ const find = (i) => {
5838
+ let r = i;
5839
+ while (parent[r] !== r) r = parent[r];
5840
+ while (parent[i] !== r) {
5841
+ const up = parent[i];
5842
+ parent[i] = r;
5843
+ i = up;
5844
+ }
5845
+ return r;
5846
+ };
5847
+ for (let k = 0; k < sim.constraintCount; k++) {
5848
+ if (broken[k]) continue;
5849
+ const ra = find(constraintA[k]);
5850
+ const rb = find(constraintB[k]);
5851
+ if (ra < rb) parent[rb] = ra;
5852
+ else if (rb < ra) parent[ra] = rb;
5853
+ }
5854
+ for (let i = 0; i < sim.count; i++) piece[i] = gone[i] ? -1 : find(i);
5855
+ const followers = [];
5856
+ const { cols, rows } = sim;
5857
+ const seen = [];
5858
+ const votes = [];
5859
+ for (let i = 0; i < sim.count; i++) {
5860
+ if (!gone[i]) continue;
5861
+ const r = i / cols | 0;
5862
+ const c = i % cols;
5863
+ seen.length = 0;
5864
+ votes.length = 0;
5865
+ for (let dr = -1; dr <= 1; dr++) {
5866
+ const nr = r + dr;
5867
+ if (nr < 0 || nr >= rows) continue;
5868
+ for (let dc = -1; dc <= 1; dc++) {
5869
+ const nc = c + dc;
5870
+ if (nc < 0 || nc >= cols) continue;
5871
+ const j = nr * cols + nc;
5872
+ if (gone[j]) continue;
5873
+ const at = seen.indexOf(piece[j]);
5874
+ if (at < 0) {
5875
+ seen.push(piece[j]);
5876
+ votes.push(1);
5877
+ } else {
5878
+ votes[at]++;
5879
+ }
5880
+ }
5881
+ }
5882
+ if (seen.length === 0) continue;
5883
+ let best = 0;
5884
+ for (let k = 1; k < seen.length; k++) {
5885
+ if (votes[k] > votes[best] || votes[k] === votes[best] && seen[k] < seen[best]) best = k;
5886
+ }
5887
+ piece[i] = seen[best];
5888
+ followers.push(i);
5889
+ }
5890
+ this.followers = followers;
5891
+ }
5892
+ /**
5893
+ * A rewound history: the sheet starts over.
5894
+ *
5895
+ * Paper that had fallen away is paper again, with every spring back to the
5896
+ * sheet it was cut from — solved from where it lies, the solve would haul it
5897
+ * up off the floor through the sheet in a tangle. And the curl it rolled
5898
+ * into belonged to a front that has not passed yet.
5899
+ */
5900
+ rewind() {
5901
+ this.sim.restore();
5902
+ this.curlWeight.fill(0);
5903
+ this.torn.fill(0);
5904
+ this.mend = true;
5905
+ }
5906
+ reset() {
5907
+ const { sim } = this;
5908
+ if (this.holed) this.rewind();
5909
+ sim.invMass.fill(1);
5910
+ sim.restBend.fill(0);
5911
+ sim.broken.fill(0);
5912
+ sim.restLength.set(sim.naturalLength);
5913
+ this.gone.fill(0);
5914
+ this.curlWeight.fill(0);
5915
+ this.followers = [];
5916
+ this.holed = false;
5917
+ this.version = -1;
5918
+ sim.wake();
5919
+ }
5920
+ };
5921
+
5341
5922
  // src/surface/PaperMaterial.tsx
5342
- import * as THREE6 from "three";
5343
- import { useEffect as useEffect4, useMemo as useMemo2 } from "react";
5923
+ import * as THREE7 from "three";
5924
+ import { useEffect as useEffect5, useMemo as useMemo3 } from "react";
5925
+ import { useFrame as useFrame2 } from "@react-three/fiber";
5344
5926
  import CustomShaderMaterial from "three-custom-shader-material";
5345
5927
 
5346
5928
  // src/surface/compose.ts
@@ -5753,6 +6335,518 @@ void plPerforation(inout vec4 color) {
5753
6335
  }
5754
6336
  `
5755
6337
  );
6338
+ var DAMAGE_CHUNK = (
6339
+ /* glsl */
6340
+ `
6341
+ uniform sampler2D uDamage;
6342
+ uniform float uDamageDetail;
6343
+ // (edgeWave mm, edgeBite mm, sparkle, \u2013) \u2014 see DamageLook.
6344
+ uniform vec4 uLook3;
6345
+
6346
+ // The damage grid's texels sit ON the sheet \u2014 texel x at u = x / (N - 1), its
6347
+ // corners on the sheet's corners, which is how the field paints and how the
6348
+ // physics reads it. A texture puts texel x at (x + 0.5) / N. Sampled straight,
6349
+ // the picture of a burn sat up to half a texel off the burn the paper feels.
6350
+ vec2 plDamageUv(vec2 uv) {
6351
+ vec2 n = vec2(textureSize(uDamage, 0));
6352
+ // Texel space, on the field's convention: texel x at u = x / (N - 1).
6353
+ vec2 t = uv * (n - 1.0) + 0.5;
6354
+ // Quintic-smoothed texel coordinates.
6355
+ //
6356
+ // Bilinear filtering is continuous but its DERIVATIVE is not: the blend
6357
+ // weight is a straight line inside each texel and turns a corner at every
6358
+ // boundary. Magnify a 64 grid to fill a macro crop \u2014 one cell is 4.9 mm of
6359
+ // A4, about 58 px there \u2014 and those corners fall on a 58 px lattice, so
6360
+ // every ramp drawn from this field can crease along it.
6361
+ //
6362
+ // Also tried against the contour rings, and also not their cause (that was
6363
+ // the scorch fingers). Kept because it costs nothing and the crease is
6364
+ // real:
6365
+ // Bending the fractional part through a quintic (6t^5 - 15t^4 + 10t^3,
6366
+ // whose first and second derivatives vanish at both ends) makes the
6367
+ // interpolation C2 without a single extra tap: the hardware still does one
6368
+ // bilinear fetch, it is just asked for a smoothed position. The sample at a
6369
+ // texel's centre is unchanged, so the half-texel alignment this function
6370
+ // exists for still holds \u2014 and a field nothing has happened to reads 1
6371
+ // everywhere whatever the position, so the untouched-sheet identity is
6372
+ // untouched too.
6373
+ vec2 i = floor(t - 0.5) + 0.5;
6374
+ vec2 f = t - i;
6375
+ f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
6376
+ return (i + f) / n;
6377
+ }
6378
+
6379
+ /**
6380
+ * The four channels here, frayed finer than the grid.
6381
+ *
6382
+ * One cell of the field is ~3.3 mm of A4, and a burnt edge is jagged at the
6383
+ * scale of a fibre (0.3\u20132 mm) \u2014 so the grid decides roughly WHERE and this
6384
+ * decides exactly where. Stretched along the fibre (the field's default grain
6385
+ * runs along u), so a tear follows the grain. Scaled by what is already
6386
+ * there, so pristine paper (char 0, presence 1) is untouched to the bit, and
6387
+ * skipped outright at detail 0: the zones still draw on every tier, only
6388
+ * without the finest octaves.
6389
+ */
6390
+ /**
6391
+ * Where on the grid this fragment reads its damage \u2014 warped.
6392
+ *
6393
+ * A burnt hole is never a circle: paper burns unevenly, and its edge wobbles
6394
+ * in long waves with small bites in and out. The field is too coarse to hold
6395
+ * that, so the READ is displaced instead: two octaves of noise in the sheet's
6396
+ * own space, a long wave of up to \xB16 mm and short bites of \xB12 mm. Every zone \u2014
6397
+ * the cut, the lip, the ember line, the char, the scorch \u2014 reads through the
6398
+ * same warp, so they all follow one uneven edge. Warping a field nothing has
6399
+ * happened to reads the same untouched value everywhere, so the identity
6400
+ * holds.
6401
+ */
6402
+ vec2 plDamageWarpAt(vec2 paperUv) {
6403
+ vec2 p = (paperUv - 0.5) * uSheetSize;
6404
+ const float mm = 1.0 / 210.0;
6405
+ vec2 wave = vec2(plFbm(p * 26.0 + 3.1), plFbm(p * 26.0 + 17.7)) - 0.5;
6406
+ vec2 bite = vec2(plNoise(p * 95.0 + 5.3), plNoise(p * 95.0 + 41.9)) - 0.5;
6407
+ vec2 shift = (wave * 2.0 * uLook3.x + bite * 2.0 * uLook3.y) * mm;
6408
+ return plDamageUv(paperUv + shift / uSheetSize);
6409
+ }
6410
+
6411
+ vec2 plDamageWarp() {
6412
+ return plDamageWarpAt(vPaperUv);
6413
+ }
6414
+
6415
+ /** Presence softened over five taps \u2014 see \`plDamageRead\`. */
6416
+ float plSoftPresence(vec2 uv) {
6417
+ vec2 texel = 0.75 / vec2(textureSize(uDamage, 0));
6418
+ return (2.0 * texture2D(uDamage, uv).a
6419
+ + texture2D(uDamage, uv + vec2(texel.x, 0.0)).a + texture2D(uDamage, uv - vec2(texel.x, 0.0)).a
6420
+ + texture2D(uDamage, uv + vec2(0.0, texel.y)).a + texture2D(uDamage, uv - vec2(0.0, texel.y)).a) / 6.0;
6421
+ }
6422
+
6423
+ /** The fray and the fibre at a point of the sheet (local units), scaled by detail. */
6424
+ vec2 plFray(vec2 p) {
6425
+ return vec2(
6426
+ (plFbm(vec2(p.x * 22.0, p.y * 48.0)) - 0.47) * uDamageDetail,
6427
+ (plNoise(vec2(p.x * 140.0, p.y * 420.0)) - 0.5) * uDamageDetail
6428
+ );
6429
+ }
6430
+
6431
+ /**
6432
+ * The presence the cut is drawn from, at any point of the paper: warped,
6433
+ * softened and frayed exactly as \`plDamageRead\` does it for this fragment.
6434
+ */
6435
+ float plDrawnPresenceAt(vec2 paperUv) {
6436
+ float a = plSoftPresence(plDamageWarpAt(paperUv));
6437
+ if (uDamageDetail > 0.0) {
6438
+ vec2 ff = plFray((paperUv - 0.5) * uSheetSize);
6439
+ a = clamp(a + (ff.x * 0.36 + ff.y * 0.05) * (1.0 - smoothstep(0.9, 1.0, a)), 0.0, 1.0);
6440
+ }
6441
+ return a;
6442
+ }
6443
+
6444
+ // The softened presence before the fray, from the last \`plDamageRead\`: what
6445
+ // the distance to the cut takes its slope from (see \`plDamage\`).
6446
+ float plDamageSmooth;
6447
+
6448
+ vec4 plDamageRead() {
6449
+ vec2 uv = plDamageWarp();
6450
+ vec4 d = texture2D(uDamage, uv);
6451
+ // The cut runs on a SOFTENED presence. Paper burns through within a single
6452
+ // texel now, so the raw field is hard-edged and its half-presence contour
6453
+ // is the grid's own staircase \u2014 the fray below can only move a line within
6454
+ // the band it is given, and a one-texel band is not enough to hide a grid.
6455
+ // Five taps round the contour; an untouched field is 1 everywhere and
6456
+ // stays exactly 1.
6457
+ d.a = plSoftPresence(uv);
6458
+ plDamageSmooth = d.a;
6459
+ if (uDamageDetail > 0.0) {
6460
+ vec2 ff = plFray(plLocal());
6461
+ float fray = ff.x;
6462
+ float fibre = ff.y;
6463
+ float edge = 1.0 - smoothstep(0.9, 1.0, d.a);
6464
+ d.r = clamp(d.r + fray * 0.3 * smoothstep(0.0, 0.15, d.r), 0.0, 1.0);
6465
+ d.a = clamp(d.a + (fray * 0.36 + fibre * 0.05) * edge, 0.0, 1.0);
6466
+ }
6467
+ return d;
6468
+ }
6469
+
6470
+ void plDamageCut(inout vec4 color, vec4 d) {
6471
+ // Missing paper: gone at half presence, on every stock alike.
6472
+ color.a *= step(0.5, d.a);
6473
+ }
6474
+ `
6475
+ );
6476
+ var DAMAGE_SHADE_CHUNK = (
6477
+ /* glsl */
6478
+ `
6479
+ uniform float uDamageTime;
6480
+ // The look, packed \u2014 see DamageLook for what each number means.
6481
+ uniform vec4 uLook0; // emberWidth mm, emberIntensity, emberCoverage, emberFlicker
6482
+ uniform vec4 uLook1; // emberGlow, lipWidth mm, lipBrightness, charWarmth
6483
+ uniform vec4 uLook2; // charCracks, scorchReach mm, scorchDarkness, fingers
6484
+ /** The light the burn gives off from the sheet itself: the ember line, and nothing else. */
6485
+ vec3 plDamageLight;
6486
+
6487
+ // World units per millimetre: a default sheet is one unit across, and A4 is 210 mm.
6488
+ const float PL_MM = 1.0 / 210.0;
6489
+
6490
+ vec3 plLinear(vec3 srgb) {
6491
+ return pow(srgb, vec3(2.2));
6492
+ }
6493
+
6494
+ /**
6495
+ * World up, as a direction in the sheet's UV, from how this fragment's UV and
6496
+ * view position change across the screen. Must be called in uniform control
6497
+ * flow \u2014 it is all derivatives.
6498
+ */
6499
+ vec2 plUpUv() {
6500
+ vec3 dpx = dFdx(-vViewPosition);
6501
+ vec3 dpy = dFdy(-vViewPosition);
6502
+ vec2 dux = dFdx(vPaperUv);
6503
+ vec2 duy = dFdy(vPaperUv);
6504
+ float det = dux.x * duy.y - dux.y * duy.x;
6505
+ if (abs(det) < 1e-14) return vec2(0.0, 1.0);
6506
+ vec3 dPdu = (dpx * duy.y - dpy * dux.y) / det;
6507
+ vec3 dPdv = (dpy * dux.x - dpx * duy.x) / det;
6508
+ vec3 up = (viewMatrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz;
6509
+ vec2 g = vec2(dot(up, dPdu) / max(dot(dPdu, dPdu), 1e-12), dot(up, dPdv) / max(dot(dPdv, dPdv), 1e-12));
6510
+ float l = length(g);
6511
+ return l > 1e-6 ? g / l : vec2(0.0, 1.0);
6512
+ }
6513
+
6514
+ /** F1 and F2 of a cell noise \u2014 the crack network in char. */
6515
+ vec2 plVoronoi(vec2 p) {
6516
+ vec2 i = floor(p);
6517
+ vec2 f = fract(p);
6518
+ float f1 = 8.0;
6519
+ float f2 = 8.0;
6520
+ for (int y = -1; y <= 1; y++) {
6521
+ for (int x = -1; x <= 1; x++) {
6522
+ vec2 o = vec2(float(x), float(y));
6523
+ vec2 r = o + vec2(plHash(i + o), plHash(i + o + 17.31)) - f;
6524
+ float dd = dot(r, r);
6525
+ if (dd < f1) {
6526
+ f2 = f1;
6527
+ f1 = dd;
6528
+ } else if (dd < f2) {
6529
+ f2 = dd;
6530
+ }
6531
+ }
6532
+ }
6533
+ return sqrt(vec2(f1, f2));
6534
+ }
6535
+
6536
+ /** The scorch ramp, sampled off Scorch.png and Hero.png, as a tint over paper. 0 is the leading edge. */
6537
+ vec3 plScorchTint(float t) {
6538
+ vec3 paper = plLinear(vec3(0.90, 0.885, 0.855));
6539
+ vec3 c0 = plLinear(vec3(0.843, 0.741, 0.569)); // #D7BD91
6540
+ vec3 c1 = plLinear(vec3(0.745, 0.514, 0.263)); // #BE8343
6541
+ vec3 c2 = plLinear(vec3(0.631, 0.384, 0.180)); // #A1622E
6542
+ vec3 c3 = plLinear(vec3(0.541, 0.310, 0.141)); // #8A4F24
6543
+ vec3 c4 = plLinear(vec3(0.471, 0.275, 0.149)); // #784626
6544
+ vec3 c5 = plLinear(vec3(0.380, 0.216, 0.133)); // #613722
6545
+ vec3 c6 = plLinear(vec3(0.310, 0.200, 0.137)); // #4F3323
6546
+ float s = clamp(t, 0.0, 1.0) * 6.0;
6547
+ // Each stop is blended with a SMOOTH step, not a linear one.
6548
+ //
6549
+ // Seven colours joined by six straight segments is a ramp with a corner at
6550
+ // every joint: the colour is continuous there but its rate of change is
6551
+ // not, and the eye reads a discontinuous gradient as a faint line (Mach
6552
+ // banding). smoothstep's derivative is zero at both ends of a segment, so
6553
+ // consecutive segments meet with matching slope and there is no corner.
6554
+ //
6555
+ // Cheap insurance, not a bug fix \u2014 this was tried first against the macro
6556
+ // crops' contour rings and did NOT remove them; the cause turned out to be
6557
+ // the scorch fingers below, sampled in a frame that follows the contours.
6558
+ float k = smoothstep(0.0, 1.0, clamp(s, 0.0, 1.0));
6559
+ vec3 c = mix(c0, c1, k);
6560
+ c = mix(c, c2, smoothstep(0.0, 1.0, clamp(s - 1.0, 0.0, 1.0)));
6561
+ c = mix(c, c3, smoothstep(0.0, 1.0, clamp(s - 2.0, 0.0, 1.0)));
6562
+ c = mix(c, c4, smoothstep(0.0, 1.0, clamp(s - 3.0, 0.0, 1.0)));
6563
+ c = mix(c, c5, smoothstep(0.0, 1.0, clamp(s - 4.0, 0.0, 1.0)));
6564
+ c = mix(c, c6, smoothstep(0.0, 1.0, clamp(s - 5.0, 0.0, 1.0)));
6565
+ return min(c / paper, vec3(1.0));
6566
+ }
6567
+
6568
+ /** The ember ramp (\xA75 zone 3): off \u2192 #870E03 \u2192 #A12108 \u2192 #F77E14 \u2192 #FEFBE0, hottest last. */
6569
+ vec3 plEmberRamp(float t) {
6570
+ vec3 c0 = plLinear(vec3(0.529, 0.055, 0.012));
6571
+ vec3 c1 = plLinear(vec3(0.631, 0.129, 0.031));
6572
+ vec3 c2 = plLinear(vec3(0.969, 0.494, 0.078));
6573
+ vec3 c3 = plLinear(vec3(0.996, 0.984, 0.878));
6574
+ vec3 c = mix(c0, c1, smoothstep(0.0, 0.3, t));
6575
+ c = mix(c, c2, smoothstep(0.3, 0.7, t));
6576
+ return mix(c, c3, smoothstep(0.75, 1.0, t));
6577
+ }
6578
+
6579
+ void plDamage(inout vec4 color, inout float roughness) {
6580
+ plDamageLight = vec3(0.0);
6581
+ vec4 d = plDamageRead();
6582
+ vec2 uv = plDamageWarp();
6583
+ vec2 texel = 1.0 / vec2(textureSize(uDamage, 0));
6584
+
6585
+ // Everything measured off screen derivatives, up front: derivatives are
6586
+ // undefined past a branch that only some fragments of a quad take.
6587
+ vec2 upUv = plUpUv();
6588
+ vec4 e = texture2D(uDamage, uv + vec2(texel.x, 0.0));
6589
+ vec4 w = texture2D(uDamage, uv - vec2(texel.x, 0.0));
6590
+ vec4 n = texture2D(uDamage, uv + vec2(0.0, texel.y));
6591
+ vec4 s = texture2D(uDamage, uv - vec2(0.0, texel.y));
6592
+ // Per world unit: one texel step is 1 / (N - 1) of the sheet.
6593
+ vec2 perWorld = (vec2(textureSize(uDamage, 0)) - 1.0) / uSheetSize;
6594
+ vec2 gradP = vec2(e.a - w.a, n.a - s.a) * 0.5 * perWorld;
6595
+ vec2 gradC = vec2(e.r - w.r, n.r - s.r) * 0.5 * perWorld;
6596
+ // Millimetres from the cut, on the paper side \u2014 measured against the field
6597
+ // the cut is DRAWN from (softened, frayed, warped), through its own screen
6598
+ // derivatives. It used to divide by the slope of the raw texels, and since
6599
+ // paper burns through within a texel that slope is a cliff while the drawn
6600
+ // edge is spread over millimetres: a point well into the char computed as a
6601
+ // hair from the cut, and the ember line drew there \u2014 found by the gate.
6602
+ // The slope, though, is the SMOOTH presence's. The fray's fibre noise
6603
+ // changes pixel to pixel, and its slope is a cliff everywhere it touches:
6604
+ // divided by that, a point deep in the char came out a hair from the cut
6605
+ // and the ember glow drew there \u2014 the gate's stray pixels. The fray still
6606
+ // moves where the edge is; it just cannot fake how steep it is.
6607
+ float smoothA = plDamageSmooth;
6608
+ vec2 cutSlope = vec2(dFdx(smoothA), dFdy(smoothA));
6609
+ float pxFromCut = (d.a - 0.5) / max(length(cutSlope), 1e-4);
6610
+ // Millimetres per pixel ALONG the way to the cut, not across the screen's
6611
+ // x: a sheet leaning away is foreshortened vertically, so above and below a
6612
+ // hole a pixel covers more paper than beside it, and a scale taken from x
6613
+ // alone drew the ember zone too many pixels deep on the top and bottom rims.
6614
+ vec2 toCutPx = cutSlope / max(length(cutSlope), 1e-6);
6615
+ float mmPerPx = length(dFdx(-vViewPosition) * toCutPx.x + dFdy(-vViewPosition) * toCutPx.y) / PL_MM;
6616
+ float mm = pxFromCut * mmPerPx;
6617
+ // About a pixel of antialiasing, never more: where the warp makes the
6618
+ // estimate jump between neighbours, fwidth(mm) is millimetres wide, and the
6619
+ // ember's soft tail \u2014 faint, but multiplied by its light \u2014 drew stray glow
6620
+ // well out into the char.
6621
+ float aa = min(fwidth(mm), 1.5 * mmPerPx);
6622
+ // Scorch reaches ahead of the char, and further UP than anywhere else:
6623
+ // the char a centimetre below this point, and the char a few millimetres
6624
+ // around it, each lent to the scorch at a discount (\xA74.2, \xA74.5).
6625
+ vec2 mmUv = vec2(PL_MM) / uSheetSize;
6626
+ float charBelow = max(texture2D(uDamage, uv - upUv * uLook2.y * mmUv).r, texture2D(uDamage, uv - upUv * 0.5 * uLook2.y * mmUv).r);
6627
+ // A ring of eight, averaged: four diagonal maxima drew the grid back in as
6628
+ // squares wherever the scorch was magnified.
6629
+ float charWide = 0.0;
6630
+ for (int k = 0; k < 8; k++) {
6631
+ float a = float(k) * 0.7853982;
6632
+ charWide += texture2D(uDamage, uv + vec2(cos(a), sin(a)) * 7.0 * mmUv).r;
6633
+ }
6634
+ charWide /= 8.0;
6635
+ // Only paper that has a CUT beside it has an edge: a half-eaten cell out
6636
+ // in the middle of the sheet reads as "half a presence" too, and without
6637
+ // this the ember line and the ash lip drew on it \u2014 islands of glow on
6638
+ // paper, the painted glow by another route.
6639
+ // Eased in, not switched: a hard threshold on the neighbouring texels drew
6640
+ // the grid back into the ember glow as small squares.
6641
+ float cutNear = 1.0 - smoothstep(0.3, 0.62, min(min(e.a, w.a), min(n.a, s.a)));
6642
+ // And the cut has to really be there. \`mm\` extrapolates this pixel's
6643
+ // slope to half presence, and the uneven edge's warp bends the field inside
6644
+ // those few millimetres \u2014 measured: with the warp off the stray ember light
6645
+ // the gate finds all but vanishes. So step across the PAPER toward where the
6646
+ // cut should be, half a millimetre past it but never beyond the ember
6647
+ // zone's own outer edge (the lip and the widest bead \u2014 2.5 mm at the
6648
+ // spec's numbers, \xA75) plus that half, and read what the cut is drawn from.
6649
+ // Still paper: this is not an edge, however close the slope said it was.
6650
+ vec2 toCutPaper = -(dFdx(vPaperUv) * toCutPx.x + dFdy(vPaperUv) * toCutPx.y);
6651
+ float reachPx = min(max(mm, 0.0) + 0.5, uLook1.y + uLook0.x + 0.5) / max(mmPerPx, 1e-6);
6652
+ float pastA = plDrawnPresenceAt(vPaperUv + toCutPaper * reachPx);
6653
+ // And a hole, not a pinhole: the fray opens specks of void finer than a
6654
+ // pixel in part-burnt paper, which the check above would take for the
6655
+ // edge. The void has to go on for another half millimetre.
6656
+ float beyondA = plDrawnPresenceAt(vPaperUv + toCutPaper * (reachPx + 0.5 / max(mmPerPx, 1e-6)));
6657
+ cutNear *= (1.0 - smoothstep(0.42, 0.55, pastA)) * (1.0 - smoothstep(0.42, 0.55, beyondA));
6658
+
6659
+ // An untouched texel is left exactly as it was \u2014 the identity the whole
6660
+ // seam is built on. Nothing here has happened to this paper.
6661
+ if (d.r <= 0.0 && d.g <= 0.0 && d.a >= 1.0 && charBelow <= 0.0 && charWide <= 0.0 && e.a >= 1.0 && w.a >= 1.0 && n.a >= 1.0 && s.a >= 1.0) {
6662
+ return;
6663
+ }
6664
+
6665
+ vec2 p = plLocal();
6666
+
6667
+ // Wet paper is darker and smoother: water fills the gaps between fibres
6668
+ // that scatter light, which is both effects from one cause.
6669
+ color.rgb *= 1.0 - 0.38 * d.g;
6670
+ roughness *= 1.0 - 0.45 * d.g;
6671
+
6672
+ // \u2500\u2500 Scorch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6673
+ // Fingers: irregular teeth ALONG the scorch front, plus the grain's own
6674
+ // streaks. Measured along the front's tangent so they point outward.
6675
+ vec2 tangentC = length(gradC) > 1e-4 ? normalize(vec2(-gradC.y, gradC.x)) : vec2(1.0, 0.0);
6676
+ float along = dot(p, tangentC) / (4.0 * PL_MM);
6677
+ // Only where there really IS a front.
6678
+ //
6679
+ // This noise is sampled in a frame built from the char gradient, so that
6680
+ // its teeth run across the front wherever the front happens to point. That
6681
+ // works at the front and is meaningless behind it: a few millimetres into
6682
+ // the char the gradient is almost flat, its direction is whatever the
6683
+ // eight-bit field rounded to, and the frame turns with position \u2014 which
6684
+ // makes the noise a function OF THE CONTOURS rather than of the sheet. It
6685
+ // drew closed loops following the edge, nested a cell apart: the
6686
+ // "concentric contour lines, like a topographic map or tree rings" the
6687
+ // review found at macro range and left unexplained. Turning this one term
6688
+ // off removes them completely and changes nothing else.
6689
+ //
6690
+ // Measured on the scripted burn's peak: the gradient runs to about 19 at
6691
+ // the front and under 1 a few millimetres behind it.
6692
+ float frontness = smoothstep(1.0, 6.0, length(gradC));
6693
+ float fingers = (plFbm(vec2(along, dot(p, vec2(tangentC.y, -tangentC.x)) / (12.0 * PL_MM))) - 0.5) * 0.28 * frontness
6694
+ + (plFbm(vec2(p.x * 9.0, p.y * 30.0)) - 0.5) * 0.08;
6695
+ fingers *= uLook2.w;
6696
+ // Fingers on the borrowed scorch too, so the outer edge is irregular
6697
+ // teeth rather than a smooth offset of the char.
6698
+ float reach = 0.75 + fingers * 4.0;
6699
+ // The scorch reads a softened char \u2014 its own texel and the four around it
6700
+ // \u2014 so a bilinear 64\xB2 field magnified shows no contours or corners. The
6701
+ // char zone keeps the sharp one: its edge is meant to be crisp.
6702
+ float soft = (2.0 * d.r + e.r + w.r + n.r + s.r) / 6.0;
6703
+ float self_ = clamp(mix(d.r, soft, 0.65) + fingers * smoothstep(0.0, 0.08, d.r), 0.0, 1.0);
6704
+ float c = 1.0 - (1.0 - self_) * (1.0 - clamp(0.62 * charBelow * reach, 0.0, 1.0)) * (1.0 - clamp(0.4 * charWide * reach, 0.0, 1.0));
6705
+ // Fibre-scale grain in the scorch, so a smooth field magnified never shows
6706
+ // its contours \u2014 real scorch is fibrous (Scorch.png), not banded.
6707
+ c = clamp(c + ((plNoise(p * 300.0) - 0.5) * 0.07 + (plNoise(p * 90.0) - 0.5) * 0.06) * smoothstep(0.0, 0.1, c), 0.0, 1.0);
6708
+ // A dither of about one and a half of the damage texture's own levels.
6709
+ //
6710
+ // The field arrives as eight bits a channel, so char comes in steps of
6711
+ // 1/255 \u2014 and the leading edge below spans 0.03 of it, which is seven and a
6712
+ // half steps. The fibre grain above would have hidden them, but it is
6713
+ // multiplied by smoothstep(0, 0.1, c), which is approximately zero exactly
6714
+ // where the leading edge lives. This is not: it applies everywhere, and
6715
+ // being finer than a level it can only ever move a value into the
6716
+ // neighbouring one.
6717
+ c = clamp(c + (plNoise(p * 900.0) - 0.5) * (1.5 / 255.0), 0.0, 1.0);
6718
+ // A steep leading edge \u2014 paper to straw in a sliver \u2014 then the long ramp.
6719
+ float lead = smoothstep(0.02, 0.05, c);
6720
+ float ramp = smoothstep(0.05, 0.7, c);
6721
+ color.rgb *= mix(vec3(1.0), pow(plScorchTint(ramp), vec3(uLook2.z)), lead);
6722
+
6723
+ // \u2500\u2500 Char \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6724
+ // A wide, smooth hand-over from scorch to char \u2014 the gradient is the point.
6725
+ float charZone = smoothstep(0.5, 0.9, c);
6726
+ vec2 cells = plVoronoi(p / (3.6 * PL_MM));
6727
+ // Sparse: a real crack network is broken, not a tiled floor \u2014 most cell
6728
+ // borders never split.
6729
+ float crack = (1.0 - smoothstep(0.008, 0.03, cells.y - cells.x)) * smoothstep(0.45, 0.65, plNoise(p / (7.0 * PL_MM)));
6730
+ float plate = plHash(floor(p / (3.6 * PL_MM)) + 3.7);
6731
+ // Dark orange where the char meets the scorch, deep brown toward the cut \u2014
6732
+ // warm all the way, and fading smoothly into the scorch's lighter browns
6733
+ // rather than stopping at a grey band.
6734
+ // Warm (dark orange \u2192 deep brown) or, turned down, the sampled greys.
6735
+ vec3 body = mix(plLinear(vec3(0.255, 0.239, 0.227)), plLinear(vec3(0.165, 0.086, 0.043)), uLook1.w);
6736
+ vec3 under = mix(plLinear(vec3(0.275, 0.169, 0.090)), plLinear(vec3(0.42, 0.18, 0.06)), uLook1.w);
6737
+ vec3 cracks = plLinear(vec3(0.039, 0.024, 0.016)); // #0A0604
6738
+ vec3 charColor = mix(under, body, smoothstep(0.66, 1.0, c));
6739
+ // Mottled at the plate scale and finer, and darker than the sampled body
6740
+ // under a bright key \u2014 the reference's char reads near black in the frame.
6741
+ charColor *= (0.75 + 0.35 * plate) * (0.8 + 0.4 * plNoise(p * 260.0));
6742
+ charColor = mix(charColor, cracks, crack * uLook2.x);
6743
+ color.rgb = mix(color.rgb, charColor, charZone);
6744
+ roughness = mix(roughness, 0.6, charZone);
6745
+ // Blistered, and each plate tilted its own way, so a raking light breaks
6746
+ // the char into plates rather than lighting it as one sheet of black.
6747
+ plHeight += charZone * ((plNoise(p * 380.0) - 0.5) * 0.00008 + (plate - 0.5) * 0.00018 - crack * 0.00016);
6748
+
6749
+ // \u2500\u2500 Ash lip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6750
+ // Inside the spec's zones (\xA75): the lip 0.5\u20131.5 mm, then the ember line
6751
+ // 0.3\u20131 mm \u2014 2.5 mm from the cut at most, which the gate holds it to.
6752
+ // Along the rim. Both the lip and the ember line below are laid out on this,
6753
+ // and unlike the scorch fingers above it is safe to sample noise in: it is
6754
+ // used ONLY within a couple of millimetres of the cut, where the presence
6755
+ // gradient is the actual edge and points somewhere real. The fingers'
6756
+ // mistake was carrying the same idea out into flat char, where the gradient
6757
+ // is rounding error and a noise sampled in its frame draws the contours.
6758
+ vec2 tangentP = length(gradP) > 1e-4 ? normalize(vec2(-gradP.y, gradP.x)) : vec2(1.0, 0.0);
6759
+
6760
+ float lipWidth = mix(0.4, max(uLook1.y, 0.41), plFbm(p / (2.5 * PL_MM)));
6761
+ // Broken, not a hem. Ash flakes off a cooling edge in pieces, so the lip
6762
+ // comes and goes along the rim and wanders in width where it is there. It
6763
+ // used to be a continuous band of even width with a highlight down the
6764
+ // middle of it, which read as a neon tube round the hole \u2014 the single most
6765
+ // synthetic thing at wide view.
6766
+ float lipAlong = dot(p, tangentP) / (3.2 * PL_MM);
6767
+ float lipBreak = smoothstep(0.34, 0.56, plFbm(vec2(lipAlong, 0.9)));
6768
+ float lip = (1.0 - smoothstep(lipWidth - aa, lipWidth + aa, mm)) * step(d.a, 0.999) * cutNear * lipBreak;
6769
+ vec3 ash = plLinear(mix(vec3(0.525, 0.498, 0.490), vec3(0.741, 0.725, 0.725), plNoise(p * 520.0) * 0.6 + plNoise(p * 90.0) * 0.4)) * uLook1.z;
6770
+ color.rgb = mix(color.rgb, ash, lip);
6771
+ roughness = mix(roughness, 0.99, lip);
6772
+ // Barely raised. THIS is where the highlight came from: a 0.7 mm ridge
6773
+ // running round the hole is a cylinder, and a cylinder under a key light
6774
+ // has a specular line down it however rough the surface is. Ash is a
6775
+ // fragile crust a fraction of a millimetre proud of the paper, not a bead
6776
+ // of solder \u2014 and the shape of it belongs to the broken edge above, not to
6777
+ // a smooth ramp.
6778
+ plHeight += lip * (1.0 - mm / max(lipWidth, 0.1)) * 0.00018;
6779
+
6780
+ // \u2500\u2500 The ember line \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6781
+ // Beads along the cut: position along the edge, crawling on the burn's
6782
+ // clock, with a flicker of their own. Only where it is hot \u2014 and as the
6783
+ // heat goes the threshold rises, so the line breaks into fewer, dimmer
6784
+ // beads and they go out one by one.
6785
+ float s1 = dot(p, tangentP) / (2.4 * PL_MM) - uDamageTime * 0.55 * uLook0.w;
6786
+ float bead = plNoise(vec2(s1, 0.37)) * 0.7 + plNoise(vec2(s1 * 2.3, 5.1)) * 0.3;
6787
+ float flicker = 0.62 + 0.38 * plNoise(vec2(floor(s1) * 7.13, uDamageTime * 9.0 * uLook0.w));
6788
+ // Down to a smoulder's last flicker: a spent edge keeps a little heat in
6789
+ // the texture (\`Afterglow\`), and a bead should glow dim red on it rather
6790
+ // than vanish while the edge still has something to give.
6791
+ // Heat softened like the cut (its texel and the four around it), so the
6792
+ // beads and the crimson glow follow the burn rather than the grid's cells.
6793
+ float heatSoft = (2.0 * d.b + e.b + w.b + n.b + s.b) / 6.0;
6794
+ float hot = smoothstep(0.012, 0.55, heatSoft);
6795
+ // Coverage slides the threshold: more of the edge lit, or less.
6796
+ float shift = (uLook0.z - 0.5) * 0.5;
6797
+ float lit = smoothstep(mix(0.82, 0.45, hot) - shift, mix(0.9, 0.58, hot) - shift, bead);
6798
+ // Each bead tapers at its ends \u2014 a bead, not a dash cut from a strip.
6799
+ float width = max(mix(0.4, max(uLook0.x, 0.41), plNoise(vec2(s1 * 0.7, 9.0))) * (0.35 + 0.65 * lit), 1.4 * aa);
6800
+ float band = smoothstep(lipWidth - aa, lipWidth + aa, mm) * (1.0 - smoothstep(lipWidth + width - aa, lipWidth + width + aa, mm)) * cutNear;
6801
+ // Mostly orange: a bead is red at its ends and yellow-white only at its
6802
+ // hottest core, which is what Ember_line.png shows. Mapped low and pushed
6803
+ // high by a power, so white is the exception rather than the line.
6804
+ // Orange through, yellow-white at the hottest cores. Dim red alone reads
6805
+ // salmon once the curve desaturates it, so the ramp starts at orange and
6806
+ // only the cooling (low heat) falls back into red.
6807
+ float t = hot * flicker * (0.45 + 0.55 * lit);
6808
+ color.rgb = mix(color.rgb, plLinear(vec3(0.12, 0.04, 0.02)), band * lit);
6809
+ // Saturated orange at orange brightness; only the hottest cores are pushed
6810
+ // high enough for the curve to roll them to white.
6811
+ plDamageLight = plEmberRamp(t) * mix(0.8, 6.0, t * t * t * t) * band * lit * hot * uLook0.y;
6812
+
6813
+ // The rest of a real burning edge (Ember_line.png): a dim crimson glow in
6814
+ // the char beside the beads \u2014 patches of it, drifting \u2014 and specks of
6815
+ // glowing fibre that come and go. Both stay under the bloom threshold, so
6816
+ // they read as heat in the char rather than as light; both only where the
6817
+ // edge is hot, and inside the zone the beads are held to \u2014 the lip plus
6818
+ // the widest bead, measured from the cut \u2014 so the glow never reaches out
6819
+ // over paper the gate says heat must not light.
6820
+ float zoneEnd = uLook1.y + uLook0.x;
6821
+ float seam = smoothstep(lipWidth - aa, lipWidth + aa, mm) * (1.0 - smoothstep(zoneEnd - 0.6, zoneEnd, mm)) * cutNear;
6822
+ float patch_ = smoothstep(0.35, 0.72, plFbm(p / (4.0 * PL_MM) + vec2(uDamageTime * 0.15 * uLook0.w, 0.0)));
6823
+ plDamageLight += vec3(0.55, 0.06, 0.01) * seam * patch_ * hot * uLook1.x;
6824
+ float speck = step(0.985, plHash(floor(p / (0.35 * PL_MM)) + floor(uDamageTime * 8.0 * uLook0.w)));
6825
+ plDamageLight += plEmberRamp(0.9) * 1.6 * speck * seam * hot * uLook3.z;
6826
+
6827
+ plDamageCut(color, d);
6828
+ }
6829
+ `
6830
+ );
6831
+ function carveDepthHelpers(helpers) {
6832
+ const darken = "uniform float uBackDarken;\n";
6833
+ const relief = helpers.indexOf("/**\n * The paper's relief");
6834
+ const bell = helpers.lastIndexOf("/**", helpers.indexOf("float plBell("));
6835
+ if (!helpers.includes(darken) || relief < 0 || bell <= relief) {
6836
+ throw new Error("compose: HELPERS changed shape \u2014 update carveDepthHelpers");
6837
+ }
6838
+ return (helpers.slice(0, relief) + helpers.slice(bell)).replace(darken, "");
6839
+ }
6840
+ var DEPTH_HELPERS = carveDepthHelpers(HELPERS);
6841
+ var DEPTH_VERTEX = (
6842
+ /* glsl */
6843
+ `
6844
+ varying vec2 vPaperUv;
6845
+ void main() {
6846
+ vPaperUv = uv;
6847
+ }
6848
+ `
6849
+ );
5756
6850
  var AGING_CHUNK = (
5757
6851
  /* glsl */
5758
6852
  `
@@ -5836,7 +6930,55 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
5836
6930
  calls.push("plAging(csm_DiffuseColor);");
5837
6931
  uniforms.uAgingAmount = { value: aging };
5838
6932
  }
5839
- const relief = grain !== void 0 || creases.length > 0;
6933
+ if (maps.hasDamage) {
6934
+ chunks.push(DAMAGE_CHUNK, DAMAGE_SHADE_CHUNK);
6935
+ calls.push("plDamage(csm_DiffuseColor, csm_Roughness);");
6936
+ uniforms.uDamage = { value: null };
6937
+ uniforms.uDamageDetail = { value: 1 };
6938
+ uniforms.uDamageTime = { value: 0 };
6939
+ const look = DAMAGE_LOOK_DEFAULTS;
6940
+ uniforms.uLook0 = {
6941
+ value: new THREE5.Vector4(look.emberWidth, look.emberIntensity, look.emberCoverage, look.emberFlicker)
6942
+ };
6943
+ uniforms.uLook1 = {
6944
+ value: new THREE5.Vector4(look.emberGlow, look.lipWidth, look.lipBrightness, look.charWarmth)
6945
+ };
6946
+ uniforms.uLook2 = {
6947
+ value: new THREE5.Vector4(look.charCracks, look.scorchReach, look.scorchDarkness, look.fingers)
6948
+ };
6949
+ uniforms.uLook3 = { value: new THREE5.Vector4(look.edgeWave, look.edgeBite, look.sparkle, 0) };
6950
+ }
6951
+ const cutting = [];
6952
+ const cuts = [];
6953
+ if (deckle) {
6954
+ cutting.push(DECKLE_CHUNK);
6955
+ cuts.push("plDeckle(color);");
6956
+ }
6957
+ if (perforation) {
6958
+ cutting.push(PERFORATION_CHUNK);
6959
+ cuts.push("plPerforation(color);");
6960
+ }
6961
+ if (maps.hasDamage) {
6962
+ cutting.push(DAMAGE_CHUNK);
6963
+ cuts.push("plDamageCut(color, plDamageRead());");
6964
+ }
6965
+ const depth = cutting.length > 0 ? {
6966
+ vertexShader: DEPTH_VERTEX,
6967
+ fragmentShader: (
6968
+ /* glsl */
6969
+ `
6970
+ ${DEPTH_HELPERS}
6971
+ ${cutting.join("\n")}
6972
+ void main() {
6973
+ vec4 color = vec4(1.0);
6974
+ float roughness = 1.0;
6975
+ ${cuts.join("\n ")}
6976
+ if (color.a < 0.5) discard;
6977
+ }
6978
+ `
6979
+ )
6980
+ } : null;
6981
+ const relief = grain !== void 0 || creases.length > 0 || Boolean(maps.hasDamage);
5840
6982
  const frontExpr = maps.hasFrontMap ? "texture2D(uFrontMap, vPaperUv).rgb" : "uStockColor";
5841
6983
  const backBaseExpr = stock.adhesive ? "vec3(0.965, 0.96, 0.945)" : maps.hasBackMap ? "texture2D(uBackMap, vec2(1.0 - vPaperUv.x, vPaperUv.y)).rgb" : "uStockColor";
5842
6984
  const fragmentShader = (
@@ -5865,6 +7007,7 @@ ${relief ? " // The relief every effect above described, spent once \u2014 see
5865
7007
  ${stock.adhesive ? "// Adhesive underside: higher specular than the printed face.\n if (!gl_FrontFacing) csm_Roughness = 0.18;" : ""}
5866
7008
  // What the key light pushes through the sheet, filtered by the ink on it.
5867
7009
  csm_Emissive = plTransmission(front);
7010
+ ${maps.hasDamage ? " // The ember line \u2014 the only light a burn gives off from the sheet itself.\n csm_Emissive += plDamageLight;" : ""}
5868
7011
  }
5869
7012
  `
5870
7013
  );
@@ -5876,11 +7019,14 @@ ${relief ? " // The relief every effect above described, spent once \u2014 see
5876
7019
  aging !== void 0 ? "a" : "",
5877
7020
  perforation ? "p" : "",
5878
7021
  stock.adhesive ? "A" : ""
5879
- ].join("")}:${maps.hasFrontMap ? "F" : ""}${maps.hasBackMap ? "B" : ""}`,
7022
+ ].join("")}:${maps.hasFrontMap ? "F" : ""}${maps.hasBackMap ? "B" : ""}${maps.hasDamage ? "D" : ""}`,
5880
7023
  vertexShader: VERTEX,
5881
7024
  fragmentShader,
5882
7025
  uniforms,
5883
- alphaTest: deckle || perforation ? 0.5 : 0
7026
+ // Anything that removes paper needs fragments discarded rather than
7027
+ // blended — a hole has to cut the depth buffer and the shadow too.
7028
+ alphaTest: deckle || perforation || maps.hasDamage ? 0.5 : 0,
7029
+ depth
5884
7030
  };
5885
7031
  }
5886
7032
  function pad(values, fill = 0) {
@@ -5889,8 +7035,43 @@ function pad(values, fill = 0) {
5889
7035
  return out;
5890
7036
  }
5891
7037
 
7038
+ // src/surface/useDamageTexture.ts
7039
+ import * as THREE6 from "three";
7040
+ import { useFrame } from "@react-three/fiber";
7041
+ import { useEffect as useEffect4, useMemo as useMemo2 } from "react";
7042
+ function useDamageTexture(source) {
7043
+ const texture = useMemo2(() => {
7044
+ if (!source) return null;
7045
+ const t = new THREE6.DataTexture(
7046
+ source.pixels,
7047
+ source.size,
7048
+ source.size,
7049
+ THREE6.RGBAFormat,
7050
+ THREE6.UnsignedByteType
7051
+ );
7052
+ t.magFilter = THREE6.LinearFilter;
7053
+ t.minFilter = THREE6.LinearFilter;
7054
+ t.wrapS = THREE6.ClampToEdgeWrapping;
7055
+ t.wrapT = THREE6.ClampToEdgeWrapping;
7056
+ t.generateMipmaps = false;
7057
+ t.colorSpace = THREE6.NoColorSpace;
7058
+ t.flipY = false;
7059
+ t.userData.version = source.version;
7060
+ t.needsUpdate = true;
7061
+ return t;
7062
+ }, [source]);
7063
+ useEffect4(() => () => texture?.dispose(), [texture]);
7064
+ useFrame(() => {
7065
+ if (!source || !texture) return;
7066
+ if (texture.userData.version === source.version) return;
7067
+ texture.userData.version = source.version;
7068
+ texture.needsUpdate = true;
7069
+ });
7070
+ return texture;
7071
+ }
7072
+
5892
7073
  // src/surface/PaperMaterial.tsx
5893
- import { jsx as jsx3 } from "react/jsx-runtime";
7074
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
5894
7075
  function PaperMaterial({
5895
7076
  stock,
5896
7077
  texture,
@@ -5899,26 +7080,31 @@ function PaperMaterial({
5899
7080
  thickness,
5900
7081
  sheet: sheet2,
5901
7082
  lighting = "studio",
5902
- creases
7083
+ creases,
7084
+ damage
5903
7085
  }) {
5904
7086
  const rig = useLightRig(lighting);
7087
+ const damageTexture = useDamageTexture(damage);
5905
7088
  const composed = composeSurface(
5906
7089
  surface,
5907
7090
  stock,
5908
7091
  thickness,
5909
7092
  {
5910
7093
  hasFrontMap: Boolean(texture),
5911
- hasBackMap: Boolean(backTexture)
7094
+ hasBackMap: Boolean(backTexture),
7095
+ hasDamage: Boolean(damageTexture)
5912
7096
  },
5913
7097
  sheet2,
5914
7098
  rig,
5915
7099
  creases ?? resolveCreases(surface, [], sheet2 ?? { width: 1, height: 1.4 })
5916
7100
  );
5917
- const bound = useMemo2(() => composed.uniforms, [composed.structureKey]);
5918
- useEffect4(() => {
7101
+ const bound = useMemo3(() => composed.uniforms, [composed.structureKey]);
7102
+ useEffect5(() => {
5919
7103
  for (const [key, uniform] of Object.entries(composed.uniforms)) {
5920
- if (!bound[key] || key === "uFrontMap" || key === "uBackMap") continue;
5921
- if (bound[key].value instanceof THREE6.Color && uniform.value instanceof THREE6.Color) {
7104
+ if (!bound[key] || key === "uFrontMap" || key === "uBackMap" || key === "uDamage" || key === "uDamageDetail" || key === "uDamageTime" || key.startsWith("uLook")) {
7105
+ continue;
7106
+ }
7107
+ if (bound[key].value instanceof THREE7.Color && uniform.value instanceof THREE7.Color) {
5922
7108
  ;
5923
7109
  bound[key].value.copy(uniform.value);
5924
7110
  } else {
@@ -5926,27 +7112,80 @@ function PaperMaterial({
5926
7112
  }
5927
7113
  }
5928
7114
  });
5929
- useEffect4(() => {
7115
+ useEffect5(() => {
5930
7116
  if (bound.uFrontMap) bound.uFrontMap.value = texture;
5931
7117
  if (bound.uBackMap) bound.uBackMap.value = backTexture ?? null;
5932
- }, [bound, texture, backTexture]);
5933
- return /* @__PURE__ */ jsx3(
5934
- CustomShaderMaterial,
5935
- {
5936
- baseMaterial: THREE6.MeshStandardMaterial,
5937
- vertexShader: composed.vertexShader,
5938
- fragmentShader: composed.fragmentShader,
5939
- uniforms: bound,
5940
- color: "#ffffff",
5941
- roughness: stock.roughness,
5942
- metalness: 0,
5943
- transparent: stock.opacity < 1,
5944
- opacity: stock.opacity,
5945
- alphaTest: composed.alphaTest,
5946
- side: THREE6.DoubleSide
5947
- },
5948
- composed.structureKey
5949
- );
7118
+ if (bound.uDamage) bound.uDamage.value = damageTexture;
7119
+ }, [bound, texture, backTexture, damageTexture]);
7120
+ useFrame2((state) => {
7121
+ if (!bound.uDamageDetail) return;
7122
+ if (bound.uDamageTime) {
7123
+ const time = damage?.time ?? state.clock.elapsedTime;
7124
+ bound.uDamageTime.value = Number.isFinite(time) ? time : 0;
7125
+ }
7126
+ if (bound.uLook0) {
7127
+ const look = { ...DAMAGE_LOOK_DEFAULTS, ...damage?.look };
7128
+ const v = (x, d2) => Number.isFinite(x) ? x : d2;
7129
+ const d = DAMAGE_LOOK_DEFAULTS;
7130
+ bound.uLook0.value.set(
7131
+ v(look.emberWidth, d.emberWidth),
7132
+ v(look.emberIntensity, d.emberIntensity),
7133
+ v(look.emberCoverage, d.emberCoverage),
7134
+ v(look.emberFlicker, d.emberFlicker)
7135
+ );
7136
+ bound.uLook1.value.set(
7137
+ v(look.emberGlow, d.emberGlow),
7138
+ v(look.lipWidth, d.lipWidth),
7139
+ v(look.lipBrightness, d.lipBrightness),
7140
+ v(look.charWarmth, d.charWarmth)
7141
+ );
7142
+ bound.uLook2.value.set(
7143
+ v(look.charCracks, d.charCracks),
7144
+ v(look.scorchReach, d.scorchReach),
7145
+ v(look.scorchDarkness, d.scorchDarkness),
7146
+ v(look.fingers, d.fingers)
7147
+ );
7148
+ bound.uLook3.value.set(
7149
+ v(look.edgeWave, d.edgeWave),
7150
+ v(look.edgeBite, d.edgeBite),
7151
+ v(look.sparkle, d.sparkle),
7152
+ 0
7153
+ );
7154
+ }
7155
+ const detail = damage?.detail ?? 1;
7156
+ bound.uDamageDetail.value = Number.isFinite(detail) ? Math.min(1, Math.max(0, detail)) : 1;
7157
+ });
7158
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
7159
+ /* @__PURE__ */ jsx3(
7160
+ CustomShaderMaterial,
7161
+ {
7162
+ baseMaterial: THREE7.MeshStandardMaterial,
7163
+ vertexShader: composed.vertexShader,
7164
+ fragmentShader: composed.fragmentShader,
7165
+ uniforms: bound,
7166
+ color: "#ffffff",
7167
+ roughness: stock.roughness,
7168
+ metalness: 0,
7169
+ transparent: stock.opacity < 1,
7170
+ opacity: stock.opacity,
7171
+ alphaTest: composed.alphaTest,
7172
+ side: THREE7.DoubleSide
7173
+ },
7174
+ composed.structureKey
7175
+ ),
7176
+ composed.depth ? /* @__PURE__ */ jsx3(
7177
+ CustomShaderMaterial,
7178
+ {
7179
+ attach: "customDepthMaterial",
7180
+ baseMaterial: THREE7.MeshDepthMaterial,
7181
+ vertexShader: composed.depth.vertexShader,
7182
+ fragmentShader: composed.depth.fragmentShader,
7183
+ uniforms: bound,
7184
+ depthPacking: THREE7.RGBADepthPacking
7185
+ },
7186
+ `${composed.structureKey}:depth`
7187
+ ) : null
7188
+ ] });
5950
7189
  }
5951
7190
 
5952
7191
  // src/motion/onTwos.ts
@@ -5960,7 +7199,7 @@ function quantizeProgress(p, duration, fps = ON_TWOS_FPS) {
5960
7199
  }
5961
7200
 
5962
7201
  // src/PaperMesh.tsx
5963
- import { jsx as jsx4, jsxs } from "react/jsx-runtime";
7202
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
5964
7203
  function configInputs(props) {
5965
7204
  return [
5966
7205
  props.preset ?? null,
@@ -5978,7 +7217,7 @@ function configInputs(props) {
5978
7217
  }
5979
7218
  function useResolvedConfig(props) {
5980
7219
  const inputs = useStable(configInputs(props));
5981
- return useMemo3(() => resolveConfig(props), [inputs]);
7220
+ return useMemo4(() => resolveConfig(props), [inputs]);
5982
7221
  }
5983
7222
  function resolveConfig(props) {
5984
7223
  const base = props.preset ? typeof props.preset === "string" ? getPreset(props.preset) : parsePreset(props.preset) : paperConfigSchema.parse({});
@@ -5997,12 +7236,12 @@ function resolveConfig(props) {
5997
7236
  }
5998
7237
  var CLOTH_MAX_SEGMENTS = 28;
5999
7238
  var PROGRESS_SAMPLES = [0, 0.25, 0.5, 0.75, 1];
6000
- var dragPlane = new THREE7.Plane();
6001
- var dragPoint = new THREE7.Vector3();
6002
- var planeNormal = new THREE7.Vector3();
6003
- var anchorScratch = new THREE7.Vector3();
6004
- var worldScratch = new THREE7.Vector3();
6005
- var quatScratch = new THREE7.Quaternion();
7239
+ var dragPlane = new THREE8.Plane();
7240
+ var dragPoint = new THREE8.Vector3();
7241
+ var planeNormal = new THREE8.Vector3();
7242
+ var anchorScratch = new THREE8.Vector3();
7243
+ var worldScratch = new THREE8.Vector3();
7244
+ var quatScratch = new THREE8.Quaternion();
6006
7245
  var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6007
7246
  const resolved = useResolvedConfig(props);
6008
7247
  const reduced = usePrefersReducedMotion(props.reducedMotion);
@@ -6031,7 +7270,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6031
7270
  const draggingRef = useRef3(null);
6032
7271
  const configRef = useRef3(config);
6033
7272
  configRef.current = config;
6034
- const baseRotation = useMemo3(() => {
7273
+ const baseRotation = useMemo4(() => {
6035
7274
  const [rx, ry, rz] = props.rotation ?? [0, 0, 0];
6036
7275
  return [rx, ry + config.scene.turn * Math.PI / 180, rz];
6037
7276
  }, [props.rotation, config.scene.turn]);
@@ -6046,16 +7285,16 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6046
7285
  const creasesRef = useRef3(null);
6047
7286
  creasesRef.current ??= new CreaseTracker(config.memory.creases);
6048
7287
  const creases = creasesRef.current;
6049
- useEffect5(() => {
7288
+ useEffect6(() => {
6050
7289
  if (!draggingRef.current && !playingRef.current) overridesRef.current = {};
6051
7290
  creases.reset();
6052
7291
  dirtyRef.current = true;
6053
7292
  }, [behaviorKey, deformersKey, sheetKey, physicsKey]);
6054
- useEffect5(() => {
7293
+ useEffect6(() => {
6055
7294
  creases.adopt(configRef.current.memory.creases);
6056
7295
  dirtyRef.current = true;
6057
7296
  }, [creaseKey]);
6058
- const { minSegments, autoSegments, animatedStack } = useMemo3(() => {
7297
+ const { minSegments, autoSegments, animatedStack } = useMemo4(() => {
6059
7298
  const cfg = configRef.current;
6060
7299
  const probe2 = withMemory(buildStack(cfg, {}), cfg);
6061
7300
  if (!probe2) {
@@ -6084,16 +7323,16 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6084
7323
  animatedStack: animated
6085
7324
  };
6086
7325
  }, [behaviorKey, deformersKey, physicsKey, memoryKey]);
6087
- const geometry = useMemo3(() => {
7326
+ const geometry = useMemo4(() => {
6088
7327
  if (isStrip) {
6089
7328
  const strip = configRef.current.physics;
6090
7329
  const nodes = stripNodeCount(config.sheet.height, strip.perforation);
6091
- return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, 1, nodes - 1);
7330
+ return new THREE8.PlaneGeometry(config.sheet.width, config.sheet.height, 1, nodes - 1);
6092
7331
  }
6093
7332
  if (!isCloth) return createSheetGeometry(config.sheet, minSegments, autoSegments);
6094
7333
  const [sx, sy] = resolveSegments(config.sheet, minSegments);
6095
7334
  const capped = Math.min(Math.max(sx, sy), CLOTH_MAX_SEGMENTS);
6096
- return new THREE7.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
7335
+ return new THREE8.PlaneGeometry(config.sheet.width, config.sheet.height, capped, capped);
6097
7336
  }, [
6098
7337
  sheetKey,
6099
7338
  minSegments,
@@ -6102,12 +7341,12 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6102
7341
  isStrip,
6103
7342
  isStrip ? config.physics.perforation : 0
6104
7343
  ]);
6105
- useEffect5(() => () => geometry.dispose(), [geometry]);
6106
- const basePositions = useMemo3(
7344
+ useEffect6(() => () => geometry.dispose(), [geometry]);
7345
+ const basePositions = useMemo4(
6107
7346
  () => Float32Array.from(geometry.attributes.position.array),
6108
7347
  [geometry]
6109
7348
  );
6110
- const stripSim = useMemo3(() => {
7349
+ const stripSim = useMemo4(() => {
6111
7350
  if (!isStrip) return null;
6112
7351
  const strip = configRef.current.physics;
6113
7352
  return new StripSim(config.sheet.height, config.sheet.width, {
@@ -6125,7 +7364,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6125
7364
  });
6126
7365
  }, [geometry, isStrip]);
6127
7366
  const lastSimRef = useRef3(null);
6128
- const sim = useMemo3(() => {
7367
+ const sim = useMemo4(() => {
6129
7368
  if (!isCloth) return null;
6130
7369
  const cloth = configRef.current.physics;
6131
7370
  const cols = geometry.parameters.widthSegments + 1;
@@ -6139,9 +7378,10 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6139
7378
  next.adopt(lastSimRef.current);
6140
7379
  return next;
6141
7380
  }, [geometry, isCloth, isCloth ? config.physics.pins : ""]);
6142
- useEffect5(() => {
7381
+ useEffect6(() => {
6143
7382
  lastSimRef.current = sim;
6144
7383
  }, [sim]);
7384
+ const coupling = useMemo4(() => sim ? new DamageCoupling(sim) : null, [sim]);
6145
7385
  const stock = getStock(config.stock);
6146
7386
  const texture = useContentTexture(config.content, config.sheet, stock);
6147
7387
  const backTexture = useContentTexture(config.content.back, config.sheet, stock);
@@ -6183,7 +7423,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6183
7423
  playingRef.current = false;
6184
7424
  tweenRef.current?.pause();
6185
7425
  };
6186
- useEffect5(() => {
7426
+ useEffect6(() => {
6187
7427
  if (props.autoplay && !reduced) play();
6188
7428
  return () => {
6189
7429
  tweenRef.current?.kill();
@@ -6226,7 +7466,22 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6226
7466
  const index = id ? handles.findIndex((h) => h.id === id) : 0;
6227
7467
  const mesh = handleRefs.current[index];
6228
7468
  if (!mesh) return null;
6229
- return mesh.getWorldPosition(target ?? new THREE7.Vector3());
7469
+ return mesh.getWorldPosition(target ?? new THREE8.Vector3());
7470
+ },
7471
+ surfacePoint(u, v, target) {
7472
+ const mesh = meshRef.current;
7473
+ if (!mesh) return null;
7474
+ const { widthSegments, heightSegments } = geometry.parameters;
7475
+ const out = surfacePointAt(
7476
+ geometry.attributes.position.array,
7477
+ widthSegments + 1,
7478
+ heightSegments + 1,
7479
+ u,
7480
+ v,
7481
+ target ?? new THREE8.Vector3()
7482
+ );
7483
+ mesh.updateWorldMatrix(true, false);
7484
+ return out.applyMatrix4(mesh.matrixWorld);
6230
7485
  },
6231
7486
  get state() {
6232
7487
  return machineState;
@@ -6236,12 +7491,12 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6236
7491
  placeProgrammatic: () => machineRef.current?.placeProgrammatic() ?? false,
6237
7492
  returnProgrammatic: () => machineRef.current?.returnProgrammatic() ?? false
6238
7493
  }));
6239
- const shadedCreases = useMemo3(
7494
+ const shadedCreases = useMemo4(
6240
7495
  () => resolveCreases(config.surface, config.memory.creases, config.sheet),
6241
7496
  [config.surface, config.memory.creases, config.sheet]
6242
7497
  );
6243
7498
  const idlePose = useRef3({ position: [0, 0, 0], rotation: [0, 0, 0] });
6244
- useFrame(({ clock }, delta) => {
7499
+ useFrame3(({ clock }, delta) => {
6245
7500
  const cfg = liveConfig();
6246
7501
  const now = reduced ? 0 : cfg.onTwos ? quantizeTime(clock.elapsedTime) : clock.elapsedTime;
6247
7502
  const hasBehaviorTransform = Boolean(behavior?.transform && cfg.behavior);
@@ -6325,8 +7580,14 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6325
7580
  stiffness: cloth.stiffness,
6326
7581
  floor: cloth.floor
6327
7582
  });
7583
+ coupling?.update(props.damage);
6328
7584
  sim.step(delta);
6329
7585
  const moved = !sim.asleep;
7586
+ if (moved && coupling) {
7587
+ coupling.follow();
7588
+ const index = geometry.index;
7589
+ if (index && coupling.tear(index.array)) index.needsUpdate = true;
7590
+ }
6330
7591
  if (!applyShape(sim.positions, moved) && moved) {
6331
7592
  const position = geometry.attributes.position;
6332
7593
  position.array.set(sim.positions);
@@ -6358,8 +7619,8 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6358
7619
  const p = overridesRef.current[behavior.progressParam];
6359
7620
  if (typeof p === "number") props.onProgress?.(p);
6360
7621
  };
6361
- const grabAnchor = useRef3(new THREE7.Vector3());
6362
- const grabOffset = useRef3(new THREE7.Vector3());
7622
+ const grabAnchor = useRef3(new THREE8.Vector3());
7623
+ const grabOffset = useRef3(new THREE8.Vector3());
6363
7624
  const clothDown = (e) => {
6364
7625
  if (!isCloth || !sim || !props.interactive || !groupRef.current) return;
6365
7626
  e.stopPropagation();
@@ -6422,7 +7683,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6422
7683
  e.target.releasePointerCapture(e.pointerId);
6423
7684
  };
6424
7685
  const sendState = (event) => machineRef.current?.send(event);
6425
- return /* @__PURE__ */ jsxs("group", { ref: groupRef, position: props.position, rotation: baseRotation, children: [
7686
+ return /* @__PURE__ */ jsxs2("group", { ref: groupRef, position: props.position, rotation: baseRotation, children: [
6426
7687
  /* @__PURE__ */ jsx4(
6427
7688
  "mesh",
6428
7689
  {
@@ -6454,7 +7715,8 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6454
7715
  thickness: config.sheet.thickness,
6455
7716
  sheet: config.sheet,
6456
7717
  lighting: config.scene.lighting,
6457
- creases: shadedCreases
7718
+ creases: shadedCreases,
7719
+ damage: props.damage
6458
7720
  }
6459
7721
  )
6460
7722
  }
@@ -6463,7 +7725,7 @@ var PaperMesh = forwardRef(function PaperMesh2(props, ref) {
6463
7725
  // is no deformer stack for a handle to drive. Harmless as `!isCloth`
6464
7726
  // only because the schema makes a sim and a behavior exclusive — the
6465
7727
  // intent is what is written here.
6466
- !simKind && behavior?.handles?.map((h, i) => /* @__PURE__ */ jsxs(
7728
+ !simKind && behavior?.handles?.map((h, i) => /* @__PURE__ */ jsxs2(
6467
7729
  "mesh",
6468
7730
  {
6469
7731
  userData: { paperlabChrome: true },
@@ -6536,13 +7798,13 @@ function buildStack(config, overrides, behavior, t = 0) {
6536
7798
  }
6537
7799
 
6538
7800
  // src/scene/PaperLighting.tsx
6539
- import * as THREE9 from "three";
6540
- import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef4 } from "react";
6541
- import { useFrame as useFrame2, useThree as useThree2 } from "@react-three/fiber";
7801
+ import * as THREE10 from "three";
7802
+ import { useEffect as useEffect7, useMemo as useMemo5, useRef as useRef4 } from "react";
7803
+ import { useFrame as useFrame4, useThree as useThree2 } from "@react-three/fiber";
6542
7804
  import { ContactShadows } from "@react-three/drei";
6543
7805
 
6544
7806
  // src/scene/environment.ts
6545
- import * as THREE8 from "three";
7807
+ import * as THREE9 from "three";
6546
7808
 
6547
7809
  // src/scene/color.ts
6548
7810
  var probe;
@@ -6591,7 +7853,7 @@ function drawSky(ctx, preset) {
6591
7853
  const x = skyU(angles.azimuth) * WIDTH;
6592
7854
  const y = skyV(angles.elevation) * HEIGHT;
6593
7855
  const radius = WIDTH * 0.3;
6594
- const color = new THREE8.Color(preset.key.color);
7856
+ const color = new THREE9.Color(preset.key.color);
6595
7857
  const strength = Math.min(1, preset.key.intensity / 3);
6596
7858
  for (const offset of [-WIDTH, 0, WIDTH]) {
6597
7859
  const glow = ctx.createRadialGradient(x + offset, y, 0, x + offset, y, radius);
@@ -6610,14 +7872,14 @@ function makeSkyEquirect(preset) {
6610
7872
  canvas.height = HEIGHT;
6611
7873
  const ctx = canvas.getContext("2d");
6612
7874
  drawSky(ctx, preset);
6613
- const texture = new THREE8.CanvasTexture(canvas);
6614
- texture.mapping = THREE8.EquirectangularReflectionMapping;
6615
- texture.colorSpace = THREE8.SRGBColorSpace;
7875
+ const texture = new THREE9.CanvasTexture(canvas);
7876
+ texture.mapping = THREE9.EquirectangularReflectionMapping;
7877
+ texture.colorSpace = THREE9.SRGBColorSpace;
6616
7878
  return texture;
6617
7879
  }
6618
7880
  function buildEnvironment(renderer, preset) {
6619
7881
  const equirect = makeSkyEquirect(preset);
6620
- const pmrem = new THREE8.PMREMGenerator(renderer);
7882
+ const pmrem = new THREE9.PMREMGenerator(renderer);
6621
7883
  const target = pmrem.fromEquirectangular(equirect);
6622
7884
  pmrem.dispose();
6623
7885
  equirect.dispose();
@@ -6628,11 +7890,11 @@ function buildEnvironment(renderer, preset) {
6628
7890
  }
6629
7891
 
6630
7892
  // src/scene/PaperLighting.tsx
6631
- import { Fragment, jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
7893
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
6632
7894
  var toneMappings = {
6633
- agx: THREE9.AgXToneMapping,
6634
- neutral: THREE9.NeutralToneMapping,
6635
- filmic: THREE9.ACESFilmicToneMapping
7895
+ agx: THREE10.AgXToneMapping,
7896
+ neutral: THREE10.NeutralToneMapping,
7897
+ filmic: THREE10.ACESFilmicToneMapping
6636
7898
  };
6637
7899
  function mulberry322(seed) {
6638
7900
  let a = seed;
@@ -6689,15 +7951,15 @@ function makeGoboTexture(kind) {
6689
7951
  }
6690
7952
  }
6691
7953
  }
6692
- const texture = new THREE9.CanvasTexture(canvas);
6693
- texture.wrapS = texture.wrapT = THREE9.RepeatWrapping;
7954
+ const texture = new THREE10.CanvasTexture(canvas);
7955
+ texture.wrapS = texture.wrapT = THREE10.RepeatWrapping;
6694
7956
  return texture;
6695
7957
  }
6696
7958
  function StudioLight({ rig }) {
6697
7959
  const gl = useThree2((s) => s.gl);
6698
7960
  const scene = useThree2((s) => s.scene);
6699
7961
  const sky = `${rig.sky.zenith}|${rig.sky.horizon}|${rig.sky.ground}|${rig.key.color}|${rig.key.intensity}|${rig.key.position.join()}`;
6700
- useEffect6(() => {
7962
+ useEffect7(() => {
6701
7963
  const environment = buildEnvironment(gl, rig);
6702
7964
  const previous = scene.environment;
6703
7965
  scene.environment = environment.texture;
@@ -6706,7 +7968,7 @@ function StudioLight({ rig }) {
6706
7968
  environment.dispose();
6707
7969
  };
6708
7970
  }, [gl, scene, sky]);
6709
- useEffect6(() => {
7971
+ useEffect7(() => {
6710
7972
  const previous = scene.environmentIntensity;
6711
7973
  scene.environmentIntensity = rig.studio;
6712
7974
  return () => {
@@ -6727,13 +7989,13 @@ function PaperLighting({
6727
7989
  contactShadow = true,
6728
7990
  environment = true
6729
7991
  }) {
6730
- const p = useMemo4(() => rig ?? resolveLighting(preset, light), [rig, preset, light]);
7992
+ const p = useMemo5(() => rig ?? resolveLighting(preset, light), [rig, preset, light]);
6731
7993
  const mapSize = shadowMapSize ?? p.shadow.mapSize;
6732
7994
  const castShadow = mapSize > 0;
6733
7995
  const reduced = usePrefersReducedMotion(reducedMotion);
6734
7996
  const gl = useThree2((s) => s.gl);
6735
7997
  const scene = useThree2((s) => s.scene);
6736
- useEffect6(() => {
7998
+ useEffect7(() => {
6737
7999
  const previousExposure = gl.toneMappingExposure;
6738
8000
  const previousFilm = gl.toneMapping;
6739
8001
  gl.toneMappingExposure = p.exposure;
@@ -6743,23 +8005,23 @@ function PaperLighting({
6743
8005
  gl.toneMapping = previousFilm;
6744
8006
  };
6745
8007
  }, [gl, p.exposure, p.film]);
6746
- useEffect6(() => {
8008
+ useEffect7(() => {
6747
8009
  if (!p.fog) return;
6748
8010
  const previous = scene.fog;
6749
- scene.fog = new THREE9.Fog(p.fog.color, p.fog.near, p.fog.far);
8011
+ scene.fog = new THREE10.Fog(p.fog.color, p.fog.near, p.fog.far);
6750
8012
  return () => {
6751
8013
  scene.fog = previous;
6752
8014
  };
6753
8015
  }, [scene, p.fog]);
6754
- const goboMap = useMemo4(() => p.gobo ? makeGoboTexture(p.gobo.kind) : null, [p.gobo?.kind]);
6755
- useEffect6(() => () => goboMap?.dispose(), [goboMap]);
8016
+ const goboMap = useMemo5(() => p.gobo ? makeGoboTexture(p.gobo.kind) : null, [p.gobo?.kind]);
8017
+ useEffect7(() => () => goboMap?.dispose(), [goboMap]);
6756
8018
  const driftRef = useRef4(0);
6757
- useFrame2((_, delta) => {
8019
+ useFrame4((_, delta) => {
6758
8020
  if (!goboMap || !p.gobo || reduced) return;
6759
8021
  driftRef.current += delta * p.gobo.drift;
6760
8022
  goboMap.offset.set(driftRef.current, driftRef.current * 0.6);
6761
8023
  });
6762
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
8024
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
6763
8025
  p.studio > 0 && (environment ? /* @__PURE__ */ jsx5(StudioLight, { rig: p }) : (
6764
8026
  // The studio light degrades rather than disappearing. A hemisphere
6765
8027
  // is the cheap half of what the room does — light from above, a
@@ -6817,15 +8079,15 @@ function PaperLighting({
6817
8079
  }
6818
8080
 
6819
8081
  // src/scene/backdrop.tsx
6820
- import * as THREE10 from "three";
6821
- import { useEffect as useEffect7, useState as useState4 } from "react";
8082
+ import * as THREE11 from "three";
8083
+ import { useEffect as useEffect8, useState as useState4 } from "react";
6822
8084
  import { useThree as useThree3 } from "@react-three/fiber";
6823
8085
  function PaperBackdrop({ backdrop }) {
6824
8086
  const scene = useThree3((s) => s.scene);
6825
8087
  const size = useThree3((s) => s.size);
6826
8088
  const [image, setImage] = useState4(null);
6827
8089
  const src = backdrop?.image ?? "";
6828
- useEffect7(() => {
8090
+ useEffect8(() => {
6829
8091
  if (!src) {
6830
8092
  setImage(null);
6831
8093
  return;
@@ -6841,7 +8103,7 @@ function PaperBackdrop({ backdrop }) {
6841
8103
  };
6842
8104
  }, [src]);
6843
8105
  const key = JSON.stringify({ backdrop: backdrop ?? null, w: size.width, h: size.height, loaded: !!image });
6844
- useEffect7(() => {
8106
+ useEffect8(() => {
6845
8107
  if (!backdrop) return;
6846
8108
  const previous = scene.background;
6847
8109
  const texture = paintBackdrop(backdrop, size.width, size.height, image);
@@ -6877,15 +8139,15 @@ function paintBackdrop(backdrop, width, height, image) {
6877
8139
  ctx.globalAlpha = 1;
6878
8140
  }
6879
8141
  }
6880
- const texture = new THREE10.CanvasTexture(canvas);
6881
- texture.colorSpace = THREE10.SRGBColorSpace;
8142
+ const texture = new THREE11.CanvasTexture(canvas);
8143
+ texture.colorSpace = THREE11.SRGBColorSpace;
6882
8144
  return texture;
6883
8145
  }
6884
8146
 
6885
8147
  // src/field/dropZones.tsx
6886
- import * as THREE11 from "three";
6887
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef5, useSyncExternalStore } from "react";
6888
- import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
8148
+ import * as THREE12 from "three";
8149
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect9, useMemo as useMemo6, useRef as useRef5, useSyncExternalStore } from "react";
8150
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
6889
8151
  var DropZoneRegistry = class {
6890
8152
  zones = /* @__PURE__ */ new Map();
6891
8153
  hoveredId = null;
@@ -6942,10 +8204,10 @@ function DropZone(props) {
6942
8204
  const registry4 = useContext2(DropZoneContext);
6943
8205
  const { id, accept, bounds, highlight = "glow", onPlace } = props;
6944
8206
  const place = useRef5(onPlace);
6945
- useEffect8(() => {
8207
+ useEffect9(() => {
6946
8208
  place.current = onPlace;
6947
8209
  }, [onPlace]);
6948
- useEffect8(() => {
8210
+ useEffect9(() => {
6949
8211
  if (!registry4) return;
6950
8212
  return registry4.register({
6951
8213
  id,
@@ -6963,15 +8225,15 @@ function DropZoneVisual({ registry: registry4, config }) {
6963
8225
  const hovered = registry4.hovered === config.id;
6964
8226
  const style = config.highlight ?? "glow";
6965
8227
  const [w, h] = config.bounds.size;
6966
- const edges = useMemo5(() => {
6967
- const plane = new THREE11.PlaneGeometry(w, h);
6968
- const geo = new THREE11.EdgesGeometry(plane);
8228
+ const edges = useMemo6(() => {
8229
+ const plane = new THREE12.PlaneGeometry(w, h);
8230
+ const geo = new THREE12.EdgesGeometry(plane);
6969
8231
  plane.dispose();
6970
8232
  return geo;
6971
8233
  }, [w, h]);
6972
- useEffect8(() => () => edges.dispose(), [edges]);
6973
- return /* @__PURE__ */ jsxs3("group", { position: config.bounds.position, children: [
6974
- style !== "outline" && /* @__PURE__ */ jsxs3("mesh", { children: [
8234
+ useEffect9(() => () => edges.dispose(), [edges]);
8235
+ return /* @__PURE__ */ jsxs4("group", { position: config.bounds.position, children: [
8236
+ style !== "outline" && /* @__PURE__ */ jsxs4("mesh", { children: [
6975
8237
  /* @__PURE__ */ jsx6("planeGeometry", { args: [w, h] }),
6976
8238
  /* @__PURE__ */ jsx6(
6977
8239
  "meshBasicMaterial",
@@ -7572,17 +8834,17 @@ registerLayout(colonnade);
7572
8834
  registerLayout(sheet);
7573
8835
 
7574
8836
  // src/PaperField.tsx
7575
- import * as THREE16 from "three";
8837
+ import * as THREE17 from "three";
7576
8838
  import { gsap as gsap5 } from "gsap";
7577
- import { Canvas, useFrame as useFrame5, useThree as useThree6 } from "@react-three/fiber";
7578
- import { forwardRef as forwardRef2, useEffect as useEffect14, useMemo as useMemo9, useRef as useRef8 } from "react";
8839
+ import { Canvas, useFrame as useFrame7, useThree as useThree6 } from "@react-three/fiber";
8840
+ import { forwardRef as forwardRef2, useEffect as useEffect15, useMemo as useMemo10, useRef as useRef8 } from "react";
7579
8841
 
7580
8842
  // src/scene/release.tsx
7581
8843
  import { useThree as useThree4 } from "@react-three/fiber";
7582
- import { useEffect as useEffect9 } from "react";
8844
+ import { useEffect as useEffect10 } from "react";
7583
8845
  function ReleaseContextOnUnmount() {
7584
8846
  const gl = useThree4((s) => s.gl);
7585
- useEffect9(
8847
+ useEffect10(
7586
8848
  () => () => {
7587
8849
  gl.forceContextLoss();
7588
8850
  gl.dispose();
@@ -7638,15 +8900,15 @@ function resolveFieldSlotConfig(slot, fallback, index, layoutId, layoutOptions)
7638
8900
  }
7639
8901
 
7640
8902
  // src/field/fieldGroup.tsx
7641
- import * as THREE13 from "three";
8903
+ import * as THREE14 from "three";
7642
8904
  import { gsap as gsap3 } from "gsap";
7643
- import { useFrame as useFrame3 } from "@react-three/fiber";
7644
- import { useEffect as useEffect11, useMemo as useMemo6, useRef as useRef6 } from "react";
8905
+ import { useFrame as useFrame5 } from "@react-three/fiber";
8906
+ import { useEffect as useEffect12, useMemo as useMemo7, useRef as useRef6 } from "react";
7645
8907
  import CustomShaderMaterial2 from "three-custom-shader-material";
7646
8908
 
7647
8909
  // src/content/atlas.ts
7648
- import * as THREE12 from "three";
7649
- import { useEffect as useEffect10, useState as useState5 } from "react";
8910
+ import * as THREE13 from "three";
8911
+ import { useEffect as useEffect11, useState as useState5 } from "react";
7650
8912
  var MAX_ATLAS = 4096;
7651
8913
  function atlasGrid(count, aspect = 1) {
7652
8914
  const cols = Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * Math.max(aspect, 0.01)))));
@@ -7655,7 +8917,7 @@ function atlasGrid(count, aspect = 1) {
7655
8917
  function useContentAtlas(contents, sheet2, stock) {
7656
8918
  const [atlas, setAtlas] = useState5(null);
7657
8919
  const stableContents = useStable(contents);
7658
- useEffect10(() => {
8920
+ useEffect11(() => {
7659
8921
  let disposed = false;
7660
8922
  const aspect = sheet2.height / sheet2.width;
7661
8923
  const { cols, rows } = atlasGrid(contents.length, aspect);
@@ -7671,8 +8933,8 @@ function useContentAtlas(contents, sheet2, stock) {
7671
8933
  const ctx = canvas.getContext("2d");
7672
8934
  ctx.fillStyle = stock.color;
7673
8935
  ctx.fillRect(0, 0, canvas.width, canvas.height);
7674
- const texture = new THREE12.CanvasTexture(canvas);
7675
- texture.colorSpace = THREE12.SRGBColorSpace;
8936
+ const texture = new THREE13.CanvasTexture(canvas);
8937
+ texture.colorSpace = THREE13.SRGBColorSpace;
7676
8938
  texture.anisotropy = 4;
7677
8939
  setAtlas({ texture, cols, rows });
7678
8940
  const drawTile = (index, tile) => {
@@ -7841,7 +9103,7 @@ function fieldShapeStack(config, progress) {
7841
9103
 
7842
9104
  // src/field/fieldGroup.tsx
7843
9105
  import { jsx as jsx7 } from "react/jsx-runtime";
7844
- var scratchObj = new THREE13.Object3D();
9106
+ var scratchObj = new THREE14.Object3D();
7845
9107
  var scratchAero = { position: [0, 0, 0], rotation: [0, 0, 0] };
7846
9108
  var FIELD_SEGMENT_CAP = 48;
7847
9109
  var FIELD_AUTO_CEILING = 128;
@@ -7864,7 +9126,7 @@ function FieldGroup({
7864
9126
  behavior ? config.behavior[behavior.progressParam] : 0
7865
9127
  );
7866
9128
  const buildStackAt = (progress) => fieldShapeStack(config, progress);
7867
- const initialStack = useMemo6(
9129
+ const initialStack = useMemo7(
7868
9130
  () => buildStackAt(progressRef.current),
7869
9131
  [
7870
9132
  JSON.stringify(config.behavior ?? null),
@@ -7873,7 +9135,7 @@ function FieldGroup({
7873
9135
  ]
7874
9136
  );
7875
9137
  const structureKey = initialStack.map((i) => i.type).join("|");
7876
- const geometry = useMemo6(() => {
9138
+ const geometry = useMemo7(() => {
7877
9139
  const floor = stackMinSegments(initialStack, config.sheet);
7878
9140
  const want = [0, 0];
7879
9141
  for (const p of PROGRESS_SAMPLES2) {
@@ -7902,28 +9164,28 @@ function FieldGroup({
7902
9164
  atlasIdx[i] = i;
7903
9165
  phase[i] = indices[i] * 0.618034 % 1 * 4;
7904
9166
  }
7905
- geo.setAttribute("aAtlas", new THREE13.InstancedBufferAttribute(atlasIdx, 1));
7906
- geo.setAttribute("aPhase", new THREE13.InstancedBufferAttribute(phase, 1));
7907
- geo.setAttribute("aBias", new THREE13.InstancedBufferAttribute(bias, 1));
9167
+ geo.setAttribute("aAtlas", new THREE14.InstancedBufferAttribute(atlasIdx, 1));
9168
+ geo.setAttribute("aPhase", new THREE14.InstancedBufferAttribute(phase, 1));
9169
+ geo.setAttribute("aBias", new THREE14.InstancedBufferAttribute(bias, 1));
7908
9170
  return geo;
7909
9171
  }, [JSON.stringify(config.sheet), structureKey, count, autoCeiling]);
7910
- useEffect11(() => () => geometry.dispose(), [geometry]);
9172
+ useEffect12(() => () => geometry.dispose(), [geometry]);
7911
9173
  const atlas = useContentAtlas(contents, config.sheet, stock);
7912
- const shader = useMemo6(() => {
9174
+ const shader = useMemo7(() => {
7913
9175
  const composed = buildDisplacementGLSL(initialStack, config.sheet);
7914
9176
  const uniforms = {};
7915
9177
  for (const [name, value] of Object.entries(composed.uniforms)) {
7916
9178
  uniforms[name] = {
7917
- value: Array.isArray(value) && value.length === 2 ? new THREE13.Vector2(...value) : value
9179
+ value: Array.isArray(value) && value.length === 2 ? new THREE14.Vector2(...value) : value
7918
9180
  };
7919
9181
  }
7920
9182
  uniforms.uPlTime = { value: 0 };
7921
9183
  uniforms.uAtlas = { value: null };
7922
- uniforms.uAtlasGrid = { value: new THREE13.Vector2(1, 1) };
9184
+ uniforms.uAtlasGrid = { value: new THREE14.Vector2(1, 1) };
7923
9185
  uniforms.uBackDarken = {
7924
9186
  value: 1 - Math.min(0.45, 0.12 + config.sheet.thickness * 0.9) * stock.opacity
7925
9187
  };
7926
- uniforms.uStockColor = { value: new THREE13.Color(stock.color) };
9188
+ uniforms.uStockColor = { value: new THREE14.Color(stock.color) };
7927
9189
  uniforms.uShowThrough = { value: config.surface.showThrough ?? stock.showThrough };
7928
9190
  Object.assign(uniforms, translucencyUniforms(config.surface.translucency ?? stock.translucency, rig));
7929
9191
  return {
@@ -7938,19 +9200,19 @@ function FieldGroup({
7938
9200
  config.surface.showThrough,
7939
9201
  config.surface.translucency
7940
9202
  ]);
7941
- useEffect11(() => {
9203
+ useEffect12(() => {
7942
9204
  const values = translucencyValues(config.surface.translucency ?? stock.translucency, rig);
7943
9205
  shader.uniforms.uTranslucency.value = values.translucency;
7944
9206
  shader.uniforms.uBackLightDir.value.copy(values.direction);
7945
9207
  shader.uniforms.uBackLightColor.value.copy(values.color);
7946
9208
  shader.uniforms.uAmbientTransmission.value = values.ambient;
7947
9209
  }, [shader, rig, config.surface.translucency, stock.translucency]);
7948
- useEffect11(() => {
9210
+ useEffect12(() => {
7949
9211
  if (!atlas) return;
7950
9212
  shader.uniforms.uAtlas.value = atlas.texture;
7951
9213
  shader.uniforms.uAtlasGrid.value.set(atlas.cols, atlas.rows);
7952
9214
  }, [atlas, shader]);
7953
- useEffect11(() => {
9215
+ useEffect12(() => {
7954
9216
  if (!behavior || shared.reduced) return;
7955
9217
  const state = { p: progressRef.current };
7956
9218
  const tween = gsap3.to(state, {
@@ -7968,7 +9230,7 @@ function FieldGroup({
7968
9230
  };
7969
9231
  }, [structureKey, shared.reduced]);
7970
9232
  const meshRef = useRef6(null);
7971
- useFrame3(({ clock }) => {
9233
+ useFrame5(({ clock }) => {
7972
9234
  const mesh = meshRef.current;
7973
9235
  if (!mesh) return;
7974
9236
  const elapsed = clock.elapsedTime - Math.max(shared.mountTimeRef.current, 0);
@@ -7978,7 +9240,7 @@ function FieldGroup({
7978
9240
  for (const [name, value] of Object.entries(values)) {
7979
9241
  const uniform = shader.uniforms[name];
7980
9242
  if (!uniform) continue;
7981
- if (uniform.value instanceof THREE13.Vector2 && Array.isArray(value)) {
9243
+ if (uniform.value instanceof THREE14.Vector2 && Array.isArray(value)) {
7982
9244
  uniform.value.set(value[0], value[1]);
7983
9245
  } else {
7984
9246
  uniform.value = value;
@@ -8063,13 +9325,13 @@ function FieldGroup({
8063
9325
  children: /* @__PURE__ */ jsx7(
8064
9326
  CustomShaderMaterial2,
8065
9327
  {
8066
- baseMaterial: THREE13.MeshStandardMaterial,
9328
+ baseMaterial: THREE14.MeshStandardMaterial,
8067
9329
  vertexShader: shader.vertexShader,
8068
9330
  fragmentShader: shader.fragmentShader,
8069
9331
  uniforms: shader.uniforms,
8070
9332
  roughness: stock.roughness,
8071
9333
  metalness: 0,
8072
- side: THREE13.DoubleSide
9334
+ side: THREE14.DoubleSide
8073
9335
  },
8074
9336
  `${structureKey}:${count}`
8075
9337
  )
@@ -8115,8 +9377,8 @@ var easeOut = (t) => 1 - (1 - t) ** 3;
8115
9377
  var easeInOut = (t) => t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
8116
9378
 
8117
9379
  // src/field/backingSheet.tsx
8118
- import * as THREE14 from "three";
8119
- import { useEffect as useEffect12, useMemo as useMemo7 } from "react";
9380
+ import * as THREE15 from "three";
9381
+ import { useEffect as useEffect13, useMemo as useMemo8 } from "react";
8120
9382
 
8121
9383
  // src/content/backing.ts
8122
9384
  function silhouetteRects(o, count) {
@@ -8181,7 +9443,7 @@ function roundedRect(ctx, x, y, w, h, r) {
8181
9443
  }
8182
9444
 
8183
9445
  // src/field/backingSheet.tsx
8184
- import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
9446
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
8185
9447
  var BACKING_TINT = "#f3ecdd";
8186
9448
  function BackingSheet({
8187
9449
  options,
@@ -8189,7 +9451,7 @@ function BackingSheet({
8189
9451
  removed
8190
9452
  }) {
8191
9453
  const { width, height } = sheetBackingSize(options);
8192
- const canvas = useMemo7(() => {
9454
+ const canvas = useMemo8(() => {
8193
9455
  if (typeof document === "undefined") return null;
8194
9456
  const c = document.createElement("canvas");
8195
9457
  const scale = Math.min(1024, Math.round(360 * Math.max(width, height))) / Math.max(width, height);
@@ -8197,26 +9459,26 @@ function BackingSheet({
8197
9459
  c.height = Math.max(2, Math.round(height * scale));
8198
9460
  return c;
8199
9461
  }, [width, height]);
8200
- const texture = useMemo7(() => canvas ? new THREE14.CanvasTexture(canvas) : null, [canvas]);
9462
+ const texture = useMemo8(() => canvas ? new THREE15.CanvasTexture(canvas) : null, [canvas]);
8201
9463
  const removedKey = [...removed].sort((a, b) => a - b).join(",");
8202
- useEffect12(() => {
9464
+ useEffect13(() => {
8203
9465
  if (!canvas || !texture) return;
8204
9466
  drawBacking(canvas, { options, count, tint: BACKING_TINT, removed });
8205
9467
  texture.needsUpdate = true;
8206
9468
  }, [canvas, texture, JSON.stringify(options), count, removedKey]);
8207
- useEffect12(() => () => texture?.dispose(), [texture]);
8208
- return /* @__PURE__ */ jsxs4("mesh", { receiveShadow: true, children: [
9469
+ useEffect13(() => () => texture?.dispose(), [texture]);
9470
+ return /* @__PURE__ */ jsxs5("mesh", { receiveShadow: true, children: [
8209
9471
  /* @__PURE__ */ jsx8("planeGeometry", { args: [width, height] }),
8210
9472
  /* @__PURE__ */ jsx8("meshStandardMaterial", { map: texture, color: "#ffffff", roughness: 0.92, metalness: 0 })
8211
9473
  ] });
8212
9474
  }
8213
9475
 
8214
9476
  // src/field/interactiveField.tsx
8215
- import * as THREE15 from "three";
9477
+ import * as THREE16 from "three";
8216
9478
  import { gsap as gsap4 } from "gsap";
8217
- import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
8218
- import { useContext as useContext3, useEffect as useEffect13, useMemo as useMemo8, useRef as useRef7, useState as useState6 } from "react";
8219
- import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
9479
+ import { useFrame as useFrame6, useThree as useThree5 } from "@react-three/fiber";
9480
+ import { useContext as useContext3, useEffect as useEffect14, useMemo as useMemo9, useRef as useRef7, useState as useState6 } from "react";
9481
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
8220
9482
  var PICK_BEHAVIORS = /* @__PURE__ */ new Set(["peel", "carry"]);
8221
9483
  function InteractiveField(props) {
8222
9484
  const { papers, fallback, layoutId, layoutOptions, sheetOptions } = props;
@@ -8224,14 +9486,14 @@ function InteractiveField(props) {
8224
9486
  const layout = getLayout(layoutId);
8225
9487
  const reduced = usePrefersReducedMotion(props.reducedMotion);
8226
9488
  const contextRegistry = useContext3(DropZoneContext);
8227
- const registry4 = useMemo8(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
9489
+ const registry4 = useMemo9(() => contextRegistry ?? new DropZoneRegistry(), [contextRegistry]);
8228
9490
  const camera = useThree5((s) => s.camera);
8229
9491
  const gl = useThree5((s) => s.gl);
8230
9492
  const controls = useThree5((s) => s.controls);
8231
9493
  const [removed, setRemoved] = useState6(EMPTY_SET);
8232
9494
  const [slotPatches, setSlotPatches] = useState6({});
8233
9495
  const [slotStates, setSlotStates] = useState6({});
8234
- const slotConfigs = useMemo8(
9496
+ const slotConfigs = useMemo9(
8235
9497
  () => papers.map((slot, i) => {
8236
9498
  const config = resolveFieldSlotConfig(slot, fallback, i, layoutId, layoutOptions);
8237
9499
  const patch = slotPatches[i];
@@ -8245,7 +9507,7 @@ function InteractiveField(props) {
8245
9507
  slotPatches
8246
9508
  ]
8247
9509
  );
8248
- const poses = useMemo8(
9510
+ const poses = useMemo9(
8249
9511
  () => slotConfigs.map((_, i) => layout.pose(i, total, layoutOptions, 0, props.sheet)),
8250
9512
  [layoutId, JSON.stringify(layoutOptions), total, props.sheet.width, props.sheet.height]
8251
9513
  );
@@ -8265,10 +9527,10 @@ function InteractiveField(props) {
8265
9527
  }
8266
9528
  props.onSlotStateChange?.(i, state);
8267
9529
  };
8268
- const raycaster = useMemo8(() => new THREE15.Raycaster(), []);
8269
- const planeScratch = useMemo8(() => new THREE15.Plane(new THREE15.Vector3(0, 0, 1), 0), []);
8270
- const pointScratch = useMemo8(() => new THREE15.Vector3(), []);
8271
- const ndcScratch = useMemo8(() => new THREE15.Vector2(), []);
9530
+ const raycaster = useMemo9(() => new THREE16.Raycaster(), []);
9531
+ const planeScratch = useMemo9(() => new THREE16.Plane(new THREE16.Vector3(0, 0, 1), 0), []);
9532
+ const pointScratch = useMemo9(() => new THREE16.Vector3(), []);
9533
+ const ndcScratch = useMemo9(() => new THREE16.Vector2(), []);
8272
9534
  const planePoint = (clientX, clientY, planeZ) => {
8273
9535
  const rect = gl.domElement.getBoundingClientRect();
8274
9536
  ndcScratch.set(
@@ -8436,7 +9698,7 @@ function InteractiveField(props) {
8436
9698
  if (zone) settleInto(carried, zone);
8437
9699
  else returnHome(carried);
8438
9700
  };
8439
- useEffect13(() => {
9701
+ useEffect14(() => {
8440
9702
  const move = (e) => onPointerMoveRef.current(e);
8441
9703
  const up = (e) => onPointerUpRef.current(e);
8442
9704
  window.addEventListener("pointermove", move);
@@ -8446,7 +9708,7 @@ function InteractiveField(props) {
8446
9708
  window.removeEventListener("pointerup", up);
8447
9709
  };
8448
9710
  }, []);
8449
- useFrame4((_, delta) => {
9711
+ useFrame6((_, delta) => {
8450
9712
  const carried = carriedRef.current;
8451
9713
  if (!carried || carried.settling) return;
8452
9714
  const group = groupRefs.current[carried.slot];
@@ -8472,8 +9734,8 @@ function InteractiveField(props) {
8472
9734
  group.position.z = carried.homePose.position[2] + 0.15;
8473
9735
  handle.set("drive", carryDrive(speed));
8474
9736
  const lag = 0.25;
8475
- group.rotation.y += (THREE15.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
8476
- group.rotation.x += (THREE15.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
9737
+ group.rotation.y += (THREE16.MathUtils.clamp(-vx * lag, -0.6, 0.6) - group.rotation.y) * 0.12;
9738
+ group.rotation.x += (THREE16.MathUtils.clamp(vy * lag * 0.7, -0.5, 0.5) - group.rotation.x) * 0.12;
8477
9739
  }
8478
9740
  const name = slotName(carried.slot);
8479
9741
  const zone = zonesLive().find(
@@ -8509,7 +9771,7 @@ function InteractiveField(props) {
8509
9771
  zoneIds: () => zonesLive().map((z25) => z25.id),
8510
9772
  slotState: (slot) => slotStates[slot] ?? "rest"
8511
9773
  };
8512
- useEffect13(() => {
9774
+ useEffect14(() => {
8513
9775
  const ref = props.a11yRef;
8514
9776
  if (!ref) return;
8515
9777
  ref.current = {
@@ -8523,7 +9785,7 @@ function InteractiveField(props) {
8523
9785
  ref.current = null;
8524
9786
  };
8525
9787
  }, [props.a11yRef]);
8526
- return /* @__PURE__ */ jsxs5("group", { children: [
9788
+ return /* @__PURE__ */ jsxs6("group", { children: [
8527
9789
  sheetOptions?.backing && /* @__PURE__ */ jsx9(BackingSheet, { options: sheetOptions, count: total, removed }),
8528
9790
  (props.zones ?? []).map((zone) => /* @__PURE__ */ jsx9(DropZoneVisual, { registry: registry4, config: zone }, zone.id)),
8529
9791
  slotConfigs.map((config, i) => {
@@ -8563,7 +9825,7 @@ function InteractiveField(props) {
8563
9825
 
8564
9826
  // src/field/keyboardMirror.tsx
8565
9827
  import { useState as useState7 } from "react";
8566
- import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
9828
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
8567
9829
  function fieldKeyboardStep(carry2, slot, key, controller) {
8568
9830
  if (!carry2) {
8569
9831
  if (key === "Enter" || key === " ") {
@@ -8625,9 +9887,9 @@ function FieldKeyboardMirror({
8625
9887
  if (handled) e.preventDefault();
8626
9888
  if (carry2 !== carrying) setCarrying(carry2);
8627
9889
  };
8628
- return /* @__PURE__ */ jsxs6("fieldset", { style: mirrorHidden, children: [
9890
+ return /* @__PURE__ */ jsxs7("fieldset", { style: mirrorHidden, children: [
8629
9891
  /* @__PURE__ */ jsx10("legend", { children: "Interactive papers" }),
8630
- papers.map((slot, i) => /* @__PURE__ */ jsxs6(
9892
+ papers.map((slot, i) => /* @__PURE__ */ jsxs7(
8631
9893
  "button",
8632
9894
  {
8633
9895
  type: "button",
@@ -8636,7 +9898,7 @@ function FieldKeyboardMirror({
8636
9898
  "aria-pressed": carrying?.slot === i,
8637
9899
  children: [
8638
9900
  paperLabel(slot, i),
8639
- carrying?.slot === i && /* @__PURE__ */ jsxs6("span", { "aria-live": "polite", children: [
9901
+ carrying?.slot === i && /* @__PURE__ */ jsxs7("span", { "aria-live": "polite", children: [
8640
9902
  " ",
8641
9903
  "\u2014 carrying; zone ",
8642
9904
  controller.current?.zoneIds()[carrying.zoneIndex] ?? "none",
@@ -8710,17 +9972,17 @@ function fitCamera(layout, n, options, sheet2, fovDeg, aspect, margin = 1.06) {
8710
9972
  }
8711
9973
 
8712
9974
  // src/PaperField.tsx
8713
- import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
9975
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
8714
9976
  var PaperFieldMesh = forwardRef2(
8715
9977
  function PaperFieldMesh2(props, ref) {
8716
9978
  const reduced = usePrefersReducedMotion(props.reducedMotion);
8717
- const papers = useMemo9(
9979
+ const papers = useMemo10(
8718
9980
  () => effectiveFieldPapers(props.papers, props.images),
8719
9981
  [props.papers, props.images]
8720
9982
  );
8721
9983
  const stablePapers = useStable(papers);
8722
9984
  const stablePreset = useStable(props.preset ?? null);
8723
- const groups = useMemo9(
9985
+ const groups = useMemo10(
8724
9986
  () => groupFieldPapers(stablePapers, stablePreset ?? void 0),
8725
9987
  [stablePapers, stablePreset]
8726
9988
  );
@@ -8729,7 +9991,7 @@ var PaperFieldMesh = forwardRef2(
8729
9991
  const layout = getLayout(layoutId);
8730
9992
  const firstSheet = groups[0]?.config.sheet;
8731
9993
  const stableLayoutOptions = useStable(props.layoutOptions ?? {});
8732
- const layoutOptions = useMemo9(
9994
+ const layoutOptions = useMemo10(
8733
9995
  // Sheet grids size their cells from the papers themselves — gutter is
8734
9996
  // then literally the spacing between stamps (explicit cell dims win).
8735
9997
  () => resolveLayoutOptions(layoutId, layout, stableLayoutOptions, firstSheet),
@@ -8744,7 +10006,7 @@ var PaperFieldMesh = forwardRef2(
8744
10006
  const driver = reduced ? "none" : props.motion?.driver ?? "autoplay";
8745
10007
  const speed = props.motion?.speed ?? 0.5;
8746
10008
  const entranceType = reduced ? "none" : props.entrance?.type ?? "rise";
8747
- useEffect14(() => {
10009
+ useEffect15(() => {
8748
10010
  const prev = prevLayout.current;
8749
10011
  if (prev.id !== layoutId || JSON.stringify(prev.options) !== JSON.stringify(layoutOptions)) {
8750
10012
  morphRef.current = { from: prev, t: 0 };
@@ -8752,7 +10014,7 @@ var PaperFieldMesh = forwardRef2(
8752
10014
  prevLayout.current = { id: layoutId, options: layoutOptions };
8753
10015
  }
8754
10016
  }, [layoutId, layoutOptions]);
8755
- useEffect14(() => {
10017
+ useEffect15(() => {
8756
10018
  if (driver !== "drag") return;
8757
10019
  const el = gl.domElement;
8758
10020
  let lastX = null;
@@ -8778,7 +10040,7 @@ var PaperFieldMesh = forwardRef2(
8778
10040
  window.removeEventListener("pointerup", up);
8779
10041
  };
8780
10042
  }, [driver, gl]);
8781
- useFrame5(({ clock }, delta) => {
10043
+ useFrame7(({ clock }, delta) => {
8782
10044
  if (mountTimeRef.current < 0) mountTimeRef.current = clock.elapsedTime;
8783
10045
  if (driver === "autoplay") phaseRef.current += delta * speed * 0.02;
8784
10046
  if (driver === "drag") {
@@ -8820,7 +10082,7 @@ var PaperFieldMesh = forwardRef2(
8820
10082
  }
8821
10083
  ) });
8822
10084
  }
8823
- return /* @__PURE__ */ jsxs7("group", { ref, children: [
10085
+ return /* @__PURE__ */ jsxs8("group", { ref, children: [
8824
10086
  sheetOptions?.backing && /* @__PURE__ */ jsx11(BackingSheet, { options: sheetOptions, count: total, removed: EMPTY_SET }),
8825
10087
  groups.map((group, gi) => /* @__PURE__ */ jsx11(
8826
10088
  FieldGroup,
@@ -8843,7 +10105,7 @@ function FitCamera(meshProps) {
8843
10105
  const imagesProp = useStable(meshProps.images ?? null);
8844
10106
  const presetProp = useStable(meshProps.preset ?? null);
8845
10107
  const optionsProp = useStable(meshProps.layoutOptions ?? {});
8846
- const field = useMemo9(() => {
10108
+ const field = useMemo10(() => {
8847
10109
  const papers = effectiveFieldPapers(papersProp ?? void 0, imagesProp ?? void 0);
8848
10110
  const layoutId = meshProps.layout ?? "ring";
8849
10111
  const layout = getLayout(layoutId);
@@ -8851,8 +10113,8 @@ function FitCamera(meshProps) {
8851
10113
  const options = resolveLayoutOptions(layoutId, layout, optionsProp, sheet2);
8852
10114
  return { layout, n: papers.length, options, sheet: sheet2 ?? DEFAULT_SHEET };
8853
10115
  }, [papersProp, imagesProp, presetProp, meshProps.layout, optionsProp]);
8854
- useEffect14(() => {
8855
- if (!(camera instanceof THREE16.PerspectiveCamera)) return;
10116
+ useEffect15(() => {
10117
+ if (!(camera instanceof THREE17.PerspectiveCamera)) return;
8856
10118
  const { position, target } = fitCamera(
8857
10119
  field.layout,
8858
10120
  field.n,
@@ -8869,20 +10131,20 @@ function FitCamera(meshProps) {
8869
10131
  }
8870
10132
  var PaperField = forwardRef2(function PaperField2({ children, className, style, scene, ...meshProps }, ref) {
8871
10133
  const rig = sceneSchema.parse(scene ?? {});
8872
- const registry4 = useMemo9(() => new DropZoneRegistry(), []);
10134
+ const registry4 = useMemo10(() => new DropZoneRegistry(), []);
8873
10135
  const a11yRef = useRef8(null);
8874
- const papers = useMemo9(
10136
+ const papers = useMemo10(
8875
10137
  () => effectiveFieldPapers(meshProps.papers, meshProps.images),
8876
10138
  [meshProps.papers, meshProps.images]
8877
10139
  );
8878
10140
  const stablePapers = useStable(papers);
8879
10141
  const stablePreset = useStable(meshProps.preset ?? null);
8880
- const interactive = useMemo9(
10142
+ const interactive = useMemo10(
8881
10143
  () => fieldIsInteractive(stablePapers, stablePreset ?? void 0, meshProps.interactive),
8882
10144
  [stablePapers, stablePreset, meshProps.interactive]
8883
10145
  );
8884
- return /* @__PURE__ */ jsx11("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs7(DropZoneContext.Provider, { value: registry4, children: [
8885
- /* @__PURE__ */ jsxs7(Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
10146
+ return /* @__PURE__ */ jsx11("div", { className, style: { width: "100%", height: "100%", ...style }, children: /* @__PURE__ */ jsxs8(DropZoneContext.Provider, { value: registry4, children: [
10147
+ /* @__PURE__ */ jsxs8(Canvas, { shadows: true, camera: { position: [0, 0.6, 5.2], fov: 45 }, dpr: [1, 2], children: [
8886
10148
  /* @__PURE__ */ jsx11(FitCamera, { ...meshProps }),
8887
10149
  /* @__PURE__ */ jsx11(PaperBackdrop, { backdrop: rig.backdrop }),
8888
10150
  /* @__PURE__ */ jsx11(PaperLighting, { preset: rig.lighting, light: rig.light, floor: -2.4, scale: 14 }),
@@ -9153,4 +10415,4 @@ export {
9153
10415
  describeConfig,
9154
10416
  buildAgentPayload
9155
10417
  };
9156
- //# sourceMappingURL=chunk-E22ILEDO.js.map
10418
+ //# sourceMappingURL=chunk-NTDVOOBA.js.map