paperlab 0.5.2 → 0.7.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 +26 -7
- package/dist/FxPostPass-M66QMVVC.js +166 -0
- package/dist/FxPostPass-M66QMVVC.js.map +1 -0
- package/dist/Grade-SBGYELKV.js +49 -0
- package/dist/Grade-SBGYELKV.js.map +1 -0
- package/dist/chunk-7TLIWPGM.js +36 -0
- package/dist/chunk-7TLIWPGM.js.map +1 -0
- package/dist/chunk-GB6BMHC3.js +811 -0
- package/dist/chunk-GB6BMHC3.js.map +1 -0
- package/dist/{chunk-HRXQTJFS.js → chunk-MZCAN3AR.js} +3629 -640
- package/dist/chunk-MZCAN3AR.js.map +1 -0
- package/dist/damageContract-DPF1YFLZ.d.cts +129 -0
- package/dist/damageContract-DPF1YFLZ.d.ts +129 -0
- package/dist/fx.cjs +4276 -0
- package/dist/fx.cjs.map +1 -0
- package/dist/fx.d.cts +1796 -0
- package/dist/fx.d.ts +1796 -0
- package/dist/fx.js +3194 -0
- package/dist/fx.js.map +1 -0
- package/dist/index.cjs +4817 -1149
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -32
- package/dist/index.d.ts +208 -32
- package/dist/index.js +26 -4
- package/dist/index.js.map +1 -1
- package/dist/slots-C5PqY0Q5.d.cts +2297 -0
- package/dist/slots-C5PqY0Q5.d.ts +2297 -0
- package/dist/stage.cjs +5021 -1335
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.d.cts +78 -636
- package/dist/stage.d.ts +78 -636
- package/dist/stage.js +41 -61
- package/dist/stage.js.map +1 -1
- package/package.json +18 -5
- package/dist/chunk-HRXQTJFS.js.map +0 -1
- package/dist/slots-CIo7FnlY.d.cts +0 -13357
- package/dist/slots-CIo7FnlY.d.ts +0 -13357
package/dist/fx.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/surface/damageContract.ts","../src/fx/field.ts","../src/fx/emission.ts","../src/fx/flames.ts","../src/fx/haze.ts","../src/fx/quality.ts","../src/fx/FxPostPass.tsx","../src/fx.ts","../src/fx/particles.ts","../src/fx/fire.ts","../src/fx/FxParticles.tsx","../src/fx/FxPost.tsx","../src/fx/afterglow.ts","../src/fx/FxFlames.tsx","../src/fx/flameShader.ts","../src/fx/FxWisps.tsx","../src/fx/FxFireFluid.tsx","../src/fx/fluid/FireFluid.ts","../src/fx/fluid/passes.ts","../src/fx/fluid/params.ts","../src/fx/FxFireLight.tsx","../src/fx/FxMatchFlame.tsx","../src/fx/sfx/graph.ts","../src/fx/sfx/fire.ts"],"sourcesContent":["/**\n * What damage IS, as far as a sheet is concerned. The whole seam.\n *\n * The split is by responsibility. What has happened to the paper belongs to\n * the sheet — it is shading, alpha and eventually stiffness, and only the\n * sheet can draw or simulate those. What CAUSES it belongs to `paperlab/fx`:\n * a flame, a cup of water, a pair of scissors. The main entry draws whatever\n * damage it is handed and has no idea where it came from, which is exactly\n * how `content` already works — `<Paper>` renders a texture without knowing\n * who painted it.\n *\n * The alternative was a public plugin API for the surface composer and the\n * cloth solver, so that fx could reach in. More flexible, and it would have\n * had to be supported for years before a single effect existed to justify its\n * shape. This is one interface and four constants.\n *\n * **This file imports nothing, on purpose.** `paperlab/fx` depends on it —\n * the field is a `DamageSource` and its channel offsets are these — and the\n * boundary test allows fx to reach this one file and nothing else in the\n * library. The moment it imports three, React or the config schema, fx starts\n * dragging the library in behind it.\n */\n\n/**\n * A grid of damage over the sheet's UV, as the sheet reads it.\n *\n * Satisfied structurally by `DamageField` in `paperlab/fx`, and by anything\n * else that can produce four bytes per texel — a baked texture, a recorded\n * burn played back, a test fixture.\n */\nexport interface DamageSource {\n /** Texels along each edge. The grid is square and covers the whole sheet. */\n readonly size: number\n /**\n * RGBA per texel, 0..255, row-major from v = 0 — the channels are\n * {@link DAMAGE_CHANNELS}. Read in place, never copied: the source mutates\n * it and bumps `version`.\n *\n * Eight bits, deliberately. The simulation behind it can run in float; what\n * reaches the GPU is 16 KB per changed frame at 64², against 256 KB for a\n * 128² float texture, and it does not depend on float-texture filtering,\n * which not every phone GPU guarantees.\n */\n readonly pixels: Uint8Array\n /** Bumped whenever `pixels` changes. The sheet uploads on a change and never otherwise. */\n readonly version: number\n /**\n * How ragged a burnt or cut edge is DRAWN, 0..1; omitted means 1.\n *\n * Presentation only: per-fragment noise that moves the edge within the\n * grid's soft band, finer than the grid itself can carry. The physics reads\n * the grid and never this. It is the one part of drawing damage that costs\n * per pixel, which is why it is a number a source can turn down — `paperlab/fx`\n * sets it from its quality tier.\n */\n readonly detail?: number\n /**\n * The burn's own clock, in seconds; omitted means the frame clock.\n *\n * Presentation only. The ember line on a burning edge is beaded and alive —\n * its beads flicker and crawl — and a source that can be replayed wants\n * that motion to replay too: the same moment of the same burn should draw\n * the same beads, which the frame clock cannot promise. `DamageField`\n * hands over its simulated time.\n */\n readonly time?: number\n /**\n * How a burn is DRAWN — widths, intensities and shapes the sheet's damage\n * shading reads each frame. Presentation only; the physics never sees it.\n * Anything left out takes {@link DAMAGE_LOOK_DEFAULTS}.\n */\n readonly look?: DamageLook\n}\n\n/**\n * The knobs on what a burn looks like, in the units a person tunes by —\n * millimetres of A4 and plain multipliers. Every one is optional.\n */\nexport interface DamageLook {\n /** The ember line's widest bead, mm. */\n emberWidth?: number\n /** How bright the beads burn, × the default. */\n emberIntensity?: number\n /** How much of the edge is lit at once, 0..1. */\n emberCoverage?: number\n /** How fast the beads flicker and crawl, × the default. */\n emberFlicker?: number\n /** The dim crimson glow beside the beads, reaching into the char, 0..2. */\n emberGlow?: number\n /** Specks of glowing fibre along the edge, 0..2. */\n sparkle?: number\n /** The pale ash lip's widest point, mm. */\n lipWidth?: number\n /** How pale the ash lip is, × the sampled grey. */\n lipBrightness?: number\n /** 0 is grey char, 1 is dark orange to deep brown. */\n charWarmth?: number\n /** How visible the crack network in the char is, 0..1. */\n charCracks?: number\n /** How far the scorch reaches UP past the burn, mm. */\n scorchReach?: number\n /** How dark the scorch browns go, × the sampled ramp. */\n scorchDarkness?: number\n /** How strongly the scorch front breaks into fingers, × the default. */\n fingers?: number\n /** The burnt edge's long waves, ±mm. */\n edgeWave?: number\n /** The burnt edge's small bites in and out, ±mm. */\n edgeBite?: number\n}\n\n/**\n * What every burn is drawn with unless told otherwise — the combination Noor\n * tuned in the lab's sidebar on 2026-09-12, which is the look this ships.\n */\nexport const DAMAGE_LOOK_DEFAULTS: Required<DamageLook> = {\n // §5 asks for 0.3–1 mm. 1.8 was outside it, and outside the slider's range\n // it was tuned in — a control at its limit is a report that something\n // underneath is wrong, which in this case was a fire nothing could see.\n emberWidth: 0.9,\n emberIntensity: 1.45,\n emberCoverage: 0.6,\n emberFlicker: 1.65,\n emberGlow: 1.25,\n sparkle: 0.5,\n lipWidth: 1.2,\n // Was 1.5, its slider's ceiling, which made the ash lip brighter than the\n // paper it sits on. Ash is pale GREY; the reference's lip is dimmer than\n // the sheet, not a highlight drawn on it.\n lipBrightness: 0.85,\n // Was 1, also a ceiling. At full warmth the char is milk chocolate —\n // closer to cardboard than to charcoal (§5). Burnt paper keeps a little\n // warmth in the plates and reads near black in a frame with a fire in it.\n charWarmth: 0.3,\n // Was 1, also a ceiling. The cracks are drawn as thin polygon outlines, so\n // at full strength the char reads as a mosaic rather than as broken plates.\n charCracks: 0.55,\n scorchReach: 30,\n scorchDarkness: 1.17,\n fingers: 1.25,\n edgeWave: 8.5,\n edgeBite: 3.6,\n}\n\n/**\n * Which byte of a texel means what.\n *\n * char — scorch colour, the brown halo; on cloth, shrinkage and a curl toward the front.\n * saturation — wet darkening and smoothing; on cloth, added mass.\n * heat — how hot the paper is. Drawn only where it burns: the ember line, not the sheet.\n * presence — how much paper is there at all; below half, none is drawn.\n */\nexport const DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 } as const\n","/**\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","import * as THREE from 'three'\nimport { BlendFunction, Effect } from 'postprocessing'\n\n/** How many flames the haze can shimmer above at once. */\nexport const HAZE_SOURCES = 16\n\nconst FRAGMENT = /* glsl */ `\nuniform vec4 uSources[${HAZE_SOURCES}];\nuniform int uCount;\nuniform float uTime;\nuniform float uAmount;\nuniform vec3 uWarm;\nuniform float uGrade;\n\nfloat hzHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat hzNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(mix(hzHash(i), hzHash(i + vec2(1.0, 0.0)), u.x), mix(hzHash(i + vec2(0.0, 1.0)), hzHash(i + vec2(1.0, 1.0)), u.x), u.y);\n}\n\n// Heat haze: the air above a flame is hotter than the air beside it, and\n// light bends through the difference. A few pixels of upward-scrolling\n// displacement, only in the column above each flame, fading with height.\nvoid mainUv(inout vec2 uv) {\n vec2 offset = vec2(0.0);\n for (int i = 0; i < ${HAZE_SOURCES}; i++) {\n if (i >= uCount) break;\n vec4 s = uSources[i];\n vec2 d = uv - s.xy;\n float h = max(s.z, 1e-4);\n float above = d.y / h;\n if (above < -0.1 || above > 3.2) continue;\n float across = abs(d.x) / (h * 0.55 + 1e-4);\n // Around the flame's own boundary as well as above it: gentle inside the\n // body, strongest in the hot column over it, gone by three flame heights.\n float column = smoothstep(0.3, 0.9, above) * (1.0 - smoothstep(1.4, 3.2, above));\n float around = smoothstep(-0.1, 0.15, above) * (1.0 - smoothstep(0.8, 1.2, above)) * 0.45;\n float m = (1.0 - smoothstep(0.45, 1.15, across)) * max(column, around) * s.w;\n vec2 q = vec2(uv.x * 140.0, uv.y * 90.0 - uTime * 3.0);\n offset += (vec2(hzNoise(q), hzNoise(q + 31.7)) - 0.5) * m;\n }\n uv += offset * uAmount;\n}\n\n// The grade: the whole frame warms as the fire grows — a few percent, no\n// more (spec §7). A multiply, so black stays black.\nvoid mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {\n outputColor = vec4(inputColor.rgb * mix(vec3(1.0), uWarm, uGrade), inputColor.a);\n}\n`\n\n/**\n * Heat haze above the flames, and the warm grade of a frame with a fire in\n * it — spec §7's last two items, as one pass because both are cheap and both\n * run on the HDR frame before the tone curve.\n *\n * Fed each frame by `FxPostPass`: `sources` holds up to {@link HAZE_SOURCES}\n * flames as (screen u, screen v, flame height in screen v, strength).\n */\nexport class HazeGradeEffect extends Effect {\n constructor() {\n super('HazeGradeEffect', FRAGMENT, {\n blendFunction: BlendFunction.NORMAL,\n uniforms: new Map<string, THREE.Uniform>([\n ['uSources', new THREE.Uniform(Array.from({ length: HAZE_SOURCES }, () => new THREE.Vector4()))],\n ['uCount', new THREE.Uniform(0)],\n ['uTime', new THREE.Uniform(0)],\n // 1–3 px at 1080p, as a fraction of the frame.\n ['uAmount', new THREE.Uniform(2.2 / 1080)],\n ['uWarm', new THREE.Uniform(new THREE.Vector3(1.04, 1.0, 0.94))],\n ['uGrade', new THREE.Uniform(0)],\n ]),\n })\n }\n\n get sources(): THREE.Vector4[] {\n return this.uniforms.get('uSources')!.value as THREE.Vector4[]\n }\n\n set count(n: number) {\n this.uniforms.get('uCount')!.value = n\n }\n\n set time(t: number) {\n this.uniforms.get('uTime')!.value = t\n }\n\n /** 0 turns the shimmer off (the low tier) and keeps the grade. */\n set amount(px1080: number) {\n this.uniforms.get('uAmount')!.value = px1080 / 1080\n }\n\n /** 0..1 — how much of the warm grade to apply. */\n set grade(g: number) {\n this.uniforms.get('uGrade')!.value = g\n }\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","import { Bloom, EffectComposer, ToneMapping } from '@react-three/postprocessing'\nimport { useFrame } from '@react-three/fiber'\nimport { ToneMappingMode } from 'postprocessing'\nimport { useMemo, useRef } from 'react'\nimport * as THREE from 'three'\nimport { FX_BLOOM, FX_BLOOM_THRESHOLD, PAPER_WHITE } from './emission'\nimport type { FxPostProps } from './FxPost'\nimport { flameAnchors, type FlameAnchor } from './flames'\nimport { HAZE_SOURCES, HazeGradeEffect } from './haze'\nimport { fxQualityFor } from './quality'\n\n/**\n * Where fire's glow comes from: bloom on the HDR frame, the heat haze and the\n * warm grade, then the tone curve.\n *\n * The ONLY file in `paperlab/fx` that imports `@react-three/postprocessing`\n * (with `haze.ts`, which only this imports), and nothing imports it\n * statically — `FxPost` reaches it through a dynamic import, so the two peers\n * stay optional the way stage's `Grade` keeps them. Importing this from\n * anywhere else would put the specifier back into fx's module graph and a\n * consumer without the peers could no longer load fx.\n *\n * Why it exists, in the words of `paperlab-fx-fire-spec.md` §0 and §7: the\n * first fire's glow was PAINT — a warm colour added to `csm_Emissive` over\n * paper that had not burnt — and red added to white makes pink. Real glow is\n * light too bright for the film: an ember is brighter than the whitest paper\n * in the frame, the lens spreads it, and the curve rolls it off to white.\n * Nothing on the sheet paints glow any more; anything that wants to glow\n * emits past paper white and this makes it bloom.\n *\n * Order is the whole argument: bloom reads the frame while it is still HDR —\n * which is what lets a threshold mean \"brighter than paper\" — the haze bends\n * the light above the flames and the grade warms the frame, and the curve\n * lands the result last.\n *\n * **The composer takes the tone curve off the renderer, so this gives it\n * back.** `<EffectComposer>` sets `gl.toneMapping = NoToneMapping` while it is\n * mounted; without the `<ToneMapping>` below, mounting this pass would\n * silently throw away the rig's film and the unburnt sheet would change. The\n * spec's own test for this pass is that it does not: with no fire in frame,\n * post on and post off are the same picture (§14.3, `pnpm test:fire-look`).\n */\n\nconst modes = {\n agx: ToneMappingMode.AGX,\n neutral: ToneMappingMode.NEUTRAL,\n filmic: ToneMappingMode.ACES_FILMIC,\n} as const\n\nconst root = new THREE.Vector3()\nconst top = new THREE.Vector3()\n\nexport function FxPostPass({\n film = 'neutral',\n quality = 'medium',\n bloom = FX_BLOOM,\n threshold = FX_BLOOM_THRESHOLD,\n field,\n locate,\n haze: hazeOverride,\n}: FxPostProps) {\n const { bloomScale, haze: tierHaze } = fxQualityFor(quality)\n const hazePx = hazeOverride ?? tierHaze\n const haze = useMemo(() => new HazeGradeEffect(), [])\n const anchors = useRef<FlameAnchor[]>([])\n\n // The flames, in screen space, for the haze; the front, for the grade.\n useFrame(({ camera }) => {\n if (!field || !locate) {\n haze.count = 0\n haze.grade = 0\n return\n }\n const n = hazePx > 0 ? flameAnchors(field, locate, HAZE_SOURCES, anchors.current) : 0\n const sources = haze.sources\n for (let i = 0; i < n; i++) {\n const a = anchors.current[i]!\n root.set(a.x, a.y, a.z).project(camera)\n top.set(a.x, a.y + a.height, a.z).project(camera)\n sources[i]!.set(root.x * 0.5 + 0.5, root.y * 0.5 + 0.5, Math.max(0, (top.y - root.y) * 0.5), a.heat)\n }\n haze.count = n\n haze.time = field.time\n haze.amount = hazePx\n // A few percent at most, and only as the fire grows (§7).\n haze.grade = Math.min(1, field.lastStats.front / 0.03)\n })\n\n return (\n <EffectComposer>\n {bloom > 0 ? (\n <Bloom\n intensity={bloom}\n luminanceThreshold={threshold}\n // Nearly a hard knee. A wide one reaches BELOW the threshold, and\n // below it is paper: the whole point of the threshold is that paper\n // never blooms (§7), and a soft knee would bloom it a little.\n luminanceSmoothing={0.02}\n mipmapBlur\n resolutionScale={bloomScale}\n />\n ) : null}\n <primitive object={haze} />\n <ToneMapping mode={modes[film]} />\n </EffectComposer>\n )\n}\n\n/**\n * Re-exported, not defined here.\n *\n * It moved to `emission.ts` because it is half of a unit — \"brighter than\n * paper\" is only meaningful beside the paper white it is measured against,\n * and every emitter now authors itself in multiples of that same number. It\n * also could not be public from here: this file is the only one in\n * `paperlab/fx` allowed to import `postprocessing` (see `boundary.test.ts`),\n * so anything exported from it is unreachable for a consumer who has not\n * installed the optional peers.\n */\nexport { FX_BLOOM, FX_BLOOM_THRESHOLD, PAPER_WHITE }\n","/**\n * `paperlab/fx` — what happens TO the paper.\n *\n * **Exported as of the release that ships fire**, and not before: it was\n * built but deliberately absent from `exports` while there was nothing in it\n * to use, because a subpath cannot be taken back once it is published. A\n * missing changeset would not have held it back — the next release for any\n * reason publishes whatever `exports` names — so the map was the thing that\n * did, and `test:consumer` pinned that.\n *\n * The main entry point is paper as a thing: a sheet, its stock, its shape,\n * the way it moves. This one is the events that damage it — burning, soaking,\n * tearing, the particles they throw off and the sounds they make.\n *\n * Its own subpath for the same reason stage has one, and it is worth stating\n * precisely because the first version of that argument was wrong. A subpath\n * saves nobody a byte: tree-shaking already removes what a `<Paper>` bundle\n * does not import, and it always did. What a subpath does is keep the import\n * SPECIFIER out of the main entry's module graph — and a specifier is the one\n * thing tree-shaking cannot remove, because resolving it happens before\n * anything knows whether it is used.\n *\n * That matters here the moment any of this reaches for something a `<Paper>`\n * consumer has not installed. Nothing does today; the damage field is plain\n * arithmetic over a Float32Array. It is the sound layer and the emitters that\n * will, and the time to draw the line is before there is anything on the\n * wrong side of it.\n *\n * The rule that keeps it true, and it wants to be a lint rule rather than a\n * promise: **nothing under `src/fx/` may be imported from `src/index.ts` or\n * anything it reaches.** The library was last measured near 38 KB gzipped\n * with its peers external and that number is part of what it is for.\n */\n\nexport {\n DamageField,\n CHAR,\n SATURATION,\n HEAT,\n PRESENCE,\n FIELD_SIZE,\n FIXED_DT,\n type DamageFieldOptions,\n type FieldStats,\n} from './fx/field'\n\n// Re-exported rather than defined here: the contract belongs to the sheet,\n// which is what draws it. See `surface/damageContract.ts`.\nexport {\n DAMAGE_CHANNELS,\n DAMAGE_LOOK_DEFAULTS,\n type DamageLook,\n type DamageSource,\n} from './surface/damageContract'\n\nexport {\n ParticlePool,\n particlePresets,\n type ParticlePreset,\n type ParticlePresetName,\n type ParticleTarget,\n} from './fx/particles'\n\nexport {\n FireEmitter,\n fireEmitterDefaults,\n type FireEmitterOptions,\n type SurfaceLocator,\n} from './fx/fire'\n\nexport { FxParticles, type FxParticlesProps } from './fx/FxParticles'\n\nexport { FxPost, type FxFilm, type FxPostProps } from './fx/FxPost'\n\n/**\n * How bright fire is, in one unit. The threshold lives here rather than in the\n * post pass so that a consumer authoring their own emissive can read it\n * without installing the pass's optional peers.\n */\nexport {\n PAPER_WHITE,\n FX_BLOOM,\n FX_BLOOM_THRESHOLD,\n FIRE_HEAT_SCALE,\n FIRE_SOOT_SCALE,\n FIRE_ZONES,\n fireZones,\n hexToLinear,\n type FireZones,\n type FireZonesInput,\n FIRE_GLOW,\n emit,\n emitHex,\n luminance,\n srgbToLinear,\n timesPaperWhite,\n} from './fx/emission'\n\nexport { Afterglow, type AfterglowOptions } from './fx/afterglow'\nexport { FxFlames, type FxFlamesProps } from './fx/FxFlames'\nexport { FxWisps, type FxWispsProps } from './fx/FxWisps'\nexport { FxFireFluid, type FxFireFluidProps } from './fx/FxFireFluid'\nexport { FireFluid, type FluidGrid } from './fx/fluid/FireFluid'\nexport {\n fireFluidControls,\n fireFluidDefaults,\n solverUniforms,\n type FireFluidParams,\n type SolverUniforms,\n} from './fx/fluid/params'\nexport { FxFireLight, type FxFireLightProps } from './fx/FxFireLight'\nexport { FxMatchFlame, type FxMatchFlameProps, type MatchFlameState } from './fx/FxMatchFlame'\nexport { flameAnchors, flamePuff, FLAME_HEIGHT, type FlameAnchor } from './fx/flames'\n\nexport {\n FxAudio,\n createAudioContext,\n type AudioLike,\n type BiquadFilterLike,\n type FxAudioOptions,\n type PannerLike,\n type Voice,\n} from './fx/sfx/graph'\n\nexport { FireSound, type FireSoundOptions, type SoundAt } from './fx/sfx/fire'\n\nexport {\n fxQualityNames,\n fxQualityFor,\n fxQualityTiers,\n FX_INITIAL_TIER,\n FX_TIER_ORDER,\n type FxQualityName,\n type FxQualitySettings,\n type FxQualityTier,\n} from './fx/quality'\n","import { emitHex } from './emission'\n\n/**\n * Particles: one pool, and presets that are only parameter sets.\n *\n * Embers, smoke and ash are not three systems. They are the same particle —\n * a position, a velocity, an age — told different things about how long it\n * lives, which way the air takes it and what it looks like. So there is one\n * pool, and a preset is a row of numbers; the next effect's droplet or dust is\n * another row, not another file. That is the P3 gate stated early: each effect\n * after the second should cost a preset, not a subsystem.\n *\n * **Simulated on the CPU,** for the same reason the damage field is: what\n * spawns them is read on the CPU. Ash leaves the texel that just burnt\n * through, and that texel is in a `Float32Array`, not a texture. A GPU\n * particle system would have to be told every spawn through an upload anyway,\n * and the counts here — hundreds, not hundreds of thousands — are ones a CPU\n * steps in a fraction of a millisecond.\n *\n * **A fixed capacity, from the tier,** allocated once and never grown. When\n * it is full, the particle closest to the end of its life gives up its slot:\n * dropping the NEW one instead would starve the burn of embers the moment the\n * room filled with old smoke, which is exactly backwards — the newest\n * particle is the one leaving the fire you are looking at.\n *\n * Drawing is `FxParticles`. Nothing in this file touches three.\n */\n\nexport interface ParticlePreset {\n /** Seconds a particle lives, drawn uniformly between the two. */\n readonly life: readonly [number, number]\n /** Launch speed in world units a second, drawn between the two. */\n readonly speed: readonly [number, number]\n /** Launch direction before spread, in world space. */\n readonly direction: readonly [number, number, number]\n /** 0 launches along `direction` exactly; 1 anywhere in the hemisphere around it. */\n readonly spread: number\n /** Acceleration straight up, world units a second squared. Hot air rises; ash is heavier than it. */\n readonly lift: number\n /** How fast the air brings a particle to its own speed, per second. */\n readonly drag: number\n /** A random acceleration, world units a second squared, drawn afresh every step — the flutter. */\n readonly jitter: number\n /** How much of the wind's velocity it takes on, 0..1. */\n readonly windCatch: number\n /** Diameter in world units, at birth and at death. */\n readonly size: readonly [number, number]\n /** Linear RGB at birth and at death. Above 1 for anything that is light rather than matter. */\n readonly color: readonly [readonly [number, number, number], readonly [number, number, number]]\n /** Opacity at birth and at death. Every particle also fades IN over the first tenth of its life. */\n readonly alpha: readonly [number, number]\n /** Spin in radians a second, drawn either way up to this. */\n readonly spin: number\n /** Added to what is behind it — light — or drawn over it — matter. */\n readonly blend: 'additive' | 'normal'\n /** A soft disc, or a jagged flake. */\n readonly shape: 'soft' | 'flake'\n /** Brightness wobble, 0..1: an ember flickers, a puff of smoke does not. */\n readonly flicker: number\n}\n\n/**\n * The fire's three. World units are the sheet's: a default sheet is one unit\n * across, about the width of A4, so 0.01 is two millimetres.\n */\nexport const particlePresets = {\n /**\n * A spark off the burning line. Short-lived, shrinking as it cools, carried\n * up and sideways by the heat.\n *\n * Authored through `emission.ts`, in multiples of paper white, which is what\n * caught the bug in the version before this one. That version said \"the hot\n * end is now ~5.6, well over the bloom threshold\" — and 5.6 was an ABSOLUTE\n * luminance, while paper white under `window` is 1.6. So the hottest spark\n * in the frame was **3.5× paper, under the spec's own 4× floor**, and its\n * own comment said otherwise. A number that cannot be compared to anything\n * is a number nobody can check.\n *\n * It was also too red. `[12, 4.2, 0.9]` is a saturated red at high\n * intensity, and the tone curve takes a bright red-dominant colour to\n * SALMON — which is `Never_this.png`. The hue is now a blackbody's: a spark\n * leaving the fire is yellow-orange, about 2200 K, and cools to a deep\n * orange-red as it dies. Hue and brightness are separate arguments now, and\n * `emit` keeps them that way.\n */\n ember: {\n life: [0.5, 1.4],\n speed: [0.15, 0.5],\n direction: [0, 1, 0],\n spread: 0.6,\n lift: 0.6,\n drag: 0.8,\n jitter: 1.5,\n windCatch: 0.8,\n // 0.3-1 mm of core (§5). A sheet is one world unit across, 210 mm, so\n // 0.0035 is 0.7 mm. It was 0.005 — a full millimetre wide before the\n // streak stretched it, which is where \"wide bars\" started.\n size: [0.0035, 0.0012],\n color: [\n // 6× paper white: inside §4.3's 4–8 band, so it blooms.\n emitHex('#FFC271', 6),\n // 0.7× — UNDER the bloom threshold on purpose, so a dying spark stops\n // glowing rather than merely fading.\n emitHex('#FF5512', 0.7),\n ],\n alpha: [1, 0],\n spin: 0,\n blend: 'additive',\n shape: 'soft',\n flicker: 0.35,\n },\n /**\n * What a paper fire mostly makes. Thin, slow, growing as it rises and\n * spreads, and gone well before it could read as a volume — no fluid\n * simulation, on purpose; see the fx plan.\n */\n smoke: {\n life: [1.8, 3.5],\n speed: [0.05, 0.15],\n direction: [0, 1, 0],\n spread: 0.3,\n lift: 0.25,\n drag: 0.9,\n jitter: 0.3,\n windCatch: 1,\n size: [0.05, 0.35],\n // #6B6560 grey-brown, linear, and thin — the background stays clear.\n color: [\n [0.147, 0.13, 0.117],\n [0.2, 0.19, 0.18],\n ],\n alpha: [0.08, 0],\n spin: 0.4,\n blend: 'normal',\n shape: 'soft',\n flicker: 0,\n },\n /**\n * Burnt-through paper, leaving. Lifted a little by the heat it came from,\n * then heavier than the air: it tumbles, flutters and falls — the part of a\n * burn that proves paper was there.\n */\n ash: {\n life: [2.5, 5],\n // Thrown UP by the heat it came off, before it is heavier than the air.\n // It used to leave at almost nothing (0.02–0.1) with a lift of −0.35, so\n // every flake began falling the moment it was born — straight through the\n // hole it came from, onto the black stage, dark on dark.\n speed: [0.14, 0.3],\n direction: [0, 1, 0],\n spread: 0.55,\n lift: -0.22,\n drag: 1.6,\n jitter: 1.2,\n windCatch: 1,\n // 5–10 mm across (§8.3). A sheet is 210 mm, so 0.03 is 6.3 mm. They were\n // 3.8 mm and there were far too many of them: a shower of specks rather\n // than a few flakes you would notice.\n size: [0.03, 0.026],\n color: [\n [0.07, 0.065, 0.06],\n [0.12, 0.115, 0.11],\n ],\n alpha: [0.95, 0],\n spin: 4,\n blend: 'normal',\n shape: 'flake',\n flicker: 0,\n },\n} as const satisfies Record<string, ParticlePreset>\n\nexport type ParticlePresetName = keyof typeof particlePresets\n\nconst PRESET_NAMES = Object.keys(particlePresets) as ParticlePresetName[]\nconst PRESETS: readonly ParticlePreset[] = PRESET_NAMES.map((name) => particlePresets[name])\n\n/**\n * Where `write` puts one blend mode's particles, as flat arrays sized for the\n * pool's capacity: xyz, rgba, and four extras — size, spin angle, shape (0\n * soft, 1 flake) and a per-particle seed the sprite uses for its outline.\n */\nexport interface ParticleTarget {\n readonly position: Float32Array\n readonly color: Float32Array\n readonly extra: Float32Array\n /** Velocity, xyz, if the target wants it — an ember is drawn stretched along its own. */\n readonly velocity?: Float32Array\n}\n\n/** A small, fast, seeded generator — the same seed makes the same fire. */\nfunction mulberry32(seed: number): () => number {\n let a = seed >>> 0\n return () => {\n a = (a + 0x6d2b79f5) >>> 0\n let t = a\n t = Math.imul(t ^ (t >>> 15), t | 1)\n t ^= t + Math.imul(t ^ (t >>> 7), t | 61)\n return ((t ^ (t >>> 14)) >>> 0) / 4294967296\n }\n}\n\nexport class ParticlePool {\n readonly capacity: number\n /**\n * The air's own velocity, world units a second. A blow on `/hands` is a\n * wind, and smoke that ignores it is smoke painted on the glass.\n */\n readonly wind: [number, number, number] = [0, 0, 0]\n private readonly kind: Uint8Array\n private readonly position: Float32Array\n private readonly velocity: Float32Array\n private readonly age: Float32Array\n private readonly life: Float32Array\n private readonly angle: Float32Array\n private readonly spin: Float32Array\n private readonly seed: Float32Array\n private live = 0\n /** Live particles of each preset, kept as they come and go, so a cap costs nothing to check. */\n private readonly perKind = new Int32Array(PRESET_NAMES.length)\n /** Embers that popped in the air since the last `takePops` — the sound follows the picture. */\n private pops = 0\n private readonly random: () => number\n\n constructor(capacity: number, seed = 1) {\n this.capacity = Math.max(0, Math.floor(capacity))\n const n = this.capacity\n this.kind = new Uint8Array(n)\n this.position = new Float32Array(n * 3)\n this.velocity = new Float32Array(n * 3)\n this.age = new Float32Array(n)\n this.life = new Float32Array(n)\n this.angle = new Float32Array(n)\n this.spin = new Float32Array(n)\n this.seed = new Float32Array(n)\n this.random = mulberry32(seed)\n }\n\n /** Particles in the air. */\n get count(): number {\n return this.live\n }\n\n /** How many live particles are of one preset. For tests and for a HUD. */\n countOf(name: ParticlePresetName): number {\n return this.perKind[PRESET_NAMES.indexOf(name)]!\n }\n\n /** Launch one particle from a point. Never allocates; at capacity it takes the most-spent slot. */\n spawn(name: ParticlePresetName, x: number, y: number, z: number): void {\n if (this.capacity === 0) return\n const preset = particlePresets[name]\n let i = this.live\n if (i < this.capacity) {\n this.live++\n } else {\n // Full: the particle nearest the end of its life gives up its slot.\n i = 0\n let spent = -1\n for (let j = 0; j < this.live; j++) {\n const f = this.age[j]! / this.life[j]!\n if (f > spent) {\n spent = f\n i = j\n }\n }\n this.perKind[this.kind[i]!] = this.perKind[this.kind[i]!]! - 1\n }\n const r = this.random\n this.kind[i] = PRESET_NAMES.indexOf(name)\n this.perKind[this.kind[i]!] = this.perKind[this.kind[i]!]! + 1\n const i3 = i * 3\n this.position[i3] = x\n this.position[i3 + 1] = y\n this.position[i3 + 2] = z\n\n // A direction around the preset's, by as much as its spread allows: a\n // random unit vector mixed in, and folded into the preset's hemisphere.\n const [dx, dy, dz] = preset.direction\n let rx = r() * 2 - 1\n let ry = r() * 2 - 1\n let rz = r() * 2 - 1\n const rl = Math.hypot(rx, ry, rz) || 1\n rx /= rl\n ry /= rl\n rz /= rl\n let ox = dx * (1 - preset.spread) + rx * preset.spread\n let oy = dy * (1 - preset.spread) + ry * preset.spread\n let oz = dz * (1 - preset.spread) + rz * preset.spread\n if (ox * dx + oy * dy + oz * dz < 0) {\n ox = -ox\n oy = -oy\n oz = -oz\n }\n const ol = Math.hypot(ox, oy, oz) || 1\n const speed = preset.speed[0] + (preset.speed[1] - preset.speed[0]) * r()\n this.velocity[i3] = (ox / ol) * speed\n this.velocity[i3 + 1] = (oy / ol) * speed\n this.velocity[i3 + 2] = (oz / ol) * speed\n\n this.age[i] = 0\n this.life[i] = preset.life[0] + (preset.life[1] - preset.life[0]) * r()\n this.angle[i] = r() * Math.PI * 2\n this.spin[i] = (r() * 2 - 1) * preset.spin\n this.seed[i] = r()\n }\n\n /** Advance every particle by `dt` seconds; the dead leave the pool. */\n step(dt: number): void {\n if (dt <= 0) return\n const r = this.random\n const [wx, wy, wz] = this.wind\n for (let i = 0; i < this.live; i++) {\n const before = this.age[i]! / this.life[i]!\n this.age[i] = this.age[i]! + dt\n // The one ember in ten that dies in the air flashes as it crosses 0.86\n // of its life (see `write`); count it as it does, for the sound.\n if (this.seed[i]! < 0.1 && before < 0.86 && this.age[i]! / this.life[i]! >= 0.86) {\n if (PRESETS[this.kind[i]!]!.blend === 'additive') this.pops++\n }\n if (this.age[i]! >= this.life[i]!) {\n this.remove(i)\n i--\n continue\n }\n const preset = PRESETS[this.kind[i]!]!\n const i3 = i * 3\n const drag = Math.min(1, preset.drag * dt)\n const catchWind = preset.windCatch\n const jitter = preset.jitter\n // Toward the air's velocity, not toward rest: in a wind, \"drag\" is the\n // wind carrying it.\n let vx = this.velocity[i3]!\n let vy = this.velocity[i3 + 1]!\n let vz = this.velocity[i3 + 2]!\n vx += (wx * catchWind - vx) * drag + (r() * 2 - 1) * jitter * dt\n vy += (wy * catchWind - vy) * drag + (preset.lift + (r() * 2 - 1) * jitter) * dt\n vz += (wz * catchWind - vz) * drag + (r() * 2 - 1) * jitter * dt\n this.velocity[i3] = vx\n this.velocity[i3 + 1] = vy\n this.velocity[i3 + 2] = vz\n this.position[i3] = this.position[i3]! + vx * dt\n this.position[i3 + 1] = this.position[i3 + 1]! + vy * dt\n this.position[i3 + 2] = this.position[i3 + 2]! + vz * dt\n this.angle[i] = this.angle[i]! + this.spin[i]! * dt\n }\n }\n\n /**\n * Write every live particle into the target for its blend mode. Returns how\n * many went into each — the draw ranges.\n */\n write(\n additive: ParticleTarget,\n normal: ParticleTarget,\n /**\n * Where flakes go, if they are to be drawn apart from the smoke — ash is\n * a tumbling plane and smoke is a soft sprite, and one blend mode is not\n * one way of drawing. Omitted, flakes go with `normal` as they always did.\n */\n flakes?: ParticleTarget,\n ): { additive: number; normal: number; flakes: number } {\n let a = 0\n let n = 0\n let f = 0\n for (let i = 0; i < this.live; i++) {\n const preset = PRESETS[this.kind[i]!]!\n const flake = flakes !== undefined && preset.shape === 'flake'\n const target = preset.blend === 'additive' ? additive : flake ? flakes : normal\n const k = preset.blend === 'additive' ? a++ : flake ? f++ : n++\n const t = this.age[i]! / this.life[i]!\n const i3 = i * 3\n const k3 = k * 3\n target.position[k3] = this.position[i3]!\n target.position[k3 + 1] = this.position[i3 + 1]!\n target.position[k3 + 2] = this.position[i3 + 2]!\n if (target.velocity) {\n target.velocity[k3] = this.velocity[i3]!\n target.velocity[k3 + 1] = this.velocity[i3 + 1]!\n target.velocity[k3 + 2] = this.velocity[i3 + 2]!\n }\n const [c0, c1] = preset.color\n const fadeIn = Math.min(1, t / 0.1)\n const flicker =\n preset.flicker > 0\n ? 1 - preset.flicker * (0.5 + 0.5 * Math.sin(this.age[i]! * 41 + this.seed[i]! * 97))\n : 1\n const k4 = k * 4\n // One ember in ten dies in the air with a flash — a tiny pop at the end\n // of its life (spec §8.1), which the sound can follow.\n const pop = preset.blend === 'additive' && this.seed[i]! < 0.1 && t > 0.86 ? 2.6 : 1\n target.color[k4] = (c0[0] + (c1[0] - c0[0]) * t) * pop\n target.color[k4 + 1] = (c0[1] + (c1[1] - c0[1]) * t) * pop\n target.color[k4 + 2] = (c0[2] + (c1[2] - c0[2]) * t) * pop\n target.color[k4 + 3] = (preset.alpha[0] + (preset.alpha[1] - preset.alpha[0]) * t) * fadeIn * flicker\n target.extra[k4] = preset.size[0] + (preset.size[1] - preset.size[0]) * t\n target.extra[k4 + 1] = this.angle[i]!\n target.extra[k4 + 2] = preset.shape === 'flake' ? 1 : 0\n target.extra[k4 + 3] = this.seed[i]!\n }\n return { additive: a, normal: n, flakes: f }\n }\n\n /**\n * How many embers popped in the air since the last call, and reset — one\n * `FireSound.pop()` each keeps the sound on the frame the flash is on.\n */\n takePops(): number {\n const n = this.pops\n this.pops = 0\n return n\n }\n\n /** Empty the air — a fresh sheet. */\n clear(): void {\n this.live = 0\n this.perKind.fill(0)\n }\n\n /** Swap the last live particle into slot `i`. Order does not matter; density does. */\n private remove(i: number): void {\n this.perKind[this.kind[i]!] = this.perKind[this.kind[i]!]! - 1\n const last = --this.live\n if (i === last) return\n this.kind[i] = this.kind[last]!\n this.age[i] = this.age[last]!\n this.life[i] = this.life[last]!\n this.angle[i] = this.angle[last]!\n this.spin[i] = this.spin[last]!\n this.seed[i] = this.seed[last]!\n this.position.copyWithin(i * 3, last * 3, last * 3 + 3)\n this.velocity.copyWithin(i * 3, last * 3, last * 3 + 3)\n }\n}\n","import { HEAT, type DamageField } from './field'\nimport type { ParticlePool } from './particles'\n\n/**\n * Where a point of the sheet is in the world, given its UV — `v = 0` at the\n * bottom edge, as the damage grid has it. Null when it cannot say (the sheet\n * has not mounted yet).\n *\n * On `<Paper>` this is `(u, v) => handle.surfacePoint(u, v, scratch)`: the\n * drawn surface this frame, after the cloth and the deformers. A callback and\n * not the handle, because `paperlab/fx` cannot name a type from the main\n * entry without reaching into it — see `fx/boundary.test.ts`.\n */\nexport type SurfaceLocator = (\n u: number,\n v: number,\n) => { readonly x: number; readonly y: number; readonly z: number } | null\n\nexport interface FireEmitterOptions {\n /** Embers a second for every texel on the burn front. */\n embers?: number\n /** Puffs of smoke a second for every texel on the burn front. */\n smoke?: number\n /** The chance a texel that burns through leaves a flake of ash. */\n ash?: number\n /** Seed for which cells are chosen, so a replayed burn throws the same sparks. */\n seed?: number\n /** The most of each kind in the air at once — `fxQualityFor(tier).caps`. Uncapped if omitted. */\n caps?: { ember?: number; smoke?: number; ash?: number }\n}\n\n// Smoke kept light: a clean fire makes little, and a frame full of it hides\n// the burn — more only where burning struggles (see `struggle` below).\n/**\n * How much each kind leaves the front, per texel per second.\n *\n * `smoke` is 0 on purpose. There were three smoke systems running at once —\n * the fire simulator's own, these sprite puffs at 8% alpha, and the smoulder\n * wisp — and the review's note was that only one of them should exist. The\n * simulator's is the one that belongs to a burning sheet: it is the same\n * fluid the flames are made of, so it rises with them instead of beside them.\n * `FxWisps` still carries the thread after the flames are out, which is the\n * one moment the simulator has nothing left to make. The preset stays: it is\n * a parameter set, and anything may still ask the pool for smoke.\n *\n * Embers and ash are both down. Ash at 0.86 a texel was a flake for nearly\n * every one that burnt through — a dust shower. Fewer and larger is the note.\n */\nexport const fireEmitterDefaults: Required<FireEmitterOptions> = {\n embers: 0.35,\n smoke: 0,\n ash: 0.12,\n seed: 7,\n caps: {},\n}\n\n/**\n * The most of each kind one `update` may emit.\n *\n * A caller's `dt` is not a promise: this is public API, and a tab returning\n * from the background can hand it a delta of minutes. Without a ceiling the\n * debt loop below runs once per whole unit of it — which is a frozen page for\n * a shower of particles the pool would immediately throw away, since it only\n * holds `capacity` of them. An infinite delta never left the loop at all.\n */\nconst PER_UPDATE = 24\n\n/**\n * What a burn throws into the air, read off the field that is burning.\n *\n * Nothing here decides where a fire IS — the field does. Embers and smoke\n * leave the front, at a rate set by how long the front is (a fire gets\n * busier as its edge gets longer, the same number its sound follows). Ash\n * leaves exactly the texels that burnt through this frame, so a hole opening\n * is a hole shedding. On a sheet nothing is happening to, `update` reads two\n * zeros and returns.\n */\nexport class FireEmitter {\n private readonly field: DamageField\n private readonly pool: ParticlePool\n private readonly locate: SurfaceLocator\n private readonly o: Required<FireEmitterOptions>\n private emberDebt = 0\n private smokeDebt = 0\n private state: number\n\n constructor(\n field: DamageField,\n pool: ParticlePool,\n locate: SurfaceLocator,\n options: FireEmitterOptions = {},\n ) {\n this.field = field\n this.pool = pool\n this.locate = locate\n this.o = { ...fireEmitterDefaults, ...options }\n this.state = this.o.seed >>> 0 || 1\n }\n\n /** Call once a frame, after the field has stepped. */\n update(dt: number): void {\n const { field, o } = this\n const front = field.frontCount\n const consumed = field.consumedCount\n if (front === 0 && consumed === 0) {\n // A fire that has gone out owes nothing: a debt carried across would\n // put out a burst of sparks the moment the next one caught.\n this.emberDebt = 0\n this.smokeDebt = 0\n return\n }\n\n for (let k = 0; k < consumed; k++) {\n if (this.next() < o.ash) this.emit('ash', field.consumedCells[k]!)\n }\n\n if (front > 0 && dt > 0) {\n // The air the page is blowing, from the same pool the smoke rides.\n const [wx, wy, wz] = this.pool.wind\n const air = Math.hypot(wx, wy, wz)\n // How well it is burning: the mean heat along the front. A clean, hot\n // front makes little smoke; one that is catching, dying or being blown\n // on makes a lot (fire spec §4.9, §8.2 — smoke peaks when burning\n // struggles, never at the peak).\n let heat = 0\n for (let k = 0; k < front; k++) heat += field.data[field.frontCells[k]! * 4 + HEAT]!\n heat /= front\n const struggle = 0.35 + 1.6 * Math.max(0, 0.7 - heat) + air * 0.8\n // Blown on, a fire gets air: the embers flare and more fly (§10.6).\n const oxygen = 1 + air * 1.5\n // Capped rather than trusted — see `PER_UPDATE`. A NaN delta falls\n // through both loops on its own, which is the right answer for it.\n this.emberDebt = Math.min(this.emberDebt + front * o.embers * oxygen * dt, PER_UPDATE)\n this.smokeDebt = Math.min(this.smokeDebt + front * o.smoke * struggle * dt, PER_UPDATE)\n while (this.emberDebt >= 1) {\n this.emberDebt -= 1\n this.emit('ember', field.frontCells[Math.floor(this.next() * front)]!)\n }\n while (this.smokeDebt >= 1) {\n this.smokeDebt -= 1\n this.emit('smoke', field.frontCells[Math.floor(this.next() * front)]!)\n }\n }\n }\n\n private emit(name: 'ember' | 'smoke' | 'ash', cell: number): void {\n const cap = this.o.caps[name]\n if (cap !== undefined && this.pool.countOf(name) >= cap) return\n const size = this.field.size\n const last = size - 1\n // The field's convention: texel x sits at u = x / (size - 1), row 0 at v = 0.\n const at = this.locate((cell % size) / last, ((cell / size) | 0) / last)\n if (!at) return\n if (name === 'ember') {\n // Sparks leave the flames as well as the edge: some start partway up a\n // tongue, a little to one side, and drift off from there.\n const lift = this.next() * this.next() * 0.09\n this.pool.spawn(name, at.x + (this.next() - 0.5) * 0.012, at.y + lift, at.z)\n } else {\n this.pool.spawn(name, at.x, at.y, at.z)\n }\n }\n\n /** xorshift32 — its own stream, so the pool's randomness cannot shift which cells are picked. */\n private next(): number {\n let s = this.state\n s ^= s << 13\n s ^= s >>> 17\n s ^= s << 5\n this.state = s >>> 0\n return this.state / 4294967296\n }\n}\n","import * as THREE from 'three'\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { useEffect, useMemo } from 'react'\nimport type { ParticlePool, ParticleTarget } from './particles'\n\n/**\n * The pool, drawn — three kinds, three ways, because the spec's three\n * particles are three different things (paperlab-fx-fire-spec.md §8).\n *\n * - **Embers** are light, moving fast: quads STRETCHED along their own\n * screen-space velocity into streaks, a bright head and a fading tail,\n * additive and HDR so they bloom. A spark is never a static dot.\n * - **Smoke** is soft sprites broken up by noise — thin, grey-brown, widening\n * as it rises. Camera-facing points: one vertex each is still the right\n * price for something this soft.\n * - **Ash** is paper: thin curled planes that tumble in three dimensions,\n * double-sided, char-dark with pale ash edges, lit by where they face\n * rather than glowing — a few keep a hot edge for their first second.\n *\n * This only draws. The page owns the order — field, then emitters, then the\n * pool's own step — because only the page knows which field belongs to which\n * sheet.\n */\n\nexport interface FxParticlesProps {\n pool: ParticlePool\n}\n\n/** Seconds of motion an ember's streak covers — long enough to read as speed, short enough to stay a spark. */\nconst STREAK = 0.045\n\nconst QUAD_STREAK = new Float32Array([-1, 0, 0, 1, 0, 0, 1, 1, 0, -1, 1, 0])\nconst QUAD_FLAKE = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0])\nconst QUAD_INDEX = [0, 1, 2, 0, 2, 3]\n\nfunction makeTarget(capacity: number): ParticleTarget {\n return {\n position: new Float32Array(capacity * 3),\n color: new Float32Array(capacity * 4),\n extra: new Float32Array(capacity * 4),\n velocity: new Float32Array(capacity * 3),\n }\n}\n\n/** Instanced quads over a target's own arrays — no copy. */\nfunction useInstanced(capacity: number, quad: Float32Array) {\n const layer = useMemo(() => {\n const target = makeTarget(capacity)\n const geometry = new THREE.InstancedBufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(quad, 3))\n geometry.setIndex(QUAD_INDEX)\n geometry.setAttribute('aPos', new THREE.InstancedBufferAttribute(target.position, 3))\n geometry.setAttribute('aVel', new THREE.InstancedBufferAttribute(target.velocity!, 3))\n geometry.setAttribute('aColor', new THREE.InstancedBufferAttribute(target.color, 4))\n geometry.setAttribute('aExtra', new THREE.InstancedBufferAttribute(target.extra, 4))\n geometry.instanceCount = 0\n geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity)\n return { target, geometry }\n }, [capacity, quad])\n useEffect(() => () => layer.geometry.dispose(), [layer])\n return layer\n}\n\n/** Points over a target's arrays, for the smoke. */\nfunction usePoints(capacity: number) {\n const layer = useMemo(() => {\n const target = makeTarget(capacity)\n const geometry = new THREE.BufferGeometry()\n geometry.setAttribute('position', new THREE.BufferAttribute(target.position, 3))\n geometry.setAttribute('pColor', new THREE.BufferAttribute(target.color, 4))\n geometry.setAttribute('pExtra', new THREE.BufferAttribute(target.extra, 4))\n geometry.setDrawRange(0, 0)\n geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity)\n return { target, geometry }\n }, [capacity])\n useEffect(() => () => layer.geometry.dispose(), [layer])\n return layer\n}\n\nconst OUTPUT = /* glsl */ `\n #include <tonemapping_fragment>\n #include <colorspace_fragment>\n`\n\nconst EMBER_VERTEX = /* glsl */ `\nattribute vec3 aPos;\nattribute vec3 aVel;\nattribute vec4 aColor;\nattribute vec4 aExtra; // size, angle, shape, seed\nuniform vec2 uResolution;\nuniform float uStreak;\nvarying vec4 vColor;\nvarying vec2 vQuad;\nvoid main() {\n vec4 head = projectionMatrix * viewMatrix * vec4(aPos, 1.0);\n vec4 tail = projectionMatrix * viewMatrix * vec4(aPos - aVel * uStreak, 1.0);\n vec2 half_ = 0.5 * uResolution;\n vec2 d = head.xy / head.w * half_ - tail.xy / tail.w * half_;\n float len = length(d);\n vec2 dir = len > 1e-3 ? d / len : vec2(0.0, 1.0);\n vec2 across = vec2(-dir.y, dir.x);\n // Diameter in pixels from a size in world units: the projection's own scale.\n float px = max(1.5, aExtra.x * projectionMatrix[1][1] * half_.y / head.w);\n float along = len + px;\n vec2 offset = across * position.x * px * 0.5 + dir * (px * 0.5 - position.y * along);\n gl_Position = head + vec4(offset / half_ * head.w, 0.0, 0.0);\n vColor = aColor;\n vQuad = position.xy;\n}\n`\n\nconst EMBER_FRAGMENT = /* glsl */ `\nvarying vec4 vColor;\nvarying vec2 vQuad;\nvoid main() {\n // A thin hot core with a soft edge, fading to NOTHING behind the head.\n //\n // It used to be flat-topped across (smoothstep from 0.35, so the middle\n // seventy per cent of the width was all at full brightness) and to keep a\n // quarter of that brightness all the way to the end of the quad. Both\n // together drew a wide bar with a squared-off end, which is what the review\n // saw up close. A spark is a point of light smeared by its own motion: it\n // is brightest on its centre line and it runs out.\n float across = abs(vQuad.x);\n float body = exp(-across * across * 7.0);\n float tail = clamp(1.0 - vQuad.y, 0.0, 1.0);\n float m = body * tail * tail;\n if (m <= 0.003) discard;\n gl_FragColor = vec4(vColor.rgb * vColor.a * m, 1.0);\n${OUTPUT}\n}\n`\n\nconst SMOKE_VERTEX = /* glsl */ `\nattribute vec4 pColor;\nattribute vec4 pExtra; // size, angle, shape, seed\nuniform float uScale;\nvarying vec4 vColor;\nvarying vec2 vSpin;\nvarying float vSeed;\nvoid main() {\n vColor = pColor;\n vSpin = vec2(cos(pExtra.y), sin(pExtra.y));\n vSeed = pExtra.w;\n vec4 view = modelViewMatrix * vec4(position, 1.0);\n bool perspective = projectionMatrix[2][3] == -1.0;\n float depth = perspective ? max(0.0001, -view.z) : 1.0;\n gl_PointSize = max(1.0, pExtra.x * uScale / depth);\n gl_Position = projectionMatrix * view;\n}\n`\n\nconst SMOKE_FRAGMENT = /* glsl */ `\nvarying vec4 vColor;\nvarying vec2 vSpin;\nvarying float vSeed;\nfloat smHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat smNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(mix(smHash(i), smHash(i + vec2(1.0, 0.0)), u.x), mix(smHash(i + vec2(0.0, 1.0)), smHash(i + vec2(1.0, 1.0)), u.x), u.y);\n}\nvoid main() {\n vec2 p = gl_PointCoord * 2.0 - 1.0;\n p = vec2(p.x * vSpin.x - p.y * vSpin.y, p.x * vSpin.y + p.y * vSpin.x);\n float r = length(p);\n // Not a disc: a soft body torn by two octaves of noise, different for\n // every puff, so a cloud of them is a texture rather than a pile of dots.\n float n = smNoise(p * 2.2 + vSeed * 19.0) * 0.65 + smNoise(p * 5.1 + vSeed * 7.0) * 0.35;\n float body = (1.0 - smoothstep(0.2, 1.0, r)) * smoothstep(0.25, 0.75, n + 0.25 * (1.0 - r));\n if (body <= 0.0) discard;\n gl_FragColor = vec4(vColor.rgb, vColor.a * body);\n${OUTPUT}\n}\n`\n\nconst FLAKE_VERTEX = /* glsl */ `\nattribute vec3 aPos;\nattribute vec4 aColor;\nattribute vec4 aExtra; // size, angle, shape, seed\nvarying vec2 vQuad;\nvarying vec4 vColor;\nvarying float vLight;\nvarying float vSeed;\nmat3 rotation(vec3 axis, float a) {\n float c = cos(a);\n float s = sin(a);\n float t = 1.0 - c;\n return mat3(\n t * axis.x * axis.x + c, t * axis.x * axis.y + s * axis.z, t * axis.x * axis.z - s * axis.y,\n t * axis.x * axis.y - s * axis.z, t * axis.y * axis.y + c, t * axis.y * axis.z + s * axis.x,\n t * axis.x * axis.z + s * axis.y, t * axis.y * axis.z - s * axis.x, t * axis.z * axis.z + c\n );\n}\nvoid main() {\n float seed = aExtra.w;\n // Its own tumbling axis, from its seed: flakes do not spin in step.\n vec3 axis = normalize(vec3(sin(seed * 40.0), cos(seed * 23.0), 0.4 + sin(seed * 71.0) * 0.5));\n mat3 r = rotation(axis, aExtra.y);\n // A little curl: burnt paper does not lie flat.\n vec3 local = vec3(position.x, position.y, position.x * position.x * 0.45 - 0.2) * aExtra.x * 0.5;\n vec3 world = aPos + r * local;\n vec3 n = r * vec3(0.0, 0.0, 1.0);\n // Lit by which way it faces — double-sided, so either face catches light.\n vLight = 0.25 + 0.75 * abs(dot(n, normalize(vec3(-0.35, 0.8, 0.5))));\n vQuad = position.xy;\n vColor = aColor;\n vSeed = seed;\n gl_Position = projectionMatrix * viewMatrix * vec4(world, 1.0);\n}\n`\n\nconst FLAKE_FRAGMENT = /* glsl */ `\nvarying vec2 vQuad;\nvarying vec4 vColor;\nvarying float vLight;\nvarying float vSeed;\nfloat flHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat flNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(mix(flHash(i), flHash(i + vec2(1.0, 0.0)), u.x), mix(flHash(i + vec2(0.0, 1.0)), flHash(i + vec2(1.0, 1.0)), u.x), u.y);\n}\nvoid main() {\n float r = length(vQuad);\n float a = atan(vQuad.y, vQuad.x);\n // A torn scrap: an outline that wanders, different for every flake.\n float edge = 0.62 + 0.3 * flNoise(vec2(a * 1.6 + vSeed * 31.0, vSeed * 7.0));\n float mask = 1.0 - smoothstep(edge - 0.06, edge, r);\n if (mask <= 0.0) discard;\n // Char in the middle, pale brittle ash at the edge (§8.3) — in patches\n // along it, not all the way round: a flake is a few pixels across, and a\n // continuous pale rim round a dark middle drew it as a hollow ring.\n float rim = smoothstep(edge - 0.32, edge - 0.04, r);\n float patches = smoothstep(0.45, 0.75, flNoise(vec2(a * 2.2 + vSeed * 17.0, vSeed * 13.0)));\n float grain = flNoise(vQuad * 3.0 + vSeed * 23.0);\n vec3 charC = vec3(0.05, 0.043, 0.038) * (0.7 + 0.6 * grain);\n // #A49E9D, the spec's own sample of ash (§8.3), in linear.\n vec3 ashC = vec3(0.372, 0.344, 0.340);\n // Pale ash with char under it, not char with a pale rim.\n //\n // It was the other way round, and the flakes came out near black: at 0.07\n // linear they fell through the hole onto a black stage and read as dust in\n // the void. Burnt paper ash is a LIGHT grey — what makes a flake flash dark\n // and light as it tumbles is vLight, the face it is showing, not its own\n // colour being nearly black to begin with.\n vec3 c = mix(ashC, charC, (1.0 - rim * patches) * 0.45) * vLight;\n // A few carry a hot edge that fades over their first second or so, on ONE\n // arc of it — the side that was burning when it tore off. All the way round\n // drew it as an orange ring.\n float young = clamp((vColor.a - 0.55) / 0.4, 0.0, 1.0);\n float arc = smoothstep(0.25, 0.8, cos(a - vSeed * 40.0));\n float hotEdge = step(vSeed, 0.3) * rim * arc * young;\n c += vec3(2.4, 0.6, 0.08) * hotEdge;\n gl_FragColor = vec4(c, smoothstep(0.0, 0.2, vColor.a) * mask);\n${OUTPUT}\n}\n`\n\nexport function FxParticles({ pool }: FxParticlesProps) {\n const embers = useInstanced(pool.capacity, QUAD_STREAK)\n const flakes = useInstanced(pool.capacity, QUAD_FLAKE)\n const smoke = usePoints(pool.capacity)\n\n const materials = useMemo(\n () => ({\n ember: new THREE.ShaderMaterial({\n vertexShader: EMBER_VERTEX,\n fragmentShader: EMBER_FRAGMENT,\n uniforms: { uResolution: { value: new THREE.Vector2(1, 1) }, uStreak: { value: STREAK } },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending,\n }),\n smoke: new THREE.ShaderMaterial({\n vertexShader: SMOKE_VERTEX,\n fragmentShader: SMOKE_FRAGMENT,\n uniforms: { uScale: { value: 100 } },\n transparent: true,\n depthWrite: false,\n }),\n flake: new THREE.ShaderMaterial({\n vertexShader: FLAKE_VERTEX,\n fragmentShader: FLAKE_FRAGMENT,\n transparent: true,\n depthWrite: false,\n side: THREE.DoubleSide,\n }),\n }),\n [],\n )\n useEffect(\n () => () => {\n materials.ember.dispose()\n materials.smoke.dispose()\n materials.flake.dispose()\n },\n [materials],\n )\n\n const size = useThree((s) => s.size)\n\n useFrame(({ gl, camera }) => {\n const counts = pool.write(embers.target, smoke.target, flakes.target)\n embers.geometry.instanceCount = counts.additive\n flakes.geometry.instanceCount = counts.flakes\n smoke.geometry.setDrawRange(0, counts.normal)\n for (const name of ['aPos', 'aVel', 'aColor', 'aExtra'] as const) {\n ;(embers.geometry.attributes[name] as THREE.BufferAttribute).needsUpdate = counts.additive > 0\n ;(flakes.geometry.attributes[name] as THREE.BufferAttribute).needsUpdate = counts.flakes > 0\n }\n if (counts.normal > 0) {\n for (const name of ['position', 'pColor', 'pExtra'] as const) {\n ;(smoke.geometry.attributes[name] as THREE.BufferAttribute).needsUpdate = true\n }\n }\n const ratio = gl.getPixelRatio()\n ;(materials.ember.uniforms.uResolution!.value as THREE.Vector2).set(\n size.width * ratio,\n size.height * ratio,\n )\n const projection = (camera as THREE.PerspectiveCamera).projectionMatrix.elements[5] ?? 1\n materials.smoke.uniforms.uScale!.value = size.height * ratio * 0.5 * projection\n })\n\n return (\n <>\n <mesh geometry={embers.geometry} material={materials.ember} frustumCulled={false} />\n <points geometry={smoke.geometry} material={materials.smoke} frustumCulled={false} />\n <mesh geometry={flakes.geometry} material={materials.flake} frustumCulled={false} />\n </>\n )\n}\n","import { Suspense, lazy } from 'react'\nimport type { ComponentType } from 'react'\nimport type { DamageField } from './field'\nimport type { SurfaceLocator } from './fire'\nimport type { FxQualityTier } from './quality'\n\n/** The rig's film. Mirrors `FilmName` — which fx cannot import; see `boundary.test.ts`. */\nexport type FxFilm = 'agx' | 'neutral' | 'filmic'\n\nexport interface FxPostProps {\n /**\n * The film the scene's lighting uses. The composer takes the tone curve off\n * the renderer, so the pass has to be told which one to put back. Every\n * built-in preset uses `neutral`.\n */\n film?: FxFilm\n /** How much the pass may cost — bloom is drawn at half resolution on `low`. */\n quality?: FxQualityTier\n /** Bloom strength; 0 keeps the tone curve and drops the bloom. */\n bloom?: number\n /** Bloom threshold in scene luminance. Must stay above paper white. */\n threshold?: number\n /**\n * The burning field, and where its sheet is — for the heat haze above its\n * flames and the frame's warm grade as it grows (§7). Leave both out and\n * the pass is bloom and the tone curve alone.\n */\n field?: DamageField\n locate?: SurfaceLocator\n /** Heat haze in pixels at 1080p, overriding the tier's; 0 turns it off. */\n haze?: number\n}\n\n/**\n * Bloom and the tone curve for a burn, loaded only if they are going to run.\n *\n * Put it inside the same `<Canvas>` as the burning sheet — as a child of\n * `<Paper>`. It is optional in exactly the way stage's print pass is:\n * `@react-three/postprocessing` and `postprocessing` are optional peers, this\n * reaches them through a dynamic import, and without them it renders nothing\n * and says so once. The fire still burns; it just does not glow.\n */\nconst Pass = lazy(() =>\n import('./FxPostPass')\n .then((m) => ({ default: m.FxPostPass as ComponentType<FxPostProps> }))\n .catch((error: unknown) => {\n warnOnce(error)\n return { default: (() => null) as ComponentType<FxPostProps> }\n }),\n)\n\nlet warned = false\n/** Says why, with the real error, rather than blaming two packages that may be installed. */\nfunction warnOnce(cause?: unknown) {\n if (warned) return\n warned = true\n console.warn(\n '[paperlab/fx] Rendering without FxPost — fire will not bloom.\\n' +\n 'It needs two optional peers:\\n' +\n ' npm i @react-three/postprocessing postprocessing\\n' +\n 'Leave <FxPost> out to go without it deliberately and silence this.',\n ...(cause === undefined ? [] : [cause]),\n )\n}\n\n/**\n * `fallback={null}`: the scene behind it is already drawn. One frame without\n * the pass, then one with it.\n */\nexport function FxPost(props: FxPostProps) {\n return (\n <Suspense fallback={null}>\n <Pass {...props} />\n </Suspense>\n )\n}\n","import type { DamageSource } from '../surface/damageContract'\nimport { FIELD_SIZE, HEAT, type DamageField } from './field'\n\nexport interface AfterglowOptions {\n /**\n * How long a bead keeps glowing after the heat under it has gone, in\n * seconds — a range, each cell drawn its own life from it by a seeded hash.\n */\n hold?: readonly [number, number]\n /** Seed for which bead outlives which. */\n seed?: number\n}\n\n/** The most a spent cell may keep glowing: a smoulder, not a fire. */\nconst SMOULDER = 0.3\n/** Below this a bead is out. */\nconst OUT = 0.004\n\n/**\n * The embers a burn leaves behind, drawn over the field it came from.\n *\n * `paperlab-fx-fire-spec.md` §9, \"Smoulder\": after the flames, glowing spots\n * crawl along the edge, flare on a breath, and go out one by one over\n * seconds (§4.10). The field cannot do this, and it is not a tuning problem:\n * a burn blown out loses its heat in a fraction of a second — below ignition\n * everywhere or it recovers — so its own heat channel has nothing left to\n * smoulder with.\n *\n * So this is presentation, and holds no simulation state. It wraps a field as\n * a `DamageSource`: every channel is the field's, except HEAT, which is the\n * field's heat or what each cell remembers of it, whichever is more. When the\n * heat under a cell goes, the cell keeps a smoulder's worth of it and lets it\n * go at its own pace — each cell's pace drawn once from a seeded hash, so\n * beads go out one at a time, and the same burn goes out the same way. A\n * breath flares what is left.\n *\n * Only the sheet reads it. Flames and the fire light read the FIELD, so the\n * flames die with the gas (§4.1) while the edge keeps glowing; the physics\n * reads char, saturation and presence, which pass through untouched.\n */\nexport class Afterglow implements DamageSource {\n readonly size = FIELD_SIZE\n readonly pixels = new Uint8Array(FIELD_SIZE * FIELD_SIZE * 4)\n private readonly field: DamageField\n private readonly glow = new Float32Array(FIELD_SIZE * FIELD_SIZE)\n private readonly rate = new Float32Array(FIELD_SIZE * FIELD_SIZE)\n private revision = 0\n private clock = 0\n private flare = 0\n private lastField = -1\n private lit = false\n\n constructor(field: DamageField, options: AfterglowOptions = {}) {\n this.field = field\n const [shortest, longest] = options.hold ?? [0.6, 2.2]\n const seed = (options.seed ?? 5) >>> 0\n for (let i = 0; i < this.rate.length; i++) {\n // A bead's life, from its own hash: e^(-rate · t) over [0.3 → out] is\n // ln(75) / rate, so these lives land the last bead between ~2 and ~8 s.\n let h = Math.imul(i ^ seed, 2654435761) >>> 0\n h ^= h >>> 15\n h = Math.imul(h, 2246822519) >>> 0\n const life = shortest + (longest - shortest) * ((h >>> 0) / 4294967296)\n this.rate[i] = 1 / life\n }\n this.pixels.set(field.pixels)\n }\n\n get version(): number {\n return this.revision\n }\n\n get detail(): number | undefined {\n return this.field.detail\n }\n\n /** Its own clock: the beads keep flickering after the field has gone to sleep. */\n get time(): number {\n return this.clock\n }\n\n /** Whether anything is still glowing that the field no longer is. */\n get smouldering(): boolean {\n return this.lit\n }\n\n /**\n * Advance by `dt`, after the field has stepped. `blow` is the breath on the\n * sheet, 0..1: it flares what is left (§10.6, \"on smoulder: beads flare,\n * then fade\").\n */\n step(dt: number, blow = 0): void {\n if (!(dt > 0)) return\n this.clock += dt\n this.flare = blow > 0.05 ? Math.min(1, this.flare + dt * 4) : Math.max(0, this.flare - dt * 1.5)\n const data = this.field.data\n const glow = this.glow\n let lit = false\n for (let i = 0; i < glow.length; i++) {\n const heat = data[i * 4 + HEAT]!\n const was = glow[i]!\n let next: number\n if (heat >= was) {\n // Follow the heat up at once.\n next = heat\n } else {\n // Let it go slowly: at most a smoulder's worth, then its own pace.\n next = Math.max(heat, Math.min(was, SMOULDER) * Math.exp(-dt * this.rate[i]!))\n }\n if (next < OUT) next = 0\n glow[i] = next\n if (next > heat + OUT) lit = true\n }\n this.lit = lit\n this.sync()\n }\n\n /** Copy the field in, and lay the remembered heat over its heat channel. */\n private sync(): void {\n const fieldVersion = this.field.version\n if (!this.lit && fieldVersion === this.lastField && this.flare === 0) return\n this.lastField = fieldVersion\n const src = this.field.pixels\n const dst = this.pixels\n dst.set(src)\n const boost = 1 + 0.9 * this.flare\n for (let i = 0; i < this.glow.length; i++) {\n const g = Math.round(Math.min(1, this.glow[i]! * boost) * 255)\n const k = i * 4 + HEAT\n if (g > dst[k]!) dst[k] = g\n }\n this.revision++\n }\n}\n","import type * as THREE from 'three'\nimport { useFrame } from '@react-three/fiber'\nimport { useEffect, useMemo, useRef } from 'react'\nimport type { DamageField } from './field'\nimport type { SurfaceLocator } from './fire'\nimport { flameAnchors, type FlameAnchor } from './flames'\nimport { flameGeometry, flameMaterial } from './flameShader'\nimport { fxQualityFor, type FxQualityTier } from './quality'\n\nexport interface FxFlamesProps {\n /** The burning field. Flames stand on its hot rim — see `flameAnchors`. */\n field: DamageField\n /** Where a point of the sheet is in the world — `(u, v) => handle.surfacePoint(u, v, scratch)`. */\n locate: SurfaceLocator\n /** How many flames may stand at once: 8 / 16 / 32 on low / medium / high. */\n quality?: FxQualityTier\n /**\n * The air, world units a second — pass the particle pool's `wind`, which is\n * read every frame. Flames lean away from it and shorten (§10.6).\n */\n wind?: readonly [number, number, number]\n}\n\n/**\n * The flames of a burn: separate tongues of burning gas, standing just off\n * the char, rising straight up whatever the sheet is doing.\n *\n * `paperlab-fx-fire-spec.md` §6. Each tongue is a quad that turns to face the\n * camera about WORLD up — never the sheet's normal (§13.9) — so a flame on a\n * sheet held flat still rises to the ceiling. The shader is upward-scrolling,\n * domain-warped noise inside a teardrop, coloured on the flame's own\n * blackbody ramp: a dim indigo root, a yellow-white core, orange edges and\n * tips, and a torn top that sheds wisps. Every flame puffs on its own noise\n * at 10–15 Hz, so no two ever move in step and nothing is a sine.\n *\n * Additive and HDR: the core is several times paper white, so `FxPost`\n * blooms it; it writes no depth, so the flames never occlude each other or\n * flicker as they sort. It animates on the FIELD's clock, so a replayed burn\n * burns with the same flames.\n */\nexport function FxFlames({ field, locate, quality = 'medium', wind }: FxFlamesProps) {\n const max = fxQualityFor(quality).flames\n const anchors = useRef<FlameAnchor[]>([])\n\n const geometry = useMemo(() => flameGeometry(max), [max])\n useEffect(() => () => geometry.dispose(), [geometry])\n const material = useMemo(() => flameMaterial(), [])\n useEffect(() => () => material.dispose(), [material])\n\n useFrame(() => {\n const n = flameAnchors(field, locate, max, anchors.current)\n const base = geometry.getAttribute('aBase') as THREE.InstancedBufferAttribute\n const shape = geometry.getAttribute('aShape') as THREE.InstancedBufferAttribute\n // The air leans them, and a strong wind shortens them — the tip is\n // blown away faster than the gas can climb.\n const [wx, wy, wz] = wind ?? [0, 0, 0]\n const gust = Math.hypot(wx, wy, wz)\n const shorten = 1 / (1 + gust * 0.6)\n for (let i = 0; i < n; i++) {\n const a = anchors.current[i]!\n base.array[i * 3] = a.x\n base.array[i * 3 + 1] = a.y\n base.array[i * 3 + 2] = a.z\n shape.array[i * 4] = a.height * shorten\n shape.array[i * 4 + 1] = a.width\n shape.array[i * 4 + 2] = a.seed\n shape.array[i * 4 + 3] = a.heat\n }\n base.needsUpdate = true\n shape.needsUpdate = true\n geometry.instanceCount = n\n material.uniforms.uTime!.value = field.time\n ;(material.uniforms.uLean!.value as THREE.Vector3).set(wx * 0.5, 0, wz * 0.5)\n })\n\n return <mesh geometry={geometry} material={material} frustumCulled={false} renderOrder={2} />\n}\n","import * as THREE from 'three'\n\n/**\n * One flame shader for every flame in paperlab/fx — the tongues on a burning\n * rim (`FxFlames`) and the match in a hand (`FxMatchFlame`) are the same fire,\n * and a match that looked different from what it lit would give the trick\n * away.\n *\n * A quad per flame, turned to the camera about WORLD up, never the sheet's\n * normal. Upward-scrolling, domain-warped noise inside a teardrop, coloured\n * on a dense, hot base: orange-white at the source, yellow above, orange at\n * the edges and tips, translucent as it thins, torn at the top into split\n * tongues, gaps and loose wisps. Each flame puffs on its own\n * value noise at 10–15 Hz (`flamePuff` in flames.ts runs the same function\n * on the CPU for the fire light). Additive, HDR, writes no depth.\n *\n * Per instance: `aBase` (root, world) and `aShape` (height, width, seed,\n * heat). Per material: `uTime`, `uLean` (the air and the motion, world units),\n * `uBlue` (0..1 — a flame starved by moving fast burns blue and dim) and\n * `uGain` (a strike's flare).\n */\n\nconst NOISE = /* glsl */ `\nfloat flHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat flNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(mix(flHash(i), flHash(i + vec2(1.0, 0.0)), u.x), mix(flHash(i + vec2(0.0, 1.0)), flHash(i + vec2(1.0, 1.0)), u.x), u.y);\n}\nfloat flFbm(vec2 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 4; i++) {\n v += a * flNoise(p);\n p = p * 2.03 + 17.0;\n a *= 0.5;\n }\n return v;\n}\nfloat flHash1(float n) { return fract(sin(n) * 43758.5453); }\nfloat flNoise1(float x) {\n float i = floor(x);\n float f = fract(x);\n f = f * f * (3.0 - 2.0 * f);\n return mix(flHash1(i), flHash1(i + 1.0), f);\n}\n`\n\nconst VERTEX = /* glsl */ `\nattribute vec3 aBase;\nattribute vec4 aShape; // height, width, seed, heat\nuniform float uTime;\nuniform vec3 uLean;\nvarying vec2 vUv;\nvarying float vSeed;\nvarying float vHeat;\n${NOISE}\nvoid main() {\n vUv = position.xy;\n vSeed = aShape.z;\n vHeat = aShape.w;\n // The same puff as \\`flamePuff\\` in flames.ts — the fire light reads it too.\n float puff = 0.72 + 0.28 * flNoise1(uTime * 12.5 + aShape.z * 37.0) + 0.12 * (flNoise1(uTime * 23.0 + aShape.z * 11.0) - 0.5);\n float h = aShape.x * puff;\n vec3 up = normalize(vec3(0.0, 1.0, 0.0) + uLean);\n vec3 toCamera = normalize(cameraPosition - aBase);\n vec3 right = normalize(cross(up, toCamera));\n // A curve, not a tilt: the root holds and the tip is carried.\n vec3 p = aBase + toCamera * 0.004 + right * position.x * aShape.y + up * position.y * h\n + uLean * position.y * position.y * h * 0.8;\n gl_Position = projectionMatrix * viewMatrix * vec4(p, 1.0);\n}\n`\n\nconst FRAGMENT = /* glsl */ `\nuniform float uTime;\nuniform float uBlue;\nuniform float uGain;\nvarying vec2 vUv;\nvarying float vSeed;\nvarying float vHeat;\n${NOISE}\nvoid main() {\n // The quad is 1.8× the flame's width, so the turbulence can carry the\n // flame sideways without the quad's own edge cutting it into a bar.\n float x = vUv.x * 1.8;\n float y = vUv.y;\n // Climbing at a speed that itself wanders — per flame, and over time — so\n // the motion never settles into a loop. An offset, not a rate × time: a\n // rate that changes would make the pattern jump.\n float climb = uTime * (2.7 + 0.9 * vSeed) + 1.8 * flNoise1(uTime * 0.37 + vSeed * 9.1);\n // Three layers of turbulence: a slow sway of the whole tongue, a flutter\n // near the tip, and a fine shiver — each warping the next.\n vec2 q = vec2(x * 1.2 + vSeed * 13.0, y * 1.9 - climb);\n float sway = flFbm(q);\n float flutter = flFbm(vec2(x * 2.7 + vSeed * 5.0 + sway * 1.3, y * 4.3 - climb * 1.55));\n float shiver = flNoise(vec2(x * 7.0 + vSeed * 3.0, y * 11.0 - climb * 2.4));\n // Every tongue leans its own way — nothing here is symmetrical.\n float lean = (fract(vSeed * 91.7) - 0.5) * 0.45;\n float xw = x + (sway - 0.5) * 1.35 * y + (flutter - 0.5) * 0.5 * y * y + (shiver - 0.5) * 0.12 * y + lean * y;\n // The teardrop: dense and wide at the source, a point at the top.\n float half_ = 1.0 * pow(max(y, 0.0), 0.3) * pow(max(1.0 - y, 0.0), 1.3);\n // Crisp and dense low down, soft and translucent higher up.\n float body = 1.0 - smoothstep(half_ * mix(0.82, 0.4, y), half_, abs(xw));\n // Tips that split: a hard threshold high up tears the flame into separate\n // tongues and wisps that come loose ...\n float tear = flFbm(vec2(x * 2.4 + vSeed * 7.0, y * 4.2 - climb * 1.7));\n body *= smoothstep(0.0, 0.14, tear + 0.82 - y * 1.1);\n // ... and small gaps open inside the upper body, where the gas is thin.\n float gaps = smoothstep(0.58, 0.72, flFbm(vec2(x * 3.3 - vSeed * 4.0, y * 6.5 - climb * 2.0)));\n body *= 1.0 - gaps * smoothstep(0.3, 0.8, y) * 0.95;\n body *= smoothstep(0.0, 0.04, y);\n // The source is hot and dense: orange-white at the root, yellow above it,\n // orange at the edges and tips, and translucent as it thins.\n vec3 white = vec3(1.0, 0.86, 0.62); // orange-white\n vec3 yellow = vec3(0.98, 0.62, 0.09); // #FDCE54\n vec3 orange = vec3(0.77, 0.2, 0.0); // #E37B04\n // The orange-white is only the very root; above it the flame is saturated\n // yellow, then orange. Mixed broadly it washed the whole tongue pale.\n float base = 1.0 - smoothstep(0.0, 0.26, y);\n float inner = 1.0 - smoothstep(0.0, half_ * 0.5, abs(xw));\n vec3 c = mix(orange, yellow, smoothstep(0.35, 0.85, body * (1.0 - y * 0.85)));\n c = mix(c, white, clamp(base * inner, 0.0, 1.0));\n // Dense at the source, and bright enough there to bloom; thinning above.\n c *= mix(1.0, 4.4, base * (0.45 + 0.55 * inner)) * (0.55 + 0.45 * vHeat) * uGain;\n // Only a match starved of air by moving fast burns blue.\n c = mix(c, vec3(0.027, 0.025, 0.099) * 2.5, uBlue * 0.75);\n float fade = 1.0 - smoothstep(0.55, 1.0, y);\n gl_FragColor = vec4(c * body * fade, 1.0);\n #include <tonemapping_fragment>\n #include <colorspace_fragment>\n}\n`\n\n/** Instanced quads for up to `max` flames. `instanceCount` starts at 0. */\nexport function flameGeometry(max: number): THREE.InstancedBufferGeometry {\n const g = new THREE.InstancedBufferGeometry()\n // A unit quad: x across the tongue, y from root (0) to tip (1).\n g.setAttribute(\n 'position',\n new THREE.BufferAttribute(new Float32Array([-1, 0, 0, 1, 0, 0, 1, 1, 0, -1, 1, 0]), 3),\n )\n g.setIndex([0, 1, 2, 0, 2, 3])\n g.setAttribute('aBase', new THREE.InstancedBufferAttribute(new Float32Array(Math.max(1, max) * 3), 3))\n g.setAttribute('aShape', new THREE.InstancedBufferAttribute(new Float32Array(Math.max(1, max) * 4), 4))\n g.instanceCount = 0\n // Flames go wherever the fire is; a bounding sphere would be recomputed a\n // frame to save a cull that never fires.\n g.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity)\n return g\n}\n\nexport function flameMaterial(): THREE.ShaderMaterial {\n return new THREE.ShaderMaterial({\n vertexShader: VERTEX,\n fragmentShader: FRAGMENT,\n uniforms: {\n uTime: { value: 0 },\n uLean: { value: new THREE.Vector3() },\n uBlue: { value: 0 },\n uGain: { value: 1 },\n },\n transparent: true,\n depthWrite: false,\n blending: THREE.AdditiveBlending,\n side: THREE.DoubleSide,\n })\n}\n","import * as THREE from 'three'\nimport { useFrame } from '@react-three/fiber'\nimport { useEffect, useMemo, useRef } from 'react'\nimport type { Afterglow } from './afterglow'\nimport { FIELD_SIZE, HEAT, PRESENCE, type DamageField } from './field'\nimport type { SurfaceLocator } from './fire'\n\nexport interface FxWispsProps {\n /** What the sheet draws: where the smouldering beads are. */\n glow: Afterglow\n /** The field under it — a wisp rises where the glow outlives the heat. */\n field: DamageField\n locate: SurfaceLocator\n /** The air, read every frame — pass the pool's `wind`. */\n wind?: readonly [number, number, number]\n}\n\n/**\n * How opaque a point of the thread is at `age` seconds: 0 unless it is alive.\n *\n * Its own function because the inline version drew white suns on the paper.\n * It was `alive * ramp * (1 - age / LIFE) ** 1.6`, and for a dead point\n * `1 - age / LIFE` is NEGATIVE — a negative number to a fractional power is\n * NaN, and `0 * NaN` is still NaN. Dead points are collapsed onto a living\n * one precisely so that nothing they carry is ever drawn, but NaN in a vertex\n * attribute only needs one fragment to reach the HDR frame, and bloom then\n * spreads it into a white disc that greys the whole stage. It showed only in\n * the smoulder (the one time wisps are drawn), only while playing, and it was\n * gone with the wisp layer switched off.\n */\nexport function wispAlpha(age: number): number {\n if (!(age >= 0 && age < LIFE)) return 0\n return Math.min(1, age / 0.25) * (1 - age / LIFE) ** 1.6 * 0.42\n}\n\n/** At most this many threads at once (spec §8.2: one or two). */\nconst WISPS = 2\n/** Points along one thread. */\nconst POINTS = 64\n/** How long a point of smoke lives before it has thinned to nothing, in seconds. */\nconst LIFE = 3.4\n/** How fast the thread climbs, world units a second — a few centimetres. */\nconst RISE = 0.075\n/** How far apart two threads' beads must be, in cells (~3 cm). */\nconst APART = 9\n\n/**\n * The smoulder wisp: the shot people screenshot (`Stage_5__cold.png`).\n *\n * Not smoke sprites. A thin, pale thread — a trail of connected points drawn\n * as a ribbon — rising from a glowing bead once the flames are gone, curving\n * in a slow S as it climbs, and lingering a few seconds after the last bead\n * has gone out. One or two at a time.\n *\n * Every point's position is a pure function of where and when it left its\n * bead, on the afterglow's clock, so a scripted burn draws the same wisp\n * every time and nothing here integrates anything that could drift.\n */\nexport function FxWisps({ glow, field, locate, wind }: FxWispsProps) {\n const state = useRef(\n Array.from({ length: WISPS }, (_, w) => ({\n cell: -1,\n phase: w * 3.1 + 0.7,\n // Ring buffer of births: root position and birth time.\n root: new Float32Array(POINTS * 3),\n born: new Float32Array(POINTS).fill(-1e9),\n next: 0,\n owed: 0,\n })),\n )\n const last = useRef(-1)\n\n const geometry = useMemo(() => {\n const g = new THREE.BufferGeometry()\n const vertices = WISPS * POINTS * 2\n g.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices * 3), 3))\n g.setAttribute('aAlpha', new THREE.BufferAttribute(new Float32Array(vertices), 1))\n const index: number[] = []\n for (let w = 0; w < WISPS; w++) {\n for (let i = 0; i < POINTS - 1; i++) {\n const a = (w * POINTS + i) * 2\n index.push(a, a + 1, a + 2, a + 1, a + 3, a + 2)\n }\n }\n g.setIndex(index)\n g.boundingSphere = new THREE.Sphere(new THREE.Vector3(), Infinity)\n return g\n }, [])\n useEffect(() => () => geometry.dispose(), [geometry])\n const material = useMemo(\n () =>\n new THREE.ShaderMaterial({\n vertexShader: /* glsl */ `\n attribute float aAlpha;\n varying float vAlpha;\n void main() {\n vAlpha = aAlpha;\n gl_Position = projectionMatrix * viewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: /* glsl */ `\n varying float vAlpha;\n void main() {\n // Pale grey smoke, thin enough that only its accumulation reads.\n gl_FragColor = vec4(vec3(0.62, 0.61, 0.6), vAlpha);\n #include <tonemapping_fragment>\n #include <colorspace_fragment>\n }\n `,\n transparent: true,\n depthWrite: false,\n side: THREE.DoubleSide,\n }),\n [],\n )\n useEffect(() => () => material.dispose(), [material])\n\n useFrame(({ camera }) => {\n const now = glow.time\n const dt = last.current < 0 ? 0 : Math.min(0.1, Math.max(0, now - last.current))\n last.current = now\n const beads = smoulderingBeads(glow, field, WISPS)\n const [wx, , wz] = wind ?? [0, 0, 0]\n const positions = geometry.getAttribute('position') as THREE.BufferAttribute\n const alphas = geometry.getAttribute('aAlpha') as THREE.BufferAttribute\n const p = new THREE.Vector3()\n const q = new THREE.Vector3()\n const toCamera = new THREE.Vector3()\n const side = new THREE.Vector3()\n const path = new Float32Array(POINTS * 3)\n const age = new Float32Array(POINTS)\n\n for (let w = 0; w < WISPS; w++) {\n const s = state.current[w]!\n // Keep a bead while it glows; take a new one when it goes out — and\n // START THE THREAD OVER when it does.\n //\n // The ring buffer holds where each point was BORN, and those roots\n // belong to the bead it was rising from. Left in place when the wisp\n // moved to another bead, the thread's newest points sat over the new\n // one while its older points were still alive over the old one, and the\n // ribbon joining them is a straight line across the sheet at full\n // alpha. During the smoulder the beads go out one by one, so the wisp\n // re-targets again and again and lays down one line per move: the\n // \"random lines\" that appear late in a burn, fanning across the burnt\n // area from bead to bead. They only showed while PLAYING, because a\n // seek rebuilds the page and never changes bead twice.\n if (!beads.includes(s.cell)) {\n const taken = beads.find((c) => !state.current.some((o) => o !== s && o.cell === c)) ?? -1\n if (taken !== s.cell) {\n s.born.fill(-1e9)\n s.next = 0\n s.owed = 0\n }\n s.cell = taken\n }\n if (s.cell >= 0 && dt > 0) {\n s.owed += dt\n const every = LIFE / POINTS\n while (s.owed >= every) {\n s.owed -= every\n const at = locate(\n (s.cell % FIELD_SIZE) / (FIELD_SIZE - 1),\n ((s.cell / FIELD_SIZE) | 0) / (FIELD_SIZE - 1),\n )\n if (!at) break\n const k = s.next\n s.root[k * 3] = at.x\n s.root[k * 3 + 1] = at.y\n s.root[k * 3 + 2] = at.z + 0.004\n s.born[k] = now\n s.next = (k + 1) % POINTS\n }\n }\n // Points in age order, newest (at the bead) first.\n for (let i = 0; i < POINTS; i++) {\n const k = (s.next - 1 - i + POINTS * 2) % POINTS\n const a = now - s.born[k]!\n age[i] = a\n const bx = s.root[k * 3]!\n const by = s.root[k * 3 + 1]!\n const bz = s.root[k * 3 + 2]!\n // A slow S: value noise in the point's age, drifting with its birth\n // time so the whole thread sways rather than holding one shape.\n const sway = (noise1(a * 0.8 + s.born[k]! * 0.3 + s.phase) - 0.5) * 0.07 * a\n const lean = (noise1(a * 0.5 + s.phase * 2.3) - 0.5) * 0.02 * a\n path[i * 3] = bx + sway + wx * a * a * 0.25\n path[i * 3 + 1] = by + RISE * a * (1 + 0.18 * a)\n path[i * 3 + 2] = bz + lean + wz * a * a * 0.25\n }\n // A point that is not alive — never born, or already gone — is pulled\n // onto the youngest living one, so the ribbon has no area there. Left\n // where its formula put it, an unborn point is a BILLION seconds old\n // and ~10^16 units away; a strip joining it to a live one covered the\n // frame, and the HDR frame's bloom turned that into a black picture.\n let anchor = -1\n for (let i = 0; i < POINTS; i++) {\n if (age[i]! >= 0 && age[i]! < LIFE) {\n anchor = i\n break\n }\n }\n for (let i = 0; i < POINTS; i++) {\n const a = age[i]!\n if (!(a >= 0 && a < LIFE)) {\n // Onto the nearest living point, so the ribbon has no area here.\n // With NOTHING alive there is no such point; collapse onto the root\n // the thread was last born at — finite, and on the sheet. (Not onto\n // point 0's own position: for a dead thread that is a billion\n // seconds of rise away, ~10^16 units.)\n if (anchor >= 0) {\n path[i * 3] = path[anchor * 3]!\n path[i * 3 + 1] = path[anchor * 3 + 1]!\n path[i * 3 + 2] = path[anchor * 3 + 2]!\n } else {\n const r = ((s.next - 1 + POINTS) % POINTS) * 3\n path[i * 3] = s.root[r]!\n path[i * 3 + 1] = s.root[r + 1]!\n path[i * 3 + 2] = s.root[r + 2]!\n }\n } else {\n anchor = i\n }\n }\n for (let i = 0; i < POINTS; i++) {\n const a = age[i]!\n p.set(path[i * 3]!, path[i * 3 + 1]!, path[i * 3 + 2]!)\n const j = Math.min(POINTS - 1, i + 1)\n const h = Math.max(0, i - 1)\n q.set(\n path[j * 3]! - path[h * 3]!,\n path[j * 3 + 1]! - path[h * 3 + 1]!,\n path[j * 3 + 2]! - path[h * 3 + 2]!,\n )\n toCamera.copy(camera.position).sub(p)\n const alive = a >= 0 && a < LIFE\n if (alive || i === 0) {\n side.crossVectors(q, toCamera)\n // A degenerate segment has no side; give it one rather than a NaN.\n if (side.lengthSq() < 1e-20) side.set(1, 0, 0)\n side.normalize()\n }\n // A dead point KEEPS the side of the living point it was collapsed\n // onto. Computing its own gave it the degenerate fallback (1, 0, 0)\n // while its neighbour used the real direction, so the two made a\n // small bow-tie with area — enough to rasterise whatever the dead\n // vertices carried. Same position and same side is no area at all.\n // A thread at the bead, widening a little as it climbs and thins.\n // Clamped, so a dead point's (huge) age cannot widen it past its anchor.\n const half = 0.0011 + 0.0022 * Math.min(1, Math.max(0, a) / LIFE)\n const v = (w * POINTS + i) * 2\n positions.setXYZ(v, p.x - side.x * half, p.y - side.y * half, p.z - side.z * half)\n positions.setXYZ(v + 1, p.x + side.x * half, p.y + side.y * half, p.z + side.z * half)\n const alpha = wispAlpha(a)\n alphas.setX(v, alpha)\n alphas.setX(v + 1, alpha)\n }\n }\n positions.needsUpdate = true\n alphas.needsUpdate = true\n })\n\n return <mesh geometry={geometry} material={material} frustumCulled={false} renderOrder={1} />\n}\n\n/**\n * The beads a wisp may rise from: rim cells where the sheet still glows and\n * the field is cold — smouldering, not burning. Brightest first, and spread\n * `APART` from each other. Deterministic: ties go to the lower cell.\n */\nfunction smoulderingBeads(glow: Afterglow, field: DamageField, max: number): number[] {\n const size = FIELD_SIZE\n const shown = glow.pixels\n const heat = field.pixels\n const found: { cell: number; g: number }[] = []\n for (let y = 1; y < size - 1; y++) {\n for (let x = 1; x < size - 1; x++) {\n const c = y * size + x\n const g = shown[c * 4 + HEAT]!\n if (g < 10 || heat[c * 4 + HEAT]! > 4 || shown[c * 4 + PRESENCE]! < 128) continue\n const rim =\n shown[(c - 1) * 4 + PRESENCE]! < 128 ||\n shown[(c + 1) * 4 + PRESENCE]! < 128 ||\n shown[(c - size) * 4 + PRESENCE]! < 128 ||\n shown[(c + size) * 4 + PRESENCE]! < 128\n if (rim) found.push({ cell: c, g })\n }\n }\n found.sort((a, b) => b.g - a.g || a.cell - b.cell)\n const picked: number[] = []\n for (const f of found) {\n if (picked.length >= max) break\n const fx = f.cell % size\n const fy = (f.cell / size) | 0\n if (picked.every((p) => Math.hypot((p % size) - fx, ((p / size) | 0) - fy) >= APART)) picked.push(f.cell)\n }\n return picked\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","import * as THREE from 'three'\nimport { useFrame, useThree } from '@react-three/fiber'\nimport { type ReactNode, useEffect, useMemo, useRef } from 'react'\nimport {\n FIRE_CONTRAST,\n FIRE_HEAT_SCALE,\n FIRE_OPACITY,\n FIRE_SOOT_SCALE,\n FIRE_THIN,\n type FireZonesInput,\n PAPER_WHITE,\n fireZones,\n hexToLinear,\n} from './emission'\nimport type { DamageField } from './field'\nimport type { SurfaceLocator } from './fire'\nimport { FLAME_HEIGHT, flameAnchors, type FlameAnchor } from './flames'\nimport { FireFluid } from './fluid/FireFluid'\nimport { type FireFluidParams, fireFluidDefaults, solverUniforms } from './fluid/params'\nimport { MAX_SOURCES, RENDER_FRAGMENT, RENDER_VERTEX } from './fluid/passes'\nimport { fxQualityFor, type FxQualityTier } from './quality'\n\nexport interface FxFireFluidProps {\n field: DamageField\n locate: SurfaceLocator\n quality?: FxQualityTier\n /** The simulator's controls; anything left out takes the panel's default. */\n params?: Partial<FireFluidParams>\n /** False freezes the fire where it is — a paused lab. */\n running?: boolean\n /**\n * Change it to start the fire over: the air is emptied and the fire is\n * warmed up from the rim as it stands now, so a burn that was just seeked\n * to a moment shows that moment's flames rather than empty air.\n */\n resetKey?: unknown\n /** How bright the fire glows, against paper white. */\n glow?: number\n /**\n * The flame's four zones — root, core, body and tip — each part laid over\n * `FIRE_ZONES`. See `FireZones` for what each zone is and why it looks the\n * way it does.\n */\n zones?: FireZonesInput\n /** The solver temperature that counts as a flame's hottest gas. */\n heatScale?: number\n /** The soot density that counts as a full flame — see `FIRE_SOOT_SCALE`. */\n sootScale?: number\n /** Gamma on the flame's temperature — above 1 darkens the body against the core. */\n contrast?: number\n /** How opaque the densest flame gas is; 0 is purely additive fire. */\n opacity?: number\n /** How opaque gas must be to glow fully; see `FIRE_THIN`. */\n thin?: number\n /**\n * Seconds of fire run, unseen, whenever it starts over (see `resetKey`).\n * A plume started from still air rolls its leading edge into a mushroom cap\n * — the starting vortex — and a fire that has burned for seconds has long\n * since shed it. Too short and a seeked-to frame shows that cap.\n */\n warm?: number\n /**\n * Error-compensated (MacCormack) advection for what is drawn. Defaults to\n * on everywhere but the `low` tier, where the two extra passes a step are\n * the first thing a throttled phone should give back.\n */\n sharp?: boolean\n /**\n * What to draw where the simulator cannot run (no half-float render\n * targets) — typically `<FxFlames>`. Drawn INSTEAD, never as well.\n */\n fallback?: ReactNode\n}\n\n/** The domain, in world units: a sheet and a half across, room above it for the flames. */\nconst DOMAIN = { width: 1.5, height: 2 } as const\n/** Below the sheet's middle, where the domain starts. */\nconst BELOW = 0.8\n/**\n * Fixed solver step. 1/120: gas leaving the rim at ~0.3 m/s crosses about a\n * velocity cell a step at this rate, which is what semi-Lagrangian advection\n * is accurate for; at 1/60 it crossed two, and smeared.\n */\nconst STEP = 1 / 120\n/** The unseen warm-up after a reset runs at the old rate — it is shed before it is shown. */\nconst WARM_STEP = 1 / 60\n/** Steps a frame may take before the fire falls behind rather than stalling the page. */\nconst MAX_STEPS = 6\n/**\n * Half-width of the band gas comes off, across the rim: ~1.2 mm of A4. The\n * char right behind the ember line, which is where paper gives off its gas.\n */\nconst BAND = 1.2 / 210\n/**\n * Spots of the rim that remember a flame, so they go on smoking after it.\n * Each flame writes itself into one slot (by its seed) every step it burns;\n * a slot not written for a while is a spot whose flame has gone out, and it\n * releases smoke — no fuel — fading over `smokeAfter` seconds.\n */\nconst SMOULDER_SLOTS = 16\n/** Seconds a spot must have had no flame before it counts as out and starts to smoke. */\nconst SMOULDER_AFTER = 1\n/**\n * How long a reset fire is run before it is shown.\n *\n * Was 0.8 s, and that is what drew the HOOKS — tongues curling over at the\n * top like ribbons. A plume started from still air rolls its leading edge\n * into a mushroom cap, the starting vortex, and 0.8 s is not long enough for\n * it to have risen out of frame; a fire that has actually burned for a few\n * seconds shed it long ago. Swept on the peak frame: at 0.8 s two hooks, at\n * 2 s nearly none, at 4 s straight vertical tongues. Turbulence and vorticity\n * were tried first and changed nothing — the hooks were never the flow's, they\n * were the seek's. Only a reset pays this (a seek, or `resetKey` changing).\n */\nconst WARM = 3\n\nconst up = new THREE.Vector3(0, 1, 0)\n\n/**\n * Fire, simulated — the flames and the smoke of a burning sheet as a fluid:\n * gas released along the hot rim, burning where it has oxygen, rising on its\n * own heat, torn by turbulence, cooling out of sight, leaving smoke.\n *\n * The damage field still decides everything about the PAPER; this is only\n * what the air above it does. The rim is read on the CPU as emission points —\n * the same irregular clusters `flameAnchors` stands the sprite flames on, so\n * the fire still gathers in tall tongues above a hole and short licks below\n * it — and nothing is ever read back from the GPU.\n *\n * It lives in a vertical plane through the sheet, facing the camera, so the\n * flames rise straight up whatever the sheet is doing. Needs half-float\n * render targets; where there are none it renders nothing, and the caller\n * falls back to `FxFlames` (see `FireFluid.supported`).\n */\nexport function FxFireFluid({\n field,\n locate,\n quality = 'medium',\n params,\n running = true,\n resetKey,\n glow = 1,\n zones,\n heatScale = FIRE_HEAT_SCALE,\n sootScale = FIRE_SOOT_SCALE,\n contrast = FIRE_CONTRAST,\n opacity = FIRE_OPACITY,\n thin = FIRE_THIN,\n warm = WARM,\n sharp,\n fallback,\n}: FxFireFluidProps) {\n const gl = useThree((s) => s.gl)\n const camera = useThree((s) => s.camera)\n const grid = fxQualityFor(quality).fluid\n const fluid = useMemo(() => (FireFluid.supported(gl) ? new FireFluid(gl, grid, DOMAIN) : null), [gl, grid])\n useEffect(() => () => fluid?.dispose(), [fluid])\n\n const merged = useMemo(() => ({ ...fireFluidDefaults, ...params }), [params])\n const material = useMemo(\n () =>\n new THREE.ShaderMaterial({\n vertexShader: RENDER_VERTEX,\n fragmentShader: RENDER_FRAGMENT,\n uniforms: {\n uA: { value: null },\n uB: { value: null },\n uTime: { value: 0 },\n uGlow: { value: 1 },\n // Thin: smoke over a clear background, not a veil across it.\n uSmokeDensity: { value: 0.9 },\n // How bright the fire is, in the one unit `emission.ts` defines.\n uPaperWhite: { value: PAPER_WHITE },\n uHeatScale: { value: FIRE_HEAT_SCALE },\n uSootScale: { value: FIRE_SOOT_SCALE },\n uContrast: { value: FIRE_CONTRAST },\n uOpacity: { value: FIRE_OPACITY },\n uThin: { value: FIRE_THIN },\n // The four zones (FireZones). Colours linear, glows in multiples of\n // paper white, boundaries in fractions of the hottest gas.\n uTipColor: { value: new THREE.Vector3() },\n uBodyColor: { value: new THREE.Vector3() },\n uCoreColor: { value: new THREE.Vector3() },\n uRootColor: { value: new THREE.Vector3() },\n uTipGlow: { value: 0 },\n uBodyGlow: { value: 0 },\n uCoreGlow: { value: 0 },\n uTipFrom: { value: 0 },\n uTipTo: { value: 0 },\n uCoreFrom: { value: 0 },\n uSoftness: { value: 0 },\n uTearing: { value: 0 },\n uRootAmount: { value: 0 },\n uRootReach: { value: 0 },\n },\n transparent: true,\n depthWrite: false,\n // The fire is always in front of the paper it rises from. A sheet that\n // drapes or curls crosses the plane in places, and a depth test cut\n // the fire off there in a hard vertical seam.\n depthTest: false,\n // Premultiplied: fire adds light, smoke covers what is behind it.\n blending: THREE.CustomBlending,\n blendSrc: THREE.OneFactor,\n blendDst: THREE.OneMinusSrcAlphaFactor,\n side: THREE.DoubleSide,\n }),\n [],\n )\n useEffect(() => () => material.dispose(), [material])\n const plane = useMemo(() => new THREE.PlaneGeometry(DOMAIN.width, DOMAIN.height), [])\n useEffect(() => () => plane.dispose(), [plane])\n const mesh = useRef<THREE.Mesh>(null)\n\n const state = useRef({\n key: Symbol('unset') as unknown,\n time: 0,\n owed: 0,\n /** The fire's clock when the rim last released anything — flame or smoulder. */\n lastGas: Number.NEGATIVE_INFINITY,\n origin: new THREE.Vector3(),\n right: new THREE.Vector3(1, 0, 0),\n normal: new THREE.Vector3(0, 0, 1),\n })\n const anchors = useRef<FlameAnchor[]>([])\n const sources = useMemo(() => new Float32Array(MAX_SOURCES * 4), [])\n const across = useMemo(() => new Float32Array(MAX_SOURCES * 4), [])\n const smoulder = useMemo(\n () =>\n Array.from({ length: SMOULDER_SLOTS }, () => ({\n seen: Number.NEGATIVE_INFINITY,\n source: new Float32Array(4),\n across: new Float32Array(4),\n })),\n [],\n )\n const scratch = useMemo(() => new THREE.Vector3(), [])\n\n /** Where the domain stands: a vertical plane through the sheet, facing the camera. */\n const place = () => {\n const s = state.current\n const centre = locate(0.5, 0.5)\n const c = new THREE.Vector3(centre?.x ?? 0, centre?.y ?? 0, centre?.z ?? 0)\n s.normal.set(camera.position.x - c.x, 0, camera.position.z - c.z)\n if (s.normal.lengthSq() < 1e-8) s.normal.set(0, 0, 1)\n s.normal.normalize()\n s.right.crossVectors(up, s.normal).normalize()\n s.origin\n .copy(c)\n .addScaledVector(s.right, -DOMAIN.width / 2)\n .addScaledVector(up, -BELOW)\n const m = mesh.current\n if (m) {\n m.position\n .copy(s.origin)\n .addScaledVector(s.right, DOMAIN.width / 2)\n .addScaledVector(up, DOMAIN.height / 2)\n // A little toward the viewer, so the flames stand in front of the paper.\n m.position.addScaledVector(s.normal, 0.03)\n m.quaternion.setFromRotationMatrix(new THREE.Matrix4().makeBasis(s.right, up, s.normal))\n }\n }\n\n /**\n * The rim, as short lines of emission in the domain's UV: one per flame,\n * as long as its tongue is wide, a band ~1.2 mm across, laid just onto the\n * paper side of the edge. Lengths are in the pass's aspect-corrected space,\n * where a world length is `length / DOMAIN.height`.\n */\n const gather = (time: number): number => {\n const s = state.current\n const room = MAX_SOURCES - SMOULDER_SLOTS\n const n = Math.min(room, flameAnchors(field, locate, room, anchors.current, time))\n const band = BAND / DOMAIN.height\n for (let i = 0; i < n; i++) {\n const a = anchors.current[i]!\n scratch.set(a.x, a.y, a.z).sub(s.origin)\n const k = i * 4\n sources[k] = scratch.dot(s.right) / DOMAIN.width\n sources[k + 1] = scratch.dot(up) / DOMAIN.height\n // Which way the paper lies, in the plane the fire is drawn in.\n const nu = a.nx * s.right.x + a.ny * s.right.y + a.nz * s.right.z\n const nv = a.ny\n const nl = Math.hypot(nu, nv)\n let area: number\n if (nl > 0.25) {\n const half = Math.max(1.5 / 210, a.width * 0.5) / DOMAIN.height\n sources[k + 2] = half\n across[k] = nu / nl\n across[k + 1] = nv / nl\n across[k + 2] = band\n across[k + 3] = band\n area = half * band\n } else {\n // A rim seen end-on has no direction in this plane: a small disc.\n sources[k + 2] = band * 2\n across[k] = 0\n across[k + 1] = 1\n across[k + 2] = band * 2\n across[k + 3] = 0\n area = band * band * 4\n }\n // Tall clusters release more gas than short licks: per length of rim,\n // in proportion to the height. It was in proportion to height × width²\n // — the disc's area — and since width follows height that is height³:\n // a lower-rim lick at a third of the height got a thirtieth of the gas,\n // never grew enough soot to show, and the ring lost its short flames.\n const disc = Math.max(0.0025, (a.width * 0.6) / DOMAIN.height)\n const tallest = (FLAME_HEIGHT[1] * 0.47 * 0.6) / DOMAIN.height\n sources[k + 3] =\n (a.height / FLAME_HEIGHT[1]) * (0.5 + 0.5 * a.heat) * Math.min(30, (disc * tallest) / area)\n // This spot is burning: remember it, so it smokes once it stops.\n const slot = smoulder[Math.floor(a.seed * SMOULDER_SLOTS) % SMOULDER_SLOTS]!\n slot.seen = time\n slot.source.set(sources.subarray(k, k + 4))\n slot.across.set(across.subarray(k, k + 4))\n }\n // Spots whose flame has gone out, smoking and fading — flagged by a\n // negative strength, which the emission pass turns into smoke alone.\n const after = Math.max(0, merged.smokeAfter ?? 0)\n let count = n\n for (const slot of smoulder) {\n const since = time - slot.seen\n // Only once the flame has really gone. Flames hop along the rim every\n // ~0.7 s, so a slot left unwritten for a step or two is a flame that\n // moved, not one that went out — and smoking at once from every such\n // slot poured a grey cloud over the sheet while it was still burning.\n if (!(since > SMOULDER_AFTER) || after <= 0 || since > after * 3) continue\n const k = count * 4\n sources.set(slot.source, k)\n across.set(slot.across, k)\n const onset = Math.min(1, (since - SMOULDER_AFTER) / 0.6)\n sources[k + 3] = -slot.source[3]! * onset * Math.exp(-since / after)\n count++\n }\n return count\n }\n\n useFrame((_, delta) => {\n if (!fluid) return\n const s = state.current\n const u = solverUniforms(merged)\n fluid.sharp = sharp ?? quality !== 'low'\n if (s.key !== resetKey) {\n s.key = resetKey\n place()\n fluid.reset(u.ambient)\n for (const slot of smoulder) slot.seen = Number.NEGATIVE_INFINITY\n const end = field.time\n // Warm up from the rim as it stands, on the burn's own clock, so the\n // same moment of the same burn draws the same fire. Not when nothing is\n // burning: the warm-up replays THIS field at earlier times, so with no\n // front it would run 180 steps releasing nothing — which /hands paid on\n // every fresh sheet.\n if (field.frontCount > 0) {\n const steps = Math.round(warm / WARM_STEP)\n for (let k = 0; k < steps; k++) {\n const t = end - warm + k * WARM_STEP\n fluid.step(WARM_STEP, u, sources, across, gather(t), t)\n }\n s.lastGas = end\n } else {\n s.lastGas = Number.NEGATIVE_INFINITY\n }\n s.time = end\n s.owed = 0\n }\n // Idle: nothing burning, and nothing released for long enough that the\n // smoke left is under 1% (five of its lifetimes). Then the fire costs\n // nothing — no solver steps, no plane drawn. It used to step every frame\n // from mount, so a page that mounts the fire before anything burns (/hands\n // does, from the first frame) ran an empty simulation for as long as it\n // was open: on a GPU a waste, and on CI's CPU-drawn WebGL the reason the\n // Hands check went from under 5 minutes to over 20. A new front wakes it.\n const idle = field.frontCount === 0 && s.time - s.lastGas > Math.max(2, merged.smokeFade * 5)\n const m = mesh.current\n if (m) m.visible = !idle\n if (running && idle) {\n s.owed = 0\n s.time += Math.min(0.1, Math.max(0, delta))\n } else if (running) {\n s.owed += Math.min(0.1, Math.max(0, delta))\n let taken = 0\n while (s.owed >= STEP && taken < MAX_STEPS) {\n s.owed -= STEP\n s.time += STEP\n const count = gather(field.time)\n fluid.step(STEP, u, sources, across, count, s.time)\n if (count > 0) s.lastGas = s.time\n taken++\n }\n }\n material.uniforms.uA!.value = fluid.scalars.read.texture\n material.uniforms.uB!.value = fluid.air.read.texture\n material.uniforms.uTime!.value = s.time\n material.uniforms.uGlow!.value = glow\n material.uniforms.uHeatScale!.value = heatScale\n material.uniforms.uSootScale!.value = sootScale\n material.uniforms.uContrast!.value = contrast\n material.uniforms.uOpacity!.value = opacity\n material.uniforms.uThin!.value = thin\n // Read every frame, so a zone tuned in the lab shows at once, paused or not.\n const z = fireZones(zones)\n const mu = material.uniforms\n ;(mu.uTipColor!.value as THREE.Vector3).fromArray(hexToLinear(z.tip.color))\n ;(mu.uBodyColor!.value as THREE.Vector3).fromArray(hexToLinear(z.body.color))\n ;(mu.uCoreColor!.value as THREE.Vector3).fromArray(hexToLinear(z.core.color))\n ;(mu.uRootColor!.value as THREE.Vector3).fromArray(hexToLinear(z.root.color))\n mu.uTipGlow!.value = z.tip.glow\n mu.uBodyGlow!.value = z.body.glow\n mu.uCoreGlow!.value = z.core.glow\n mu.uTipFrom!.value = z.tip.from\n mu.uTipTo!.value = z.tip.to\n mu.uCoreFrom!.value = z.core.from\n mu.uSoftness!.value = z.tip.softness\n mu.uTearing!.value = z.tip.tearing\n mu.uRootAmount!.value = z.root.amount\n mu.uRootReach!.value = z.root.reach\n })\n\n if (!fluid) return <>{fallback ?? null}</>\n return <mesh ref={mesh} geometry={plane} material={material} frustumCulled={false} renderOrder={2} />\n}\n","import * as THREE from 'three'\nimport type { SolverUniforms } from './params'\nimport {\n ADVECT,\n CURL,\n DIVERGENCE,\n EMIT,\n FILL,\n FORCES,\n GRADIENT,\n MACCORMACK,\n MAX_SOURCES,\n PASS_VERTEX,\n PRESSURE,\n REACT,\n VORTICITY,\n} from './passes'\n\n/** Grid sizes for one tier. Both grids share the domain's aspect, so cells are square. */\nexport interface FluidGrid {\n /** Velocity and pressure — coarse, because they are smooth. */\n velocity: readonly [number, number]\n /** Fuel, heat, smoke, flame and air — fine, because that is what is drawn. */\n dye: readonly [number, number]\n /** Jacobi iterations for the pressure solve. */\n iterations: number\n}\n\n/** A pair of targets, read one and write the other, swapped after each pass. */\nclass Pair {\n read: THREE.WebGLRenderTarget\n write: THREE.WebGLRenderTarget\n constructor(w: number, h: number) {\n this.read = target(w, h)\n this.write = target(w, h)\n }\n swap(): void {\n const t = this.read\n this.read = this.write\n this.write = t\n }\n dispose(): void {\n this.read.dispose()\n this.write.dispose()\n }\n}\n\nfunction target(w: number, h: number): THREE.WebGLRenderTarget {\n return new THREE.WebGLRenderTarget(w, h, {\n type: THREE.HalfFloatType,\n format: THREE.RGBAFormat,\n minFilter: THREE.LinearFilter,\n magFilter: THREE.LinearFilter,\n wrapS: THREE.ClampToEdgeWrapping,\n wrapT: THREE.ClampToEdgeWrapping,\n depthBuffer: false,\n stencilBuffer: false,\n generateMipmaps: false,\n })\n}\n\nfunction pass(fragmentShader: string, uniforms: Record<string, THREE.IUniform>): THREE.ShaderMaterial {\n return new THREE.ShaderMaterial({\n vertexShader: PASS_VERTEX,\n fragmentShader,\n uniforms,\n depthTest: false,\n depthWrite: false,\n blending: THREE.NoBlending,\n })\n}\n\n/**\n * A fire, simulated on the GPU: a 2D grid fluid with combustion.\n *\n * Owns its render targets and the passes over them, and nothing about where\n * the fire is drawn — `FxFireFluid` places it. Every step reads the burning\n * rim as a list of emission points (position, radius, strength) the caller\n * works out on the CPU from the damage field, so nothing is ever read back\n * from the GPU.\n */\nexport class FireFluid {\n /** (fuel, heat, smoke, soot) on the fine grid — what is drawn. */\n readonly scalars: Pair\n /** (premixed oxygen, ambient oxygen, burn rate, flame heat) on the fine grid. */\n readonly air: Pair\n readonly velocity: Pair\n readonly pressure: Pair\n /** The rim's emission this step, on the fine grid — see EMIT. */\n private readonly emit: THREE.WebGLRenderTarget\n private readonly divergence: THREE.WebGLRenderTarget\n private readonly curl: THREE.WebGLRenderTarget\n /** The two intermediate advections the MacCormack step compares. */\n private readonly forward: THREE.WebGLRenderTarget\n private readonly backward: THREE.WebGLRenderTarget\n /**\n * Error-compensated advection for the drawn fields. On by default; off is\n * a single semi-Lagrangian step, which is two passes a step cheaper and\n * visibly softer — a knob for the lowest tier, and for an A/B.\n */\n sharp = true\n private readonly scene = new THREE.Scene()\n private readonly camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)\n private readonly mesh: THREE.Mesh\n private readonly m: Record<string, THREE.ShaderMaterial>\n private readonly sources: THREE.Vector4[] = Array.from({ length: MAX_SOURCES }, () => new THREE.Vector4())\n private readonly across: THREE.Vector4[] = Array.from({ length: MAX_SOURCES }, () => new THREE.Vector4())\n\n /** Whether this renderer can draw into half-float targets at all. */\n static supported(renderer: THREE.WebGLRenderer): boolean {\n return (\n renderer.capabilities.isWebGL2 &&\n (renderer.extensions.has('EXT_color_buffer_float') ||\n renderer.extensions.has('EXT_color_buffer_half_float'))\n )\n }\n\n constructor(\n private readonly renderer: THREE.WebGLRenderer,\n readonly grid: FluidGrid,\n /** The domain's size in world units — width, height. */\n readonly domain: { width: number; height: number },\n ) {\n const [vw, vh] = grid.velocity\n const [dw, dh] = grid.dye\n this.scalars = new Pair(dw, dh)\n this.air = new Pair(dw, dh)\n this.velocity = new Pair(vw, vh)\n this.pressure = new Pair(vw, vh)\n this.emit = target(dw, dh)\n this.divergence = target(vw, vh)\n this.curl = target(vw, vh)\n this.forward = target(dw, dh)\n this.backward = target(dw, dh)\n\n const aspect = domain.width / domain.height\n const vTexel = new THREE.Vector2(1 / vw, 1 / vh)\n const dTexel = new THREE.Vector2(1 / dw, 1 / dh)\n const cell = domain.width / vw\n this.m = {\n emit: pass(EMIT, {\n uSources: { value: this.sources },\n uAcross: { value: this.across },\n uCount: { value: 0 },\n uAspect: { value: aspect },\n }),\n advect: pass(ADVECT, {\n uVelocity: { value: null },\n uSource: { value: null },\n uDomain: { value: new THREE.Vector2(domain.width, domain.height) },\n uDt: { value: 0 },\n uKeep: { value: new THREE.Vector4(1, 1, 1, 1) },\n }),\n react: pass(REACT, {\n uA: { value: null },\n uB: { value: null },\n uEmit: { value: this.emit.texture },\n uVelocity: { value: null },\n uDomain: { value: new THREE.Vector2(domain.width, domain.height) },\n uTexel: { value: dTexel },\n uOut: { value: 0 },\n uDt: { value: 0 },\n uFuel: { value: 0 },\n uHeat: { value: 0 },\n uSmoke: { value: 0 },\n uPremixed: { value: 0 },\n uAmbient: { value: 0 },\n uBurnRate: { value: 0 },\n uHeatRelease: { value: 0 },\n uCooling: { value: 0 },\n uSmokeProduction: { value: 0 },\n uSmokeFade: { value: 1 },\n uPersistence: { value: 0.1 },\n uMixing: { value: 0 },\n uEntrain: { value: 0 },\n uFuelBlock: { value: 1 },\n uSootYield: { value: 0 },\n uSootHeat: { value: 1 },\n uStoich: { value: 1 },\n uSmoulderSmoke: { value: 0 },\n uSmoulderHeat: { value: 0 },\n }),\n forces: pass(FORCES, {\n uVelocity: { value: null },\n uA: { value: null },\n uEmit: { value: this.emit.texture },\n uTexel: { value: vTexel },\n uDomain: { value: new THREE.Vector2(domain.width, domain.height) },\n uDt: { value: 0 },\n uTime: { value: 0 },\n uBuoyancy: { value: 0 },\n uWind: { value: 0 },\n uTurbulence: { value: 0 },\n uTurbScale: { value: 1 },\n uTurbEvolve: { value: 0 },\n uRadial: { value: 0 },\n uInitVel: { value: new THREE.Vector2() },\n }),\n curl: pass(CURL, { uVelocity: { value: null }, uTexel: { value: vTexel }, uCell: { value: cell } }),\n vorticity: pass(VORTICITY, {\n uVelocity: { value: null },\n uCurl: { value: null },\n uTexel: { value: vTexel },\n uCell: { value: cell },\n uDt: { value: 0 },\n uVorticity: { value: 0 },\n }),\n divergence: pass(DIVERGENCE, {\n uVelocity: { value: null },\n uB: { value: null },\n uTexel: { value: vTexel },\n uCell: { value: cell },\n uExpansion: { value: 0 },\n }),\n pressure: pass(PRESSURE, {\n uPressure: { value: null },\n uDivergence: { value: null },\n uTexel: { value: vTexel },\n uCell: { value: cell },\n }),\n gradient: pass(GRADIENT, {\n uVelocity: { value: null },\n uPressure: { value: null },\n uTexel: { value: vTexel },\n uCell: { value: cell },\n }),\n fill: pass(FILL, { uValue: { value: new THREE.Vector4() } }),\n maccormack: pass(MACCORMACK, {\n uVelocity: { value: null },\n uSource: { value: null },\n uForward: { value: null },\n uBackward: { value: null },\n uDomain: { value: new THREE.Vector2(domain.width, domain.height) },\n uTexel: { value: new THREE.Vector2(1 / dw, 1 / dh) },\n uDt: { value: 0 },\n }),\n }\n const triangle = new THREE.BufferGeometry()\n triangle.setAttribute(\n 'position',\n new THREE.BufferAttribute(new Float32Array([-1, -1, 0, 3, -1, 0, -1, 3, 0]), 3),\n )\n this.mesh = new THREE.Mesh(triangle, this.m.fill)\n this.mesh.frustumCulled = false\n this.scene.add(this.mesh)\n }\n\n /** Empty air, at the given ambient oxygen. */\n reset(ambient: number): void {\n this.withRenderer(() => {\n const fill = this.m.fill!\n const value = fill.uniforms.uValue!.value as THREE.Vector4\n value.set(0, 0, 0, 0)\n for (const t of [this.scalars, this.velocity, this.pressure]) {\n this.run(fill, t.read)\n this.run(fill, t.write)\n }\n this.run(fill, this.divergence)\n this.run(fill, this.curl)\n this.run(fill, this.emit)\n value.set(0, ambient, 0, 0)\n this.run(fill, this.air.read)\n this.run(fill, this.air.write)\n })\n }\n\n /**\n * One step of `dt` seconds. `sources` is (u, v, half-length, strength) per\n * rim segment and `across` is (toward-paper x, y, half-width, offset onto\n * the paper) — see EMIT — `count` of each; `time` drives the turbulence.\n */\n step(\n dt: number,\n u: SolverUniforms,\n sources: Float32Array,\n across: Float32Array,\n count: number,\n time: number,\n ): void {\n const n = Math.min(MAX_SOURCES, count)\n for (let i = 0; i < n; i++) {\n this.sources[i]!.fromArray(sources, i * 4)\n this.across[i]!.fromArray(across, i * 4)\n }\n const m = this.m\n m.emit!.uniforms.uCount!.value = n\n\n this.withRenderer(() => {\n // 0. Where the rim releases gas this step, once, for every pass after.\n this.run(m.emit!, this.emit)\n\n // 1. Carry everything along the flow.\n const advect = m.advect!\n advect.uniforms.uDt!.value = dt\n advect.uniforms.uVelocity!.value = this.velocity.read.texture\n ;(advect.uniforms.uKeep!.value as THREE.Vector4).set(0.998, 0.998, 1, 1)\n advect.uniforms.uSource!.value = this.velocity.read.texture\n this.run(advect, this.velocity.write)\n this.velocity.swap()\n advect.uniforms.uVelocity!.value = this.velocity.read.texture\n ;(advect.uniforms.uKeep!.value as THREE.Vector4).set(1, 1, 1, 1)\n if (this.sharp) {\n // What is drawn, advected with its own error given back — see\n // MACCORMACK. Forward, back again, then the corrected, clamped result.\n advect.uniforms.uSource!.value = this.scalars.read.texture\n this.run(advect, this.forward)\n advect.uniforms.uDt!.value = -dt\n advect.uniforms.uSource!.value = this.forward.texture\n this.run(advect, this.backward)\n advect.uniforms.uDt!.value = dt\n const mc = m.maccormack!\n mc.uniforms.uDt!.value = dt\n mc.uniforms.uVelocity!.value = this.velocity.read.texture\n mc.uniforms.uSource!.value = this.scalars.read.texture\n mc.uniforms.uForward!.value = this.forward.texture\n mc.uniforms.uBackward!.value = this.backward.texture\n this.run(mc, this.scalars.write)\n } else {\n advect.uniforms.uSource!.value = this.scalars.read.texture\n this.run(advect, this.scalars.write)\n }\n this.scalars.swap()\n advect.uniforms.uSource!.value = this.air.read.texture\n this.run(advect, this.air.write)\n this.air.swap()\n\n // 2. Release gas at the rim, and burn what has oxygen.\n const react = m.react!\n const r = react.uniforms\n r.uDt!.value = dt\n r.uFuel!.value = u.fuel\n r.uHeat!.value = u.heat\n r.uSmoke!.value = u.smoke\n r.uPremixed!.value = u.premixed\n r.uAmbient!.value = u.ambient\n r.uBurnRate!.value = u.burnRate\n r.uHeatRelease!.value = u.heatRelease\n r.uCooling!.value = u.cooling\n r.uSmokeProduction!.value = u.smokeProduction\n r.uSmokeFade!.value = u.smokeFade\n r.uPersistence!.value = u.persistence\n r.uMixing!.value = u.mixing\n r.uEntrain!.value = u.entrain\n r.uFuelBlock!.value = u.fuelBlock\n r.uSootYield!.value = u.sootYield\n r.uSootHeat!.value = u.sootHeat\n r.uStoich!.value = u.stoich\n r.uSmoulderSmoke!.value = u.smoulderSmoke\n r.uSmoulderHeat!.value = u.smoulderHeat\n r.uVelocity!.value = this.velocity.read.texture\n r.uA!.value = this.scalars.read.texture\n r.uB!.value = this.air.read.texture\n r.uOut!.value = 0\n this.run(react, this.scalars.write)\n r.uOut!.value = 1\n this.run(react, this.air.write)\n this.scalars.swap()\n this.air.swap()\n\n // 3. Forces: buoyancy, wind, turbulence, the gas's own launch.\n const forces = m.forces!\n const f = forces.uniforms\n f.uVelocity!.value = this.velocity.read.texture\n f.uA!.value = this.scalars.read.texture\n f.uDt!.value = dt\n f.uTime!.value = time\n f.uBuoyancy!.value = u.buoyancy\n f.uWind!.value = u.wind\n f.uTurbulence!.value = u.turbulence\n f.uTurbScale!.value = u.turbulenceScale\n f.uTurbEvolve!.value = u.turbulenceEvolve\n f.uRadial!.value = u.radial\n ;(f.uInitVel!.value as THREE.Vector2).set(u.initialVelocity[0], u.initialVelocity[1])\n this.run(forces, this.velocity.write)\n this.velocity.swap()\n\n // 4. Vorticity confinement.\n m.curl!.uniforms.uVelocity!.value = this.velocity.read.texture\n this.run(m.curl!, this.curl)\n const vort = m.vorticity!\n vort.uniforms.uVelocity!.value = this.velocity.read.texture\n vort.uniforms.uCurl!.value = this.curl.texture\n vort.uniforms.uDt!.value = dt\n vort.uniforms.uVorticity!.value = u.vorticity\n this.run(vort, this.velocity.write)\n this.velocity.swap()\n\n // 5. Keep the air from compressing — except where burning expands it.\n const div = m.divergence!\n div.uniforms.uVelocity!.value = this.velocity.read.texture\n div.uniforms.uB!.value = this.air.read.texture\n div.uniforms.uExpansion!.value = u.expansion\n this.run(div, this.divergence)\n const pressure = m.pressure!\n pressure.uniforms.uDivergence!.value = this.divergence.texture\n for (let k = 0; k < this.grid.iterations; k++) {\n pressure.uniforms.uPressure!.value = this.pressure.read.texture\n this.run(pressure, this.pressure.write)\n this.pressure.swap()\n }\n const gradient = m.gradient!\n gradient.uniforms.uVelocity!.value = this.velocity.read.texture\n gradient.uniforms.uPressure!.value = this.pressure.read.texture\n this.run(gradient, this.velocity.write)\n this.velocity.swap()\n })\n }\n\n dispose(): void {\n for (const p of [this.scalars, this.air, this.velocity, this.pressure]) p.dispose()\n this.emit.dispose()\n this.divergence.dispose()\n this.curl.dispose()\n this.forward.dispose()\n this.backward.dispose()\n for (const m of Object.values(this.m)) m.dispose()\n this.mesh.geometry.dispose()\n }\n\n private run(material: THREE.ShaderMaterial, out: THREE.WebGLRenderTarget): void {\n this.mesh.material = material\n this.renderer.setRenderTarget(out)\n this.renderer.render(this.scene, this.camera)\n }\n\n /** Passes change the renderer's target and clearing; put both back. */\n private withRenderer(body: () => void): void {\n const previous = this.renderer.getRenderTarget()\n const autoClear = this.renderer.autoClear\n this.renderer.autoClear = false\n try {\n body()\n } finally {\n this.renderer.setRenderTarget(previous)\n this.renderer.autoClear = autoClear\n }\n }\n}\n","/**\n * The fire solver's passes, as GLSL. A grid fluid of the kind real-time fire\n * has been built on since Stam's stable fluids: semi-Lagrangian advection, a\n * Jacobi pressure solve that keeps the air from compressing, buoyancy from\n * temperature, curl-noise turbulence and vorticity confinement for the small\n * eddies that make fire restless — and a combustion step that burns fuel\n * only where AIR HAS REACHED IT, turning it into heat, soot and smoke.\n *\n * That last clause is the whole difference between a flame and a plume of hot\n * smoke, and the first version of this file got it wrong: it refilled every\n * cell toward ambient oxygen, inside the flame as much as outside it, so fuel\n * burnt within a twelfth of a second of leaving the paper wherever it was. A\n * flame is the surface where fuel meets air. The fuel inside it cannot burn\n * until air has worked its way in from the sides, so the fuel core is eaten\n * from the outside and closes to a point — the TIP — at the height where the\n * last of it meets air. Nothing else gives a tongue that shape.\n *\n * Every pass is a full-screen triangle over one field. Velocity and pressure\n * live on a coarser grid; fuel, heat, smoke, soot and air on a finer one,\n * sampling the velocity bilinearly.\n */\n\n/** Maximum emission points the passes loop over. */\nexport const MAX_SOURCES = 64\n\nexport const PASS_VERTEX = /* glsl */ `\nvarying vec2 vUv;\nvoid main() {\n vUv = position.xy * 0.5 + 0.5;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`\n\n/**\n * Hashes with no \\`sin\\` in them (Dave Hoskins' \"hash without sine\").\n *\n * The classic \\`fract(sin(dot(p, k)) * 43758.5)\\` asks \\`sin\\` for an answer\n * to many digits at arguments in the tens of thousands, and every GPU gives a\n * different one: the same fire measured on Metal and on SwiftShader differed\n * in 13% of its pixels, and — because only the hottest percent of a flame\n * clears the bloom threshold — bloomed 1.47% of the frame on one and 0.02% on\n * the other. A fire has to look the same on every device.\n */\nconst NOISE = /* glsl */ `\nfloat fxHash(vec2 p) {\n vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n p3 += dot(p3, p3.yzx + 33.33);\n return fract((p3.x + p3.y) * p3.z);\n}\nfloat fxNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(mix(fxHash(i), fxHash(i + vec2(1.0, 0.0)), u.x), mix(fxHash(i + vec2(0.0, 1.0)), fxHash(i + vec2(1.0, 1.0)), u.x), u.y);\n}\nfloat fxFbm(vec2 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 3; i++) {\n v += a * fxNoise(p);\n p = p * 2.07 + 13.0;\n a *= 0.5;\n }\n return v;\n}\nfloat fxHash3(vec3 p) {\n vec3 p3 = fract(p * 0.1031);\n p3 += dot(p3, p3.zyx + 31.32);\n return fract((p3.x + p3.y) * p3.z);\n}\nfloat fxNoise3(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n vec3 u = f * f * (3.0 - 2.0 * f);\n float a = mix(mix(fxHash3(i), fxHash3(i + vec3(1.0, 0.0, 0.0)), u.x), mix(fxHash3(i + vec3(0.0, 1.0, 0.0)), fxHash3(i + vec3(1.0, 1.0, 0.0)), u.x), u.y);\n float b = mix(mix(fxHash3(i + vec3(0.0, 0.0, 1.0)), fxHash3(i + vec3(1.0, 0.0, 1.0)), u.x), mix(fxHash3(i + vec3(0.0, 1.0, 1.0)), fxHash3(i + vec3(1.0, 1.0, 1.0)), u.x), u.y);\n return mix(a, b, u.z);\n}\nfloat fxFbm3(vec3 p) {\n float v = 0.0;\n float a = 0.5;\n for (int i = 0; i < 3; i++) {\n v += a * fxNoise3(p);\n p = p * 2.07 + 13.0;\n a *= 0.5;\n }\n return v;\n}\n`\n\n/**\n * Sources are short LINES along the rim, not discs.\n *\n * `uSources`: (u, v, half-length along the rim, strength). A NEGATIVE\n * strength is a spot that has stopped burning and only smokes —\n * see `FxFireFluid`'s smoulder.\n * `uAcross`: (toward the paper — a unit vector in aspect-corrected domain\n * space —, half-width across the rim, how far onto the paper).\n *\n * A disc was the old shape: up to 15 mm in radius, releasing gas into the\n * hole and over the paper alike, and it was what drew every flame as a\n * droplet. Gas comes off the char just behind the ember line, along it, in a\n * band a couple of millimetres wide; that band is what this draws.\n */\nconst SOURCES = /* glsl */ `\nuniform vec4 uSources[${MAX_SOURCES}];\nuniform vec4 uAcross[${MAX_SOURCES}];\nuniform int uCount;\nuniform float uAspect;\nfloat fxEmission(vec2 uv, out vec2 outward, out float smoulder) {\n float e = 0.0;\n outward = vec2(0.0);\n smoulder = 0.0;\n for (int i = 0; i < ${MAX_SOURCES}; i++) {\n if (i >= uCount) break;\n vec4 s = uSources[i];\n vec4 k = uAcross[i];\n vec2 d = (uv - s.xy) * vec2(uAspect, 1.0);\n vec2 n = k.xy;\n float across = dot(d, n) - k.w;\n float along = dot(d, vec2(-n.y, n.x));\n float g = abs(s.w) * exp(-along * along / (s.z * s.z) - across * across / (k.z * k.z));\n if (s.w < 0.0) {\n smoulder += g;\n continue;\n }\n e += g;\n // Off the edge, away from the paper it came from.\n outward -= g * n;\n }\n return e;\n}\n`\n\n/**\n * The rim's emission, once per step: (strength, outward.x, outward.y,\n * smouldering strength).\n *\n * The combustion pass used to loop over every source for every texel, TWICE\n * (it runs once per output target), and the forces pass a third time. Written\n * once here and read back as a texture, it costs one loop a step.\n */\nexport const EMIT = /* glsl */ `\nvarying vec2 vUv;\n${SOURCES}\nvoid main() {\n vec2 outward;\n float smoulder;\n float e = fxEmission(vUv, outward, smoulder);\n gl_FragColor = vec4(e, outward, smoulder);\n}\n`\n\n/** Carry a field along the velocity, backward in time. */\nexport const ADVECT = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform sampler2D uSource;\nuniform vec2 uDomain;\nuniform float uDt;\nuniform vec4 uKeep;\nvarying vec2 vUv;\nvoid main() {\n vec2 v = texture2D(uVelocity, vUv).xy;\n vec2 back = vUv - v * uDt / uDomain;\n gl_FragColor = texture2D(uSource, back) * uKeep;\n}\n`\n\n/**\n * The MacCormack correction, for what is drawn.\n *\n * One semi-Lagrangian back-trace loses about half a cell of detail a step,\n * because every read is a bilinear average of four cells; at sixty steps a\n * second, anything the solver resolves is butter within a few frames.\n *\n * MacCormack measures its own error and gives it back: advect forward\n * (uForward), advect THAT back again (uBackward), and the difference between\n * where it ended up and where it started is twice the error one step made.\n * Half of it is added back. Then the result is clamped to the four cells the\n * forward step actually read from, because an uncorrected overshoot is worse\n * than a blur — it grows hot spots brighter than anything the rim ever\n * released, and they ring.\n */\nexport const MACCORMACK = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform sampler2D uSource;\nuniform sampler2D uForward;\nuniform sampler2D uBackward;\nuniform vec2 uDomain;\nuniform vec2 uTexel;\nuniform float uDt;\nvarying vec2 vUv;\nvoid main() {\n vec4 forward = texture2D(uForward, vUv);\n vec4 phi = forward + 0.5 * (texture2D(uSource, vUv) - texture2D(uBackward, vUv));\n vec2 v = texture2D(uVelocity, vUv).xy;\n vec2 back = vUv - v * uDt / uDomain;\n vec2 corner = (floor(back / uTexel - 0.5) + 0.5) * uTexel;\n vec4 a = texture2D(uSource, corner);\n vec4 b = texture2D(uSource, corner + vec2(uTexel.x, 0.0));\n vec4 c = texture2D(uSource, corner + vec2(0.0, uTexel.y));\n vec4 d = texture2D(uSource, corner + uTexel);\n gl_FragColor = clamp(phi, min(min(a, b), min(c, d)), max(max(a, b), max(c, d)));\n}\n`\n\n/**\n * Emission and combustion, for the fine grid. Run twice with `uOut` 0 and 1:\n * once to write (fuel, heat, smoke, soot), once for (premixed, oxygen, burn\n * rate, flame heat). The same arithmetic both times, so the two agree.\n *\n * FLAME HEAT is the part of the heat that burning made, carried and cooled\n * like the rest; it is what a flame's colour is drawn from. The rest of the\n * heat — what the gas left the paper with — makes soot form and lifts the\n * gas, but it is hottest at the root, where a real flame is at its coolest\n * and dimmest, and colouring by it put a white-hot band on every rim.\n *\n * Oxygen reaches fuel two ways, and neither is \"everywhere at once\":\n *\n * mixing from the air beside it, in the plane — a diffusion step\n * over the four neighbours.\n * entrainment from in front and behind: a slice through a fire has no\n * third dimension for air to come from, so it is put back\n * here — but dense fuel BLOCKS it, because the air drawn into\n * a real tongue meets the fuel's outside first. That is what\n * keeps a fuel core fuel-rich and burning only at its skin.\n *\n * Soot is what a flame's light comes from, and the only thing drawn as flame\n * (the render pass reads it, not the heat). It forms from fuel that is hot,\n * burns away where there is oxygen, and goes out when the gas cools — so a\n * tongue glows from the paper to the height where its fuel runs out, and no\n * further. `persistence` is how long soot lasts in open air.\n */\nexport const REACT = /* glsl */ `\nuniform sampler2D uA;\nuniform sampler2D uB;\nuniform sampler2D uEmit;\nuniform sampler2D uVelocity;\nuniform vec2 uDomain;\nuniform vec2 uTexel;\nuniform int uOut;\nuniform float uDt;\nuniform float uFuel;\nuniform float uHeat;\nuniform float uSmoke;\nuniform float uPremixed;\nuniform float uAmbient;\nuniform float uBurnRate;\nuniform float uHeatRelease;\nuniform float uCooling;\nuniform float uSmokeProduction;\nuniform float uSmokeFade;\nuniform float uPersistence;\nuniform float uMixing;\nuniform float uEntrain;\nuniform float uFuelBlock;\nuniform float uSootYield;\nuniform float uSootHeat;\nuniform float uStoich;\nuniform float uSmoulderSmoke;\nuniform float uSmoulderHeat;\nvarying vec2 vUv;\nvoid main() {\n vec4 a = texture2D(uA, vUv);\n vec4 b = texture2D(uB, vUv);\n // What a parcel picked up crossing the rim during the step — sampled back\n // along its path, not at the one point it ended at. The band gas comes off\n // is ~2 mm across and the gas crosses it at ~5 mm a step at 60 Hz, so a\n // point sample drew one line of fuel per step: horizontal stripes up every\n // tongue.\n vec2 path = texture2D(uVelocity, vUv).xy * uDt / uDomain;\n vec4 em = 0.25 * (texture2D(uEmit, vUv) + texture2D(uEmit, vUv - path * 0.25) +\n texture2D(uEmit, vUv - path * 0.5) + texture2D(uEmit, vUv - path * 0.75));\n float e = em.r;\n // What the rim releases this step — and, where a flame has gone out, the\n // warm smoke the spot goes on giving off for a while.\n float fuel = a.r + e * uFuel * uDt;\n float heat = a.g + (e * uHeat + em.a * uSmoulderHeat) * uDt;\n float smoke = a.b + (e * uSmoke + em.a * uSmoulderSmoke) * uDt;\n float premixed = b.r + e * uFuel * uPremixed * uDt;\n // Air, mixed in from beside and entrained from in front and behind.\n float around = 0.25 * (\n texture2D(uB, vUv + vec2(uTexel.x, 0.0)).g + texture2D(uB, vUv - vec2(uTexel.x, 0.0)).g +\n texture2D(uB, vUv + vec2(0.0, uTexel.y)).g + texture2D(uB, vUv - vec2(0.0, uTexel.y)).g);\n float oxygen = mix(b.g, around, uMixing);\n float reach = exp(-fuel / max(uFuelBlock, 1e-4));\n oxygen += (uAmbient - oxygen) * (1.0 - exp(-uEntrain * reach * uDt));\n // Fuel burns only as far as there is oxygen for it — premixed first — and a\n // unit of fuel takes uStoich of air. At one to one (as it once was) fuel was\n // never denser than the air around it, so nothing ever ran out of air and\n // every scrap of fuel burnt where it stood: no core, no tip.\n float burn = min(fuel, premixed + oxygen / uStoich) * (1.0 - exp(-uBurnRate * uDt));\n fuel = max(0.0, fuel - burn) * exp(-uDt * 0.6);\n float fromPremixed = min(premixed, burn);\n premixed = max(0.0, premixed - fromPremixed) * exp(-uDt * 0.6);\n oxygen = max(0.0, oxygen - (burn - fromPremixed) * uStoich);\n heat = (heat + burn * uHeatRelease) * exp(-uCooling * uDt);\n float flameHeat = (b.a + burn * uHeatRelease) * exp(-uCooling * uDt);\n smoke = (smoke + burn * uSmokeProduction) * exp(-uDt / uSmokeFade);\n float rate = burn / max(uDt, 1e-4);\n // Soot: forms from hot fuel, burns where there is air, and is gone once the\n // gas is too cool to glow.\n float hot = heat / max(uSootHeat, 1e-3);\n float soot = a.a + uSootYield * (fuel + burn) * smoothstep(0.3, 0.8, hot) * uDt;\n float air = oxygen / max(uAmbient, 0.05);\n // Soot that cools stops glowing within a few frames — it is smoke now, and\n // the smoke channel already carries that. Held to glowing only below a\n // twentieth of uSootHeat, it outlived the flame by a hand's breadth and\n // drew ribbons and hooks of flame colour high over the sheet.\n float cold = 1.0 - smoothstep(0.2, 0.6, hot);\n soot *= exp(-uDt * (air / max(uPersistence, 1e-3) + cold * 25.0));\n // An open top: whatever reaches it leaves.\n float open = 1.0 - smoothstep(0.9, 1.0, vUv.y);\n if (uOut == 0) gl_FragColor = vec4(fuel, heat, smoke * open, soot) * vec4(open, open, 1.0, open);\n else gl_FragColor = vec4(premixed * open, oxygen, rate, flameHeat * open);\n}\n`\n\n/** Buoyancy, wind, turbulence and the emission's own push, on the coarse grid. */\nexport const FORCES = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform sampler2D uA;\nuniform sampler2D uEmit;\nuniform vec2 uTexel;\nuniform vec2 uDomain;\nuniform float uDt;\nuniform float uTime;\nuniform float uBuoyancy;\nuniform float uWind;\nuniform float uTurbulence;\nuniform float uTurbScale;\nuniform float uTurbEvolve;\nuniform float uRadial;\nuniform vec2 uInitVel;\nvarying vec2 vUv;\n${NOISE}\nvoid main() {\n vec2 v = texture2D(uVelocity, vUv).xy;\n vec4 a = texture2D(uA, vUv);\n // Hot gas rises.\n v.y += uBuoyancy * a.g * uDt;\n v.x += uWind * uDt;\n // Curl noise: divergence-free eddies, in world units, carried up with the\n // gas AND changing as they go. The third coordinate is time: a 2D pattern\n // that only scrolled was a wave travelling up a still flame, never a flame\n // that flickers.\n float drift = uTime * 0.8 + 1.7 * fxNoise(vec2(uTime * 0.23, 3.1));\n vec3 p = vec3(vUv * uDomain * uTurbScale + vec2(0.37 * fxNoise(vec2(uTime * 0.17, 9.0)), -drift), uTime * uTurbEvolve);\n float h = 0.04;\n float n = fxFbm3(p);\n vec2 curl = vec2(fxFbm3(p + vec3(0.0, h, 0.0)) - n, -(fxFbm3(p + vec3(h, 0.0, 0.0)) - n)) / h;\n v += curl * uTurbulence * uDt * (0.2 + clamp(a.g, 0.0, 2.0));\n // The gas leaves the paper at its own speed, pushed off the edge if asked.\n vec4 em = texture2D(uEmit, vUv);\n float e = em.r;\n vec2 launch = uInitVel + uRadial * em.gb / max(e, 1e-4);\n v = mix(v, launch, clamp(e * uDt * 6.0, 0.0, 1.0));\n // Walls on three sides; the top is open.\n if (vUv.x < uTexel.x || vUv.x > 1.0 - uTexel.x || vUv.y < uTexel.y) v = vec2(0.0);\n gl_FragColor = vec4(v, 0.0, 1.0);\n}\n`\n\nexport const CURL = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform vec2 uTexel;\nuniform float uCell;\nvarying vec2 vUv;\nvoid main() {\n float l = texture2D(uVelocity, vUv - vec2(uTexel.x, 0.0)).y;\n float r = texture2D(uVelocity, vUv + vec2(uTexel.x, 0.0)).y;\n float b = texture2D(uVelocity, vUv - vec2(0.0, uTexel.y)).x;\n float t = texture2D(uVelocity, vUv + vec2(0.0, uTexel.y)).x;\n gl_FragColor = vec4((r - l - t + b) * 0.5 / uCell, 0.0, 0.0, 1.0);\n}\n`\n\n/** Vorticity confinement: give back the small swirls the grid smooths away. */\nexport const VORTICITY = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform sampler2D uCurl;\nuniform vec2 uTexel;\nuniform float uCell;\nuniform float uDt;\nuniform float uVorticity;\nvarying vec2 vUv;\nvoid main() {\n float l = abs(texture2D(uCurl, vUv - vec2(uTexel.x, 0.0)).x);\n float r = abs(texture2D(uCurl, vUv + vec2(uTexel.x, 0.0)).x);\n float b = abs(texture2D(uCurl, vUv - vec2(0.0, uTexel.y)).x);\n float t = abs(texture2D(uCurl, vUv + vec2(0.0, uTexel.y)).x);\n float w = texture2D(uCurl, vUv).x;\n vec2 n = vec2(r - l, t - b);\n n /= length(n) + 1e-5;\n vec2 v = texture2D(uVelocity, vUv).xy + uVorticity * uCell * vec2(n.y, -n.x) * w * uDt;\n gl_FragColor = vec4(v, 0.0, 1.0);\n}\n`\n\n/** How much the flow compresses — minus the gas burning expands by. */\nexport const DIVERGENCE = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform sampler2D uB;\nuniform vec2 uTexel;\nuniform float uCell;\nuniform float uExpansion;\nvarying vec2 vUv;\nvoid main() {\n float l = texture2D(uVelocity, vUv - vec2(uTexel.x, 0.0)).x;\n float r = texture2D(uVelocity, vUv + vec2(uTexel.x, 0.0)).x;\n float b = texture2D(uVelocity, vUv - vec2(0.0, uTexel.y)).y;\n float t = texture2D(uVelocity, vUv + vec2(0.0, uTexel.y)).y;\n float div = (r - l + t - b) * 0.5 / uCell - uExpansion * texture2D(uB, vUv).b;\n gl_FragColor = vec4(div, 0.0, 0.0, 1.0);\n}\n`\n\nexport const PRESSURE = /* glsl */ `\nuniform sampler2D uPressure;\nuniform sampler2D uDivergence;\nuniform vec2 uTexel;\nuniform float uCell;\nvarying vec2 vUv;\nvoid main() {\n float l = texture2D(uPressure, vUv - vec2(uTexel.x, 0.0)).x;\n float r = texture2D(uPressure, vUv + vec2(uTexel.x, 0.0)).x;\n float b = texture2D(uPressure, vUv - vec2(0.0, uTexel.y)).x;\n float t = texture2D(uPressure, vUv + vec2(0.0, uTexel.y)).x;\n float div = texture2D(uDivergence, vUv).x;\n gl_FragColor = vec4((l + r + b + t - div * uCell * uCell) * 0.25, 0.0, 0.0, 1.0);\n}\n`\n\nexport const GRADIENT = /* glsl */ `\nuniform sampler2D uVelocity;\nuniform sampler2D uPressure;\nuniform vec2 uTexel;\nuniform float uCell;\nvarying vec2 vUv;\nvoid main() {\n float l = texture2D(uPressure, vUv - vec2(uTexel.x, 0.0)).x;\n float r = texture2D(uPressure, vUv + vec2(uTexel.x, 0.0)).x;\n float b = texture2D(uPressure, vUv - vec2(0.0, uTexel.y)).x;\n float t = texture2D(uPressure, vUv + vec2(0.0, uTexel.y)).x;\n vec2 v = texture2D(uVelocity, vUv).xy - vec2(r - l, t - b) * 0.5 / uCell;\n gl_FragColor = vec4(v, 0.0, 1.0);\n}\n`\n\nexport const FILL = /* glsl */ `\nuniform vec4 uValue;\nvoid main() {\n gl_FragColor = uValue;\n}\n`\n\n/** How the fine grid is drawn: soot glowing at its temperature, and smoke lit warm from below. */\nexport const RENDER_VERTEX = /* glsl */ `\nvarying vec2 vUv;\nvoid main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n`\n\n/**\n * Two fields, two jobs. The first version drew everything — outline, colour,\n * opacity, brightness — from temperature alone, and the contours of one\n * smooth scalar are nested smooth rings: an airbrushed blob with a white spot\n * in it, however the bands were tuned.\n *\n * soot WHERE the flame is and how dense: its outline, its opacity,\n * how much light it can give. It ends where the fuel runs out.\n * flame heat WHAT COLOUR that soot glows: the zones, tip to core. The\n * heat burning made, not the gas's own (see REACT).\n */\nexport const RENDER_FRAGMENT = /* glsl */ `\nuniform sampler2D uA;\nuniform sampler2D uB;\nuniform float uTime;\nuniform float uGlow;\nuniform float uSmokeDensity;\nuniform float uPaperWhite;\nuniform float uHeatScale;\nuniform float uSootScale;\nuniform float uContrast;\nuniform float uOpacity;\nuniform float uThin;\nuniform vec3 uTipColor;\nuniform vec3 uBodyColor;\nuniform vec3 uCoreColor;\nuniform vec3 uRootColor;\nuniform float uTipGlow;\nuniform float uBodyGlow;\nuniform float uCoreGlow;\nuniform float uTipFrom;\nuniform float uTipTo;\nuniform float uCoreFrom;\nuniform float uSoftness;\nuniform float uTearing;\nuniform float uRootAmount;\nuniform float uRootReach;\nvarying vec2 vUv;\n${NOISE}\nvoid main() {\n // Detail finer than the grid, none of it touching the solve: a small domain\n // warp so a tongue's outline is never the grid's.\n vec2 w = vUv * vec2(26.0, 18.0) + vec2(0.0, -uTime * 1.6);\n vec2 warp = vec2(fxFbm(w), fxFbm(w + 31.7)) - 0.5;\n vec4 a = texture2D(uA, vUv + warp * 0.004);\n float fuel = a.r;\n float heat = texture2D(uB, vUv + warp * 0.004).a;\n float smoke = a.b;\n // How dense the glowing soot is, against a full flame's (FIRE_SOOT_SCALE).\n float rho = a.a / max(uSootScale, 1e-4);\n // The tip's tearing: carved into the soot where it is thin, so tongues come\n // apart at their edges and keep their bodies.\n vec2 n1 = vUv * vec2(34.0, 24.0) + vec2(0.0, -uTime * 2.4);\n vec2 n2 = vUv * vec2(92.0, 64.0) + vec2(0.0, -uTime * 5.5);\n float grain = (fxFbm(n1) - 0.5) * 0.72 + (fxNoise(n2) - 0.5) * 0.28;\n rho = max(0.0, rho * (1.0 + grain * uTearing * (1.0 - smoothstep(0.1, 0.8, rho))));\n // Temperature against the hottest gas a flame has (FIRE_HEAT_SCALE).\n float t = pow(clamp(heat / max(uHeatScale, 1e-3), 0.0, 2.0), uContrast);\n\n // WHERE: the outline, from the soot.\n float shape = smoothstep(uTipFrom, uTipFrom + uSoftness, rho);\n // WHAT COLOUR: the zones, as bands of temperature.\n float toBody = smoothstep(uTipTo - 0.12, uTipTo + 0.12, t);\n float toCore = smoothstep(uCoreFrom, uCoreFrom + 0.35, t);\n vec3 color = mix(mix(uTipColor, uBodyColor, toBody), uCoreColor, toCore);\n float glow = mix(mix(uTipGlow, uBodyGlow, toBody), uCoreGlow, toCore);\n // Soot emits and absorbs together: thin soot glows as much as it covers, and\n // a dense flame covers what is behind it only as far as uOpacity says.\n float cover = 1.0 - exp(-rho * 3.0);\n float fireAlpha = shape * cover * clamp(uOpacity / 5.0, 0.0, 1.0);\n vec3 fire = color * glow * shape * uPaperWhite * uGlow * smoothstep(0.0, uThin, cover);\n // The root: added as LIGHT, where fresh fuel is still near the paper and the\n // gas has not heated through. Mixed in instead of added, blue makes grey.\n float rootMask = smoothstep(mix(0.4, 0.02, uRootReach), mix(0.6, 0.12, uRootReach), fuel) * (1.0 - smoothstep(0.25, 0.6, t));\n fire += uRootColor * rootMask * uRootAmount * uPaperWhite * uGlow * 0.6;\n\n // Smoke: warm grey-brown (a cool grey over cream reads lavender), lit by\n // the fire under it.\n float smokeAlpha = 1.0 - exp(-smoke * uSmokeDensity);\n vec3 smokeColor = vec3(0.16, 0.13, 0.10) + vec3(0.22, 0.09, 0.02) * smoothstep(0.15, 0.9, t);\n float alpha = smokeAlpha + fireAlpha - smokeAlpha * fireAlpha;\n // No square edge: the domain fades out before its borders.\n float edge = smoothstep(0.0, 0.06, vUv.x) * smoothstep(1.0, 0.94, vUv.x) * smoothstep(0.0, 0.03, vUv.y) * smoothstep(1.0, 0.9, vUv.y);\n gl_FragColor = vec4((fire + smokeColor * smokeAlpha) * edge, alpha * edge);\n #include <tonemapping_fragment>\n #include <colorspace_fragment>\n}\n`\n","/**\n * The fire simulator's controls — by the names, and at the defaults, of the\n * fluid-fire tool Noor pointed at (Emission, Combustion, Fuel & air, Motion\n * & turbulence), so the lab's panel reads like the one she knows.\n *\n * The numbers are the panel's, not the solver's. `solverUniforms` is the one\n * place they are turned into this solver's units — world units (a default\n * sheet is one unit, 210 mm, across) and seconds — so a value can be copied\n * between the two tools and mean roughly the same thing, and so tuning the\n * look never means renaming a control.\n *\n * What the chemistry terms mean here, since no tool documents its own:\n *\n * fuel gas released at the burning rim, per second.\n * premixed oxygen the share of that gas that arrives already mixed with\n * oxygen — it can burn the moment it leaves the paper.\n * ambient oxygen oxygen in the surrounding air, which fuel burns with as\n * it mixes in. At 0 only the premixed share ever burns,\n * so flames stay close to the rim.\n * heat temperature released with the gas — paper's gas leaves\n * it hot, and that heat is what lets its soot form and\n * glow before it has burnt (swept to 1, most of the flame\n * went out).\n * flame persistence how long glowing soot lasts in open air before it\n * burns away — the soot a flame's light comes from.\n * air mixing how fast air works into the fuel, and so how far up a\n * tongue's fuel core survives.\n */\nexport interface FireFluidParams {\n // Emission\n fuel: number\n premixedOxygen: number\n heat: number\n smoke: number\n radialImpulse: number\n initialVelocity: readonly [number, number, number]\n // Combustion\n burnRate: number\n gasExpansion: number\n buoyancy: number\n cooling: number\n smokeProduction: number\n // Fuel & air\n ambientOxygen: number\n flamePersistence: number\n /**\n * How fast air works its way into the fuel, 0..1. Ours, not the panel's.\n * Low and the fuel core survives a long way up — tall tongues with long\n * tips; high and the fuel burns out close to the paper — short licks.\n */\n airMixing: number\n // Motion & turbulence\n turbulence: number\n turbulenceScale: number\n vorticity: number\n wind: number\n /** How long smoke lingers, in seconds — long enough and it hangs as a haze. Ours, not the panel's. */\n smokeFade: number\n /**\n * How long a spot of the rim goes on smoking once its flame is out, in\n * seconds. Ours, not the panel's. Paper does not stop smoking when it stops\n * burning: the char is still hot, and a thin thread goes on rising from it.\n */\n smokeAfter: number\n}\n\n/**\n * The defaults — the panel's names, at the values Noor tuned in the lab on\n * 2026-09-12. No turbulence of its own: the motion comes from vorticity,\n * the radial impulse and the uneven rim that feeds it.\n */\nexport const fireFluidDefaults: FireFluidParams = {\n fuel: 33.5,\n premixedOxygen: 0.28,\n heat: 3.2,\n smoke: 0.7,\n // Was 5 — a full unit a second, 210 mm/s, of gas pushed sideways out of the\n // rim. Against a buoyancy of 1.7 units/s² that is what rolled the flames\n // into mushroom caps: the grey spirals over the text that the review called\n // the most synthetic thing in the frame. A flame leaves paper going UP.\n radialImpulse: 0.8,\n // In world units a second now (see `solverUniforms`): 1.4 is ~0.3 m/s, the\n // speed gas leaves burning paper with. It was 0.5 at a quarter scale —\n // 26 mm/s — and gas held that slow pooled into a bulb at the rim before\n // buoyancy stretched it into a neck: every flame was a droplet.\n initialVelocity: [0, 1.4, 0],\n burnRate: 6.1,\n // Was 0.65. Expansion is divergence where the gas burns, and it pushes the\n // gas SIDEWAYS as much as up: at 0.65 each tongue swelled into a puff and\n // the smoke billowed out over the paper. Your web reference is thin licking\n // tongues, and swept at 0.2 / 0.65 / 1.3 the low end is the one that looks\n // like it.\n gasExpansion: 0.3,\n // Was 3, when the flame body was authored below paper white and a tongue\n // needed carrying further to register. Now the gas leaves the rim at its\n // real speed and glows above paper white, and 3 threw tongues past the\n // text; 2 holds them lower. (1.5 was no shorter — height is set by how\n // soon the gas cools, below — only broader and slower.)\n buoyancy: 2,\n // Was 0.92. Gas that stays hot all the way up pools into ONE column: the\n // rim's forty-odd emission points merge a few centimetres above the paper\n // and the fire reads as a single plume with a couple of licks beside it.\n // Hero.png is many separate tongues around the whole rim — tall above,\n // short below — and cooling them faster is what keeps them apart, because\n // each one runs out of glow before it can merge with its neighbour.\n //\n // 1.8 since a flame's light comes from soot that only lives in hot gas:\n // cooling is now what sets a tongue's HEIGHT. Swept in live play: 1.15\n // reached the text (~150 mm), 1.6 ~100 mm, 2.1 short licks (~60 mm);\n // Hero.png's tallest is ~70 mm.\n cooling: 1.8,\n // Was 1.4, which made a thick grey column, then 0.5. Noor's direction is\n // light smoke, and with MacCormack advection keeping the smoke's fine\n // structure it needs less of it to read: at 0.5 it veiled the upper sheet.\n smokeProduction: 0.25,\n ambientOxygen: 0.47,\n // Was 0.005 — the slider's own floor, which is to say switched off. The\n // flame channel decayed with a 5 ms time constant, so it was an\n // instantaneous quantity with no history and therefore no shape.\n flamePersistence: 0.06,\n airMixing: 0.5,\n // Was 0, also switched off, with all the motion coming from vorticity and\n // the radial impulse — a few big eddies instead of many small ones, which\n // is the difference between a fire that tears and one that curls. Swept\n // against the peak frame: at 2 the flames are still a single plume, at 9\n // they break into separate tongues around the rim.\n turbulence: 9,\n // Finer eddies with it: 6.8 was ~24 mm, 12 is ~14 mm, and the smoke stops\n // reading as a few big hooks.\n turbulenceScale: 12,\n // Was 6.1. Vorticity confinement gives back the swirls the grid smooths\n // away; at 6 it was manufacturing swirls the flow never had.\n vorticity: 3,\n wind: 0.8,\n // Was 4.2 s. Smoke that lingers that long is stretched by the flow into\n // long grey veins, and sharper advection keeps every one of them — so the\n // whole upper sheet and the black stage behind it came out MARBLED. Real\n // paper smoke thins out within a second or two. Swept in live play against\n // the default: 1.5 s is thin threads and a clean sheet.\n smokeFade: 1.5,\n smokeAfter: 4,\n}\n\n/** Slider ranges for the lab, grouped the way the panel groups them. */\nexport const fireFluidControls: readonly {\n group: string\n key: Exclude<keyof FireFluidParams, 'initialVelocity'>\n label: string\n min: number\n max: number\n step: number\n}[] = [\n { group: 'Emission', key: 'fuel', label: 'Fuel', min: 0, max: 50, step: 0.1 },\n { group: 'Emission', key: 'premixedOxygen', label: 'Premixed oxygen', min: 0, max: 1, step: 0.01 },\n { group: 'Emission', key: 'heat', label: 'Heat', min: 0, max: 5, step: 0.1 },\n { group: 'Emission', key: 'smoke', label: 'Smoke', min: 0, max: 2, step: 0.05 },\n { group: 'Emission', key: 'radialImpulse', label: 'Radial impulse', min: 0, max: 5, step: 0.1 },\n { group: 'Combustion', key: 'burnRate', label: 'Burn rate', min: 0, max: 10, step: 0.1 },\n { group: 'Combustion', key: 'gasExpansion', label: 'Gas expansion', min: 0, max: 3, step: 0.05 },\n { group: 'Combustion', key: 'buoyancy', label: 'Buoyancy', min: 0, max: 6, step: 0.1 },\n { group: 'Combustion', key: 'cooling', label: 'Cooling', min: 0, max: 3, step: 0.01 },\n { group: 'Combustion', key: 'smokeProduction', label: 'Smoke production', min: 0, max: 4, step: 0.05 },\n { group: 'Fuel & air', key: 'ambientOxygen', label: 'Ambient oxygen', min: 0, max: 1, step: 0.01 },\n {\n group: 'Fuel & air',\n key: 'flamePersistence',\n label: 'Flame persistence',\n min: 0.005,\n max: 1,\n step: 0.005,\n },\n { group: 'Fuel & air', key: 'airMixing', label: 'Air mixing', min: 0, max: 1, step: 0.01 },\n // 0–10 while the default was 0. Now that turbulence is what breaks the\n // flames into tongues, the tuned value has to sit somewhere a slider can\n // move in BOTH directions — see the test.\n { group: 'Motion & turbulence', key: 'turbulence', label: 'Turbulence', min: 0, max: 20, step: 0.1 },\n {\n group: 'Motion & turbulence',\n key: 'turbulenceScale',\n label: 'Turbulence scale',\n min: 0.5,\n max: 24,\n step: 0.1,\n },\n { group: 'Motion & turbulence', key: 'vorticity', label: 'Vorticity', min: 0, max: 8, step: 0.1 },\n { group: 'Motion & turbulence', key: 'wind', label: 'Wind', min: -3, max: 3, step: 0.05 },\n { group: 'Look', key: 'smokeFade', label: 'Smoke lingers (s)', min: 0.2, max: 10, step: 0.1 },\n { group: 'Look', key: 'smokeAfter', label: 'Smoke after the flames (s)', min: 0, max: 12, step: 0.1 },\n]\n\n/** What the solver's passes read, in its own units. */\nexport interface SolverUniforms {\n fuel: number\n premixed: number\n heat: number\n smoke: number\n radial: number\n initialVelocity: [number, number]\n burnRate: number\n heatRelease: number\n expansion: number\n buoyancy: number\n cooling: number\n smokeProduction: number\n smokeFade: number\n ambient: number\n persistence: number\n /** Share of the neighbours' oxygen mixed in per step, in the plane. */\n mixing: number\n /** Per second: air drawn in from in front of and behind the slice. */\n entrain: number\n /** Fuel density at which entrainment is down to 1/e — dense fuel keeps air out. */\n fuelBlock: number\n /** Soot formed per unit of hot fuel a second. */\n sootYield: number\n /** The temperature soot needs to form and glow at. */\n sootHeat: number\n /** Units of air a unit of fuel burns with. */\n stoich: number\n /** Smoke a second, per unit of smouldering strength, where a flame has gone out. */\n smoulderSmoke: number\n /** Heat with it — enough that the smoke rises, not enough to glow. */\n smoulderHeat: number\n turbulence: number\n turbulenceScale: number\n /** Noise units a second the turbulence changes by, on top of rising with the gas. */\n turbulenceEvolve: number\n vorticity: number\n wind: number\n}\n\n/**\n * The panel's numbers, in the solver's units. Every scale here is a tuning\n * decision made by eye against the references, stated once so it is never\n * smeared across shaders.\n */\nexport function solverUniforms(p: FireFluidParams): SolverUniforms {\n const finite = (x: number, fallback: number) => (Number.isFinite(x) ? x : fallback)\n return {\n // Gas per second per unit of source strength.\n fuel: Math.max(0, finite(p.fuel, 0)) * 0.12,\n premixed: Math.min(1, Math.max(0, finite(p.premixedOxygen, 0))),\n // Temperature released with the gas, per second. At 1.8 the gas at the\n // source settled near a third of glowing — cooling took the heat faster\n // than the rim gave it — and the fire was a faint blush, not a flame.\n heat: Math.max(0, finite(p.heat, 0)) * 7,\n smoke: Math.max(0, finite(p.smoke, 0)) * 0.45,\n // World units a second: A4 flames climb a few tens of centimetres a second.\n radial: Math.max(0, finite(p.radialImpulse, 0)) * 0.2,\n // World units a second, one to one — a unit is 210 mm.\n initialVelocity: [finite(p.initialVelocity[0], 0), finite(p.initialVelocity[1], 0)],\n // Per second.\n burnRate: Math.max(0, finite(p.burnRate, 0)) * 2,\n // Temperature a unit of burnt fuel adds.\n heatRelease: 3,\n // Divergence per unit of burning a second — the gas expanding as it burns.\n expansion: Math.max(0, finite(p.gasExpansion, 0)) * 1.5,\n // World units a second squared per unit of temperature.\n buoyancy: Math.max(0, finite(p.buoyancy, 0)) * 0.9,\n // Per second: a flame at A4 scale has cooled out of sight 10–40 mm up.\n cooling: Math.max(0, finite(p.cooling, 0)) * 6,\n smokeProduction: Math.max(0, finite(p.smokeProduction, 0)) * 0.3,\n smokeFade: Math.max(0.05, finite(p.smokeFade, 2.5)),\n ambient: Math.min(1, Math.max(0, finite(p.ambientOxygen, 0))),\n persistence: Math.max(0.005, finite(p.flamePersistence, 0.085)),\n mixing: Math.min(1, Math.max(0, finite(p.airMixing, 0.5))) * 0.5,\n entrain: Math.min(1, Math.max(0, finite(p.airMixing, 0.5))) * 40,\n fuelBlock: 0.05,\n sootYield: 30,\n sootHeat: 2,\n // Paper's gas takes several times its own mass of air to burn; what\n // matters here is only that it is well above one, so fuel near the rim is\n // denser than the air that can reach it and burns from the outside in.\n stoich: 4,\n // Several times the rim's own smoke per unit strength: a smouldering spot\n // has no flame to lift its smoke fast, so it pools and rises slowly, and\n // at 1.5× it read 0.03 at the 99th percentile — about 3% opacity, gone —\n // at 8× 0.07, and at 30× thin wisps you had to look for.\n smoulderSmoke: Math.max(0, finite(p.smoke, 0)) * 0.45 * 80,\n smoulderHeat: 1.2,\n turbulence: Math.max(0, finite(p.turbulence, 0)) * 0.35,\n // Noise frequency per WORLD unit — the pass multiplies by the domain's\n // size, which it did not before (it read UV, and the domain is two units\n // tall, so every eddy was half the size this said). 12 → ~14 mm eddies.\n turbulenceScale: Math.max(0.1, finite(p.turbulenceScale, 4.6)) * 1.25,\n turbulenceEvolve: 3,\n vorticity: Math.max(0, finite(p.vorticity, 0)),\n wind: finite(p.wind, 0) * 0.3,\n }\n}\n","import * as THREE from 'three'\nimport { useFrame } from '@react-three/fiber'\nimport { useRef } from 'react'\nimport type { DamageField } from './field'\nimport type { SurfaceLocator } from './fire'\nimport { flameAnchors, flamePuff, type FlameAnchor } from './flames'\n\nexport interface FxFireLightProps {\n field: DamageField\n locate: SurfaceLocator\n /** How bright the fire is per unit of burning front. Tuned against Hero.png. */\n gain?: number\n}\n\n/**\n * Fire is a light source (§4.8, §7). A fire that changes nothing around it\n * looks pasted on.\n *\n * A warm point light — about 1900 K — at the middle of the burning rim,\n * raised by half a flame, as bright as the front is long and flickering on\n * the very noise the flames puff on, so light and flame agree. It reaches the\n * whole sheet: `decay` is held below the physical 2, because a sheet is a\n * small thing next to a fire and Hero.png warms it to the top edge.\n *\n * And a dimmer twin on the FAR side of the sheet, for translucency: paper is\n * thin, and a fire in front of it glows through to the back, warm and\n * diffused (§7). A second light is the whole trick — the sheet's own\n * material already lights its back face from whatever is behind it.\n */\nexport function FxFireLight({ field, locate, gain = FIRE_LIGHT_GAIN }: FxFireLightProps) {\n const front = useRef<THREE.PointLight>(null)\n const through = useRef<THREE.PointLight>(null)\n const anchors = useRef<FlameAnchor[]>([])\n\n useFrame(({ camera }) => {\n const a = front.current\n const b = through.current\n if (!a || !b) return\n const n = flameAnchors(field, locate, 32, anchors.current)\n if (n === 0) {\n a.intensity = 0\n b.intensity = 0\n return\n }\n let x = 0\n let y = 0\n let z = 0\n let h = 0\n let flicker = 0\n for (let i = 0; i < n; i++) {\n const f = anchors.current[i]!\n x += f.x\n y += f.y\n z += f.z\n h += f.height\n flicker += flamePuff(f.seed, field.time)\n }\n x /= n\n y /= n\n z /= n\n h /= n\n flicker /= n\n // Off the sheet toward whoever is looking, a little; its twin the same\n // distance behind.\n const toCamera = new THREE.Vector3(camera.position.x - x, 0, camera.position.z - z)\n toCamera.normalize().multiplyScalar(0.035)\n const lift = h * 0.5\n a.position.set(x + toCamera.x, y + lift, z + toCamera.z)\n b.position.set(x - toCamera.x, y + lift, z - toCamera.z)\n const level = field.lastStats.front * gain * flicker\n a.intensity = level\n b.intensity = level * 0.35\n })\n\n return (\n <>\n <pointLight ref={front} color={FIRE_COLOR} intensity={0} decay={0.9} distance={0} />\n <pointLight ref={through} color={FIRE_COLOR} intensity={0} decay={0.9} distance={0} />\n </>\n )\n}\n\n/** ~1900 K. */\n// Peach rather than red: a red-heavy light on white paper is exactly what\n// the tone curve turns salmon — the pink the spec forbids.\nconst FIRE_COLOR = new THREE.Color('#ffa24c')\n\n/** Light per unit of front length; the front runs to ~0.045 at a burn's peak. */\nexport const FIRE_LIGHT_GAIN = 42\n","import * as THREE from 'three'\nimport { useFrame } from '@react-three/fiber'\nimport { useEffect, useMemo, useRef } from 'react'\nimport { FxParticles } from './FxParticles'\nimport { flameGeometry, flameMaterial } from './flameShader'\nimport { ParticlePool } from './particles'\n\n/** What the page says about the match this frame. Written in place — never a prop change. */\nexport interface MatchFlameState {\n /** Where the flame's root is, in world space; null when there is no match. */\n position: { x: number; y: number; z: number } | null\n /** Arming is the dwell before a pinch becomes a match; lit is a match. */\n state: 'none' | 'arming' | 'lit'\n /** How hard the viewer is blowing, 0..1. */\n blow: number\n /** Whether the flame is held against the sheet. */\n touching: boolean\n /**\n * A clock to animate on, in seconds, instead of the frame clock — and\n * `litAt`, when on that clock the match was struck.\n *\n * For a scripted burn (`/fx-lab`) that has to photograph the same match\n * twice: given both, the flare is a function of time rather than of which\n * frame saw the strike, and no random sparks or puffs are thrown. A live\n * hand leaves both out.\n */\n time?: number\n litAt?: number\n}\n\nexport interface FxMatchFlameProps {\n /** Read every frame. A ref, so a hand moving the match re-renders nothing. */\n match: { readonly current: MatchFlameState }\n}\n\n/** A match flame at A4 scale: 15–25 mm tall (§10.3). */\nconst HEIGHT = 20 / 210\n/** How long a strike flares for, in seconds, and by how much. */\nconst FLARE = 0.42\nconst FLARE_GAIN = 1\n/** How long a pinch has to be held to arm — `MATCH_DWELL_MS` on /hands. */\nconst ARM = 0.32\n\n/**\n * A match, held: `paperlab-fx-fire-spec.md` §10, the parts that are the\n * flame's to draw.\n *\n * - **Arming.** Friction building: a few tiny sparks and a warm point that\n * grows at the pinch. A pinch released early (a flick) fizzles — nothing\n * lights, because the page never says `lit`.\n * - **Strike.** A one-frame flash of light, a burst of sparks, a puff, and\n * the flame flares to about twice its height for under half a second and\n * settles — a real match head flares as it burns off.\n * - **Held.** A 20 mm teardrop that flickers, leans away from where the hand\n * is going with a lag, and stretches thin and burns blue and dim when moved\n * fast. Against the paper it flattens and spills upward.\n * - **Blown.** It leans away and flickers hard; when the page says it has\n * gone out, it leaves a soft puff behind.\n * - **Light.** It lights the sheet warm before anything scorches — bringing\n * the flame near the paper is the first thing people notice (§10.4).\n *\n * It owns its own clock and its own handful of particles: nothing here is\n * part of a burn anyone replays.\n */\nexport function FxMatchFlame({ match }: FxMatchFlameProps) {\n const geometry = useMemo(() => flameGeometry(1), [])\n const material = useMemo(() => flameMaterial(), [])\n const pool = useMemo(() => new ParticlePool(160, 3), [])\n useEffect(\n () => () => {\n geometry.dispose()\n material.dispose()\n },\n [geometry, material],\n )\n const light = useRef<THREE.PointLight>(null)\n const memory = useRef({\n time: 0,\n last: 'none' as MatchFlameState['state'],\n armedAt: 0,\n flare: 0,\n flash: 0,\n spark: 0,\n was: new THREE.Vector3(),\n velocity: new THREE.Vector3(),\n lean: new THREE.Vector3(),\n at: new THREE.Vector3(),\n hasAt: false,\n })\n\n useFrame((_, delta) => {\n const now = match.current\n const m = memory.current\n // Scripted: the given clock, and nothing that depends on which frame\n // happened to see a transition.\n const scripted = now.time !== undefined\n const dt = scripted ? Math.max(0, now.time! - m.time) : Math.min(0.1, Math.max(0, delta))\n m.time = scripted ? now.time! : m.time + dt\n\n if (now.position) {\n m.at.set(now.position.x, now.position.y, now.position.z)\n if (!m.hasAt) m.was.copy(m.at)\n m.hasAt = true\n }\n\n // Transitions: what a strike and a blow-out LEAVE, spawned once — live\n // only; a scripted strike's flare is read off `litAt` below.\n if (!scripted && now.state === 'lit' && m.last !== 'lit') {\n m.flare = FLARE\n m.flash = 0.06\n for (let i = 0; i < 16; i++) pool.spawn('ember', m.at.x, m.at.y + HEIGHT * 0.2, m.at.z)\n for (let i = 0; i < 3; i++) pool.spawn('smoke', m.at.x, m.at.y + HEIGHT * 0.5, m.at.z)\n }\n if (!scripted && now.state !== 'lit' && m.last === 'lit' && m.hasAt) {\n for (let i = 0; i < 5; i++) pool.spawn('smoke', m.at.x, m.at.y + HEIGHT * 0.6, m.at.z)\n }\n if (now.state === 'arming' && m.last !== 'arming') m.armedAt = m.time\n m.last = now.state\n\n // Velocity, and a lean that follows it 100–200 ms late — the flame is\n // gas, and gas is left behind.\n if (dt > 0 && m.hasAt) {\n const v = m.at.clone().sub(m.was).divideScalar(dt)\n m.velocity.lerp(v, Math.min(1, dt / 0.08))\n m.was.copy(m.at)\n }\n const speed = m.velocity.length()\n const target = m.velocity.clone().multiplyScalar(-0.35)\n // Blowing comes from the viewer: it pushes the flame away, into the scene.\n target.z -= now.blow * 1.2\n m.lean.lerp(target, Math.min(1, dt / 0.15))\n\n m.flare = scripted ? Math.max(0, FLARE - (m.time - (now.litAt ?? -FLARE))) : Math.max(0, m.flare - dt)\n m.flash = scripted ? 0 : Math.max(0, m.flash - dt)\n\n const base = geometry.getAttribute('aBase') as THREE.InstancedBufferAttribute\n const shape = geometry.getAttribute('aShape') as THREE.InstancedBufferAttribute\n let height = 0\n let width = 0\n let gain = 1\n let lightLevel = 0\n if (now.state === 'arming' && m.hasAt) {\n // Friction building: a warm point, growing, and a spark now and then.\n const p = Math.min(1, (m.time - m.armedAt) / ARM)\n height = HEIGHT * 0.18 * p\n width = height * 0.5\n gain = 0.5 + p\n lightLevel = 0.012 * p\n m.spark -= dt\n if (!scripted && m.spark <= 0) {\n m.spark = 0.07\n pool.spawn('ember', m.at.x, m.at.y, m.at.z)\n }\n } else if (now.state === 'lit' && m.hasAt) {\n const flare = 1 + FLARE_GAIN * (m.flare / FLARE) ** 2\n // Stretched thin and taller when swung; flattened against the paper.\n const stretch = 1 + Math.min(1.2, speed * 0.9)\n height = HEIGHT * flare * stretch * (now.touching ? 0.6 : 1)\n width = (HEIGHT * 0.32 * (now.touching ? 1.6 : 1)) / Math.sqrt(stretch)\n // A hard blow makes it gutter.\n gain = 1 - now.blow * 0.45 * (0.5 + 0.5 * Math.sin(m.time * 61))\n // Enough to warm the sheet as it comes near (§10.4), not to scald it:\n // at 0.12 the paper a few centimetres away read ~12× white and bloomed\n // into a peach disc on the capture of the contact frame.\n lightLevel = 0.035 * flare + (m.flash > 0 ? 0.25 : 0)\n }\n material.uniforms.uBlue!.value = Math.min(1, Math.max(0, speed * 0.7 - 0.35))\n material.uniforms.uGain!.value = gain\n material.uniforms.uTime!.value = m.time\n ;(material.uniforms.uLean!.value as THREE.Vector3).copy(m.lean)\n base.array[0] = m.at.x\n base.array[1] = m.at.y\n base.array[2] = m.at.z\n shape.array[0] = height\n shape.array[1] = width\n shape.array[2] = 0.37\n shape.array[3] = 1\n base.needsUpdate = true\n shape.needsUpdate = true\n geometry.instanceCount = height > 0 ? 1 : 0\n\n if (light.current) {\n // Half a flame up and a little toward the viewer — a light ON the paper\n // is a hot spot, not a flame's glow.\n light.current.position.set(m.at.x, m.at.y + height * 0.5, m.at.z + 0.06)\n light.current.intensity = lightLevel\n }\n pool.step(dt)\n })\n\n return (\n <>\n <mesh geometry={geometry} material={material} frustumCulled={false} renderOrder={3} />\n <pointLight ref={light} color=\"#ffb066\" intensity={0} decay={1.6} distance={0} />\n <FxParticles pool={pool} />\n </>\n )\n}\n","import { fxQualityFor, type FxQualityName, type FxQualitySettings } from '../quality'\n\n/**\n * The audio graph: a context, a ceiling, and somewhere for a sound to go.\n *\n * There was no audio anywhere in this library or its three apps before this —\n * not an `AudioContext`, not a clip. Sound was not under-built, it was\n * absent, and that is most of why the wind reads as weightless: a sheet\n * moving in silence is a picture of a sheet moving.\n *\n * Two decisions are baked in here rather than left to each effect.\n *\n * **Synthesis, not samples.** A triggered crumple clip is the same crumple\n * every time, it does not know how fast you crushed it, and two overlapping\n * copies turn to mush. Paper sounds are broadband noise shaped by an\n * envelope, which is the cheapest thing a synthesiser does and the thing\n * sample playback is worst at. It also means no licensing question at all, in\n * a repo that has already had to reason carefully about the licence on the\n * hand-tracking weights.\n *\n * **A hard ceiling, from the tier.** Fire and crumple both want to be grain\n * clouds — dozens of short bursts a second — and a grain cloud with no\n * ceiling is an unbounded number of live nodes on a phone that is already\n * running a camera, a hand tracker and a cloth simulation. So voices come\n * from a fixed pool sized by `fx/quality.ts`, and asking for one past the\n * ceiling steals the least audible voice rather than allocating. Dropping the\n * NEW sound would be the obvious alternative and it is wrong: the newest\n * sound is the one the viewer just caused, and silence in response to an\n * action reads as a broken page.\n *\n * **What is testable, and what is not.** Everything above is policy — pool\n * size, stealing, the unlock state machine, gain arithmetic — and none of it\n * needs a browser. The DSP is a handful of node connections that only a\n * browser can make. So the context is injected rather than constructed, the\n * policy is tested against a stub, and what ships to the browser is the same\n * code path with a real `AudioContext` in it.\n */\n\n/**\n * The part of the Web Audio API this uses.\n *\n * Written out rather than referring to `AudioContext` so that the policy can\n * be tested without a DOM, and so that the surface this depends on is small\n * enough to read. Anything added here is a new thing to stub.\n */\nexport interface AudioLike {\n readonly currentTime: number\n readonly state: 'suspended' | 'running' | 'closed'\n readonly destination: AudioNodeLike\n createGain(): GainLike\n createBufferSource(): BufferSourceLike\n /**\n * Every paper sound is noise with the wrong frequencies taken out of it —\n * a fire is a low rumble under a bright crackle, a tear is the same noise\n * with the bottom removed. Without a filter there is one sound in the whole\n * library: white noise at different volumes.\n */\n createBiquadFilter(): BiquadFilterLike\n /**\n * Where the sound is. A sheet you are holding at arm's length, burning at\n * one corner, is not a sound in the middle of your head.\n */\n createPanner(): PannerLike\n createBuffer(channels: number, length: number, sampleRate: number): AudioBufferLike\n readonly sampleRate: number\n resume(): Promise<void>\n close(): Promise<void>\n}\n\nexport interface AudioNodeLike {\n connect(destination: AudioNodeLike): void\n disconnect(): void\n}\n\nexport interface AudioParamLike {\n value: number\n setValueAtTime(value: number, when: number): void\n linearRampToValueAtTime(value: number, when: number): void\n cancelScheduledValues(when: number): void\n}\n\nexport interface GainLike extends AudioNodeLike {\n readonly gain: AudioParamLike\n}\n\nexport interface BiquadFilterLike extends AudioNodeLike {\n type: 'lowpass' | 'highpass' | 'bandpass' | 'notch' | 'peaking' | 'lowshelf' | 'highshelf' | 'allpass'\n readonly frequency: AudioParamLike\n readonly Q: AudioParamLike\n}\n\nexport interface PannerLike extends AudioNodeLike {\n panningModel: 'equalpower' | 'HRTF'\n distanceModel: 'linear' | 'inverse' | 'exponential'\n refDistance: number\n maxDistance: number\n rolloffFactor: number\n readonly positionX: AudioParamLike\n readonly positionY: AudioParamLike\n readonly positionZ: AudioParamLike\n}\n\nexport interface AudioBufferLike {\n getChannelData(channel: number): Float32Array\n readonly length: number\n}\n\nexport interface BufferSourceLike extends AudioNodeLike {\n buffer: AudioBufferLike | null\n loop: boolean\n /**\n * `offset` and `duration` are what make one shared second of noise into\n * every crackle in a fire: a different slice each time, ending itself\n * without anything having to schedule a stop.\n */\n start(when?: number, offset?: number, duration?: number): void\n stop(when?: number): void\n onended: (() => void) | null\n}\n\nexport interface FxAudioOptions {\n quality?: FxQualityName | FxQualitySettings\n /**\n * The context to use. Required, and never made for you: constructing an\n * `AudioContext` is a side effect with a user-visible cost (a browser tab\n * shows an audio indicator), and a class that made one on construction\n * would take that from a page that never asked. Get a browser one from\n * `createAudioContext()`, inside the gesture that should start the sound.\n */\n context: AudioLike\n /** Master level, 0..1. */\n volume?: number\n}\n\n/** A sound in flight. Handed back so a caller can shape or stop it. */\nexport interface Voice {\n readonly id: number\n /** What this voice is for. Only used to explain who got stolen. */\n readonly kind: string\n /**\n * How much this voice deserves to survive, 0..1.\n *\n * The ONLY input to stealing, and it is not loudness. A near-silent tail of\n * a crackle is worth less than a quiet new ignition, because one is ending\n * and the other is something the viewer just did. Effects set this and the\n * pool obeys it.\n */\n priority: number\n readonly gain: GainLike\n readonly startedAt: number\n /**\n * Hand a source to this voice, so it lives and dies with it.\n *\n * Every source that plays through a voice must be given to it, because the\n * voice is the only thing that knows when to stop it. A voice that did not\n * own its sources could only fade its gain when stolen — and a looping bed\n * behind a silent gain keeps running, and keeps its nodes, for the life of\n * the page.\n *\n * A one-shot that ends on its own ends the voice with it; a voice already\n * released stops the source at once rather than letting it play unheard.\n */\n own(source: BufferSourceLike): void\n /**\n * Hand a NODE to this voice, so it is disconnected with it.\n *\n * The same rule as {@link own}, for the filters and panners a sound is\n * shaped by. A crackle makes its own filter — a few dozen a second in a\n * good burn — and a filter left connected to a freed gain is the same leak\n * the sources used to have, in a part of the graph nothing was watching.\n */\n use(node: AudioNodeLike): void\n /** Stop and return this voice to the pool. */\n stop(): void\n}\n\n/** Fades applied when a voice is taken away, so stealing never clicks. */\nconst STEAL_FADE = 0.012\n\nexport class FxAudio {\n readonly quality: FxQualitySettings\n private readonly ctx: AudioLike\n private readonly master: GainLike\n private readonly live: Voice[] = []\n /**\n * What each voice owns, until the last of it has actually stopped.\n *\n * Outlives the voice's place in `live` on purpose: a stolen voice leaves the\n * pool at once, so the new sound can have its slot, but its sources are\n * still fading for a few milliseconds and its gain is still connected. The\n * nodes are freed when the last source reports `ended`, not before — cut\n * sooner and the fade that stops stealing from clicking is cut with it.\n */\n private readonly slots = new Map<\n number,\n { gain: GainLike; sources: BufferSourceLike[]; nodes: AudioNodeLike[] }\n >()\n private nextId = 1\n private unlocked = false\n private noise: AudioBufferLike | null = null\n\n constructor(options: FxAudioOptions) {\n const { quality = 'auto', context, volume = 0.8 } = options\n this.quality = typeof quality === 'string' ? fxQualityFor(quality) : quality\n // The type already requires it; this is for callers without one.\n if (!context) throw new Error('FxAudio needs a context — see createAudioContext()')\n this.ctx = context\n this.master = this.ctx.createGain()\n this.master.gain.value = volume\n this.master.connect(this.ctx.destination)\n }\n\n get context(): AudioLike {\n return this.ctx\n }\n\n /** Live voices right now. Never more than the tier's ceiling. */\n get voices(): readonly Voice[] {\n return this.live\n }\n\n get isUnlocked(): boolean {\n return this.unlocked\n }\n\n get volume(): number {\n return this.master.gain.value\n }\n\n set volume(value: number) {\n this.master.gain.value = Math.min(1, Math.max(0, value))\n }\n\n /**\n * Let sound happen, from inside a user gesture.\n *\n * Every browser starts an `AudioContext` suspended until a real interaction\n * resumes it, and a page that calls this from anywhere else is a page whose\n * audio silently never starts. Here the cost is zero: `/hands` already has\n * a button that turns the camera on, and nothing can be heard before the\n * camera is on anyway.\n *\n * Safe to call more than once — it is a latch, not a toggle.\n */\n async unlock(): Promise<void> {\n if (this.unlocked) return\n if (this.ctx.state === 'suspended') await this.ctx.resume()\n this.unlocked = this.ctx.state === 'running'\n }\n\n /**\n * One second of white noise, made once and shared.\n *\n * Nearly every paper sound is filtered noise — a crumple is a cloud of\n * short bursts of it, a fire is a bed of it, a tear is a fast train of it —\n * so this is the single most reused object in the graph. Generating it per\n * voice would allocate a second of audio per crackle.\n */\n noiseBuffer(): AudioBufferLike {\n if (this.noise) return this.noise\n const length = Math.floor(this.ctx.sampleRate)\n const buffer = this.ctx.createBuffer(1, length, this.ctx.sampleRate)\n const data = buffer.getChannelData(0)\n // Deterministic rather than Math.random: a reproducible bed is one that\n // can be compared between two runs when something sounds wrong.\n let seed = 0x9e3779b9\n for (let i = 0; i < length; i++) {\n seed = (Math.imul(seed, 1664525) + 1013904223) | 0\n data[i] = (seed >>> 0) / 2147483648 - 1\n }\n this.noise = buffer\n return buffer\n }\n\n /**\n * Take a voice, stealing the least deserving one if the pool is full.\n *\n * Returns null only when the ceiling is zero or the caller's priority is\n * below everything already playing — the one case where refusing is right,\n * because the alternative is cutting off something more important to play\n * something less.\n */\n take(kind: string, priority = 0.5): Voice | null {\n if (this.quality.voices <= 0) return null\n\n if (this.live.length >= this.quality.voices) {\n // The weakest voice, tie-broken by age so the older of two equals goes.\n let weakest = 0\n for (let i = 1; i < this.live.length; i++) {\n const a = this.live[i]!\n const b = this.live[weakest]!\n if (a.priority < b.priority || (a.priority === b.priority && a.startedAt < b.startedAt)) {\n weakest = i\n }\n }\n const victim = this.live[weakest]!\n if (victim.priority > priority) return null\n victim.stop()\n }\n\n const gain = this.ctx.createGain()\n gain.connect(this.master)\n const id = this.nextId++\n this.slots.set(id, { gain, sources: [], nodes: [] })\n const voice: Voice = {\n id,\n kind,\n priority,\n gain,\n startedAt: this.ctx.currentTime,\n own: (source) => this.own(id, source),\n use: (node) => this.useNode(id, node),\n stop: () => this.release(id),\n }\n this.live.push(voice)\n return voice\n }\n\n private own(id: number, source: BufferSourceLike): void {\n const slot = this.slots.get(id)\n const alive = this.live.some((v) => v.id === id)\n if (!slot || !alive) {\n // A source given to a voice that has already gone: it must not play\n // through a gain that is fading out, or through nothing at all.\n stopNow(source)\n source.disconnect()\n return\n }\n slot.sources.push(source)\n source.onended = () => this.ended(id, source)\n }\n\n private useNode(id: number, node: AudioNodeLike): void {\n const slot = this.slots.get(id)\n const alive = this.live.some((v) => v.id === id)\n if (!slot || !alive) {\n // Given to a voice that has already gone: it feeds nothing, so it goes.\n node.disconnect()\n return\n }\n slot.nodes.push(node)\n }\n\n private ended(id: number, source: BufferSourceLike): void {\n source.disconnect()\n const slot = this.slots.get(id)\n if (!slot) return\n const at = slot.sources.indexOf(source)\n if (at >= 0) slot.sources.splice(at, 1)\n if (slot.sources.length > 0) return\n // The last of it. If the voice is still in the pool it finished on its\n // own — a one-shot — and is done; if it was released, its fade is over.\n if (this.live.some((v) => v.id === id)) this.release(id)\n else this.free(id)\n }\n\n /** Disconnect a voice's gain. Only once nothing that feeds it is still running. */\n private free(id: number): void {\n const slot = this.slots.get(id)\n if (!slot) return\n for (const node of slot.nodes) node.disconnect()\n slot.gain.disconnect()\n this.slots.delete(id)\n }\n\n /**\n * Return a voice to the pool, fading it out first.\n *\n * The fade is the whole reason this is not just a splice. Cutting a\n * waveform mid-cycle produces a step, and a step is a click — audible,\n * cheap-sounding, and most likely to happen exactly when the most is going\n * on, since that is when voices get stolen.\n */\n private release(id: number): void {\n const index = this.live.findIndex((v) => v.id === id)\n if (index < 0) return\n const [voice] = this.live.splice(index, 1)\n if (!voice) return\n const slot = this.slots.get(id)\n // Nothing is playing through it, so there is nothing to fade and nothing\n // to wait for.\n if (!slot || slot.sources.length === 0) {\n this.free(id)\n return\n }\n const now = this.ctx.currentTime\n voice.gain.gain.cancelScheduledValues(now)\n voice.gain.gain.setValueAtTime(voice.gain.gain.value, now)\n voice.gain.gain.linearRampToValueAtTime(0, now + STEAL_FADE)\n // Stopped at the END of the fade, which is what lets `ended` free the\n // nodes without cutting the ramp short. This is the half the first\n // version was missing: it faded the gain and never stopped a thing, so a\n // looping bed on a stolen voice ran silently forever.\n for (const source of [...slot.sources]) {\n try {\n source.stop(now + STEAL_FADE)\n } catch {\n // Never started: nothing will ever report `ended`, so do it here.\n this.ended(id, source)\n }\n }\n }\n\n /** Stop everything. The panic button, and what unmounting calls. */\n stopAll(): void {\n for (const voice of [...this.live]) voice.stop()\n }\n\n /**\n * A short tone, to prove the chain end to end.\n *\n * Deliberately part of the shipped surface rather than a test fixture:\n * \"is the audio graph actually connected\" is a question that comes up on\n * every device, and the honest answer is a sound you can hear. It uses the\n * same path everything else does — a voice from the pool, the noise buffer,\n * the master gain — so if this is audible the path works.\n */\n test(duration = 0.15): Voice | null {\n const voice = this.take('test', 1)\n if (!voice) return null\n const source = this.ctx.createBufferSource()\n source.buffer = this.noiseBuffer()\n source.connect(voice.gain)\n const now = this.ctx.currentTime\n voice.gain.gain.setValueAtTime(0, now)\n voice.gain.gain.linearRampToValueAtTime(0.4, now + 0.01)\n voice.gain.gain.linearRampToValueAtTime(0, now + duration)\n source.start(now)\n source.stop(now + duration)\n // Owned, so its end frees the voice and every node it used.\n voice.own(source)\n return voice\n }\n\n /** Release the context. Nothing survives this. */\n async dispose(): Promise<void> {\n this.stopAll()\n // Closing the context ends every source at once, and `ended` may never\n // arrive after that — so let go of everything here rather than wait.\n for (const id of [...this.slots.keys()]) this.free(id)\n this.master.disconnect()\n await this.ctx.close()\n }\n}\n\n/** Stop a source that may or may not have been started. */\nfunction stopNow(source: BufferSourceLike): void {\n try {\n source.stop()\n } catch {\n // Never started — which is the same as stopped.\n }\n}\n\n/**\n * Make a real browser context.\n *\n * Separate from the constructor so that importing this module never starts an\n * audio context — a browser shows an indicator for one, and a library that\n * lights it up on import is a library that has taken something that was not\n * offered.\n */\nexport function createAudioContext(): AudioLike {\n const Ctor = (globalThis as { AudioContext?: new () => AudioLike }).AudioContext\n if (!Ctor) throw new Error('no Web Audio support in this environment')\n return new Ctor()\n}\n","import type { FieldStats } from '../field'\nimport type { BiquadFilterLike, FxAudio, PannerLike, Voice } from './graph'\n\n/**\n * What a burning sheet sounds like, driven by the numbers that make it burn.\n *\n * Two layers, and the split is the whole design.\n *\n * A **bed**: one looping band of filtered noise whose level follows the\n * LENGTH of the burn front. Not the burnt area — a fire gets louder as its\n * edge gets longer, and a nearly-consumed sheet is quiet again, which a level\n * driven by char would get exactly backwards. It brightens as it grows too:\n * a big fire is not a small fire turned up.\n *\n * A **crackle**: one short burst of bright noise for every few texels that\n * char, so the rate is the rate the paper is actually catching. Each is a\n * different slice of the shared second of noise through its own bandpass, so\n * no two are the same sound; each ends itself.\n *\n * Both read `FieldStats` and nothing else. Sound and picture therefore cannot\n * drift apart: the number that raises the flame's glow is the number that\n * raises its voice. A seed makes a replayed burn crackle the same way, which\n * is the same property the untiered simulation exists to give.\n */\n\nexport interface FireSoundOptions {\n /** Loudest the bed gets, 0..1. */\n volume?: number\n /**\n * The front length that counts as a fire at full blast, as the fraction of\n * the sheet's texels on it. Four percent of a 64² grid is about 160 cells,\n * which is a sheet burning across its whole width.\n */\n fullFront?: number\n /** Crackles per texel that chars. */\n crackle?: number\n /** So the same burn sounds the same twice. */\n seed?: number\n}\n\nconst DEFAULTS: Required<FireSoundOptions> = { volume: 0.55, fullFront: 0.04, crackle: 0.3, seed: 5 }\n\n/** How fast the bed follows the front, in seconds. Fire breathes; it does not jump. */\nconst BED_SMOOTHING = 0.12\n\n/**\n * At most this many crackles in one frame.\n *\n * A frame that chars a hundred texels at once — a whole dry sheet catching —\n * would otherwise ask for a hundred voices, steal every one of them from the\n * others and turn the pool over twice. Past a handful at once it is a noise\n * burst anyway, which is what the bed is for.\n */\nconst CRACKLES_PER_FRAME = 4\n\n/** A place in the world, as little of one as this needs. */\nexport interface SoundAt {\n readonly x: number\n readonly y: number\n readonly z: number\n}\n\nexport class FireSound {\n private readonly audio: FxAudio\n private readonly o: Required<FireSoundOptions>\n private bed: { voice: Voice; filter: BiquadFilterLike; panner: PannerLike | null } | null = null\n private level = 0\n private debt = 0\n private state: number\n\n constructor(audio: FxAudio, options: FireSoundOptions = {}) {\n this.audio = audio\n this.o = { ...DEFAULTS, ...options }\n this.state = this.o.seed >>> 0 || 1\n }\n\n /** True while the bed is playing. */\n get burning(): boolean {\n return this.bed !== null\n }\n\n /**\n * Call once a frame with what the field just did, and where the sheet is.\n *\n * `at` is optional: without it the fire is not placed in the room, which is\n * right for a sheet filling the frame and wrong for one held at arm's\n * length.\n */\n update(dt: number, stats: FieldStats, at?: SoundAt | null): void {\n const target = stats.front > 0 ? Math.min(1, Math.sqrt(stats.front / this.o.fullFront)) : 0\n // Toward the target rather than at it, so the level cannot flicker with\n // the front's own frame-to-frame wobble.\n const follow = dt > 0 ? Math.min(1, dt / BED_SMOOTHING) : 1\n this.level += (target - this.level) * follow\n\n if (target > 0 || this.level > 0.02) this.startBed(at)\n else this.stopBed()\n if (this.bed) this.driveBed(at)\n\n this.debt += stats.charred * this.o.crackle\n let budget = CRACKLES_PER_FRAME\n while (this.debt >= 1 && budget > 0) {\n this.debt -= 1\n budget--\n this.crackle()\n }\n // Whatever the budget could not play is dropped outright, down to the\n // fraction still accumulating toward the next one. A crackle carried into\n // a frame where nothing charred is a sound with no cause — and a sheet\n // that flashes over would otherwise go on crackling after it had stopped\n // burning.\n if (budget === 0) this.debt %= 1\n }\n\n /**\n * A match struck: the scratch of the head across the box, then the hiss of\n * it flaring as the head burns off (spec §10.2). Two bursts of the shared\n * noise, one bright and short, one breathier and longer.\n */\n strike(): void {\n this.burst('fire-strike', 0.9, { type: 'bandpass', frequency: 3200, q: 1.1 }, 0.004, 0.07, 0.8)\n this.burst('fire-flare', 0.8, { type: 'highpass', frequency: 1800, q: 0.7 }, 0.03, 0.45, 0.45, 0.05)\n }\n\n /** Blown out: a soft, low breath of noise (spec §10.6). */\n puff(): void {\n this.burst('fire-puff', 0.6, { type: 'lowpass', frequency: 520, q: 0.6 }, 0.02, 0.28, 0.55)\n }\n\n /** One ember popping in the air — a tiny click on the frame it flashes (spec §8.1). */\n pop(): void {\n const f = 3000 + this.next() * 3000\n this.burst(\n 'fire-pop',\n 0.2,\n { type: 'bandpass', frequency: f, q: 3 },\n 0.001,\n 0.012 + this.next() * 0.01,\n 0.35,\n )\n }\n\n /** Silence, now — the flame blown out, or the page going away. */\n stop(): void {\n this.stopBed()\n this.level = 0\n this.debt = 0\n }\n\n private startBed(at?: SoundAt | null): void {\n if (this.bed) return\n const ctx = this.audio.context\n // Above the crackles on purpose: the bed IS the fire, and a pool full of\n // crackles that stole it would leave a burning sheet sounding like static.\n const voice = this.audio.take('fire-bed', 0.7)\n if (!voice) return\n const source = ctx.createBufferSource()\n source.buffer = this.audio.noiseBuffer()\n source.loop = true\n const filter = ctx.createBiquadFilter()\n filter.type = 'bandpass'\n filter.frequency.value = 380\n filter.Q.value = 0.7\n const panner = at ? ctx.createPanner() : null\n source.connect(filter)\n if (panner) {\n panner.panningModel = 'equalpower'\n panner.distanceModel = 'inverse'\n panner.refDistance = 1\n filter.connect(panner)\n panner.connect(voice.gain)\n } else {\n filter.connect(voice.gain)\n }\n // From silence: a bed that arrives at full level is a click.\n voice.gain.gain.setValueAtTime(0, ctx.currentTime)\n source.start(ctx.currentTime)\n voice.own(source)\n voice.use(filter)\n if (panner) voice.use(panner)\n this.bed = { voice, filter, panner }\n }\n\n private driveBed(at?: SoundAt | null): void {\n const bed = this.bed\n if (!bed) return\n const ctx = this.audio.context\n const now = ctx.currentTime\n const gain = bed.voice.gain.gain\n gain.cancelScheduledValues(now)\n gain.setValueAtTime(gain.value, now)\n gain.linearRampToValueAtTime(this.level * this.o.volume, now + BED_SMOOTHING / 2)\n // A bigger fire is brighter as well as louder.\n bed.filter.frequency.setValueAtTime(380 + 900 * this.level, now)\n if (at && bed.panner) {\n bed.panner.positionX.setValueAtTime(at.x, now)\n bed.panner.positionY.setValueAtTime(at.y, now)\n bed.panner.positionZ.setValueAtTime(at.z, now)\n }\n }\n\n private stopBed(): void {\n if (!this.bed) return\n this.bed.voice.stop()\n this.bed = null\n }\n\n private crackle(): void {\n const voice = this.audio.take('fire-crackle', 0.25)\n if (!voice) return\n const ctx = this.audio.context\n const now = ctx.currentTime\n const buffer = this.audio.noiseBuffer()\n const seconds = buffer.length / ctx.sampleRate\n const duration = 0.018 + this.next() * 0.05\n const source = ctx.createBufferSource()\n source.buffer = buffer\n const filter = ctx.createBiquadFilter()\n filter.type = 'bandpass'\n // High and narrow: a crackle is the sound of fibres letting go, not a\n // small version of the rumble.\n filter.frequency.value = 1400 + this.next() * 2800\n filter.Q.value = 1.6 + this.next() * 3\n source.connect(filter)\n filter.connect(voice.gain)\n const peak = (0.25 + this.next() * 0.5) * this.o.volume * Math.max(0.25, this.level)\n const gain = voice.gain.gain\n gain.setValueAtTime(0, now)\n gain.linearRampToValueAtTime(peak, now + 0.004)\n gain.linearRampToValueAtTime(0, now + duration)\n // A different slice of the shared noise every time, ending itself.\n source.start(now, this.next() * Math.max(0, seconds - duration), duration)\n voice.own(source)\n voice.use(filter)\n }\n\n /**\n * One shaped burst of the shared noise through one filter — what strike,\n * puff and pop are made of. `delay` starts it a moment late, which is how\n * the flare follows the scratch.\n */\n private burst(\n name: string,\n priority: number,\n filterSpec: { type: 'bandpass' | 'highpass' | 'lowpass'; frequency: number; q: number },\n attack: number,\n duration: number,\n level: number,\n delay = 0,\n ): void {\n const voice = this.audio.take(name, priority)\n if (!voice) return\n const ctx = this.audio.context\n const now = ctx.currentTime + delay\n const buffer = this.audio.noiseBuffer()\n const seconds = buffer.length / ctx.sampleRate\n const source = ctx.createBufferSource()\n source.buffer = buffer\n const filter = ctx.createBiquadFilter()\n filter.type = filterSpec.type\n filter.frequency.value = filterSpec.frequency\n filter.Q.value = filterSpec.q\n source.connect(filter)\n filter.connect(voice.gain)\n const gain = voice.gain.gain\n gain.setValueAtTime(0, now)\n gain.linearRampToValueAtTime(level * this.o.volume, now + attack)\n gain.linearRampToValueAtTime(0, now + attack + duration)\n source.start(now, this.next() * Math.max(0, seconds - attack - duration), attack + duration)\n voice.own(source)\n voice.use(filter)\n }\n\n /** xorshift32 — its own stream, so nothing else can shift the crackle. */\n private next(): number {\n let s = this.state\n s ^= s << 13\n s ^= s >>> 17\n s ^= s << 5\n this.state = s >>> 0\n return this.state / 4294967296\n }\n}\n"],"mappings":"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","names":["rim","THREE","import_postprocessing","import_fiber","import_react","THREE","import_jsx_runtime","target","target","import_react","import_jsx_runtime","import_fiber","import_react","THREE","FRAGMENT","import_jsx_runtime","THREE","import_fiber","import_react","import_jsx_runtime","noise1","hash1","THREE","import_fiber","import_react","THREE","NOISE","import_jsx_runtime","THREE","import_fiber","import_react","import_jsx_runtime","THREE","import_fiber","import_react","import_jsx_runtime","target","DEFAULTS","target"]}
|