paperlab 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-V6YDK4H2.js} +23 -5
- package/dist/FxPostPass-V6YDK4H2.js.map +1 -0
- package/dist/chunk-232SU3AQ.js +34 -0
- package/dist/chunk-232SU3AQ.js.map +1 -0
- package/dist/{chunk-GB6BMHC3.js → chunk-7WOGU2D4.js} +7 -19
- package/dist/{chunk-GB6BMHC3.js.map → chunk-7WOGU2D4.js.map} +1 -1
- package/dist/{chunk-MZCAN3AR.js → chunk-SVWWC5UL.js} +576 -160
- package/dist/chunk-SVWWC5UL.js.map +1 -0
- package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-nF1fgK7P.d.cts} +31 -3
- package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-nF1fgK7P.d.ts} +31 -3
- package/dist/fx.cjs +320 -91
- package/dist/fx.cjs.map +1 -1
- package/dist/fx.d.cts +81 -55
- package/dist/fx.d.ts +81 -55
- package/dist/fx.js +278 -48
- package/dist/fx.js.map +1 -1
- package/dist/index.cjs +606 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/{slots-C5PqY0Q5.d.cts → slots-D_EE8GXn.d.cts} +12 -0
- package/dist/{slots-C5PqY0Q5.d.ts → slots-D_EE8GXn.d.ts} +12 -0
- package/dist/stage.cjs +600 -188
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.d.cts +1 -1
- package/dist/stage.d.ts +1 -1
- package/dist/stage.js +2 -2
- package/package.json +1 -1
- package/dist/FxPostPass-M66QMVVC.js.map +0 -1
- package/dist/chunk-7TLIWPGM.js +0 -36
- package/dist/chunk-7TLIWPGM.js.map +0 -1
- package/dist/chunk-MZCAN3AR.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/fx/field.ts","../src/fx/emission.ts","../src/fx/flames.ts","../src/fx/quality.ts"],"sourcesContent":["/**\n * The damage field: what has happened to this sheet, as four numbers per\n * texel, over the sheet's own UV.\n *\n * One primitive rather than one subsystem per effect. Fire is heat diffusing\n * into char and char eating presence; water is saturation diffusing along the\n * fibre; a tear is presence zeroed along a path. They share a grid, a step\n * and a set of paint operations, which is the whole reason the second effect\n * costs a chunk and a preset instead of a new system.\n *\n * R char scorch colour, the brown halo, shrinkage and curl, smoke density\n * G saturation wet darkening, roughness, translucency, added mass, sag\n * B heat the glowing ignition line\n * A presence alpha erosion — burn-away, tear-away, punched holes\n *\n * **It runs on the CPU because of who reads it.** Three of the four layers an\n * effect has to land on consume the field on the CPU: the physics coupling\n * needs stiffness and mass per vertex, the emitters spawn ash where presence\n * reaches zero and drips from the lowest wet vertex, and the sound is driven\n * by the length of the burn front every frame. A GPU field would have fed all\n * three through a `readPixels` per frame — a pipeline stall that tile-based\n * phone GPUs pay the most for. Only the shading reads the field on the GPU,\n * and it gets an 8-bit texture uploaded from here. Testability without a\n * renderer comes free with that, and it is how every property below is known.\n *\n * It stays on the main thread. A worker would need `SharedArrayBuffer` to\n * avoid copying the field every frame, and that needs COOP/COEP headers that\n * GitHub Pages cannot send. The main thread is affordable only because the\n * field costs nothing when nothing is happening to it — see `step`.\n *\n * **It is not tiered.** Every device runs the same grid at the same timestep\n * and produces the same fire. With explicit diffusion, halving the cell size\n * quadruples both the cells and the steps stability needs, so the same fire\n * costs N⁴: a 128 grid is sixteen times a 64 one. The first version tiered\n * grid size and substeps as though they were independent, and the measured\n * result was a fire whose speed depended on the device. Presentation is what\n * tiers now — see `fx/quality.ts`.\n */\n\nimport { DAMAGE_CHANNELS, type DamageSource } from '../surface/damageContract'\n\n/**\n * Channel offsets into a texel — the contract's, not this file's. The sheet\n * defines what the four bytes mean because the sheet is what draws them;\n * the field is one thing that writes them.\n */\nexport const CHAR = DAMAGE_CHANNELS.char\nexport const SATURATION = DAMAGE_CHANNELS.saturation\nexport const HEAT = DAMAGE_CHANNELS.heat\nexport const PRESENCE = DAMAGE_CHANNELS.presence\n\n/**\n * The grid, in texels along each edge. One number for every device.\n *\n * 64 until a real phone says otherwise — the P1 gate is where this gets\n * settled for good, by rendering a burn at 64 and 96 and looking. What the\n * field carries are soft quantities with soft edges; the RAGGED edge a burn\n * reads as comes from the anisotropy and the grain, which survive a coarse\n * grid, and from per-fragment detail in the shader, which does not need one.\n */\nexport const FIELD_SIZE = 64\n\n/**\n * The simulation's own clock, independent of the frame rate.\n *\n * The first version ran a fixed number of substeps per FRAME, so a slow frame\n * meant a longer substep, the stability clamp scaled the diffusion down to\n * compensate, and the same simulated second burned half as far at 30 fps as\n * at 144. Measured on the high tier: 0.202, 0.290 and 0.439 UV of burn radius\n * at 30, 60 and 144 fps. A fire that burns slower when the phone is busy is a\n * fire that behaves differently on every device and every run.\n *\n * `ClothSim` already solved this with the same accumulator and the same\n * number, and the field now matches it. The rates are chosen so that the\n * stability bound is never reached at 1/120 — the clamp in `stencil` is a\n * safety net for extreme options, not something defaults lean on, and a test\n * says so.\n */\nexport const FIXED_DT = 1 / 120\n\n/**\n * How far behind the accumulator may fall before it drops time.\n *\n * Eight steps covers any frame down to 15 fps exactly. Below that the fire\n * slows rather than the page spiralling — each late frame asking for more\n * steps, which make the next frame later still.\n */\nconst MAX_STEPS_PER_FRAME = 8\n\n/**\n * Below this, a cell is at rest: it no longer keeps the field awake.\n *\n * Cooling and drying are exponential and never arrive at zero on their own,\n * so without a threshold a fire that went out ten minutes ago would keep\n * every cell it touched awake forever.\n *\n * It decides whether a cell is AWAKE, and never rewrites a value. The first\n * version snapped values below it to zero, and that is a leak with a worse\n * shape than it sounds: the leading edge of anything spreading is exactly the\n * set of cells receiving less than this per step, so every step zeroed the\n * front before it could advance, and a wet patch drained from its rim —\n * three-quarters of it gone in three seconds, with drying switched off. When\n * every cell is below this the field sleeps with the residue frozen in place:\n * nothing lost, and invisible at 8 bits.\n */\nconst REST = 1e-3\n\n/**\n * How hot paper has to get before it starts to char, as a fraction of a\n * texel's own tinder value.\n *\n * Below the heat a flame deposits and above what diffusion alone leaves\n * behind — which is the gap the whole fire lives in. Too high and a match\n * cannot light the sheet; too low and the preheated region ahead of the\n * front ignites all at once and the burn is a disc rather than a front.\n */\nconst IGNITION = 0.35\n\n/**\n * The largest the stencil's coefficients may sum to in one step.\n *\n * The positive nine-point stencil below is monotone — no overshoot, so the\n * 0..1 clamp never eats mass — while this stays under a half. 0.4 leaves\n * margin. Defaults sit well below it; see `FIXED_DT`.\n */\nconst STABLE = 0.4\n\nexport interface DamageFieldOptions {\n /**\n * Which way the paper's fibres run, in radians over the sheet's UV, where\n * 0 points along +u.\n *\n * Paper is not isotropic and this is the single number that makes it look\n * like paper rather than like a fluid. Fibres are laid down along the\n * machine direction when the sheet is made; liquid wicks along them far\n * faster than across them, and a flame follows them for the same reason.\n * A wet front with anisotropy at 1 is a circle, which reads as a stain on\n * cloth; with it raised, the front is an ellipse along this angle, which\n * reads as paper. Any angle — see `diffusionTensor`.\n */\n fibre?: number\n /** How much faster things travel along the fibre than across it. */\n anisotropy?: number\n /** Heat spread, in UV² per second, averaged over direction. */\n heatDiffusion?: number\n /** Liquid wicking, in UV² per second, averaged over direction. */\n wicking?: number\n /** How fast heat turns paper to char. */\n charRate?: number\n /** How fast char consumes presence — the burn-away. */\n consumeRate?: number\n /** How much heat burning gives back. Above ~1 the front sustains itself. */\n combustion?: number\n /** Heat lost to the room each second, as a fraction. */\n cooling?: number\n /** How much heat it costs to boil a wet cell dry before it can char. */\n wetResistance?: number\n /** How fast standing water leaves the sheet, as a fraction per second. */\n drying?: number\n /**\n * Per-texel variation in how readily the paper takes light, 0..1.\n *\n * Real paper is not uniform, and a burn front on a uniform sheet advances\n * as a perfect circle — the single most synthetic-looking thing this\n * simulation could do. This is what makes the edge ragged, and it is fixed\n * per field rather than per frame, because a sheet's grain does not change\n * while you hold it.\n */\n grain?: number\n /** Seed for the grain, so a field is reproducible. */\n seed?: number\n}\n\n/**\n * **Fire's four rates were all six times too fast, and they were wrong\n * together.**\n *\n * Measured on the scripted burn: the front spread at 39 mm/s from the centre\n * and 27 mm/s from a corner, against the 3–8 mm/s\n * `paperlab-fx-fire-spec.md` §9 asks for. A sheet was 76% gone in 4.8 s, so\n * there was no time to watch it spread and every phase was judged at the\n * wrong size — \"dying\" was a strip of paper under huge flames, and \"smoulder\"\n * and \"cold\" were the same frame.\n *\n * The fix is a **uniform time dilation**, not four independent retunes. This\n * is a reaction–diffusion front, so its speed goes as `sqrt(D · k)` and the\n * charred band behind it as `D / v`: divide every RATE by the same number and\n * the front slows by that number while the burn keeps its exact shape. Divide\n * them unevenly and it does not — measured, cutting `heatDiffusion` and\n * `charRate` alone (leaving `cooling`) put the front below the threshold that\n * sustains it and the fire went out with 1% of the sheet gone.\n *\n * So `heatDiffusion`, `charRate` and `cooling` are the old numbers over six,\n * which leaves every ratio between them — and `combustion`, which is already\n * a ratio — untouched. Measured after: **6.8 mm/s from the centre and 3.6 from\n * a corner** (a corner spreads along the sheet's fibre as a line rather than\n * a disc, which is slower and is the field's own doing), a hole 22 mm across\n * at 4 s, and a fire that lives about 11 s.\n *\n * Water's rates (`wicking`, `drying`) are deliberately NOT dilated: nothing\n * has ever measured a wet front against a target, and the burn is identical\n * either way (saturation is 0 in a dry burn), so changing them here would be\n * an unmeasured change riding along with a measured one.\n *\n * `consumeRate` comes back DOWN, from 20 to 6, for the same reason it went up.\n * At 20 it was holding the charred band to 8.5 mm against a front moving six\n * times too fast; the band is `v / consumeRate`, so once the front slowed, 20\n * squeezed it to 2.9 mm. 6 puts it at 5.3 mm — mid-range of §5's 2–8 mm.\n * Measured band against this number, at the new pace: 5 → 6.0 mm, 6 → 5.3,\n * 7 → 4.7, 9 → 4.0, 20 → 2.9.\n */\nconst DEFAULTS = {\n fibre: 0,\n anisotropy: 3,\n heatDiffusion: 0.00058,\n wicking: 0.0045,\n charRate: 2.7,\n consumeRate: 6,\n combustion: 2.4,\n cooling: 0.18,\n wetResistance: 2.6,\n drying: 0.015,\n grain: 0.34,\n seed: 1,\n} satisfies Required<DamageFieldOptions>\n\n/** What the field did this step. The numbers the sound and the emitters read. */\nexport interface FieldStats {\n /**\n * How much sheet is actively burning right now, 0..1.\n *\n * The fraction of texels part-way through charring AND still hot.\n * Resolution-independent by construction. Both halves matter: combustion\n * pumps heat into everything it has already burnt, so \"hot\" alone lights\n * up the whole interior; and a fire that went out leaves partly charred\n * cells behind, so \"part-charred\" alone would report a burn that is over as\n * one still going.\n *\n * This is the number fire's audio is driven by, and it is the right one:\n * a fire gets louder as the front gets LONGER, not as the burnt area gets\n * bigger. A sheet nearly consumed is quiet again, which is true, and which\n * a level driven by char would get exactly backwards.\n */\n front: number\n /** Texels that crossed into char this step. Drives crackle rate and smoke. */\n charred: number\n /** Texels whose presence reached zero this step. Drives ash, and burn-through. */\n consumed: number\n /** Texels that became wetter this step. Drives the water bed's level. */\n wetted: number\n /** Mean saturation over the sheet, 0..1. Drives added mass in the coupling. */\n saturation: number\n /** Fraction of the sheet still present, 1 at the start. */\n remaining: number\n}\n\n/**\n * The per-neighbour weights of one diffusing quantity, already multiplied by\n * the timestep over the cell size squared.\n */\nexport interface Stencil {\n /** East and west neighbours. */\n ax: number\n /** North and south neighbours. */\n ay: number\n /** The two neighbours on one diagonal. */\n ad: number\n /** Which diagonal: +1 is north-east/south-west, -1 is north-west/south-east. */\n diagonal: 1 | -1\n /** True if the stability bound had to scale these down. Never, at defaults. */\n clamped: boolean\n}\n\n/**\n * The diffusion tensor for paper with its fibre at `fibre` radians.\n *\n * Along the fibre things travel `anisotropy` times faster than across it, and\n * the two are normalised so that their mean is `rate` — turning the grain\n * changes which way things go, never how fast overall.\n *\n * The first version reduced this to two per-axis weights, `[cos², sin²]`,\n * which is the tensor with its off-diagonal term thrown away. That can only\n * describe ellipses aligned to the UV axes, so it was right at 0° and 90° —\n * the two angles the tests used — and wrong everywhere else: a 30° grain made\n * a 0° front with the anisotropy halved, and a 45° grain made a circle.\n */\nexport function diffusionTensor(rate: number, anisotropy: number, fibre: number) {\n const along = (rate * 2 * anisotropy) / (1 + anisotropy)\n const across = (rate * 2) / (1 + anisotropy)\n const c = Math.cos(fibre)\n const s = Math.sin(fibre)\n return {\n xx: along * c * c + across * s * s,\n yy: along * s * s + across * c * c,\n xy: (along - across) * c * s,\n }\n}\n\n/**\n * A positive nine-point stencil for that tensor.\n *\n * The textbook cross-derivative puts NEGATIVE weights on two of the diagonal\n * neighbours, and a negative weight overshoots: the update dips below zero,\n * the 0..1 clamp clips the dip, and the clip is not symmetric — which is\n * precisely how the first version of this file lost a wet patch in two\n * seconds. So the cross term is written as a second difference along ONE\n * diagonal instead, the one the fibre leans toward:\n *\n * Dxx u_xx + 2 Dxy u_xy + Dyy u_yy\n * = (Dxx − |Dxy|) u_xx + (Dyy − |Dxy|) u_yy + 2 |Dxy| u_dd\n *\n * Every weight is non-negative while |Dxy| ≤ min(Dxx, Dyy), which holds at\n * any angle for anisotropy up to about 5. Beyond that |Dxy| is capped at the\n * bound: the front comes out slightly rounder than asked for, which is a far\n * better failure than a front that eats itself.\n */\nexport function stencil(rate: number, anisotropy: number, fibre: number, dt = FIXED_DT): Stencil {\n const d = diffusionTensor(rate, anisotropy, fibre)\n const cross = Math.min(Math.abs(d.xy), d.xx, d.yy)\n const scale = dt * (FIELD_SIZE - 1) * (FIELD_SIZE - 1)\n let ax = (d.xx - cross) * scale\n let ay = (d.yy - cross) * scale\n let ad = cross * scale\n const total = ax + ay + ad\n const clamped = total > STABLE\n if (clamped) {\n const k = STABLE / total\n ax *= k\n ay *= k\n ad *= k\n }\n return { ax, ay, ad, diagonal: d.xy >= 0 ? 1 : -1, clamped }\n}\n\n/** Cheap reproducible hash noise — same input, same sheet, every time. */\nfunction hash(x: number, y: number, seed: number): number {\n // Multipliers kept inside 32 bits on purpose: anything larger is silently\n // rounded by a double before `^` truncates it, which makes the \"hash\"\n // lose its low bits and the grain come out in visible bands.\n let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(seed, 1274126177)\n h = Math.imul(h ^ (h >>> 13), 1274126177)\n return ((h ^ (h >>> 16)) >>> 0) / 4294967296\n}\n\n/** An inclusive rectangle of cells. Empty when x0 > x1. */\ninterface Box {\n x0: number\n y0: number\n x1: number\n y1: number\n}\n\nconst EMPTY = (): Box => ({ x0: 1, y0: 1, x1: 0, y1: 0 })\n\nexport class DamageField implements DamageSource {\n readonly size = FIELD_SIZE\n /** RGBA per texel in float, row-major from v = 0. The simulation's own state. */\n readonly data: Float32Array\n /** The same, at 8 bits, for upload. Kept in step with `data` over what changed. */\n readonly pixels: Uint8Array\n /**\n * How ragged the sheet DRAWS this field's edges, 0..1 — see\n * `DamageSource.detail`. Not a simulation option, because it changes\n * nothing the field computes: set it from `fxQualityFor(tier).detail`, and\n * again whenever the tier moves.\n */\n detail = 1\n private revision = 0\n\n private readonly next: Float32Array\n /** Per-texel ignition threshold, fixed for the life of the sheet. */\n private readonly tinder: Float32Array\n private readonly o: Required<DamageFieldOptions>\n private readonly heat: Stencil\n private readonly water: Stencil\n private accumulator = 0\n /** Fixed steps run since the field was made — its own clock. See {@link time}. */\n private steps = 0\n /** Cells that might change on the next step. Everything outside is at rest. */\n private active: Box = EMPTY()\n /** Cells written since the last pack into `pixels`. */\n private touched: Box = EMPTY()\n private visited = 0\n /** Running totals, so the stats never have to walk the whole grid. */\n private present: number\n private wetTotal = 0\n private stats: FieldStats = {\n front: 0,\n charred: 0,\n consumed: 0,\n wetted: 0,\n saturation: 0,\n remaining: 1,\n }\n /**\n * Texels whose presence reached zero during the last `step`, in the first\n * {@link consumedCount} slots — where ash leaves from.\n *\n * WHERE, not just how many. The stats were enough for the sound, which\n * wants a level; an emitter wants a place. Fixed buffers the size of the\n * grid and written in place: a burning sheet must not allocate a list a\n * step, and a texel is consumed once, so a step can never fill it twice.\n */\n readonly consumedCells = new Int32Array(FIELD_SIZE * FIELD_SIZE)\n /** The burn front as of the last step that ran, in the first {@link frontCount} slots — where embers and smoke leave from. */\n readonly frontCells = new Int32Array(FIELD_SIZE * FIELD_SIZE)\n private consumedLength = 0\n private frontLength = 0\n\n constructor(options: DamageFieldOptions = {}) {\n this.o = { ...DEFAULTS, ...options }\n const n = FIELD_SIZE * FIELD_SIZE\n this.data = new Float32Array(n * 4)\n this.next = new Float32Array(n * 4)\n this.pixels = new Uint8Array(n * 4)\n this.tinder = new Float32Array(n)\n this.present = n\n\n for (let i = 0; i < n; i++) {\n // Presence starts at 1 — the whole sheet is there. Everything else at 0.\n this.data[i * 4 + PRESENCE] = 1\n this.pixels[i * 4 + PRESENCE] = 255\n const x = i % FIELD_SIZE\n const y = (i / FIELD_SIZE) | 0\n // Two octaves: a coarse one that makes whole regions catch before their\n // neighbours, and a fine one that frays the edge between them. Centred\n // on 1, so `grain` changes only the VARIANCE — written as\n // `1 - grain * noise` it also lowered the mean, and the knob was\n // silently two knobs.\n const coarse = hash(x >> 2, y >> 2, this.o.seed)\n const fine = hash(x, y, this.o.seed * 7 + 11)\n this.tinder[i] = 1 + this.o.grain * (coarse * 0.65 + fine * 0.35 - 0.5)\n }\n this.next.set(this.data)\n\n // Constant for the life of the field, now that the timestep is.\n this.heat = stencil(this.o.heatDiffusion, this.o.anisotropy, this.o.fibre)\n this.water = stencil(this.o.wicking, this.o.anisotropy, this.o.fibre)\n }\n\n /** Bumped whenever `pixels` changes. */\n get version(): number {\n return this.revision\n }\n\n /**\n * Simulated seconds this field has burned for — whole fixed steps, never\n * wall time.\n *\n * What anything DRAWN from the field animates on: the ember line's beads\n * flicker and crawl, flames puff, and all of it has to be the same at the\n * same moment of the same burn, or a replay flickers differently from the\n * original and a capture can never be taken twice. Stops while the field\n * sleeps, which is right — a sheet at rest has nothing hot left to move.\n */\n get time(): number {\n return this.steps * FIXED_DT\n }\n\n /** Nothing is happening to this sheet, and stepping it costs nothing. */\n get asleep(): boolean {\n return this.active.x0 > this.active.x1\n }\n\n /**\n * Cells the last `step` visited.\n *\n * The cost of the field, stated in the one unit that means the same thing on\n * every machine. Milliseconds would make a test of it a test of whoever ran\n * it — which is how the hands harness spent weeks passing on one laptop.\n */\n get cellsVisited(): number {\n return this.visited\n }\n\n /** What the last `step` produced. */\n get lastStats(): FieldStats {\n return this.stats\n }\n\n /** How many of {@link consumedCells} the last `step` wrote. Always `lastStats.consumed`. */\n get consumedCount(): number {\n return this.consumedLength\n }\n\n /**\n * How many of {@link frontCells} are current. `lastStats.front` times the\n * texel count — and held, like it, across a frame too short to step.\n */\n get frontCount(): number {\n return this.frontLength\n }\n\n /** Texel index for a UV, clamped to the sheet. */\n private at(u: number, v: number): number {\n const x = Math.min(FIELD_SIZE - 1, Math.max(0, Math.round(u * (FIELD_SIZE - 1))))\n const y = Math.min(FIELD_SIZE - 1, Math.max(0, Math.round(v * (FIELD_SIZE - 1))))\n return y * FIELD_SIZE + x\n }\n\n /** The four channels at a UV, for anything that needs to ask a question of a point. */\n sample(u: number, v: number): [number, number, number, number] {\n const i = this.at(u, v) * 4\n return [this.data[i]!, this.data[i + 1]!, this.data[i + 2]!, this.data[i + 3]!]\n }\n\n /**\n * Add to one channel in a soft disc.\n *\n * Every paint operation is this with a different channel, which is the\n * point of having one primitive: `ignite` and `wet` are not two systems\n * that happen to look alike, they are the same write.\n *\n * `plateau` is the fraction of the radius that takes the full amount\n * before the falloff starts. Zero for anything added, so the deposit has a\n * soft peak — a flame held near paper does not deposit a stamped disc of\n * heat, and a too-perfect edge is the first thing that reads as fake.\n * Raised for anything removed, because a pure smoothstep never quite\n * reaches zero even at its centre, and \"almost all the way through\" is not\n * a hole.\n */\n paint(channel: number, u: number, v: number, radius: number, amount: number, plateau = 0): void {\n if (radius <= 0 || amount === 0) return\n const last = FIELD_SIZE - 1\n const cx = u * last\n const cy = v * last\n const r = radius * last\n const box: Box = {\n x0: Math.max(0, Math.floor(cx - r)),\n y0: Math.max(0, Math.floor(cy - r)),\n x1: Math.min(last, Math.ceil(cx + r)),\n y1: Math.min(last, Math.ceil(cy + r)),\n }\n if (box.x0 > box.x1 || box.y0 > box.y1) return\n const r2 = r * r\n const { data, next } = this\n\n for (let y = box.y0; y <= box.y1; y++) {\n for (let x = box.x0; x <= box.x1; x++) {\n const dx = x - cx\n const dy = y - cy\n const d2 = dx * dx + dy * dy\n if (d2 > r2) continue\n const t = plateau >= 1 ? 1 : Math.min(1, (1 - Math.sqrt(d2) / r) / (1 - plateau))\n const falloff = t * t * (3 - 2 * t)\n const i = (y * FIELD_SIZE + x) * 4 + channel\n const before = data[i]!\n // Presence is the one channel that is REMOVED rather than added, and\n // it must never come back — see `cut`.\n const after = Math.min(1, Math.max(0, before + amount * falloff))\n if (after === before) continue\n data[i] = after\n next[i] = after\n if (channel === PRESENCE) this.present += after - before\n if (channel === SATURATION) this.wetTotal += after - before\n }\n }\n\n grow(this.active, box)\n grow(this.touched, box)\n this.pack()\n }\n\n /** Hold a flame near the sheet. Heat, not char — the burning is the field's job. */\n ignite(u: number, v: number, radius = 0.06, amount = 1): void {\n this.paint(HEAT, u, v, radius, amount)\n }\n\n /** Wet the sheet. Saturation wicks along the fibre from wherever it lands. */\n wet(u: number, v: number, radius = 0.08, amount = 0.9): void {\n this.paint(SATURATION, u, v, radius, amount)\n }\n\n /**\n * Take the paper away along a path: a tear, a cut, a punched hole.\n *\n * Presence only ever decreases. A sheet does not grow back, and a paint op\n * that could raise it would make every burn reversible by accident.\n */\n cut(u0: number, v0: number, u1: number, v1: number, width = 0.02): void {\n const steps = Math.max(1, Math.ceil(Math.hypot(u1 - u0, v1 - v0) * FIELD_SIZE))\n for (let s = 0; s <= steps; s++) {\n const t = s / steps\n this.paint(PRESENCE, u0 + (u1 - u0) * t, v0 + (v1 - v0) * t, width, -1, 0.5)\n }\n }\n\n /** Punch a hole. The middle of the sheet, which a fixed-topology mesh cannot do. */\n punch(u: number, v: number, radius = 0.04): void {\n this.paint(PRESENCE, u, v, radius, -1, 0.5)\n }\n\n /**\n * Advance the field by a frame's worth of real time.\n *\n * Fixed steps from an accumulator, so the same simulated second is the same\n * fire at any frame rate. A sheet nothing is happening to returns at once\n * and visits no cells at all — which is the condition for keeping this on\n * the main thread, and the thing the first version could not do: an\n * untouched sheet cost exactly what a burning one did.\n */\n step(delta: number): FieldStats {\n this.visited = 0\n // The transient outputs belong to the step that produced them, and they\n // are cleared BEFORE the guards below rather than after. A frame with no\n // time in it — the first one, or one whose clock went backwards — used to\n // hand back the LAST frame's `consumed` cells and charred count, and the\n // emitters and the sound duly shed the same ash and played the same\n // crackles a second time. `front` is not transient: it is the state of\n // the burn, and it is held across a frame too short to step on purpose.\n this.consumedLength = 0\n if (delta <= 0) {\n this.stats = { ...this.stats, charred: 0, consumed: 0, wetted: 0 }\n return this.stats\n }\n if (this.asleep) {\n // Asleep means nothing CAN change, so there is no time owed either;\n // banking it would replay a burst of steps the moment something wakes.\n this.accumulator = 0\n this.frontLength = 0\n this.stats = { ...this.stats, front: 0, charred: 0, consumed: 0, wetted: 0 }\n return this.stats\n }\n\n this.accumulator = Math.min(this.accumulator + delta, FIXED_DT * MAX_STEPS_PER_FRAME)\n let charred = 0\n let consumed = 0\n let wetted = 0\n let front = 0\n let stepped = false\n while (this.accumulator >= FIXED_DT && !this.asleep) {\n this.accumulator -= FIXED_DT\n this.steps++\n stepped = true\n const done = this.substep()\n charred += done.charred\n consumed += done.consumed\n wetted += done.wetted\n front = done.front\n }\n this.pack()\n\n const n = FIELD_SIZE * FIELD_SIZE\n this.stats = {\n // A frame shorter than one fixed step runs none — one frame in six at\n // 144 Hz — and nothing about the fire changed on it. Reporting 0 there\n // would drop the burn's sound to silence mid-burn, six times a second.\n front: stepped ? front / n : this.stats.front,\n charred,\n consumed,\n wetted,\n saturation: this.wetTotal / n,\n remaining: this.present / n,\n }\n return this.stats\n }\n\n /**\n * One fixed step of diffusion and reaction, over the active region only.\n *\n * The region is the box around every cell that could change, grown by one\n * cell because diffusion reaches exactly one neighbour per step. Cells\n * outside it are at rest and read-only here — their neighbours may read\n * them, and nothing writes them.\n *\n * Written into `next` and copied back over the same region, rather than\n * swapping the two buffers. A swap needs both buffers to agree everywhere\n * the step did not write, and a region that SHRINKS breaks that: a cell the\n * last step wrote into one buffer still holds an older value in the other.\n * Copying the region back keeps them identical outside it, for the cost of\n * the region itself — which is the cost that matters, since it is zero on\n * a sheet at rest rather than a whole grid every step.\n */\n private substep(): { charred: number; consumed: number; wetted: number; front: number } {\n const { data, next, tinder, o } = this\n const heat = this.heat\n const water = this.water\n const size = FIELD_SIZE\n const last = size - 1\n const dt = FIXED_DT\n\n const region: Box = {\n x0: Math.max(0, this.active.x0 - 1),\n y0: Math.max(0, this.active.y0 - 1),\n x1: Math.min(last, this.active.x1 + 1),\n y1: Math.min(last, this.active.y1 + 1),\n }\n const awake = EMPTY()\n\n let charred = 0\n let consumed = 0\n let wetted = 0\n let front = 0\n // This step's front replaces the last one's; see `frontCells`.\n const { frontCells, consumedCells } = this\n\n // The diagonal the fibre leans toward, as index offsets. Constant per\n // field; hoisted so the inner loop is arithmetic and nothing else.\n const hd = heat.diagonal\n const wd = water.diagonal\n\n // The region's edge is a NO-FLUX boundary, exactly like the sheet's.\n // A cell on the rim reading a neighbour outside the region would take or\n // give heat and water to a cell that is not being updated — a transfer\n // with no second half, which leaked 7% of a wet patch in three seconds.\n // Treating the outside as absent makes every exchange mirrored. Nothing\n // stops spreading because of it: a rim cell that gathers more than\n // `REST` wakes, and the region grows past it on the next step. What\n // cannot cross is a residue too thin to wake anything — which is also\n // why a wet front on paper has an edge instead of an infinite tail.\n for (let y = region.y0; y <= region.y1; y++) {\n const hasN = y < region.y1\n const hasS = y > region.y0\n for (let x = region.x0; x <= region.x1; x++) {\n this.visited++\n const i = y * size + x\n const b = i * 4\n const presence = data[b + PRESENCE]!\n\n // Gone is gone: a hole neither conducts heat nor holds water, which\n // is what makes a burnt-through region stop the fire rather than\n // carry it. The A channel is a boundary condition, not just a mask.\n if (presence <= 0) {\n // Whatever water this cell held went with the paper. Take it off the\n // running total as well as the cell, or `saturation` reports water\n // that is gone — and the coupling reads that number as added mass.\n this.wetTotal -= data[b + SATURATION]!\n next[b + HEAT] = 0\n next[b + SATURATION] = 0\n next[b + CHAR] = data[b + CHAR]!\n next[b + PRESENCE] = 0\n continue\n }\n\n const hasE = x < region.x1\n const hasW = x > region.x0\n // Missing neighbours are this cell, which makes the edge of the sheet\n // a no-flux boundary without a branch in the arithmetic below.\n const e = hasE ? i + 1 : i\n const w = hasW ? i - 1 : i\n const n = hasN ? i + size : i\n const s = hasS ? i - size : i\n // The two diagonal neighbours on each stencil's own diagonal.\n const hd1 = hd > 0 ? (hasN && hasE ? i + size + 1 : i) : hasN && hasW ? i + size - 1 : i\n const hd2 = hd > 0 ? (hasS && hasW ? i - size - 1 : i) : hasS && hasE ? i - size + 1 : i\n const wd1 = wd > 0 ? (hasN && hasE ? i + size + 1 : i) : hasN && hasW ? i + size - 1 : i\n const wd2 = wd > 0 ? (hasS && hasW ? i - size - 1 : i) : hasS && hasE ? i - size + 1 : i\n\n // Flux between two cells is weighted by the LESSER of their\n // presences. Symmetric, so what leaves one cell is exactly what\n // arrives in the other and nothing is created or lost at the edge of\n // a hole — the first version weighted by the neighbour alone, which\n // is not.\n const me = e * 4\n const mw = w * 4\n const mn = n * 4\n const ms = s * 4\n const pe = Math.min(presence, data[me + PRESENCE]!)\n const pw = Math.min(presence, data[mw + PRESENCE]!)\n const pn = Math.min(presence, data[mn + PRESENCE]!)\n const ps = Math.min(presence, data[ms + PRESENCE]!)\n\n // ── Heat ─────────────────────────────────────────────────────────\n const h0 = data[b + HEAT]!\n const h1 = hd1 * 4\n const h2 = hd2 * 4\n let h =\n h0 +\n heat.ax * ((data[me + HEAT]! - h0) * pe + (data[mw + HEAT]! - h0) * pw) +\n heat.ay * ((data[mn + HEAT]! - h0) * pn + (data[ms + HEAT]! - h0) * ps) +\n heat.ad *\n ((data[h1 + HEAT]! - h0) * Math.min(presence, data[h1 + PRESENCE]!) +\n (data[h2 + HEAT]! - h0) * Math.min(presence, data[h2 + PRESENCE]!))\n h -= h * o.cooling * dt\n\n // ── Water ────────────────────────────────────────────────────────\n const sat = data[b + SATURATION]!\n const w1 = wd1 * 4\n const w2 = wd2 * 4\n let g =\n sat +\n water.ax * ((data[me + SATURATION]! - sat) * pe + (data[mw + SATURATION]! - sat) * pw) +\n water.ay * ((data[mn + SATURATION]! - sat) * pn + (data[ms + SATURATION]! - sat) * ps) +\n water.ad *\n ((data[w1 + SATURATION]! - sat) * Math.min(presence, data[w1 + PRESENCE]!) +\n (data[w2 + SATURATION]! - sat) * Math.min(presence, data[w2 + PRESENCE]!))\n // Boiling off costs heat, which is exactly why a wet sheet will not\n // light: the flame front spends itself drying the paper in front of\n // it and arrives with nothing left. This one term is the whole of\n // \"dunk the burning corner\".\n if (g > 0 && h > 0) {\n const boiled = Math.min(g, h * o.wetResistance * dt)\n g -= boiled\n h -= boiled / o.wetResistance\n }\n // Proportional, not absolute: an absolute sink annihilates a thin\n // film the moment diffusion spreads it below the per-step amount.\n g -= g * o.drying * dt\n\n // ── Char ─────────────────────────────────────────────────────────\n // Only paper dry enough to burn chars, and only above its own\n // threshold, which varies per texel — that is the ragged front.\n const char = data[b + CHAR]!\n let c = char\n const threshold = tinder[i]! * IGNITION\n if (h > threshold && g < 0.25) {\n const made = Math.min(1 - c, o.charRate * (h - threshold) * dt)\n if (made > 0) {\n c += made\n // Burning is exothermic; above 1 the front feeds itself and runs.\n h += made * o.combustion\n if (char < 0.5 && c >= 0.5) charred++\n }\n }\n\n // ── Presence ─────────────────────────────────────────────────────\n // Fully charred paper is consumed, one way. The only thing besides a\n // cut that removes it.\n let p = presence\n if (c > 0.85) {\n p = Math.max(0, p - o.consumeRate * (c - 0.85) * dt)\n if (p <= 0) {\n consumed++\n consumedCells[this.consumedLength++] = i\n }\n }\n\n h = Math.min(1, Math.max(0, h))\n g = Math.min(1, Math.max(0, g))\n c = Math.min(1, c)\n if (g > sat + 1e-6) wetted++\n if (p > 0.15 && c > 0.08 && c < 0.92 && h > 0.1) frontCells[front++] = i\n\n next[b + CHAR] = c\n next[b + SATURATION] = g\n next[b + HEAT] = h\n next[b + PRESENCE] = p\n this.present += p - presence\n this.wetTotal += g - sat\n\n // Still able to change next step: hot, wet, or being consumed.\n if (h > REST || g > REST || (c > 0.85 && p > 0)) {\n if (x < awake.x0) awake.x0 = x\n if (x > awake.x1) awake.x1 = x\n if (y < awake.y0) awake.y0 = y\n if (y > awake.y1) awake.y1 = y\n }\n }\n }\n\n // Back over the same region only — see the note above.\n for (let y = region.y0; y <= region.y1; y++) {\n const from = (y * size + region.x0) * 4\n const to = (y * size + region.x1 + 1) * 4\n data.set(next.subarray(from, to), from)\n }\n\n grow(this.touched, region)\n this.active = awake\n this.frontLength = front\n return { charred, consumed, wetted, front }\n }\n\n /** Quantise everything touched since the last pack, and mark it uploadable. */\n private pack(): void {\n const t = this.touched\n if (t.x0 > t.x1) return\n const { data, pixels } = this\n for (let y = t.y0; y <= t.y1; y++) {\n const from = (y * FIELD_SIZE + t.x0) * 4\n const to = (y * FIELD_SIZE + t.x1 + 1) * 4\n for (let k = from; k < to; k++) pixels[k] = Math.round(data[k]! * 255)\n }\n this.touched = EMPTY()\n this.revision++\n }\n}\n\n/** Grow a box to include another. */\nfunction grow(into: Box, box: Box): void {\n if (box.x0 > box.x1) return\n if (into.x0 > into.x1) {\n into.x0 = box.x0\n into.y0 = box.y0\n into.x1 = box.x1\n into.y1 = box.y1\n return\n }\n if (box.x0 < into.x0) into.x0 = box.x0\n if (box.y0 < into.y0) into.y0 = box.y0\n if (box.x1 > into.x1) into.x1 = box.x1\n if (box.y1 > into.y1) into.y1 = box.y1\n}\n","/**\n * How bright fire is, in one unit: **multiples of paper white.**\n *\n * `paperlab-fx-fire-spec.md` §4.3 makes exactly one quantitative claim about\n * what fire must look like — an ember is 4–8× brighter than the whitest paper\n * in the frame, which is what makes a bloom pass spread it and a tone curve\n * roll it to white. Everything else about the look is a colour or a\n * millimetre; this is the only number, and it is the one the first fire got\n * wrong twice.\n *\n * It got it wrong twice because until this file there was **no way to say it.**\n * The same claim was authored four different ways:\n *\n * - `plEmberRamp` (`surface/compose.ts`) as sRGB hex through `plLinear`,\n * scaled by a `DamageLook` multiplier;\n * - `particlePresets.ember` as a raw linear triple, `[12, 4.2, 0.9]`;\n * - the fluid's render pass as a `glow` curve times a blackbody;\n * - and the bloom threshold as one absolute constant, 3.6.\n *\n * Four conventions, one threshold, and nothing converting between them. The\n * consequence was not that a number was slightly off. It was that **being 2×\n * under was invisible**: the fluid's flames — the biggest light in the frame —\n * peaked at a scene luminance of 1.68 against a threshold of 3.6, so they\n * never bloomed at all, and turning bloom on at the burn's peak changed 648 of\n * 960,000 pixels. Nobody could have seen that without doing this file's\n * arithmetic by hand, and somebody eventually did, in a review.\n *\n * So: emitters are authored HERE, in multiples of {@link PAPER_WHITE}, and\n * `emission.test.ts` asserts that each one lands in the band the spec asks\n * for. A number that drifts out of range now fails a test instead of waiting\n * for an afternoon with a screenshot and a calculator.\n *\n * Nothing in this file imports anything. It is the unit, so it has to be\n * readable from the sheet's shader constants, from a particle preset and from\n * the post pass alike — and `FxPostPass` is the only file in `paperlab/fx`\n * allowed to name `postprocessing`, so the threshold cannot live there and\n * still be public (see `boundary.test.ts`).\n */\n\n/**\n * Rec. 709 luminance weights.\n *\n * Not an aesthetic choice: this is what `postprocessing`'s bloom pass\n * measures a pixel's brightness with, so it is what \"brighter than paper\"\n * has to mean for the threshold to do what it says.\n */\nexport const LUMA = [0.2126, 0.7152, 0.0722] as const\n\n/**\n * The scene luminance of the brightest paper any built-in preset lights.\n *\n * **Measured, not chosen.** A clean sheet was photographed under every preset\n * with bloom on and bloom off, stepping the threshold until the two pictures\n * were identical: most presets are safe from 0.8–0.9, `studio` from 1.1, and\n * `window` — the brightest — only from 1.6. The brightest preset has to set\n * this, because a threshold that lets paper bloom under any preset brings back\n * the painted-glow failure of §0.\n *\n * Re-measure it with `pnpm test:fire-look` if a preset's lighting changes.\n */\nexport const PAPER_WHITE = 1.6\n\n/**\n * The bloom threshold, in scene luminance before the tone curve.\n *\n * Paper white plus headroom for a highlight at a grazing angle that the test\n * camera does not happen to see. Stated as a multiple so that the reason for\n * the number survives: it is not \"3.6\", it is \"clear of the brightest paper\".\n *\n * **Do not lower it to make fire bloom.** That is the wrong end of the\n * problem, and the review that found the fluid's flames under it said so:\n * `window`'s paper blooms from 1.6, so lowering the threshold blooms paper,\n * which is the painted glow again. Raise what the fire EMITS.\n */\nexport const FX_BLOOM_THRESHOLD = PAPER_WHITE * 2.25\n\n/**\n * What the spec asks a glowing part of a fire to reach, in multiples of paper\n * white (§4.3). The band `emission.test.ts` holds every emitter to.\n */\nexport const FIRE_GLOW = [4, 8] as const\n\n/** One sRGB channel (0..1) to linear. The transfer function, not a gamma. */\nexport function srgbToLinear(c: number): number {\n return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4\n}\n\n/** Rec. 709 luminance of a linear colour. */\nexport function luminance(c: readonly [number, number, number]): number {\n return c[0] * LUMA[0] + c[1] * LUMA[1] + c[2] * LUMA[2]\n}\n\n/** How many times paper white a linear colour is — the inverse of {@link emit}. */\nexport function timesPaperWhite(c: readonly [number, number, number]): number {\n return luminance(c) / PAPER_WHITE\n}\n\n/**\n * An emissive colour, authored as a hue and a brightness.\n *\n * `srgb` is the colour it should LOOK — the ordinary 0..1 sRGB you would pick\n * in a colour picker — and `times` is how many times paper white it should\n * BE. The two are independent on purpose: hue is a judgement about fire, and\n * brightness is a number the spec fixes and a test can check. Authoring them\n * together in one linear triple is what let `[12, 4.2, 0.9]` sit at 3.5×\n * paper white under a comment claiming it cleared 4×.\n *\n * The returned triple has exactly `times * PAPER_WHITE` luminance, so it can\n * be handed straight to a shader.\n */\nexport function emit(srgb: readonly [number, number, number], times: number): [number, number, number] {\n const linear: [number, number, number] = [\n srgbToLinear(srgb[0]),\n srgbToLinear(srgb[1]),\n srgbToLinear(srgb[2]),\n ]\n const lum = luminance(linear)\n // A colour with no luminance has no hue to scale — black stays black rather\n // than becoming a division by zero.\n if (lum <= 1e-6) return [0, 0, 0]\n const gain = (times * PAPER_WHITE) / lum\n return [linear[0] * gain, linear[1] * gain, linear[2] * gain]\n}\n\n/** `emit` from a `#rrggbb` string, for colours sampled off a reference still. */\nexport function emitHex(hex: string, times: number): [number, number, number] {\n const n = Number.parseInt(hex.replace('#', ''), 16)\n return emit([((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255], times)\n}\n\n/**\n * The FLAME HEAT that counts as the hottest gas in a flame — the heat burning\n * made (see `REACT`), which is what a flame's colour is drawn from.\n *\n * It has no natural ceiling, so the render pass needs a reference before\n * \"hot\" can mean anything. Every band in `RENDER_FRAGMENT`\n * is a fraction of this, which is what gives a flame a core, a body and a\n * tip instead of one saturated colour.\n *\n * It is NOT independent of the solver's `cooling` and heat terms: this is a\n * reference temperature, and they decide how much gas ever reaches it.\n * `pnpm test:fire-budget` is what holds the pair honest.\n *\n * 0.65, measured: inside a flame (where its soot is visible) flame heat runs\n * 0.17 / 0.27 / 0.40 / 0.48 at the 10th / 50th / 90th / 99th percentile.\n * At 0.4 most of the flame sat past the core's threshold and was 36%\n * near-white; at 0.8 all of it sat in the tip's band and was 93% orange.\n * 0.65 puts the median in the body and only the top few percent in the core.\n */\nexport const FIRE_HEAT_SCALE = 0.65\n\n/**\n * The soot density that counts as a full flame.\n *\n * Soot is what the render pass draws a flame FROM — its outline, its opacity,\n * how much light it can give — while temperature only picks the colour (see\n * `RENDER_FRAGMENT`). Like the heat, the solver's soot has no natural unit,\n * so this is the reference: `tip.from` is a fraction of it. At 1 the soot's\n * 90th percentile (~1.4) was already past it, so every tongue was solid to\n * its edge with a hard outline; 3 leaves the thin parts room to fade.\n */\nexport const FIRE_SOOT_SCALE = 3\n\n/**\n * How strongly light past {@link FX_BLOOM_THRESHOLD} spreads.\n *\n * It lives beside the threshold because the two are one setting: bloom is a\n * multiplier on whatever clears the threshold, so its right value depends\n * entirely on how much fire is authored above it. 1.55 was tuned when the\n * flames were UNDER the threshold and bloom had almost nothing to work on —\n * measured, 0.07% of the frame. Once the flames cleared it, that same 1.55\n * tinted the entire black stage olive. Swept at 0.25, 0.6 and 1.2 against the\n * peak frame; past about 0.4 the stage stops being black.\n */\nexport const FX_BLOOM = 0.55\n\n/**\n * How much darker a flame's mid-tones are than a linear ramp would make them.\n *\n * A gamma on the normalised temperature. 1 is the ramp as the solver hands it\n * over; above 1 the body falls away from the core faster, below 1 mid\n * temperatures are lifted up the emission ramp. It was 1.15 while the gradient\n * of temperature WAS the look; now that the sheets and the outline carry the\n * contrast, 0.9 gives fuller tongues and a little more amber and yellow.\n */\nexport const FIRE_CONTRAST = 0.9\n\n/**\n * How opaque the densest flame gas is, as an extinction coefficient.\n *\n * Fire used to be pure added light, which is why a tongue standing in front\n * of the sheet came out salmon: orange added to cream paper is pink, the one\n * colour §13.3 forbids. A flame is thin at the tip and nearly opaque through\n * its bright heart, and that opacity is what lets it read as its own colour\n * instead of as a tint on whatever is behind it. 0 restores the old purely\n * additive fire.\n *\n * 2.2 left the body of a flame about half see-through at mid temperature\n * (1 - e^-(0.3 x 2.2) ~ 0.5), so half of every tongue in front of the sheet\n * was cream paper — which is what turned orange into PEACH. Measured on the\n * flame's own pixels, 35-44% of them were pale against the reference's 17%.\n * At 5 the body covers what is behind it and only the thin edges and tips\n * stay translucent.\n */\nexport const FIRE_OPACITY = 5\n\n/**\n * How opaque flame gas must be before it glows at full strength, 0..1.\n *\n * Soot emits and absorbs together, so thin gas should glow in proportion to\n * how much of the background it covers; at a low value a nearly transparent\n * tip still emits fully and turns cream paper salmon, at a high value every\n * dim edge fades out before it can show its deep orange.\n */\nexport const FIRE_THIN = 0.55\n\n/**\n * A flame, in four zones — drawn the way the sheet's burn is (ember line, ash\n * lip, char, scorch): each part named for what it is, with its own controls.\n * Base to tip, as a flame spreading over a sheet actually is:\n *\n * root the blue leading edge where fresh gas meets the air at the paper.\n * Its light comes from excited molecules, not soot, so it is faint\n * and blue — and it starts OFF, because blue light added over cream\n * paper reads lavender.\n * core the hottest gas, where soot forms densest and glows pale. The only\n * zone allowed to over-expose past the bloom threshold, and so the\n * one that blooms.\n * body the luminous bulk: soot glowing yellow-orange as it rises.\n * tip where soot cools and burns off at the outer edge — orange-red,\n * dimmer, tearing into tongues. What survives escapes as smoke.\n *\n * Colour and brightness are separate in every zone, the way professional\n * fire shading keeps an intensity ramp apart from a colour ramp. The DEFAULTS\n * keep the order a hot body glows in — each zone brighter and yellower than\n * the one outside it — because breaking it is how dim yellow turns olive;\n * `emission.test.ts` holds them to that.\n *\n * WHERE the flame is comes from its soot: it begins at `tip.from`, a fraction\n * of FIRE_SOOT_SCALE, over `tip.softness`. WHAT COLOUR it is comes from its\n * temperature (a fraction of FIRE_HEAT_SCALE): the tip gives way to the body\n * around `tip.to`, and the core begins at `core.from`. Brightness is in\n * multiples of paper white; colours are sRGB, the way a colour picker gives\n * them.\n *\n * The body is ABOVE paper white. It was once held below it, because under a\n * bright preset paper sits at the top of the tone curve and anything brighter\n * loses its colour — and a flame dimmer than the paper behind it, drawn\n * opaque, is a yellow decal on the sheet, not light. Fire photographed in a\n * bright room DOES wash out; that is the lighting's to answer (the lab shoots\n * under `noir`, as the references were), not the flame's.\n */\nexport interface FireZones {\n root: { color: string; amount: number; reach: number }\n core: { color: string; glow: number; from: number }\n body: { color: string; glow: number }\n tip: { color: string; glow: number; from: number; to: number; softness: number; tearing: number }\n}\n\n/** Any part of any zone, over the defaults. */\nexport type FireZonesInput = { [Z in keyof FireZones]?: Partial<FireZones[Z]> }\n\n/**\n * The defaults reproduce the look arrived at by measurement against\n * Flame_base.png (yellow ~30%, orange ~40%, pale ~18% of a flame's pixels),\n * now expressed zone by zone. Each colour is the linear emission the previous\n * ramp used in that band, written as the sRGB a picker would show.\n */\nexport const FIRE_ZONES: FireZones = {\n root: { color: '#3b6bff', amount: 0, reach: 0.5 },\n // glow 6 (was 4.6): once the core became the hottest few percent of a\n // flame instead of its whole root, 4.6 no longer cleared the bloom\n // threshold — bloom's share of the peak frame fell to 0.48%, under the\n // budget. Brighter keeps the same small area: 1.47% bloom, near-white 0.8%.\n // Starting it lower (from 0.45) bloomed 6.4% but put near-white back at 4.7%.\n core: { color: '#fff7d4', glow: 6, from: 0.55 },\n // Saturated, because the tone curve takes saturation away from anything\n // above paper white: #ffdd7c at this glow came out pale yellow (s ~0.4),\n // and measured 7.5% yellow against Flame_base.png's 29%.\n body: { color: '#ffcf3a', glow: 1.3 },\n // to: 0.4, measured against Flame_base.png with bloom off (bloom's halo\n // counts as flame in any diff): near-white / pale / yellow / orange came out\n // 0.7 / 21 / 34 / 44% against the reference's 0.9 / 17 / 29 / 43. At 0.32\n // the body took half the flame and it was 51% yellow; at 0.45, 64% orange.\n tip: { color: '#ff9e2c', glow: 0.9, from: 0.08, to: 0.4, softness: 0.15, tearing: 0.35 },\n}\n\n/** The defaults with `input` laid over them, zone by zone. */\nexport function fireZones(input?: FireZonesInput): FireZones {\n return {\n root: { ...FIRE_ZONES.root, ...input?.root },\n core: { ...FIRE_ZONES.core, ...input?.core },\n body: { ...FIRE_ZONES.body, ...input?.body },\n tip: { ...FIRE_ZONES.tip, ...input?.tip },\n }\n}\n\n/** A `#rrggbb` colour as linear RGB — what a shader multiplies light by. */\nexport function hexToLinear(hex: string): [number, number, number] {\n const n = Number.parseInt(hex.replace('#', ''), 16)\n return [\n srgbToLinear(((n >> 16) & 255) / 255),\n srgbToLinear(((n >> 8) & 255) / 255),\n srgbToLinear((n & 255) / 255),\n ]\n}\n","import type { DamageField } from './field'\nimport { HEAT, PRESENCE } from './field'\nimport type { SurfaceLocator } from './fire'\n\n/**\n * Where a burn's flames stand, read off the field.\n *\n * `paperlab-fx-fire-spec.md` §4.1: paper does not burn, the gas does. Heat\n * cooks the cellulose into gas, which burns just ABOVE the surface, rooted\n * over the char right behind the ember line — so a flame belongs on the edge\n * of the hole, where it is hot, and nowhere else. Where the heat has gone\n * there is no gas left and no flame, even if the edge still glows.\n *\n * Pure and deterministic, because flames are part of the picture a capture\n * compares: the same burn at the same step stands the same flames in the\n * same places. Anchors are spread along the rim by binning the grid — the\n * hottest edge cell in each bin — rather than taken in scan order, which\n * would put all of them on the first rows of the hole.\n */\n\n/** One flame: where it stands, how big it is, and who it is. */\nexport interface FlameAnchor {\n /** Root, in world space. */\n x: number\n y: number\n z: number\n /** World units. 10–40 mm at A4 (§6), scaled by heat and by which rim it is on. */\n height: number\n width: number\n /** Stable while the cell burns, so a flame keeps its own flicker frame to frame. */\n seed: number\n /** 0..1 — how hot the paper under it is. */\n heat: number\n /** -1 on the lower rim of a hole, +1 on the upper. */\n upper: number\n /**\n * Which way the paper lies from the root, as a world-space unit vector —\n * across the rim, into the sheet. Zero where it could not be found. The\n * simulator lays its gas along the rim with this, rather than in a disc.\n */\n nx: number\n ny: number\n nz: number\n}\n\n/** 10 and 40 mm, in world units: a default sheet is one unit (210 mm) across. */\nexport const FLAME_HEIGHT = [10 / 210, 40 / 210] as const\n\n/** Below this there is no gas to burn, whatever the edge is doing. */\nexport const FLAME_HEAT = 0.22\n\n/**\n * The least a frame's flames may vary in height, as a coefficient of\n * variation. A ring of equal tongues — a crown — is the regularity that\n * gives a fake fire away, and it is treated as an error: a frame that comes\n * out more even than this is re-drawn with more spread (see the end of\n * `flameAnchors`), and `flames.test.ts` fails if any burn ever produces one.\n */\nexport const IRREGULAR = 0.4\n\n/** A millimetre of A4, in field cells (one cell is ~3.3 mm). */\nconst MM = 63 / 210\n\nconst scratchA = { x: 0, y: 0, z: 0 }\n\ninterface Pick {\n cell: number\n seed: number\n /** How strong the cluster is here, 0..1. */\n cluster: number\n /** How present this flame is right now, 0..1 — flames come and go. */\n life: number\n /** Where along the rim it stands, in mm from its cell. */\n along: number\n /** A side tongue's share of its cluster's height. */\n scale: number\n}\n\n/**\n * Fill `out` with up to `max` flames for the field as it stands at `time`.\n * Returns how many. `out` is reused and grown, never shrunk.\n *\n * Not one flame per stretch of rim — that was a crown. A living, uneven\n * ring: a noise field around the rim, drifting in time, decides where the\n * fire gathers. Where it is strong there are clusters of tall tongues with\n * short ones packed beside them; where it is weak, short licks and gaps.\n * Which cells carry a flame is redrawn every fraction of a second, and each\n * flame has its own life that fades it in and out, so clusters break apart\n * and re-form rather than standing still. All of it is a pure function of\n * the field and the time: the same burn stands the same flames.\n */\nexport function flameAnchors(\n field: DamageField,\n locate: SurfaceLocator,\n max: number,\n out: FlameAnchor[],\n time = field.time,\n): number {\n if (max <= 0 || field.frontCount === 0) return 0\n const size = field.size\n const last = size - 1\n const data = field.data\n\n // The rim: hot paper with a cut beside it.\n const rim: number[] = []\n let cx = 0\n let cy = 0\n for (let y = 1; y < last; y++) {\n for (let x = 1; x < last; x++) {\n const cell = y * size + x\n if (data[cell * 4 + HEAT]! < FLAME_HEAT || data[cell * 4 + PRESENCE]! < 0.5) continue\n if (\n data[(cell - 1) * 4 + PRESENCE]! >= 0.5 &&\n data[(cell + 1) * 4 + PRESENCE]! >= 0.5 &&\n data[(cell - size) * 4 + PRESENCE]! >= 0.5 &&\n data[(cell + size) * 4 + PRESENCE]! >= 0.5\n ) {\n continue\n }\n rim.push(cell)\n cx += x\n cy += y\n }\n }\n if (rim.length === 0) return 0\n cx /= rim.length\n cy /= rim.length\n\n // Around the rim, in order, so \"along the rim\" means something.\n const angle = (cell: number) => Math.atan2(((cell / size) | 0) - cy, (cell % size) - cx)\n rim.sort((a, b) => angle(a) - angle(b) || a - b)\n\n const picks: Pick[] = []\n for (const cell of rim) {\n const seed = hash2(cell, 1)\n const a = angle(cell)\n // The cluster field: periodic around the rim (so there is no seam where\n // the angle wraps), two octaves, drifting at two speeds.\n const cluster = clamp01(\n 0.62 * noise2(Math.cos(a) * 1.6 + time * 0.42, Math.sin(a) * 1.6 - time * 0.17) +\n 0.38 * noise2(Math.cos(a) * 4.1 - time * 0.73 + 7.3, Math.sin(a) * 4.1 + time * 0.31) -\n 0.12,\n )\n // Which cells carry a flame is redrawn every ~0.7 s, each on its own\n // beat, so the ring is never the same two moments running.\n const epoch = Math.floor(time * 1.4 + seed * 10)\n // Most of the rim carries something: short licks where the cluster is\n // weak, but only a few cells are left bare at any moment.\n if (hash2(cell, epoch + 7) > 0.38 + 0.55 * cluster) continue\n // Each flame breathes of its own accord — most shrink and swell, and only\n // some go out entirely before coming back.\n const breath = noise1(time * 0.9 + seed * 31)\n if (breath < 0.16) continue\n const life = 0.3 + 0.7 * smoothstep(0.16, 0.5, breath)\n picks.push({ cell, seed, cluster, life, along: (hash2(cell, 5) - 0.5) * 3, scale: 1 })\n // A strong cluster packs side tongues in beside the main one.\n if (cluster > 0.5) {\n const extra = cluster > 0.72 ? 2 : 1\n for (let k = 0; k < extra; k++) {\n const side = hash2(cell, 11 + k)\n picks.push({\n cell,\n seed: hash2(cell, 21 + k),\n cluster,\n life: life * (0.6 + 0.4 * side),\n along: (side < 0.5 ? -1 : 1) * (1.5 + 3 * hash2(cell, 31 + k)),\n scale: 0.4 + 0.45 * hash2(cell, 41 + k),\n })\n }\n }\n }\n\n // Keep the strongest if the tier cannot afford them all — by cluster and\n // chance, never by anything that depends on how the sheet is turned.\n picks.sort((p, q) => q.cluster * 0.7 + q.seed * 0.3 - (p.cluster * 0.7 + p.seed * 0.3) || p.cell - q.cell)\n const count = Math.min(max, picks.length)\n\n let n = 0\n for (let i = 0; i < count; i++) {\n const pick = picks[i]!\n const cell = pick.cell\n const x = cell % size\n const y = (cell / size) | 0\n // Which way the paper lies from here, and the rim's tangent across it.\n const gx = data[(cell + 1) * 4 + PRESENCE]! - data[(cell - 1) * 4 + PRESENCE]!\n const gy = data[(cell + size) * 4 + PRESENCE]! - data[(cell - size) * 4 + PRESENCE]!\n const gl = Math.hypot(gx, gy) || 1\n const shift = (pick.along * MM) / last\n const u = x / last + (-gy / gl) * shift\n const v = y / last + (gx / gl) * shift\n const at = locate(u, v)\n if (!at) continue\n const rx = at.x\n const ry = at.y\n const rz = at.z\n const toward = locate(u + gx / gl / last, v + gy / gl / last)\n let upper = 0\n scratchA.x = 0\n scratchA.y = 0\n scratchA.z = 0\n if (toward) {\n const l = Math.hypot(toward.x - rx, toward.y - ry, toward.z - rz) || 1\n scratchA.x = (toward.x - rx) / l\n scratchA.y = (toward.y - ry) / l\n scratchA.z = (toward.z - rz) / l\n upper = scratchA.y\n }\n const heat = data[cell * 4 + HEAT]!\n // Square root: the edge of a hole is rarely at full heat.\n const hot = Math.sqrt(Math.min(1, (heat - FLAME_HEAT) / 0.35))\n // Tall on the upper rim, where the gas rises over paper it is preheating;\n // short on the lower one, where it rises across the hole (§6).\n const rim = 0.35 + 0.65 * (0.5 + 0.5 * upper)\n const base = (FLAME_HEIGHT[0] + (FLAME_HEIGHT[1] - FLAME_HEIGHT[0]) * hot) * rim\n // Tall tongues where the cluster is strong, short licks where it is weak.\n const height = base * (0.2 + 1.3 * pick.cluster ** 1.4) * (0.6 + 0.8 * pick.seed) * pick.life * pick.scale\n let anchor = out[n]\n if (!anchor) {\n anchor = { x: 0, y: 0, z: 0, height: 0, width: 0, seed: 0, heat: 0, upper: 0, nx: 0, ny: 0, nz: 0 }\n out[n] = anchor\n }\n anchor.nx = scratchA.x\n anchor.ny = scratchA.y\n anchor.nz = scratchA.z\n anchor.x = rx\n anchor.y = ry\n anchor.z = rz\n anchor.height = Math.min(FLAME_HEIGHT[1], height)\n anchor.width = anchor.height * (0.32 + 0.3 * hash2(cell, 3))\n anchor.seed = pick.seed\n anchor.heat = hot\n anchor.upper = upper\n n++\n }\n\n // A crown is an error: too even a ring is re-drawn with more spread.\n if (n >= 3 && variation(out, n) < IRREGULAR) {\n for (let i = 0; i < n; i++) {\n const a = out[i]!\n a.height = Math.min(FLAME_HEIGHT[1], a.height * (0.3 + 1.4 * hash2(Math.floor(a.seed * 1e6), 9)))\n a.width = a.height * (0.32 + 0.3 * hash2(Math.floor(a.seed * 1e6), 3))\n }\n }\n return n\n}\n\n/** Coefficient of variation of the first `n` heights. */\nexport function variation(anchors: readonly FlameAnchor[], n: number): number {\n let sum = 0\n for (let i = 0; i < n; i++) sum += anchors[i]!.height\n const mean = sum / n\n if (!(mean > 0)) return 0\n let sq = 0\n for (let i = 0; i < n; i++) sq += (anchors[i]!.height - mean) ** 2\n return Math.sqrt(sq / n) / mean\n}\n\nfunction clamp01(x: number): number {\n return x < 0 ? 0 : x > 1 ? 1 : x\n}\n\nfunction smoothstep(a: number, b: number, x: number): number {\n const t = clamp01((x - a) / (b - a))\n return t * t * (3 - 2 * t)\n}\n\n/** A seeded hash of two integers, 0..1. */\nfunction hash2(a: number, b: number): number {\n let h = Math.imul(a ^ Math.imul(b, 0x27d4eb2d), 2654435761) >>> 0\n h ^= h >>> 15\n h = Math.imul(h, 2246822519) >>> 0\n h ^= h >>> 13\n return (h >>> 0) / 4294967296\n}\n\n/** Value noise in two dimensions. */\nfunction noise2(x: number, y: number): number {\n const ix = Math.floor(x)\n const iy = Math.floor(y)\n let fx = x - ix\n let fy = y - iy\n fx = fx * fx * (3 - 2 * fx)\n fy = fy * fy * (3 - 2 * fy)\n const h = (i: number, j: number) => hash2(i * 73856093, j * 19349663 + 7)\n const a = h(ix, iy) + (h(ix + 1, iy) - h(ix, iy)) * fx\n const b = h(ix, iy + 1) + (h(ix + 1, iy + 1) - h(ix, iy + 1)) * fx\n return a + (b - a) * fy\n}\n\n/**\n * How much a flame has puffed up at a moment — the same function the flame\n * shader runs, so the fire light flickers WITH the flames rather than beside\n * them (§7). Value noise, 10–15 Hz, seeded per flame: neighbours never move\n * in step, and nothing is a sine.\n */\nexport function flamePuff(seed: number, time: number): number {\n return 0.72 + 0.28 * noise1(time * 12.5 + seed * 37) + 0.12 * (noise1(time * 23 + seed * 11) - 0.5)\n}\n\nfunction hash1(n: number): number {\n const s = Math.sin(n) * 43758.5453\n return s - Math.floor(s)\n}\n\nfunction noise1(x: number): number {\n const i = Math.floor(x)\n let f = x - i\n f = f * f * (3 - 2 * f)\n return hash1(i) + (hash1(i + 1) - hash1(i)) * f\n}\n","/**\n * What an effect is allowed to cost to SHOW, per device class.\n *\n * Presentation only, deliberately. The simulation is not tiered: every device\n * runs the same damage field on the same grid at the same timestep, so every\n * device gets the same fire — which is also the only way a burn can be shared\n * or replayed and come out the same. See the note at the top of\n * `fx/field.ts`.\n *\n * That is a correction. The first version of this table tiered the field's\n * grid size and its substep count as two independent knobs, and they are not\n * independent: with explicit diffusion, halving the cell size quadruples the\n * cells AND the steps stability needs, so the same fire costs N⁴ — the 128\n * grid was sixteen times the 64 one. What the table actually produced was a\n * fire whose SPEED depended on the device. It is the same shape of mistake as\n * the `stage/quality.ts` knob this file's first preamble warned about, made\n * one file over.\n *\n * What remains here is what a weaker device can genuinely show less of\n * without the effect meaning something different: how many particles are in\n * the air, how many sounds play at once, and how finely a burnt edge is drawn.\n * A knob belongs here only when something reads it.\n */\n\nexport const fxQualityNames = ['auto', 'low', 'medium', 'high'] as const\nexport type FxQualityName = (typeof fxQualityNames)[number]\nexport type FxQualityTier = Exclude<FxQualityName, 'auto'>\n\nexport interface FxQualitySettings {\n /**\n * Hard ceiling on live particles across every emitter at once.\n *\n * Allocated at this size and never grown, so it is a size rather than a\n * suggestion. Read by the emitters, which arrive with fire.\n */\n particles: number\n /**\n * Simultaneous synthesised voices.\n *\n * A phone running MediaPipe and WebGL has no headroom for an unbounded\n * grain cloud, and a grain cloud is exactly what fire and crumple both\n * want to be. The ceiling is the whole design, not a safety net. Read by\n * `FxAudio`.\n */\n voices: number\n /**\n * How ragged a burnt edge is drawn, 0..1 — per-fragment noise that frays\n * the edge finer than the damage grid can. Handed to the sheet as the\n * field's `detail`. The one part of drawing damage that costs per PIXEL,\n * so the part a fill-rate-bound phone gives up first; at 0 the shader\n * skips the noise outright.\n */\n detail: number\n /**\n * The resolution bloom is drawn at, as a fraction of the frame.\n *\n * Bloom is a blur, and a blur at half resolution is a quarter of the\n * fill-rate for a glow that is soft anyway — the cheapest thing a phone\n * can give up that still leaves every zone and every flame on screen\n * (spec §12: tiers thin post quality, never what is shown). Read by\n * `FxPost`.\n */\n bloomScale: number\n /**\n * The most flames standing at once. The spec's §12 said 8 / 16 / 32; a ring\n * of fire that is dense in clusters and broken by gaps needs more tongues\n * than that, and Noor's direction (a living, uneven ring, never a crown)\n * came later and wins — 16 / 36 / 64. Every tier has real flames; a phone\n * gets fewer tongues, never the cheap version.\n * Read by `FxFlames`.\n */\n flames: number\n /**\n * The most of each particle kind in the air at once (spec §12), inside the\n * `particles` budget. Read by `FireEmitter` through its `caps` option.\n */\n caps: { ember: number; smoke: number; ash: number }\n /**\n * Heat haze strength in pixels at 1080p; 0 turns it off. Read by `FxPost`.\n *\n * High tier only, and half what it was. It was 3.8 px on two tiers against\n * §10's 1–3, and invisible in every capture at either — a few pixels of\n * wobble reads as nothing while the fluid beside it is moving. It is also\n * still driven from `flameAnchors`, which is where the SPRITE flames stand,\n * not where the fluid actually burns; until it reads the fluid's own heat\n * texture it is the last polish item rather than a look, so it is off\n * everywhere it cannot be afforded to be good.\n */\n haze: number\n /**\n * The fire simulator's grids (`FxFireFluid`): a velocity grid, a finer one\n * for what is drawn, and the pressure solve's iterations. Every tier\n * simulates a real fire; a phone solves a coarser one.\n *\n * The velocity grid is half the dye's resolution, not a quarter as it was.\n * At a quarter its cells were 4.4 mm and a tongue was one to five cells\n * wide, so everything that makes a flame flicker happened inside a cell and\n * the solve never saw it — sharper advection of the dye could only draw the\n * edges of a motion that was itself butter. The pressure solve is the cheap\n * part (a quarter of the dye grid's texels); the iterations rise with it\n * because Jacobi needs more of them to converge over more cells.\n */\n fluid: { velocity: readonly [number, number]; dye: readonly [number, number]; iterations: number }\n}\n\nexport const fxQualityTiers: Record<FxQualityTier, FxQualitySettings> = {\n /** A desktop GPU, or a phone that has measured its way up here. */\n high: {\n particles: 2000,\n voices: 16,\n detail: 1,\n bloomScale: 1,\n flames: 64,\n caps: { ember: 200, smoke: 200, ash: 120 },\n haze: 2,\n fluid: { velocity: [192, 256], dye: [384, 512], iterations: 36 },\n },\n /** The default worth aiming at: a recent phone, or an integrated laptop GPU. */\n medium: {\n particles: 900,\n voices: 10,\n detail: 1,\n bloomScale: 1,\n flames: 36,\n caps: { ember: 80, smoke: 80, ash: 60 },\n haze: 0,\n fluid: { velocity: [144, 192], dye: [288, 384], iterations: 30 },\n },\n /**\n * A throttled phone with the camera and the tracker already running, which\n * is the realistic case rather than the pessimistic one. The fire is the\n * same fire; the shower is thinner, fewer crackles overlap, and its edge is\n * the grid's own.\n */\n low: {\n particles: 350,\n voices: 6,\n detail: 0,\n bloomScale: 0.5,\n flames: 16,\n caps: { ember: 30, smoke: 30, ash: 20 },\n haze: 0,\n fluid: { velocity: [96, 128], dye: [192, 256], iterations: 20 },\n },\n}\n\n/** Where `auto` starts before anything has been measured. */\nexport const FX_INITIAL_TIER: FxQualityTier = 'medium'\n\nexport const FX_TIER_ORDER: FxQualityTier[] = ['low', 'medium', 'high']\n\nexport function fxQualityFor(name: FxQualityName): FxQualitySettings {\n return fxQualityTiers[name === 'auto' ? FX_INITIAL_TIER : name]\n}\n"],"mappings":"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","names":["rim"]}
|
|
1
|
+
{"version":3,"sources":["../src/fx/field.ts","../src/fx/emission.ts","../src/fx/flames.ts","../src/fx/quality.ts"],"sourcesContent":["/**\n * The damage field: what has happened to this sheet, as four numbers per\n * texel, over the sheet's own UV.\n *\n * One primitive rather than one subsystem per effect. Fire is heat diffusing\n * into char and char eating presence; water is saturation diffusing along the\n * fibre; a tear is presence zeroed along a path. They share a grid, a step\n * and a set of paint operations, which is the whole reason the second effect\n * costs a chunk and a preset instead of a new system.\n *\n * R char scorch colour, the brown halo, shrinkage and curl, smoke density\n * G saturation wet darkening, roughness, translucency, added mass, sag\n * B heat the glowing ignition line\n * A presence alpha erosion — burn-away, tear-away, punched holes\n *\n * **It runs on the CPU because of who reads it.** Three of the four layers an\n * effect has to land on consume the field on the CPU: the physics coupling\n * needs stiffness and mass per vertex, the emitters spawn ash where presence\n * reaches zero and drips from the lowest wet vertex, and the sound is driven\n * by the length of the burn front every frame. A GPU field would have fed all\n * three through a `readPixels` per frame — a pipeline stall that tile-based\n * phone GPUs pay the most for. Only the shading reads the field on the GPU,\n * and it gets an 8-bit texture uploaded from here. Testability without a\n * renderer comes free with that, and it is how every property below is known.\n *\n * It stays on the main thread. A worker would need `SharedArrayBuffer` to\n * avoid copying the field every frame, and that needs COOP/COEP headers that\n * GitHub Pages cannot send. The main thread is affordable only because the\n * field costs nothing when nothing is happening to it — see `step`.\n *\n * **It is not tiered.** Every device runs the same grid at the same timestep\n * and produces the same fire. With explicit diffusion, halving the cell size\n * quadruples both the cells and the steps stability needs, so the same fire\n * costs N⁴: a 128 grid is sixteen times a 64 one. The first version tiered\n * grid size and substeps as though they were independent, and the measured\n * result was a fire whose speed depended on the device. Presentation is what\n * tiers now — see `fx/quality.ts`.\n */\n\nimport { DAMAGE_CHANNELS, type DamageSource } from '../surface/damageContract'\n\n/**\n * Channel offsets into a texel — the contract's, not this file's. The sheet\n * defines what the four bytes mean because the sheet is what draws them;\n * the field is one thing that writes them.\n */\nexport const CHAR = DAMAGE_CHANNELS.char\nexport const SATURATION = DAMAGE_CHANNELS.saturation\nexport const HEAT = DAMAGE_CHANNELS.heat\nexport const PRESENCE = DAMAGE_CHANNELS.presence\n\n/**\n * The grid, in texels along each edge. One number for every device.\n *\n * 64 until a real phone says otherwise — the P1 gate is where this gets\n * settled for good, by rendering a burn at 64 and 96 and looking. What the\n * field carries are soft quantities with soft edges; the RAGGED edge a burn\n * reads as comes from the anisotropy and the grain, which survive a coarse\n * grid, and from per-fragment detail in the shader, which does not need one.\n */\nexport const FIELD_SIZE = 64\n\n/**\n * The simulation's own clock, independent of the frame rate.\n *\n * The first version ran a fixed number of substeps per FRAME, so a slow frame\n * meant a longer substep, the stability clamp scaled the diffusion down to\n * compensate, and the same simulated second burned half as far at 30 fps as\n * at 144. Measured on the high tier: 0.202, 0.290 and 0.439 UV of burn radius\n * at 30, 60 and 144 fps. A fire that burns slower when the phone is busy is a\n * fire that behaves differently on every device and every run.\n *\n * `ClothSim` already solved this with the same accumulator and the same\n * number, and the field now matches it. The rates are chosen so that the\n * stability bound is never reached at 1/120 — the clamp in `stencil` is a\n * safety net for extreme options, not something defaults lean on, and a test\n * says so.\n */\nexport const FIXED_DT = 1 / 120\n\n/**\n * How far behind the accumulator may fall before it drops time.\n *\n * Eight steps covers any frame down to 15 fps exactly. Below that the fire\n * slows rather than the page spiralling — each late frame asking for more\n * steps, which make the next frame later still.\n */\nconst MAX_STEPS_PER_FRAME = 8\n\n/**\n * Below this, a cell is at rest: it no longer keeps the field awake.\n *\n * Cooling and drying are exponential and never arrive at zero on their own,\n * so without a threshold a fire that went out ten minutes ago would keep\n * every cell it touched awake forever.\n *\n * It decides whether a cell is AWAKE, and never rewrites a value. The first\n * version snapped values below it to zero, and that is a leak with a worse\n * shape than it sounds: the leading edge of anything spreading is exactly the\n * set of cells receiving less than this per step, so every step zeroed the\n * front before it could advance, and a wet patch drained from its rim —\n * three-quarters of it gone in three seconds, with drying switched off. When\n * every cell is below this the field sleeps with the residue frozen in place:\n * nothing lost, and invisible at 8 bits.\n */\nconst REST = 1e-3\n\n/**\n * How hot paper has to get before it starts to char, as a fraction of a\n * texel's own tinder value.\n *\n * Below the heat a flame deposits and above what diffusion alone leaves\n * behind — which is the gap the whole fire lives in. Too high and a match\n * cannot light the sheet; too low and the preheated region ahead of the\n * front ignites all at once and the burn is a disc rather than a front.\n */\nconst IGNITION = 0.35\n\n/**\n * The largest the stencil's coefficients may sum to in one step.\n *\n * The positive nine-point stencil below is monotone — no overshoot, so the\n * 0..1 clamp never eats mass — while this stays under a half. 0.4 leaves\n * margin. Defaults sit well below it; see `FIXED_DT`.\n */\nconst STABLE = 0.4\n\nexport interface DamageFieldOptions {\n /**\n * Which way the paper's fibres run, in radians over the sheet's UV, where\n * 0 points along +u.\n *\n * Paper is not isotropic and this is the single number that makes it look\n * like paper rather than like a fluid. Fibres are laid down along the\n * machine direction when the sheet is made; liquid wicks along them far\n * faster than across them, and a flame follows them for the same reason.\n * A wet front with anisotropy at 1 is a circle, which reads as a stain on\n * cloth; with it raised, the front is an ellipse along this angle, which\n * reads as paper. Any angle — see `diffusionTensor`.\n */\n fibre?: number\n /** How much faster things travel along the fibre than across it. */\n anisotropy?: number\n /** Heat spread, in UV² per second, averaged over direction. */\n heatDiffusion?: number\n /** Liquid wicking, in UV² per second, averaged over direction. */\n wicking?: number\n /** How fast heat turns paper to char. */\n charRate?: number\n /** How fast char consumes presence — the burn-away. */\n consumeRate?: number\n /** How much heat burning gives back. Above ~1 the front sustains itself. */\n combustion?: number\n /** Heat lost to the room each second, as a fraction. */\n cooling?: number\n /** How much heat it costs to boil a wet cell dry before it can char. */\n wetResistance?: number\n /** How fast standing water leaves the sheet, as a fraction per second. */\n drying?: number\n /**\n * Per-texel variation in how readily the paper takes light, 0..1.\n *\n * Real paper is not uniform, and a burn front on a uniform sheet advances\n * as a perfect circle — the single most synthetic-looking thing this\n * simulation could do. This is what makes the edge ragged, and it is fixed\n * per field rather than per frame, because a sheet's grain does not change\n * while you hold it.\n */\n grain?: number\n /** Seed for the grain, so a field is reproducible. */\n seed?: number\n}\n\n/**\n * **Fire's four rates were all six times too fast, and they were wrong\n * together.**\n *\n * Measured on the scripted burn: the front spread at 39 mm/s from the centre\n * and 27 mm/s from a corner, against the 3–8 mm/s\n * `paperlab-fx-fire-spec.md` §9 asks for. A sheet was 76% gone in 4.8 s, so\n * there was no time to watch it spread and every phase was judged at the\n * wrong size — \"dying\" was a strip of paper under huge flames, and \"smoulder\"\n * and \"cold\" were the same frame.\n *\n * The fix is a **uniform time dilation**, not four independent retunes. This\n * is a reaction–diffusion front, so its speed goes as `sqrt(D · k)` and the\n * charred band behind it as `D / v`: divide every RATE by the same number and\n * the front slows by that number while the burn keeps its exact shape. Divide\n * them unevenly and it does not — measured, cutting `heatDiffusion` and\n * `charRate` alone (leaving `cooling`) put the front below the threshold that\n * sustains it and the fire went out with 1% of the sheet gone.\n *\n * So `heatDiffusion`, `charRate` and `cooling` are the old numbers over six,\n * which leaves every ratio between them — and `combustion`, which is already\n * a ratio — untouched. Measured after: **6.8 mm/s from the centre and 3.6 from\n * a corner** (a corner spreads along the sheet's fibre as a line rather than\n * a disc, which is slower and is the field's own doing), a hole 22 mm across\n * at 4 s, and a fire that lives about 11 s.\n *\n * Water's rates (`wicking`, `drying`) are deliberately NOT dilated: nothing\n * has ever measured a wet front against a target, and the burn is identical\n * either way (saturation is 0 in a dry burn), so changing them here would be\n * an unmeasured change riding along with a measured one.\n *\n * `consumeRate` comes back DOWN, from 20 to 6, for the same reason it went up.\n * At 20 it was holding the charred band to 8.5 mm against a front moving six\n * times too fast; the band is `v / consumeRate`, so once the front slowed, 20\n * squeezed it to 2.9 mm. 6 puts it at 5.3 mm — mid-range of §5's 2–8 mm.\n * Measured band against this number, at the new pace: 5 → 6.0 mm, 6 → 5.3,\n * 7 → 4.7, 9 → 4.0, 20 → 2.9.\n */\nconst DEFAULTS = {\n fibre: 0,\n anisotropy: 3,\n heatDiffusion: 0.00058,\n wicking: 0.0045,\n charRate: 2.7,\n consumeRate: 6,\n combustion: 2.4,\n cooling: 0.18,\n wetResistance: 2.6,\n drying: 0.015,\n grain: 0.34,\n seed: 1,\n} satisfies Required<DamageFieldOptions>\n\n/** What the field did this step. The numbers the sound and the emitters read. */\nexport interface FieldStats {\n /**\n * How much sheet is actively burning right now, 0..1.\n *\n * The fraction of texels part-way through charring AND still hot.\n * Resolution-independent by construction. Both halves matter: combustion\n * pumps heat into everything it has already burnt, so \"hot\" alone lights\n * up the whole interior; and a fire that went out leaves partly charred\n * cells behind, so \"part-charred\" alone would report a burn that is over as\n * one still going.\n *\n * This is the number fire's audio is driven by, and it is the right one:\n * a fire gets louder as the front gets LONGER, not as the burnt area gets\n * bigger. A sheet nearly consumed is quiet again, which is true, and which\n * a level driven by char would get exactly backwards.\n */\n front: number\n /** Texels that crossed into char this step. Drives crackle rate and smoke. */\n charred: number\n /** Texels whose presence reached zero this step. Drives ash, and burn-through. */\n consumed: number\n /** Texels that became wetter this step. Drives the water bed's level. */\n wetted: number\n /** Mean saturation over the sheet, 0..1. Drives added mass in the coupling. */\n saturation: number\n /** Fraction of the sheet still present, 1 at the start. */\n remaining: number\n}\n\n/**\n * The per-neighbour weights of one diffusing quantity, already multiplied by\n * the timestep over the cell size squared.\n */\nexport interface Stencil {\n /** East and west neighbours. */\n ax: number\n /** North and south neighbours. */\n ay: number\n /** The two neighbours on one diagonal. */\n ad: number\n /** Which diagonal: +1 is north-east/south-west, -1 is north-west/south-east. */\n diagonal: 1 | -1\n /** True if the stability bound had to scale these down. Never, at defaults. */\n clamped: boolean\n}\n\n/**\n * The diffusion tensor for paper with its fibre at `fibre` radians.\n *\n * Along the fibre things travel `anisotropy` times faster than across it, and\n * the two are normalised so that their mean is `rate` — turning the grain\n * changes which way things go, never how fast overall.\n *\n * The first version reduced this to two per-axis weights, `[cos², sin²]`,\n * which is the tensor with its off-diagonal term thrown away. That can only\n * describe ellipses aligned to the UV axes, so it was right at 0° and 90° —\n * the two angles the tests used — and wrong everywhere else: a 30° grain made\n * a 0° front with the anisotropy halved, and a 45° grain made a circle.\n */\nexport function diffusionTensor(rate: number, anisotropy: number, fibre: number) {\n const along = (rate * 2 * anisotropy) / (1 + anisotropy)\n const across = (rate * 2) / (1 + anisotropy)\n const c = Math.cos(fibre)\n const s = Math.sin(fibre)\n return {\n xx: along * c * c + across * s * s,\n yy: along * s * s + across * c * c,\n xy: (along - across) * c * s,\n }\n}\n\n/**\n * A positive nine-point stencil for that tensor.\n *\n * The textbook cross-derivative puts NEGATIVE weights on two of the diagonal\n * neighbours, and a negative weight overshoots: the update dips below zero,\n * the 0..1 clamp clips the dip, and the clip is not symmetric — which is\n * precisely how the first version of this file lost a wet patch in two\n * seconds. So the cross term is written as a second difference along ONE\n * diagonal instead, the one the fibre leans toward:\n *\n * Dxx u_xx + 2 Dxy u_xy + Dyy u_yy\n * = (Dxx − |Dxy|) u_xx + (Dyy − |Dxy|) u_yy + 2 |Dxy| u_dd\n *\n * Every weight is non-negative while |Dxy| ≤ min(Dxx, Dyy), which holds at\n * any angle for anisotropy up to about 5. Beyond that |Dxy| is capped at the\n * bound: the front comes out slightly rounder than asked for, which is a far\n * better failure than a front that eats itself.\n */\nexport function stencil(rate: number, anisotropy: number, fibre: number, dt = FIXED_DT): Stencil {\n const d = diffusionTensor(rate, anisotropy, fibre)\n const cross = Math.min(Math.abs(d.xy), d.xx, d.yy)\n const scale = dt * (FIELD_SIZE - 1) * (FIELD_SIZE - 1)\n let ax = (d.xx - cross) * scale\n let ay = (d.yy - cross) * scale\n let ad = cross * scale\n const total = ax + ay + ad\n const clamped = total > STABLE\n if (clamped) {\n const k = STABLE / total\n ax *= k\n ay *= k\n ad *= k\n }\n return { ax, ay, ad, diagonal: d.xy >= 0 ? 1 : -1, clamped }\n}\n\n/** Cheap reproducible hash noise — same input, same sheet, every time. */\nfunction hash(x: number, y: number, seed: number): number {\n // Multipliers kept inside 32 bits on purpose: anything larger is silently\n // rounded by a double before `^` truncates it, which makes the \"hash\"\n // lose its low bits and the grain come out in visible bands.\n let h = Math.imul(x, 374761393) ^ Math.imul(y, 668265263) ^ Math.imul(seed, 1274126177)\n h = Math.imul(h ^ (h >>> 13), 1274126177)\n return ((h ^ (h >>> 16)) >>> 0) / 4294967296\n}\n\n/** An inclusive rectangle of cells. Empty when x0 > x1. */\ninterface Box {\n x0: number\n y0: number\n x1: number\n y1: number\n}\n\nconst EMPTY = (): Box => ({ x0: 1, y0: 1, x1: 0, y1: 0 })\n\nexport class DamageField implements DamageSource {\n readonly size = FIELD_SIZE\n /** RGBA per texel in float, row-major from v = 0. The simulation's own state. */\n readonly data: Float32Array\n /** The same, at 8 bits, for upload. Kept in step with `data` over what changed. */\n readonly pixels: Uint8Array\n /**\n * How ragged the sheet DRAWS this field's edges, 0..1 — see\n * `DamageSource.detail`. Not a simulation option, because it changes\n * nothing the field computes: set it from `fxQualityFor(tier).detail`, and\n * again whenever the tier moves.\n */\n detail = 1\n private revision = 0\n\n private readonly next: Float32Array\n /** Per-texel ignition threshold, fixed for the life of the sheet. */\n private readonly tinder: Float32Array\n private readonly o: Required<DamageFieldOptions>\n private readonly heat: Stencil\n private readonly water: Stencil\n private accumulator = 0\n /** Fixed steps run since the field was made — its own clock. See {@link time}. */\n private steps = 0\n /** Cells that might change on the next step. Everything outside is at rest. */\n private active: Box = EMPTY()\n /** Cells written since the last pack into `pixels`. */\n private touched: Box = EMPTY()\n private visited = 0\n /** Running totals, so the stats never have to walk the whole grid. */\n private present: number\n private wetTotal = 0\n private stats: FieldStats = {\n front: 0,\n charred: 0,\n consumed: 0,\n wetted: 0,\n saturation: 0,\n remaining: 1,\n }\n /**\n * Texels whose presence reached zero during the last `step`, in the first\n * {@link consumedCount} slots — where ash leaves from.\n *\n * WHERE, not just how many. The stats were enough for the sound, which\n * wants a level; an emitter wants a place. Fixed buffers the size of the\n * grid and written in place: a burning sheet must not allocate a list a\n * step, and a texel is consumed once, so a step can never fill it twice.\n */\n readonly consumedCells = new Int32Array(FIELD_SIZE * FIELD_SIZE)\n /** The burn front as of the last step that ran, in the first {@link frontCount} slots — where embers and smoke leave from. */\n readonly frontCells = new Int32Array(FIELD_SIZE * FIELD_SIZE)\n private consumedLength = 0\n private frontLength = 0\n\n constructor(options: DamageFieldOptions = {}) {\n this.o = { ...DEFAULTS, ...options }\n const n = FIELD_SIZE * FIELD_SIZE\n this.data = new Float32Array(n * 4)\n this.next = new Float32Array(n * 4)\n this.pixels = new Uint8Array(n * 4)\n this.tinder = new Float32Array(n)\n this.present = n\n\n for (let i = 0; i < n; i++) {\n // Presence starts at 1 — the whole sheet is there. Everything else at 0.\n this.data[i * 4 + PRESENCE] = 1\n this.pixels[i * 4 + PRESENCE] = 255\n const x = i % FIELD_SIZE\n const y = (i / FIELD_SIZE) | 0\n // Two octaves: a coarse one that makes whole regions catch before their\n // neighbours, and a fine one that frays the edge between them. Centred\n // on 1, so `grain` changes only the VARIANCE — written as\n // `1 - grain * noise` it also lowered the mean, and the knob was\n // silently two knobs.\n const coarse = hash(x >> 2, y >> 2, this.o.seed)\n const fine = hash(x, y, this.o.seed * 7 + 11)\n this.tinder[i] = 1 + this.o.grain * (coarse * 0.65 + fine * 0.35 - 0.5)\n }\n this.next.set(this.data)\n\n // Constant for the life of the field, now that the timestep is.\n this.heat = stencil(this.o.heatDiffusion, this.o.anisotropy, this.o.fibre)\n this.water = stencil(this.o.wicking, this.o.anisotropy, this.o.fibre)\n }\n\n /** Bumped whenever `pixels` changes. */\n get version(): number {\n return this.revision\n }\n\n /**\n * Simulated seconds this field has burned for — whole fixed steps, never\n * wall time.\n *\n * What anything DRAWN from the field animates on: the ember line's beads\n * flicker and crawl, flames puff, and all of it has to be the same at the\n * same moment of the same burn, or a replay flickers differently from the\n * original and a capture can never be taken twice. Stops while the field\n * sleeps, which is right — a sheet at rest has nothing hot left to move.\n */\n get time(): number {\n return this.steps * FIXED_DT\n }\n\n /** Nothing is happening to this sheet, and stepping it costs nothing. */\n get asleep(): boolean {\n return this.active.x0 > this.active.x1\n }\n\n /**\n * Cells the last `step` visited.\n *\n * The cost of the field, stated in the one unit that means the same thing on\n * every machine. Milliseconds would make a test of it a test of whoever ran\n * it — which is how the hands harness spent weeks passing on one laptop.\n */\n get cellsVisited(): number {\n return this.visited\n }\n\n /** What the last `step` produced. */\n get lastStats(): FieldStats {\n return this.stats\n }\n\n /** How many of {@link consumedCells} the last `step` wrote. Always `lastStats.consumed`. */\n get consumedCount(): number {\n return this.consumedLength\n }\n\n /**\n * How many of {@link frontCells} are current. `lastStats.front` times the\n * texel count — and held, like it, across a frame too short to step.\n */\n get frontCount(): number {\n return this.frontLength\n }\n\n /** Texel index for a UV, clamped to the sheet. */\n private at(u: number, v: number): number {\n const x = Math.min(FIELD_SIZE - 1, Math.max(0, Math.round(u * (FIELD_SIZE - 1))))\n const y = Math.min(FIELD_SIZE - 1, Math.max(0, Math.round(v * (FIELD_SIZE - 1))))\n return y * FIELD_SIZE + x\n }\n\n /** The four channels at a UV, for anything that needs to ask a question of a point. */\n sample(u: number, v: number): [number, number, number, number] {\n const i = this.at(u, v) * 4\n return [this.data[i]!, this.data[i + 1]!, this.data[i + 2]!, this.data[i + 3]!]\n }\n\n /**\n * Add to one channel in a soft disc.\n *\n * Every paint operation is this with a different channel, which is the\n * point of having one primitive: `ignite` and `wet` are not two systems\n * that happen to look alike, they are the same write.\n *\n * `plateau` is the fraction of the radius that takes the full amount\n * before the falloff starts. Zero for anything added, so the deposit has a\n * soft peak — a flame held near paper does not deposit a stamped disc of\n * heat, and a too-perfect edge is the first thing that reads as fake.\n * Raised for anything removed, because a pure smoothstep never quite\n * reaches zero even at its centre, and \"almost all the way through\" is not\n * a hole.\n */\n paint(channel: number, u: number, v: number, radius: number, amount: number, plateau = 0): void {\n if (radius <= 0 || amount === 0) return\n const last = FIELD_SIZE - 1\n const cx = u * last\n const cy = v * last\n const r = radius * last\n const box: Box = {\n x0: Math.max(0, Math.floor(cx - r)),\n y0: Math.max(0, Math.floor(cy - r)),\n x1: Math.min(last, Math.ceil(cx + r)),\n y1: Math.min(last, Math.ceil(cy + r)),\n }\n if (box.x0 > box.x1 || box.y0 > box.y1) return\n const r2 = r * r\n const { data, next } = this\n\n for (let y = box.y0; y <= box.y1; y++) {\n for (let x = box.x0; x <= box.x1; x++) {\n const dx = x - cx\n const dy = y - cy\n const d2 = dx * dx + dy * dy\n if (d2 > r2) continue\n const t = plateau >= 1 ? 1 : Math.min(1, (1 - Math.sqrt(d2) / r) / (1 - plateau))\n const falloff = t * t * (3 - 2 * t)\n const i = (y * FIELD_SIZE + x) * 4 + channel\n const before = data[i]!\n // Presence is the one channel that is REMOVED rather than added, and\n // it must never come back — see `cut`.\n const after = Math.min(1, Math.max(0, before + amount * falloff))\n if (after === before) continue\n data[i] = after\n next[i] = after\n if (channel === PRESENCE) this.present += after - before\n if (channel === SATURATION) this.wetTotal += after - before\n }\n }\n\n grow(this.active, box)\n grow(this.touched, box)\n this.pack()\n }\n\n /** Hold a flame near the sheet. Heat, not char — the burning is the field's job. */\n ignite(u: number, v: number, radius = 0.06, amount = 1): void {\n this.paint(HEAT, u, v, radius, amount)\n }\n\n /** Wet the sheet. Saturation wicks along the fibre from wherever it lands. */\n wet(u: number, v: number, radius = 0.08, amount = 0.9): void {\n this.paint(SATURATION, u, v, radius, amount)\n }\n\n /**\n * Take the paper away along a path: a tear, a cut, a punched hole.\n *\n * Presence only ever decreases. A sheet does not grow back, and a paint op\n * that could raise it would make every burn reversible by accident.\n */\n cut(u0: number, v0: number, u1: number, v1: number, width = 0.02): void {\n const steps = Math.max(1, Math.ceil(Math.hypot(u1 - u0, v1 - v0) * FIELD_SIZE))\n for (let s = 0; s <= steps; s++) {\n const t = s / steps\n this.paint(PRESENCE, u0 + (u1 - u0) * t, v0 + (v1 - v0) * t, width, -1, 0.5)\n }\n }\n\n /** Punch a hole. The middle of the sheet, which a fixed-topology mesh cannot do. */\n punch(u: number, v: number, radius = 0.04): void {\n this.paint(PRESENCE, u, v, radius, -1, 0.5)\n }\n\n /**\n * Advance the field by a frame's worth of real time.\n *\n * Fixed steps from an accumulator, so the same simulated second is the same\n * fire at any frame rate. A sheet nothing is happening to returns at once\n * and visits no cells at all — which is the condition for keeping this on\n * the main thread, and the thing the first version could not do: an\n * untouched sheet cost exactly what a burning one did.\n */\n step(delta: number): FieldStats {\n this.visited = 0\n // The transient outputs belong to the step that produced them, and they\n // are cleared BEFORE the guards below rather than after. A frame with no\n // time in it — the first one, or one whose clock went backwards — used to\n // hand back the LAST frame's `consumed` cells and charred count, and the\n // emitters and the sound duly shed the same ash and played the same\n // crackles a second time. `front` is not transient: it is the state of\n // the burn, and it is held across a frame too short to step on purpose.\n this.consumedLength = 0\n if (delta <= 0) {\n this.stats = { ...this.stats, charred: 0, consumed: 0, wetted: 0 }\n return this.stats\n }\n if (this.asleep) {\n // Asleep means nothing CAN change, so there is no time owed either;\n // banking it would replay a burst of steps the moment something wakes.\n this.accumulator = 0\n this.frontLength = 0\n this.stats = { ...this.stats, front: 0, charred: 0, consumed: 0, wetted: 0 }\n return this.stats\n }\n\n this.accumulator = Math.min(this.accumulator + delta, FIXED_DT * MAX_STEPS_PER_FRAME)\n let charred = 0\n let consumed = 0\n let wetted = 0\n let front = 0\n let stepped = false\n while (this.accumulator >= FIXED_DT && !this.asleep) {\n this.accumulator -= FIXED_DT\n this.steps++\n stepped = true\n const done = this.substep()\n charred += done.charred\n consumed += done.consumed\n wetted += done.wetted\n front = done.front\n }\n this.pack()\n\n const n = FIELD_SIZE * FIELD_SIZE\n this.stats = {\n // A frame shorter than one fixed step runs none — one frame in six at\n // 144 Hz — and nothing about the fire changed on it. Reporting 0 there\n // would drop the burn's sound to silence mid-burn, six times a second.\n front: stepped ? front / n : this.stats.front,\n charred,\n consumed,\n wetted,\n saturation: this.wetTotal / n,\n remaining: this.present / n,\n }\n return this.stats\n }\n\n /**\n * One fixed step of diffusion and reaction, over the active region only.\n *\n * The region is the box around every cell that could change, grown by one\n * cell because diffusion reaches exactly one neighbour per step. Cells\n * outside it are at rest and read-only here — their neighbours may read\n * them, and nothing writes them.\n *\n * Written into `next` and copied back over the same region, rather than\n * swapping the two buffers. A swap needs both buffers to agree everywhere\n * the step did not write, and a region that SHRINKS breaks that: a cell the\n * last step wrote into one buffer still holds an older value in the other.\n * Copying the region back keeps them identical outside it, for the cost of\n * the region itself — which is the cost that matters, since it is zero on\n * a sheet at rest rather than a whole grid every step.\n */\n private substep(): { charred: number; consumed: number; wetted: number; front: number } {\n const { data, next, tinder, o } = this\n const heat = this.heat\n const water = this.water\n const size = FIELD_SIZE\n const last = size - 1\n const dt = FIXED_DT\n\n const region: Box = {\n x0: Math.max(0, this.active.x0 - 1),\n y0: Math.max(0, this.active.y0 - 1),\n x1: Math.min(last, this.active.x1 + 1),\n y1: Math.min(last, this.active.y1 + 1),\n }\n const awake = EMPTY()\n\n let charred = 0\n let consumed = 0\n let wetted = 0\n let front = 0\n // This step's front replaces the last one's; see `frontCells`.\n const { frontCells, consumedCells } = this\n\n // The diagonal the fibre leans toward, as index offsets. Constant per\n // field; hoisted so the inner loop is arithmetic and nothing else.\n const hd = heat.diagonal\n const wd = water.diagonal\n\n // The region's edge is a NO-FLUX boundary, exactly like the sheet's.\n // A cell on the rim reading a neighbour outside the region would take or\n // give heat and water to a cell that is not being updated — a transfer\n // with no second half, which leaked 7% of a wet patch in three seconds.\n // Treating the outside as absent makes every exchange mirrored. Nothing\n // stops spreading because of it: a rim cell that gathers more than\n // `REST` wakes, and the region grows past it on the next step. What\n // cannot cross is a residue too thin to wake anything — which is also\n // why a wet front on paper has an edge instead of an infinite tail.\n for (let y = region.y0; y <= region.y1; y++) {\n const hasN = y < region.y1\n const hasS = y > region.y0\n for (let x = region.x0; x <= region.x1; x++) {\n this.visited++\n const i = y * size + x\n const b = i * 4\n const presence = data[b + PRESENCE]!\n\n // Gone is gone: a hole neither conducts heat nor holds water, which\n // is what makes a burnt-through region stop the fire rather than\n // carry it. The A channel is a boundary condition, not just a mask.\n if (presence <= 0) {\n // Whatever water this cell held went with the paper. Take it off the\n // running total as well as the cell, or `saturation` reports water\n // that is gone — and the coupling reads that number as added mass.\n this.wetTotal -= data[b + SATURATION]!\n next[b + HEAT] = 0\n next[b + SATURATION] = 0\n next[b + CHAR] = data[b + CHAR]!\n next[b + PRESENCE] = 0\n continue\n }\n\n const hasE = x < region.x1\n const hasW = x > region.x0\n // Missing neighbours are this cell, which makes the edge of the sheet\n // a no-flux boundary without a branch in the arithmetic below.\n const e = hasE ? i + 1 : i\n const w = hasW ? i - 1 : i\n const n = hasN ? i + size : i\n const s = hasS ? i - size : i\n // The two diagonal neighbours on each stencil's own diagonal.\n const hd1 = hd > 0 ? (hasN && hasE ? i + size + 1 : i) : hasN && hasW ? i + size - 1 : i\n const hd2 = hd > 0 ? (hasS && hasW ? i - size - 1 : i) : hasS && hasE ? i - size + 1 : i\n const wd1 = wd > 0 ? (hasN && hasE ? i + size + 1 : i) : hasN && hasW ? i + size - 1 : i\n const wd2 = wd > 0 ? (hasS && hasW ? i - size - 1 : i) : hasS && hasE ? i - size + 1 : i\n\n // Flux between two cells is weighted by the LESSER of their\n // presences. Symmetric, so what leaves one cell is exactly what\n // arrives in the other and nothing is created or lost at the edge of\n // a hole — the first version weighted by the neighbour alone, which\n // is not.\n const me = e * 4\n const mw = w * 4\n const mn = n * 4\n const ms = s * 4\n const pe = Math.min(presence, data[me + PRESENCE]!)\n const pw = Math.min(presence, data[mw + PRESENCE]!)\n const pn = Math.min(presence, data[mn + PRESENCE]!)\n const ps = Math.min(presence, data[ms + PRESENCE]!)\n\n // ── Heat ─────────────────────────────────────────────────────────\n const h0 = data[b + HEAT]!\n const h1 = hd1 * 4\n const h2 = hd2 * 4\n let h =\n h0 +\n heat.ax * ((data[me + HEAT]! - h0) * pe + (data[mw + HEAT]! - h0) * pw) +\n heat.ay * ((data[mn + HEAT]! - h0) * pn + (data[ms + HEAT]! - h0) * ps) +\n heat.ad *\n ((data[h1 + HEAT]! - h0) * Math.min(presence, data[h1 + PRESENCE]!) +\n (data[h2 + HEAT]! - h0) * Math.min(presence, data[h2 + PRESENCE]!))\n h -= h * o.cooling * dt\n\n // ── Water ────────────────────────────────────────────────────────\n const sat = data[b + SATURATION]!\n const w1 = wd1 * 4\n const w2 = wd2 * 4\n let g =\n sat +\n water.ax * ((data[me + SATURATION]! - sat) * pe + (data[mw + SATURATION]! - sat) * pw) +\n water.ay * ((data[mn + SATURATION]! - sat) * pn + (data[ms + SATURATION]! - sat) * ps) +\n water.ad *\n ((data[w1 + SATURATION]! - sat) * Math.min(presence, data[w1 + PRESENCE]!) +\n (data[w2 + SATURATION]! - sat) * Math.min(presence, data[w2 + PRESENCE]!))\n // Boiling off costs heat, which is exactly why a wet sheet will not\n // light: the flame front spends itself drying the paper in front of\n // it and arrives with nothing left. This one term is the whole of\n // \"dunk the burning corner\".\n if (g > 0 && h > 0) {\n const boiled = Math.min(g, h * o.wetResistance * dt)\n g -= boiled\n h -= boiled / o.wetResistance\n }\n // Proportional, not absolute: an absolute sink annihilates a thin\n // film the moment diffusion spreads it below the per-step amount.\n g -= g * o.drying * dt\n\n // ── Char ─────────────────────────────────────────────────────────\n // Only paper dry enough to burn chars, and only above its own\n // threshold, which varies per texel — that is the ragged front.\n const char = data[b + CHAR]!\n let c = char\n const threshold = tinder[i]! * IGNITION\n if (h > threshold && g < 0.25) {\n const made = Math.min(1 - c, o.charRate * (h - threshold) * dt)\n if (made > 0) {\n c += made\n // Burning is exothermic; above 1 the front feeds itself and runs.\n h += made * o.combustion\n if (char < 0.5 && c >= 0.5) charred++\n }\n }\n\n // ── Presence ─────────────────────────────────────────────────────\n // Fully charred paper is consumed, one way. The only thing besides a\n // cut that removes it.\n let p = presence\n if (c > 0.85) {\n p = Math.max(0, p - o.consumeRate * (c - 0.85) * dt)\n if (p <= 0) {\n consumed++\n consumedCells[this.consumedLength++] = i\n }\n }\n\n h = Math.min(1, Math.max(0, h))\n g = Math.min(1, Math.max(0, g))\n c = Math.min(1, c)\n if (g > sat + 1e-6) wetted++\n if (p > 0.15 && c > 0.08 && c < 0.92 && h > 0.1) frontCells[front++] = i\n\n next[b + CHAR] = c\n next[b + SATURATION] = g\n next[b + HEAT] = h\n next[b + PRESENCE] = p\n this.present += p - presence\n this.wetTotal += g - sat\n\n // Still able to change next step: hot, wet, or being consumed.\n if (h > REST || g > REST || (c > 0.85 && p > 0)) {\n if (x < awake.x0) awake.x0 = x\n if (x > awake.x1) awake.x1 = x\n if (y < awake.y0) awake.y0 = y\n if (y > awake.y1) awake.y1 = y\n }\n }\n }\n\n // Back over the same region only — see the note above.\n for (let y = region.y0; y <= region.y1; y++) {\n const from = (y * size + region.x0) * 4\n const to = (y * size + region.x1 + 1) * 4\n data.set(next.subarray(from, to), from)\n }\n\n grow(this.touched, region)\n this.active = awake\n this.frontLength = front\n return { charred, consumed, wetted, front }\n }\n\n /** Quantise everything touched since the last pack, and mark it uploadable. */\n private pack(): void {\n const t = this.touched\n if (t.x0 > t.x1) return\n const { data, pixels } = this\n for (let y = t.y0; y <= t.y1; y++) {\n const from = (y * FIELD_SIZE + t.x0) * 4\n const to = (y * FIELD_SIZE + t.x1 + 1) * 4\n for (let k = from; k < to; k++) pixels[k] = Math.round(data[k]! * 255)\n }\n this.touched = EMPTY()\n this.revision++\n }\n}\n\n/** Grow a box to include another. */\nfunction grow(into: Box, box: Box): void {\n if (box.x0 > box.x1) return\n if (into.x0 > into.x1) {\n into.x0 = box.x0\n into.y0 = box.y0\n into.x1 = box.x1\n into.y1 = box.y1\n return\n }\n if (box.x0 < into.x0) into.x0 = box.x0\n if (box.y0 < into.y0) into.y0 = box.y0\n if (box.x1 > into.x1) into.x1 = box.x1\n if (box.y1 > into.y1) into.y1 = box.y1\n}\n","/**\n * How bright fire is, in one unit: **multiples of paper white.**\n *\n * `paperlab-fx-fire-spec.md` §4.3 makes exactly one quantitative claim about\n * what fire must look like — an ember is 4–8× brighter than the whitest paper\n * in the frame, which is what makes a bloom pass spread it and a tone curve\n * roll it to white. Everything else about the look is a colour or a\n * millimetre; this is the only number, and it is the one the first fire got\n * wrong twice.\n *\n * It got it wrong twice because until this file there was **no way to say it.**\n * The same claim was authored four different ways:\n *\n * - `plEmberRamp` (`surface/compose.ts`) as sRGB hex through `plLinear`,\n * scaled by a `DamageLook` multiplier;\n * - `particlePresets.ember` as a raw linear triple, `[12, 4.2, 0.9]`;\n * - the fluid's render pass as a `glow` curve times a blackbody;\n * - and the bloom threshold as one absolute constant, 3.6.\n *\n * Four conventions, one threshold, and nothing converting between them. The\n * consequence was not that a number was slightly off. It was that **being 2×\n * under was invisible**: the fluid's flames — the biggest light in the frame —\n * peaked at a scene luminance of 1.68 against a threshold of 3.6, so they\n * never bloomed at all, and turning bloom on at the burn's peak changed 648 of\n * 960,000 pixels. Nobody could have seen that without doing this file's\n * arithmetic by hand, and somebody eventually did, in a review.\n *\n * So: emitters are authored HERE, in multiples of {@link PAPER_WHITE}, and\n * `emission.test.ts` asserts that each one lands in the band the spec asks\n * for. A number that drifts out of range now fails a test instead of waiting\n * for an afternoon with a screenshot and a calculator.\n *\n * Nothing in this file imports anything. It is the unit, so it has to be\n * readable from the sheet's shader constants, from a particle preset and from\n * the post pass alike — and `FxPostPass` is the only file in `paperlab/fx`\n * allowed to name `postprocessing`, so the threshold cannot live there and\n * still be public (see `boundary.test.ts`).\n */\n\n/**\n * Rec. 709 luminance weights.\n *\n * Not an aesthetic choice: this is what `postprocessing`'s bloom pass\n * measures a pixel's brightness with, so it is what \"brighter than paper\"\n * has to mean for the threshold to do what it says.\n */\nexport const LUMA = [0.2126, 0.7152, 0.0722] as const\n\n/**\n * The scene luminance of the brightest paper any built-in preset lights.\n *\n * **Measured, not chosen.** A clean sheet was photographed under every preset\n * with bloom on and bloom off, stepping the threshold until the two pictures\n * were identical: most presets are safe from 0.8–0.9, `studio` from 1.1, and\n * `window` — the brightest — only from 1.6. The brightest preset has to set\n * this, because a threshold that lets paper bloom under any preset brings back\n * the painted-glow failure of §0.\n *\n * Re-measure it with `pnpm test:fire-look` if a preset's lighting changes.\n */\nexport const PAPER_WHITE = 1.6\n\n/**\n * The bloom threshold, in scene luminance before the tone curve.\n *\n * Paper white plus headroom for a highlight at a grazing angle that the test\n * camera does not happen to see. Stated as a multiple so that the reason for\n * the number survives: it is not \"3.6\", it is \"clear of the brightest paper\".\n *\n * **Do not lower it to make fire bloom.** That is the wrong end of the\n * problem, and the review that found the fluid's flames under it said so:\n * `window`'s paper blooms from 1.6, so lowering the threshold blooms paper,\n * which is the painted glow again. Raise what the fire EMITS.\n */\nexport const FX_BLOOM_THRESHOLD = PAPER_WHITE * 2.25\n\n/**\n * What the spec asks a glowing part of a fire to reach, in multiples of paper\n * white (§4.3). The band `emission.test.ts` holds every emitter to.\n */\nexport const FIRE_GLOW = [4, 8] as const\n\n/** One sRGB channel (0..1) to linear. The transfer function, not a gamma. */\nexport function srgbToLinear(c: number): number {\n return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4\n}\n\n/** Rec. 709 luminance of a linear colour. */\nexport function luminance(c: readonly [number, number, number]): number {\n return c[0] * LUMA[0] + c[1] * LUMA[1] + c[2] * LUMA[2]\n}\n\n/** How many times paper white a linear colour is — the inverse of {@link emit}. */\nexport function timesPaperWhite(c: readonly [number, number, number]): number {\n return luminance(c) / PAPER_WHITE\n}\n\n/**\n * An emissive colour, authored as a hue and a brightness.\n *\n * `srgb` is the colour it should LOOK — the ordinary 0..1 sRGB you would pick\n * in a colour picker — and `times` is how many times paper white it should\n * BE. The two are independent on purpose: hue is a judgement about fire, and\n * brightness is a number the spec fixes and a test can check. Authoring them\n * together in one linear triple is what let `[12, 4.2, 0.9]` sit at 3.5×\n * paper white under a comment claiming it cleared 4×.\n *\n * The returned triple has exactly `times * PAPER_WHITE` luminance, so it can\n * be handed straight to a shader.\n */\nexport function emit(srgb: readonly [number, number, number], times: number): [number, number, number] {\n const linear: [number, number, number] = [\n srgbToLinear(srgb[0]),\n srgbToLinear(srgb[1]),\n srgbToLinear(srgb[2]),\n ]\n const lum = luminance(linear)\n // A colour with no luminance has no hue to scale — black stays black rather\n // than becoming a division by zero.\n if (lum <= 1e-6) return [0, 0, 0]\n const gain = (times * PAPER_WHITE) / lum\n return [linear[0] * gain, linear[1] * gain, linear[2] * gain]\n}\n\n/** `emit` from a `#rrggbb` string, for colours sampled off a reference still. */\nexport function emitHex(hex: string, times: number): [number, number, number] {\n const n = Number.parseInt(hex.replace('#', ''), 16)\n return emit([((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255], times)\n}\n\n/**\n * The FLAME HEAT that counts as the hottest gas in a flame — the heat burning\n * made (see `REACT`), which is what a flame's colour is drawn from.\n *\n * It has no natural ceiling, so the render pass needs a reference before\n * \"hot\" can mean anything. Every band in `RENDER_FRAGMENT`\n * is a fraction of this, which is what gives a flame a core, a body and a\n * tip instead of one saturated colour.\n *\n * It is NOT independent of the solver's `cooling` and heat terms: this is a\n * reference temperature, and they decide how much gas ever reaches it.\n * `pnpm test:fire-budget` is what holds the pair honest.\n *\n * 0.65, measured: inside a flame (where its soot is visible) flame heat runs\n * 0.17 / 0.27 / 0.40 / 0.48 at the 10th / 50th / 90th / 99th percentile.\n * At 0.4 most of the flame sat past the core's threshold and was 36%\n * near-white; at 0.8 all of it sat in the tip's band and was 93% orange.\n * 0.65 puts the median in the body and only the top few percent in the core.\n */\nexport const FIRE_HEAT_SCALE = 0.65\n\n/**\n * The soot density that counts as a full flame.\n *\n * Soot is what the render pass draws a flame FROM — its outline, its opacity,\n * how much light it can give — while temperature only picks the colour (see\n * `RENDER_FRAGMENT`). Like the heat, the solver's soot has no natural unit,\n * so this is the reference: `tip.from` is a fraction of it. At 1 the soot's\n * 90th percentile (~1.4) was already past it, so every tongue was solid to\n * its edge with a hard outline; 3 leaves the thin parts room to fade.\n */\nexport const FIRE_SOOT_SCALE = 3\n\n/**\n * How strongly light past {@link FX_BLOOM_THRESHOLD} spreads.\n *\n * It lives beside the threshold because the two are one setting: bloom is a\n * multiplier on whatever clears the threshold, so its right value depends\n * entirely on how much fire is authored above it. With a core authored to\n * over-expose, 0.55 was the most the black stage could take; the flame of\n * {@link FIRE_ZONES} (Noor, 2026-09-13) authors none of it past the\n * threshold, and 1.25 is her tune for it. `pnpm test:fire-budget` holds the\n * stage black and the paper unbloomed whatever this is set to.\n */\nexport const FX_BLOOM = 1.25\n\n/**\n * How much darker a flame's mid-tones are than a linear ramp would make them.\n *\n * A gamma on the normalised temperature. 1 is the ramp as the solver hands it\n * over; above 1 the body falls away from the core faster, below 1 mid\n * temperatures are lifted up the emission ramp. It was 1.15 while the gradient\n * of temperature WAS the look; now that the sheets and the outline carry the\n * contrast, 0.9 gives fuller tongues and a little more amber and yellow.\n */\nexport const FIRE_CONTRAST = 0.9\n\n/**\n * How opaque the densest flame gas is, as an extinction coefficient.\n *\n * Fire used to be pure added light, which is why a tongue standing in front\n * of the sheet came out salmon: orange added to cream paper is pink, the one\n * colour §13.3 forbids. A flame is thin at the tip and nearly opaque through\n * its bright heart, and that opacity is what lets it read as its own colour\n * instead of as a tint on whatever is behind it. 0 restores the old purely\n * additive fire.\n *\n * 2.2 left the body of a flame about half see-through at mid temperature\n * (1 - e^-(0.3 x 2.2) ~ 0.5), so half of every tongue in front of the sheet\n * was cream paper — which is what turned orange into PEACH. Measured on the\n * flame's own pixels, 35-44% of them were pale against the reference's 17%.\n * At 5 the body covers what is behind it and only the thin edges and tips\n * stay translucent.\n */\nexport const FIRE_OPACITY = 5\n\n/**\n * How opaque flame gas must be before it glows at full strength, 0..1.\n *\n * Soot emits and absorbs together, so thin gas should glow in proportion to\n * how much of the background it covers; at a low value a nearly transparent\n * tip still emits fully and turns cream paper salmon, at a high value every\n * dim edge fades out before it can show its deep orange.\n */\nexport const FIRE_THIN = 0.55\n\n/**\n * A flame, in four zones — drawn the way the sheet's burn is (ember line, ash\n * lip, char, scorch): each part named for what it is, with its own controls.\n * Base to tip, as a flame spreading over a sheet actually is:\n *\n * root the blue leading edge where fresh gas meets the air at the paper.\n * Its light comes from excited molecules, not soot, so it is faint\n * and blue — and only ever a trace, because blue light added over\n * cream paper reads lavender.\n * core the hottest gas, where soot forms densest and glows pale — the\n * brightest zone, and the one the bloom finds first.\n * body the luminous bulk: soot glowing yellow-orange as it rises.\n * tip where soot cools and burns off at the outer edge — orange-red,\n * dimmer, tearing into tongues. What survives escapes as smoke.\n *\n * Colour and brightness are separate in every zone, the way professional\n * fire shading keeps an intensity ramp apart from a colour ramp. The DEFAULTS\n * keep the order a hot body glows in — each zone brighter and yellower than\n * the one outside it — because breaking it is how dim yellow turns olive;\n * `emission.test.ts` holds them to that.\n *\n * WHERE the flame is comes from its soot: it begins at `tip.from`, a fraction\n * of FIRE_SOOT_SCALE, over `tip.softness`. WHAT COLOUR it is comes from its\n * temperature (a fraction of FIRE_HEAT_SCALE): the tip gives way to the body\n * around `tip.to`, and the core begins at `core.from`. Brightness is in\n * multiples of paper white; colours are sRGB, the way a colour picker gives\n * them.\n *\n * How bright the body is against paper white is a judgement, and it has gone\n * both ways. Above it, a flame keeps its light in a bright room but loses its\n * colour to the tone curve; below it, the colour holds, and a flame drawn\n * opaque risks reading as a decal on the sheet rather than as light. The\n * defaults sit below it (Noor, 2026-09-13), with the bloom carrying the glow.\n */\nexport interface FireZones {\n root: { color: string; amount: number; reach: number }\n core: { color: string; glow: number; from: number }\n body: { color: string; glow: number }\n tip: { color: string; glow: number; from: number; to: number; softness: number; tearing: number }\n}\n\n/** Any part of any zone, over the defaults. */\nexport type FireZonesInput = { [Z in keyof FireZones]?: Partial<FireZones[Z]> }\n\n/**\n * Noor's tune in the lab on 2026-09-13, and the source of truth for how a\n * flame looks: the lab starts from it and `/hands` inherits it. A gentler\n * flame than the one it replaced — every zone sits under the bloom threshold,\n * so its glow comes from the bloom's strength ({@link FX_BLOOM}) spreading\n * the brightest gas, not from a core authored to over-expose. The values it\n * replaced, measured against Flame_base.png, are in the history of this file.\n */\nexport const FIRE_ZONES: FireZones = {\n root: { color: '#3b6bff', amount: 0.01, reach: 0.39 },\n core: { color: '#fff7d4', glow: 2.1, from: 0.63 },\n body: { color: '#ffcf3a', glow: 0.8 },\n tip: { color: '#ff9e2c', glow: 0.21, from: 0, to: 0.5, softness: 0.02, tearing: 0.64 },\n}\n\n/** The defaults with `input` laid over them, zone by zone. */\nexport function fireZones(input?: FireZonesInput): FireZones {\n return {\n root: { ...FIRE_ZONES.root, ...input?.root },\n core: { ...FIRE_ZONES.core, ...input?.core },\n body: { ...FIRE_ZONES.body, ...input?.body },\n tip: { ...FIRE_ZONES.tip, ...input?.tip },\n }\n}\n\n/** A `#rrggbb` colour as linear RGB — what a shader multiplies light by. */\nexport function hexToLinear(hex: string): [number, number, number] {\n const n = Number.parseInt(hex.replace('#', ''), 16)\n return [\n srgbToLinear(((n >> 16) & 255) / 255),\n srgbToLinear(((n >> 8) & 255) / 255),\n srgbToLinear((n & 255) / 255),\n ]\n}\n","import type { DamageField } from './field'\nimport { HEAT, PRESENCE } from './field'\nimport type { SurfaceLocator } from './fire'\n\n/**\n * Where a burn's flames stand, read off the field.\n *\n * `paperlab-fx-fire-spec.md` §4.1: paper does not burn, the gas does. Heat\n * cooks the cellulose into gas, which burns just ABOVE the surface, rooted\n * over the char right behind the ember line — so a flame belongs on the edge\n * of the hole, where it is hot, and nowhere else. Where the heat has gone\n * there is no gas left and no flame, even if the edge still glows.\n *\n * Pure and deterministic, because flames are part of the picture a capture\n * compares: the same burn at the same step stands the same flames in the\n * same places. Anchors are spread along the rim by binning the grid — the\n * hottest edge cell in each bin — rather than taken in scan order, which\n * would put all of them on the first rows of the hole.\n */\n\n/** One flame: where it stands, how big it is, and who it is. */\nexport interface FlameAnchor {\n /** Root, in world space. */\n x: number\n y: number\n z: number\n /** World units. 10–40 mm at A4 (§6), scaled by heat and by which rim it is on. */\n height: number\n width: number\n /** Stable while the cell burns, so a flame keeps its own flicker frame to frame. */\n seed: number\n /** 0..1 — how hot the paper under it is. */\n heat: number\n /** -1 on the lower rim of a hole, +1 on the upper. */\n upper: number\n /**\n * Which way the paper lies from the root, as a world-space unit vector —\n * across the rim, into the sheet. Zero where it could not be found. The\n * simulator lays its gas along the rim with this, rather than in a disc.\n */\n nx: number\n ny: number\n nz: number\n}\n\n/** 10 and 40 mm, in world units: a default sheet is one unit (210 mm) across. */\nexport const FLAME_HEIGHT = [10 / 210, 40 / 210] as const\n\n/** Below this there is no gas to burn, whatever the edge is doing. */\nexport const FLAME_HEAT = 0.22\n\n/**\n * The least a frame's flames may vary in height, as a coefficient of\n * variation. A ring of equal tongues — a crown — is the regularity that\n * gives a fake fire away, and it is treated as an error: a frame that comes\n * out more even than this is re-drawn with more spread (see the end of\n * `flameAnchors`), and `flames.test.ts` fails if any burn ever produces one.\n */\nexport const IRREGULAR = 0.4\n\n/** A millimetre of A4, in field cells (one cell is ~3.3 mm). */\nconst MM = 63 / 210\n\nconst scratchA = { x: 0, y: 0, z: 0 }\n\ninterface Pick {\n cell: number\n seed: number\n /** How strong the cluster is here, 0..1. */\n cluster: number\n /** How present this flame is right now, 0..1 — flames come and go. */\n life: number\n /** Where along the rim it stands, in mm from its cell. */\n along: number\n /** A side tongue's share of its cluster's height. */\n scale: number\n}\n\n/**\n * Fill `out` with up to `max` flames for the field as it stands at `time`.\n * Returns how many. `out` is reused and grown, never shrunk.\n *\n * Not one flame per stretch of rim — that was a crown. A living, uneven\n * ring: a noise field around the rim, drifting in time, decides where the\n * fire gathers. Where it is strong there are clusters of tall tongues with\n * short ones packed beside them; where it is weak, short licks and gaps.\n * Which cells carry a flame is redrawn every fraction of a second, and each\n * flame has its own life that fades it in and out, so clusters break apart\n * and re-form rather than standing still. All of it is a pure function of\n * the field and the time: the same burn stands the same flames.\n */\nexport function flameAnchors(\n field: DamageField,\n locate: SurfaceLocator,\n max: number,\n out: FlameAnchor[],\n time = field.time,\n): number {\n if (max <= 0 || field.frontCount === 0) return 0\n const size = field.size\n const last = size - 1\n const data = field.data\n\n // The rim: hot paper with a cut beside it.\n const rim: number[] = []\n let cx = 0\n let cy = 0\n for (let y = 1; y < last; y++) {\n for (let x = 1; x < last; x++) {\n const cell = y * size + x\n if (data[cell * 4 + HEAT]! < FLAME_HEAT || data[cell * 4 + PRESENCE]! < 0.5) continue\n if (\n data[(cell - 1) * 4 + PRESENCE]! >= 0.5 &&\n data[(cell + 1) * 4 + PRESENCE]! >= 0.5 &&\n data[(cell - size) * 4 + PRESENCE]! >= 0.5 &&\n data[(cell + size) * 4 + PRESENCE]! >= 0.5\n ) {\n continue\n }\n rim.push(cell)\n cx += x\n cy += y\n }\n }\n if (rim.length === 0) return 0\n cx /= rim.length\n cy /= rim.length\n\n // Around the rim, in order, so \"along the rim\" means something.\n const angle = (cell: number) => Math.atan2(((cell / size) | 0) - cy, (cell % size) - cx)\n rim.sort((a, b) => angle(a) - angle(b) || a - b)\n\n const picks: Pick[] = []\n for (const cell of rim) {\n const seed = hash2(cell, 1)\n const a = angle(cell)\n // The cluster field: periodic around the rim (so there is no seam where\n // the angle wraps), two octaves, drifting at two speeds.\n const cluster = clamp01(\n 0.62 * noise2(Math.cos(a) * 1.6 + time * 0.42, Math.sin(a) * 1.6 - time * 0.17) +\n 0.38 * noise2(Math.cos(a) * 4.1 - time * 0.73 + 7.3, Math.sin(a) * 4.1 + time * 0.31) -\n 0.12,\n )\n // Which cells carry a flame is redrawn every ~0.7 s, each on its own\n // beat, so the ring is never the same two moments running.\n const epoch = Math.floor(time * 1.4 + seed * 10)\n // Most of the rim carries something: short licks where the cluster is\n // weak, but only a few cells are left bare at any moment.\n if (hash2(cell, epoch + 7) > 0.38 + 0.55 * cluster) continue\n // Each flame breathes of its own accord — most shrink and swell, and only\n // some go out entirely before coming back.\n const breath = noise1(time * 0.9 + seed * 31)\n if (breath < 0.16) continue\n const life = 0.3 + 0.7 * smoothstep(0.16, 0.5, breath)\n picks.push({ cell, seed, cluster, life, along: (hash2(cell, 5) - 0.5) * 3, scale: 1 })\n // A strong cluster packs side tongues in beside the main one.\n if (cluster > 0.5) {\n const extra = cluster > 0.72 ? 2 : 1\n for (let k = 0; k < extra; k++) {\n const side = hash2(cell, 11 + k)\n picks.push({\n cell,\n seed: hash2(cell, 21 + k),\n cluster,\n life: life * (0.6 + 0.4 * side),\n along: (side < 0.5 ? -1 : 1) * (1.5 + 3 * hash2(cell, 31 + k)),\n scale: 0.4 + 0.45 * hash2(cell, 41 + k),\n })\n }\n }\n }\n\n // Keep the strongest if the tier cannot afford them all — by cluster and\n // chance, never by anything that depends on how the sheet is turned.\n picks.sort((p, q) => q.cluster * 0.7 + q.seed * 0.3 - (p.cluster * 0.7 + p.seed * 0.3) || p.cell - q.cell)\n const count = Math.min(max, picks.length)\n\n let n = 0\n for (let i = 0; i < count; i++) {\n const pick = picks[i]!\n const cell = pick.cell\n const x = cell % size\n const y = (cell / size) | 0\n // Which way the paper lies from here, and the rim's tangent across it.\n const gx = data[(cell + 1) * 4 + PRESENCE]! - data[(cell - 1) * 4 + PRESENCE]!\n const gy = data[(cell + size) * 4 + PRESENCE]! - data[(cell - size) * 4 + PRESENCE]!\n const gl = Math.hypot(gx, gy) || 1\n const shift = (pick.along * MM) / last\n const u = x / last + (-gy / gl) * shift\n const v = y / last + (gx / gl) * shift\n const at = locate(u, v)\n if (!at) continue\n const rx = at.x\n const ry = at.y\n const rz = at.z\n const toward = locate(u + gx / gl / last, v + gy / gl / last)\n let upper = 0\n scratchA.x = 0\n scratchA.y = 0\n scratchA.z = 0\n if (toward) {\n const l = Math.hypot(toward.x - rx, toward.y - ry, toward.z - rz) || 1\n scratchA.x = (toward.x - rx) / l\n scratchA.y = (toward.y - ry) / l\n scratchA.z = (toward.z - rz) / l\n upper = scratchA.y\n }\n const heat = data[cell * 4 + HEAT]!\n // Square root: the edge of a hole is rarely at full heat.\n const hot = Math.sqrt(Math.min(1, (heat - FLAME_HEAT) / 0.35))\n // Tall on the upper rim, where the gas rises over paper it is preheating;\n // short on the lower one, where it rises across the hole (§6).\n const rim = 0.35 + 0.65 * (0.5 + 0.5 * upper)\n const base = (FLAME_HEIGHT[0] + (FLAME_HEIGHT[1] - FLAME_HEIGHT[0]) * hot) * rim\n // Tall tongues where the cluster is strong, short licks where it is weak.\n const height = base * (0.2 + 1.3 * pick.cluster ** 1.4) * (0.6 + 0.8 * pick.seed) * pick.life * pick.scale\n let anchor = out[n]\n if (!anchor) {\n anchor = { x: 0, y: 0, z: 0, height: 0, width: 0, seed: 0, heat: 0, upper: 0, nx: 0, ny: 0, nz: 0 }\n out[n] = anchor\n }\n anchor.nx = scratchA.x\n anchor.ny = scratchA.y\n anchor.nz = scratchA.z\n anchor.x = rx\n anchor.y = ry\n anchor.z = rz\n anchor.height = Math.min(FLAME_HEIGHT[1], height)\n anchor.width = anchor.height * (0.32 + 0.3 * hash2(cell, 3))\n anchor.seed = pick.seed\n anchor.heat = hot\n anchor.upper = upper\n n++\n }\n\n // A crown is an error: too even a ring is re-drawn with more spread.\n if (n >= 3 && variation(out, n) < IRREGULAR) {\n for (let i = 0; i < n; i++) {\n const a = out[i]!\n a.height = Math.min(FLAME_HEIGHT[1], a.height * (0.3 + 1.4 * hash2(Math.floor(a.seed * 1e6), 9)))\n a.width = a.height * (0.32 + 0.3 * hash2(Math.floor(a.seed * 1e6), 3))\n }\n }\n return n\n}\n\n/** Coefficient of variation of the first `n` heights. */\nexport function variation(anchors: readonly FlameAnchor[], n: number): number {\n let sum = 0\n for (let i = 0; i < n; i++) sum += anchors[i]!.height\n const mean = sum / n\n if (!(mean > 0)) return 0\n let sq = 0\n for (let i = 0; i < n; i++) sq += (anchors[i]!.height - mean) ** 2\n return Math.sqrt(sq / n) / mean\n}\n\nfunction clamp01(x: number): number {\n return x < 0 ? 0 : x > 1 ? 1 : x\n}\n\nfunction smoothstep(a: number, b: number, x: number): number {\n const t = clamp01((x - a) / (b - a))\n return t * t * (3 - 2 * t)\n}\n\n/** A seeded hash of two integers, 0..1. */\nfunction hash2(a: number, b: number): number {\n let h = Math.imul(a ^ Math.imul(b, 0x27d4eb2d), 2654435761) >>> 0\n h ^= h >>> 15\n h = Math.imul(h, 2246822519) >>> 0\n h ^= h >>> 13\n return (h >>> 0) / 4294967296\n}\n\n/** Value noise in two dimensions. */\nfunction noise2(x: number, y: number): number {\n const ix = Math.floor(x)\n const iy = Math.floor(y)\n let fx = x - ix\n let fy = y - iy\n fx = fx * fx * (3 - 2 * fx)\n fy = fy * fy * (3 - 2 * fy)\n const h = (i: number, j: number) => hash2(i * 73856093, j * 19349663 + 7)\n const a = h(ix, iy) + (h(ix + 1, iy) - h(ix, iy)) * fx\n const b = h(ix, iy + 1) + (h(ix + 1, iy + 1) - h(ix, iy + 1)) * fx\n return a + (b - a) * fy\n}\n\n/**\n * How much a flame has puffed up at a moment — the same function the flame\n * shader runs, so the fire light flickers WITH the flames rather than beside\n * them (§7). Value noise, 10–15 Hz, seeded per flame: neighbours never move\n * in step, and nothing is a sine.\n */\nexport function flamePuff(seed: number, time: number): number {\n return 0.72 + 0.28 * noise1(time * 12.5 + seed * 37) + 0.12 * (noise1(time * 23 + seed * 11) - 0.5)\n}\n\nfunction hash1(n: number): number {\n const s = Math.sin(n) * 43758.5453\n return s - Math.floor(s)\n}\n\nfunction noise1(x: number): number {\n const i = Math.floor(x)\n let f = x - i\n f = f * f * (3 - 2 * f)\n return hash1(i) + (hash1(i + 1) - hash1(i)) * f\n}\n","/**\n * What an effect is allowed to cost to SHOW, per device class.\n *\n * Presentation only, deliberately. The simulation is not tiered: every device\n * runs the same damage field on the same grid at the same timestep, so every\n * device gets the same fire — which is also the only way a burn can be shared\n * or replayed and come out the same. See the note at the top of\n * `fx/field.ts`.\n *\n * That is a correction. The first version of this table tiered the field's\n * grid size and its substep count as two independent knobs, and they are not\n * independent: with explicit diffusion, halving the cell size quadruples the\n * cells AND the steps stability needs, so the same fire costs N⁴ — the 128\n * grid was sixteen times the 64 one. What the table actually produced was a\n * fire whose SPEED depended on the device. It is the same shape of mistake as\n * the `stage/quality.ts` knob this file's first preamble warned about, made\n * one file over.\n *\n * What remains here is what a weaker device can genuinely show less of\n * without the effect meaning something different: how many particles are in\n * the air, how many sounds play at once, and how finely a burnt edge is drawn.\n * A knob belongs here only when something reads it.\n */\n\nexport const fxQualityNames = ['auto', 'low', 'medium', 'high'] as const\nexport type FxQualityName = (typeof fxQualityNames)[number]\nexport type FxQualityTier = Exclude<FxQualityName, 'auto'>\n\nexport interface FxQualitySettings {\n /**\n * Hard ceiling on live particles across every emitter at once.\n *\n * Allocated at this size and never grown, so it is a size rather than a\n * suggestion. Read by the emitters, which arrive with fire.\n */\n particles: number\n /**\n * Simultaneous synthesised voices.\n *\n * A phone running MediaPipe and WebGL has no headroom for an unbounded\n * grain cloud, and a grain cloud is exactly what fire and crumple both\n * want to be. The ceiling is the whole design, not a safety net. Read by\n * `FxAudio`.\n */\n voices: number\n /**\n * How ragged a burnt edge is drawn, 0..1 — per-fragment noise that frays\n * the edge finer than the damage grid can. Handed to the sheet as the\n * field's `detail`. The one part of drawing damage that costs per PIXEL,\n * so the part a fill-rate-bound phone gives up first; at 0 the shader\n * skips the noise outright.\n */\n detail: number\n /**\n * The resolution bloom is drawn at, as a fraction of the frame.\n *\n * Bloom is a blur, and a blur at half resolution is a quarter of the\n * fill-rate for a glow that is soft anyway — the cheapest thing a phone\n * can give up that still leaves every zone and every flame on screen\n * (spec §12: tiers thin post quality, never what is shown). Read by\n * `FxPost`.\n */\n bloomScale: number\n /**\n * The most flames standing at once. The spec's §12 said 8 / 16 / 32; a ring\n * of fire that is dense in clusters and broken by gaps needs more tongues\n * than that, and Noor's direction (a living, uneven ring, never a crown)\n * came later and wins — 16 / 36 / 64. Every tier has real flames; a phone\n * gets fewer tongues, never the cheap version.\n * Read by `FxFlames`.\n */\n flames: number\n /**\n * The most of each particle kind in the air at once (spec §12), inside the\n * `particles` budget. Read by `FireEmitter` through its `caps` option.\n */\n caps: { ember: number; smoke: number; ash: number }\n /**\n * Heat haze strength in pixels at 1080p; 0 turns it off. Read by `FxPost`.\n *\n * High tier only, and half what it was. It was 3.8 px on two tiers against\n * §10's 1–3, and invisible in every capture at either — a few pixels of\n * wobble reads as nothing while the fluid beside it is moving. It is also\n * still driven from `flameAnchors`, which is where the SPRITE flames stand,\n * not where the fluid actually burns; until it reads the fluid's own heat\n * texture it is the last polish item rather than a look, so it is off\n * everywhere it cannot be afforded to be good.\n */\n haze: number\n /**\n * The fire simulator's grids (`FxFireFluid`): a velocity grid, a finer one\n * for what is drawn, and the pressure solve's iterations. Every tier\n * simulates a real fire; a phone solves a coarser one.\n *\n * The velocity grid is half the dye's resolution, not a quarter as it was.\n * At a quarter its cells were 4.4 mm and a tongue was one to five cells\n * wide, so everything that makes a flame flicker happened inside a cell and\n * the solve never saw it — sharper advection of the dye could only draw the\n * edges of a motion that was itself butter. The pressure solve is the cheap\n * part (a quarter of the dye grid's texels); the iterations rise with it\n * because Jacobi needs more of them to converge over more cells.\n */\n fluid: { velocity: readonly [number, number]; dye: readonly [number, number]; iterations: number }\n}\n\nexport const fxQualityTiers: Record<FxQualityTier, FxQualitySettings> = {\n /** A desktop GPU, or a phone that has measured its way up here. */\n high: {\n particles: 2000,\n voices: 16,\n detail: 1,\n bloomScale: 1,\n flames: 64,\n caps: { ember: 200, smoke: 200, ash: 120 },\n haze: 2,\n fluid: { velocity: [192, 256], dye: [384, 512], iterations: 36 },\n },\n /** The default worth aiming at: a recent phone, or an integrated laptop GPU. */\n medium: {\n particles: 900,\n voices: 10,\n detail: 1,\n bloomScale: 1,\n flames: 36,\n caps: { ember: 80, smoke: 80, ash: 60 },\n haze: 0,\n fluid: { velocity: [144, 192], dye: [288, 384], iterations: 30 },\n },\n /**\n * A throttled phone with the camera and the tracker already running, which\n * is the realistic case rather than the pessimistic one. The fire is the\n * same fire; the shower is thinner, fewer crackles overlap, and its edge is\n * the grid's own.\n */\n low: {\n particles: 350,\n voices: 6,\n detail: 0,\n bloomScale: 0.5,\n flames: 16,\n caps: { ember: 30, smoke: 30, ash: 20 },\n haze: 0,\n fluid: { velocity: [96, 128], dye: [192, 256], iterations: 20 },\n },\n}\n\n/** Where `auto` starts before anything has been measured. */\nexport const FX_INITIAL_TIER: FxQualityTier = 'medium'\n\nexport const FX_TIER_ORDER: FxQualityTier[] = ['low', 'medium', 'high']\n\nexport function fxQualityFor(name: FxQualityName): FxQualitySettings {\n return fxQualityTiers[name === 'auto' ? FX_INITIAL_TIER : name]\n}\n"],"mappings":"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","names":["rim"]}
|