paperlab 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +7 -5
  2. package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-V6YDK4H2.js} +23 -5
  3. package/dist/FxPostPass-V6YDK4H2.js.map +1 -0
  4. package/dist/chunk-232SU3AQ.js +34 -0
  5. package/dist/chunk-232SU3AQ.js.map +1 -0
  6. package/dist/{chunk-GB6BMHC3.js → chunk-7WOGU2D4.js} +7 -19
  7. package/dist/{chunk-GB6BMHC3.js.map → chunk-7WOGU2D4.js.map} +1 -1
  8. package/dist/{chunk-MZCAN3AR.js → chunk-SVWWC5UL.js} +576 -160
  9. package/dist/chunk-SVWWC5UL.js.map +1 -0
  10. package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-nF1fgK7P.d.cts} +31 -3
  11. package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-nF1fgK7P.d.ts} +31 -3
  12. package/dist/fx.cjs +320 -91
  13. package/dist/fx.cjs.map +1 -1
  14. package/dist/fx.d.cts +81 -55
  15. package/dist/fx.d.ts +81 -55
  16. package/dist/fx.js +278 -48
  17. package/dist/fx.js.map +1 -1
  18. package/dist/index.cjs +606 -181
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +13 -5
  21. package/dist/index.d.ts +13 -5
  22. package/dist/index.js +17 -4
  23. package/dist/index.js.map +1 -1
  24. package/dist/{slots-C5PqY0Q5.d.cts → slots-D_EE8GXn.d.cts} +12 -0
  25. package/dist/{slots-C5PqY0Q5.d.ts → slots-D_EE8GXn.d.ts} +12 -0
  26. package/dist/stage.cjs +600 -188
  27. package/dist/stage.cjs.map +1 -1
  28. package/dist/stage.d.cts +1 -1
  29. package/dist/stage.d.ts +1 -1
  30. package/dist/stage.js +2 -2
  31. package/package.json +1 -1
  32. package/dist/FxPostPass-M66QMVVC.js.map +0 -1
  33. package/dist/chunk-7TLIWPGM.js +0 -36
  34. package/dist/chunk-7TLIWPGM.js.map +0 -1
  35. package/dist/chunk-MZCAN3AR.js.map +0 -1
package/dist/fx.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DamageSource } from './damageContract-DPF1YFLZ.cjs';
2
- export { a as DAMAGE_CHANNELS, b as DAMAGE_LOOK_DEFAULTS, c as DamageLook } from './damageContract-DPF1YFLZ.cjs';
1
+ import { D as DamageSource } from './damageContract-nF1fgK7P.cjs';
2
+ export { a as DAMAGE_CHANNELS, b as DAMAGE_LOOK_DEFAULTS, c as DamageLook } from './damageContract-nF1fgK7P.cjs';
3
3
  import * as react from 'react';
4
4
  import { ReactNode } from 'react';
5
5
  import * as THREE from 'three';
@@ -400,7 +400,7 @@ declare const particlePresets: {
400
400
  * simulation, on purpose; see the fx plan.
401
401
  */
402
402
  readonly smoke: {
403
- readonly life: readonly [1.8, 3.5];
403
+ readonly life: readonly [1.4, 2.6];
404
404
  readonly speed: readonly [0.05, 0.15];
405
405
  readonly direction: readonly [0, 1, 0];
406
406
  readonly spread: 0.3;
@@ -408,9 +408,9 @@ declare const particlePresets: {
408
408
  readonly drag: 0.9;
409
409
  readonly jitter: 0.3;
410
410
  readonly windCatch: 1;
411
- readonly size: readonly [0.05, 0.35];
411
+ readonly size: readonly [0.05, 0.18];
412
412
  readonly color: readonly [readonly [0.147, 0.13, 0.117], readonly [0.2, 0.19, 0.18]];
413
- readonly alpha: readonly [0.08, 0];
413
+ readonly alpha: readonly [0.07, 0];
414
414
  readonly spin: 0.4;
415
415
  readonly blend: "normal";
416
416
  readonly shape: "soft";
@@ -422,16 +422,16 @@ declare const particlePresets: {
422
422
  * burn that proves paper was there.
423
423
  */
424
424
  readonly ash: {
425
- readonly life: readonly [2.5, 5];
425
+ readonly life: readonly [1.2, 2.6];
426
426
  readonly speed: readonly [0.14, 0.3];
427
427
  readonly direction: readonly [0, 1, 0];
428
428
  readonly spread: 0.55;
429
- readonly lift: -0.22;
429
+ readonly lift: -0.42;
430
430
  readonly drag: 1.6;
431
431
  readonly jitter: 1.2;
432
432
  readonly windCatch: 1;
433
433
  readonly size: readonly [0.03, 0.026];
434
- readonly color: readonly [readonly [0.07, 0.065, 0.06], readonly [0.12, 0.115, 0.11]];
434
+ readonly color: readonly [readonly [0.04, 0.037, 0.034], readonly [0.07, 0.066, 0.062]];
435
435
  readonly alpha: readonly [0.95, 0];
436
436
  readonly spin: 4;
437
437
  readonly blend: "normal";
@@ -540,19 +540,13 @@ interface FireEmitterOptions {
540
540
  };
541
541
  }
542
542
  /**
543
- * How much each kind leaves the front, per texel per second.
544
- *
545
- * `smoke` is 0 on purpose. There were three smoke systems running at once —
546
- * the fire simulator's own, these sprite puffs at 8% alpha, and the smoulder
547
- * wisp and the review's note was that only one of them should exist. The
548
- * simulator's is the one that belongs to a burning sheet: it is the same
549
- * fluid the flames are made of, so it rises with them instead of beside them.
550
- * `FxWisps` still carries the thread after the flames are out, which is the
551
- * one moment the simulator has nothing left to make. The preset stays: it is
552
- * a parameter set, and anything may still ask the pool for smoke.
553
- *
554
- * Embers and ash are both down. Ash at 0.86 a texel was a flake for nearly
555
- * every one that burnt through — a dust shower. Fewer and larger is the note.
543
+ * How much each kind leaves the front, per texel per second — Noor's tune in
544
+ * the lab on 2026-09-13, which the lab starts from and `/hands` inherits.
545
+ *
546
+ * The simulator's smoke is the main smoke of a burning sheet, since it is the
547
+ * same fluid the flames are made of; these sprite puffs, once switched off
548
+ * for doubling it, are back as a light thread beside it. Ash at 0.86 a texel
549
+ * was a dust shower; 0.36 is a light fall of it.
556
550
  */
557
551
  declare const fireEmitterDefaults: Required<FireEmitterOptions>;
558
552
  /**
@@ -744,6 +738,15 @@ interface FxPostProps {
744
738
  locate?: SurfaceLocator;
745
739
  /** Heat haze in pixels at 1080p, overriding the tier's; 0 turns it off. */
746
740
  haze?: number;
741
+ /**
742
+ * Shallow focus on the burn, 0..1; 0 keeps everything sharp.
743
+ *
744
+ * A macro lens held close to a burning edge has a few millimetres of
745
+ * focus and nothing else (§K4): the rim is sharp, the rest of the sheet
746
+ * goes soft. It focuses on the fire itself — the burning rim is what the
747
+ * shot is about — so it needs `field` and `locate` like the haze does.
748
+ */
749
+ focus?: number;
747
750
  }
748
751
  /**
749
752
  * `fallback={null}`: the scene behind it is already drawn. One frame without
@@ -840,13 +843,13 @@ declare const FIRE_SOOT_SCALE = 3;
840
843
  *
841
844
  * It lives beside the threshold because the two are one setting: bloom is a
842
845
  * multiplier on whatever clears the threshold, so its right value depends
843
- * entirely on how much fire is authored above it. 1.55 was tuned when the
844
- * flames were UNDER the threshold and bloom had almost nothing to work on —
845
- * measured, 0.07% of the frame. Once the flames cleared it, that same 1.55
846
- * tinted the entire black stage olive. Swept at 0.25, 0.6 and 1.2 against the
847
- * peak frame; past about 0.4 the stage stops being black.
846
+ * entirely on how much fire is authored above it. With a core authored to
847
+ * over-expose, 0.55 was the most the black stage could take; the flame of
848
+ * {@link FIRE_ZONES} (Noor, 2026-09-13) authors none of it past the
849
+ * threshold, and 1.25 is her tune for it. `pnpm test:fire-budget` holds the
850
+ * stage black and the paper unbloomed whatever this is set to.
848
851
  */
849
- declare const FX_BLOOM = 0.55;
852
+ declare const FX_BLOOM = 1.25;
850
853
  /**
851
854
  * A flame, in four zones — drawn the way the sheet's burn is (ember line, ash
852
855
  * lip, char, scorch): each part named for what it is, with its own controls.
@@ -854,11 +857,10 @@ declare const FX_BLOOM = 0.55;
854
857
  *
855
858
  * root the blue leading edge where fresh gas meets the air at the paper.
856
859
  * Its light comes from excited molecules, not soot, so it is faint
857
- * and blue — and it starts OFF, because blue light added over cream
858
- * paper reads lavender.
859
- * core the hottest gas, where soot forms densest and glows pale. The only
860
- * zone allowed to over-expose past the bloom threshold, and so the
861
- * one that blooms.
860
+ * and blue — and only ever a trace, because blue light added over
861
+ * cream paper reads lavender.
862
+ * core the hottest gas, where soot forms densest and glows pale the
863
+ * brightest zone, and the one the bloom finds first.
862
864
  * body the luminous bulk: soot glowing yellow-orange as it rises.
863
865
  * tip where soot cools and burns off at the outer edge — orange-red,
864
866
  * dimmer, tearing into tongues. What survives escapes as smoke.
@@ -876,12 +878,11 @@ declare const FX_BLOOM = 0.55;
876
878
  * multiples of paper white; colours are sRGB, the way a colour picker gives
877
879
  * them.
878
880
  *
879
- * The body is ABOVE paper white. It was once held below it, because under a
880
- * bright preset paper sits at the top of the tone curve and anything brighter
881
- * loses its colour and a flame dimmer than the paper behind it, drawn
882
- * opaque, is a yellow decal on the sheet, not light. Fire photographed in a
883
- * bright room DOES wash out; that is the lighting's to answer (the lab shoots
884
- * under `noir`, as the references were), not the flame's.
881
+ * How bright the body is against paper white is a judgement, and it has gone
882
+ * both ways. Above it, a flame keeps its light in a bright room but loses its
883
+ * colour to the tone curve; below it, the colour holds, and a flame drawn
884
+ * opaque risks reading as a decal on the sheet rather than as light. The
885
+ * defaults sit below it (Noor, 2026-09-13), with the bloom carrying the glow.
885
886
  */
886
887
  interface FireZones {
887
888
  root: {
@@ -912,10 +913,12 @@ type FireZonesInput = {
912
913
  [Z in keyof FireZones]?: Partial<FireZones[Z]>;
913
914
  };
914
915
  /**
915
- * The defaults reproduce the look arrived at by measurement against
916
- * Flame_base.png (yellow ~30%, orange ~40%, pale ~18% of a flame's pixels),
917
- * now expressed zone by zone. Each colour is the linear emission the previous
918
- * ramp used in that band, written as the sRGB a picker would show.
916
+ * Noor's tune in the lab on 2026-09-13, and the source of truth for how a
917
+ * flame looks: the lab starts from it and `/hands` inherits it. A gentler
918
+ * flame than the one it replaced every zone sits under the bloom threshold,
919
+ * so its glow comes from the bloom's strength ({@link FX_BLOOM}) spreading
920
+ * the brightest gas, not from a core authored to over-expose. The values it
921
+ * replaced, measured against Flame_base.png, are in the history of this file.
919
922
  */
920
923
  declare const FIRE_ZONES: FireZones;
921
924
  /** The defaults with `input` laid over them, zone by zone. */
@@ -1313,25 +1316,48 @@ declare class FireFluid {
1313
1316
  interface FxFireLightProps {
1314
1317
  field: DamageField;
1315
1318
  locate: SurfaceLocator;
1316
- /** How bright the fire is per unit of burning front. Tuned against Hero.png. */
1319
+ /** How bright the fire is per unit of burning front. */
1317
1320
  gain?: number;
1321
+ /**
1322
+ * The brightest cluster's light casts shadows: a curled edge, a lifted
1323
+ * plate and a hanging flap throw flickering shade across the sheet, which
1324
+ * is much of what makes firelight read as firelight. One shadow pass a
1325
+ * frame, so off unless asked for.
1326
+ */
1327
+ shadows?: boolean;
1318
1328
  }
1319
1329
  /**
1320
1330
  * Fire is a light source (§4.8, §7). A fire that changes nothing around it
1321
1331
  * looks pasted on.
1322
1332
  *
1323
- * A warm point light about 1900 K at the middle of the burning rim,
1324
- * raised by half a flame, as bright as the front is long and flickering on
1325
- * the very noise the flames puff on, so light and flame agree. It reaches the
1326
- * whole sheet: `decay` is held below the physical 2, because a sheet is a
1327
- * small thing next to a fire and Hero.png warms it to the top edge.
1328
- *
1329
- * And a dimmer twin on the FAR side of the sheet, for translucency: paper is
1330
- * thin, and a fire in front of it glows through to the back, warm and
1331
- * diffused (§7). A second light is the whole trick the sheet's own
1332
- * material already lights its back face from whatever is behind it.
1333
+ * One warm point light for each place the fire is gathered up to
1334
+ * {@link FIRE_CLUSTERS} of them, from {@link fireStateOf} standing at its
1335
+ * cluster raised by half a flame, as bright as its share of the fire and
1336
+ * flickering on its own tongues' puff. A tall tongue on the left lights the
1337
+ * left. It used to be one light at the middle of the whole rim, which lit the
1338
+ * sheet the same wherever the flames actually were.
1339
+ *
1340
+ * Each light's colour follows how hot the paper under its tongues is: the
1341
+ * pale peach of a fire at its height, deepening toward orange as it cools —
1342
+ * never toward red, which on white paper is the pink the spec forbids.
1343
+ * Together they give off what the single light did: the front's length times
1344
+ * `gain`, shared out.
1345
+ *
1346
+ * And one dimmer light on the FAR side of the sheet, at the fire as a whole,
1347
+ * for translucency: paper is thin, and a fire in front of it glows through to
1348
+ * the back (§7). The sheet's own material lights its back face from whatever
1349
+ * is behind it.
1350
+ *
1351
+ * Every light is mounted all the time and only its intensity changes, so a
1352
+ * fire gathering into more places or fewer never makes three recompile a
1353
+ * shader.
1354
+ */
1355
+ declare function FxFireLight({ field, locate, gain, shadows }: FxFireLightProps): react.JSX.Element;
1356
+ /**
1357
+ * Light per unit of front length; the front runs to ~0.045 at a burn's peak.
1358
+ * Noor's tune, 2026-09-13 (it was 42).
1333
1359
  */
1334
- declare function FxFireLight({ field, locate, gain }: FxFireLightProps): react.JSX.Element;
1360
+ declare const FIRE_LIGHT_GAIN = 26;
1335
1361
 
1336
1362
  /** What the page says about the match this frame. Written in place — never a prop change. */
1337
1363
  interface MatchFlameState {
@@ -1793,4 +1819,4 @@ declare class FireSound {
1793
1819
  private next;
1794
1820
  }
1795
1821
 
1796
- export { Afterglow, type AfterglowOptions, type AudioLike, type BiquadFilterLike, CHAR, DamageField, type DamageFieldOptions, DamageSource, FIELD_SIZE, FIRE_GLOW, FIRE_HEAT_SCALE, FIRE_SOOT_SCALE, FIRE_ZONES, FIXED_DT, FLAME_HEIGHT, FX_BLOOM, FX_BLOOM_THRESHOLD, FX_INITIAL_TIER, FX_TIER_ORDER, type FieldStats, FireEmitter, type FireEmitterOptions, FireFluid, type FireFluidParams, FireSound, type FireSoundOptions, type FireZones, type FireZonesInput, type FlameAnchor, type FluidGrid, FxAudio, type FxAudioOptions, type FxFilm, FxFireFluid, type FxFireFluidProps, FxFireLight, type FxFireLightProps, FxFlames, type FxFlamesProps, FxMatchFlame, type FxMatchFlameProps, FxParticles, type FxParticlesProps, FxPost, type FxPostProps, type FxQualityName, type FxQualitySettings, type FxQualityTier, FxWisps, type FxWispsProps, HEAT, type MatchFlameState, PAPER_WHITE, PRESENCE, type PannerLike, ParticlePool, type ParticlePreset, type ParticlePresetName, type ParticleTarget, SATURATION, type SolverUniforms, type SoundAt, type SurfaceLocator, type Voice, createAudioContext, emit, emitHex, fireEmitterDefaults, fireFluidControls, fireFluidDefaults, fireZones, flameAnchors, flamePuff, fxQualityFor, fxQualityNames, fxQualityTiers, hexToLinear, luminance, particlePresets, solverUniforms, srgbToLinear, timesPaperWhite };
1822
+ export { Afterglow, type AfterglowOptions, type AudioLike, type BiquadFilterLike, CHAR, DamageField, type DamageFieldOptions, DamageSource, FIELD_SIZE, FIRE_GLOW, FIRE_HEAT_SCALE, FIRE_LIGHT_GAIN, FIRE_SOOT_SCALE, FIRE_ZONES, FIXED_DT, FLAME_HEIGHT, FX_BLOOM, FX_BLOOM_THRESHOLD, FX_INITIAL_TIER, FX_TIER_ORDER, type FieldStats, FireEmitter, type FireEmitterOptions, FireFluid, type FireFluidParams, FireSound, type FireSoundOptions, type FireZones, type FireZonesInput, type FlameAnchor, type FluidGrid, FxAudio, type FxAudioOptions, type FxFilm, FxFireFluid, type FxFireFluidProps, FxFireLight, type FxFireLightProps, FxFlames, type FxFlamesProps, FxMatchFlame, type FxMatchFlameProps, FxParticles, type FxParticlesProps, FxPost, type FxPostProps, type FxQualityName, type FxQualitySettings, type FxQualityTier, FxWisps, type FxWispsProps, HEAT, type MatchFlameState, PAPER_WHITE, PRESENCE, type PannerLike, ParticlePool, type ParticlePreset, type ParticlePresetName, type ParticleTarget, SATURATION, type SolverUniforms, type SoundAt, type SurfaceLocator, type Voice, createAudioContext, emit, emitHex, fireEmitterDefaults, fireFluidControls, fireFluidDefaults, fireZones, flameAnchors, flamePuff, fxQualityFor, fxQualityNames, fxQualityTiers, hexToLinear, luminance, particlePresets, solverUniforms, srgbToLinear, timesPaperWhite };
package/dist/fx.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DamageSource } from './damageContract-DPF1YFLZ.js';
2
- export { a as DAMAGE_CHANNELS, b as DAMAGE_LOOK_DEFAULTS, c as DamageLook } from './damageContract-DPF1YFLZ.js';
1
+ import { D as DamageSource } from './damageContract-nF1fgK7P.js';
2
+ export { a as DAMAGE_CHANNELS, b as DAMAGE_LOOK_DEFAULTS, c as DamageLook } from './damageContract-nF1fgK7P.js';
3
3
  import * as react from 'react';
4
4
  import { ReactNode } from 'react';
5
5
  import * as THREE from 'three';
@@ -400,7 +400,7 @@ declare const particlePresets: {
400
400
  * simulation, on purpose; see the fx plan.
401
401
  */
402
402
  readonly smoke: {
403
- readonly life: readonly [1.8, 3.5];
403
+ readonly life: readonly [1.4, 2.6];
404
404
  readonly speed: readonly [0.05, 0.15];
405
405
  readonly direction: readonly [0, 1, 0];
406
406
  readonly spread: 0.3;
@@ -408,9 +408,9 @@ declare const particlePresets: {
408
408
  readonly drag: 0.9;
409
409
  readonly jitter: 0.3;
410
410
  readonly windCatch: 1;
411
- readonly size: readonly [0.05, 0.35];
411
+ readonly size: readonly [0.05, 0.18];
412
412
  readonly color: readonly [readonly [0.147, 0.13, 0.117], readonly [0.2, 0.19, 0.18]];
413
- readonly alpha: readonly [0.08, 0];
413
+ readonly alpha: readonly [0.07, 0];
414
414
  readonly spin: 0.4;
415
415
  readonly blend: "normal";
416
416
  readonly shape: "soft";
@@ -422,16 +422,16 @@ declare const particlePresets: {
422
422
  * burn that proves paper was there.
423
423
  */
424
424
  readonly ash: {
425
- readonly life: readonly [2.5, 5];
425
+ readonly life: readonly [1.2, 2.6];
426
426
  readonly speed: readonly [0.14, 0.3];
427
427
  readonly direction: readonly [0, 1, 0];
428
428
  readonly spread: 0.55;
429
- readonly lift: -0.22;
429
+ readonly lift: -0.42;
430
430
  readonly drag: 1.6;
431
431
  readonly jitter: 1.2;
432
432
  readonly windCatch: 1;
433
433
  readonly size: readonly [0.03, 0.026];
434
- readonly color: readonly [readonly [0.07, 0.065, 0.06], readonly [0.12, 0.115, 0.11]];
434
+ readonly color: readonly [readonly [0.04, 0.037, 0.034], readonly [0.07, 0.066, 0.062]];
435
435
  readonly alpha: readonly [0.95, 0];
436
436
  readonly spin: 4;
437
437
  readonly blend: "normal";
@@ -540,19 +540,13 @@ interface FireEmitterOptions {
540
540
  };
541
541
  }
542
542
  /**
543
- * How much each kind leaves the front, per texel per second.
544
- *
545
- * `smoke` is 0 on purpose. There were three smoke systems running at once —
546
- * the fire simulator's own, these sprite puffs at 8% alpha, and the smoulder
547
- * wisp and the review's note was that only one of them should exist. The
548
- * simulator's is the one that belongs to a burning sheet: it is the same
549
- * fluid the flames are made of, so it rises with them instead of beside them.
550
- * `FxWisps` still carries the thread after the flames are out, which is the
551
- * one moment the simulator has nothing left to make. The preset stays: it is
552
- * a parameter set, and anything may still ask the pool for smoke.
553
- *
554
- * Embers and ash are both down. Ash at 0.86 a texel was a flake for nearly
555
- * every one that burnt through — a dust shower. Fewer and larger is the note.
543
+ * How much each kind leaves the front, per texel per second — Noor's tune in
544
+ * the lab on 2026-09-13, which the lab starts from and `/hands` inherits.
545
+ *
546
+ * The simulator's smoke is the main smoke of a burning sheet, since it is the
547
+ * same fluid the flames are made of; these sprite puffs, once switched off
548
+ * for doubling it, are back as a light thread beside it. Ash at 0.86 a texel
549
+ * was a dust shower; 0.36 is a light fall of it.
556
550
  */
557
551
  declare const fireEmitterDefaults: Required<FireEmitterOptions>;
558
552
  /**
@@ -744,6 +738,15 @@ interface FxPostProps {
744
738
  locate?: SurfaceLocator;
745
739
  /** Heat haze in pixels at 1080p, overriding the tier's; 0 turns it off. */
746
740
  haze?: number;
741
+ /**
742
+ * Shallow focus on the burn, 0..1; 0 keeps everything sharp.
743
+ *
744
+ * A macro lens held close to a burning edge has a few millimetres of
745
+ * focus and nothing else (§K4): the rim is sharp, the rest of the sheet
746
+ * goes soft. It focuses on the fire itself — the burning rim is what the
747
+ * shot is about — so it needs `field` and `locate` like the haze does.
748
+ */
749
+ focus?: number;
747
750
  }
748
751
  /**
749
752
  * `fallback={null}`: the scene behind it is already drawn. One frame without
@@ -840,13 +843,13 @@ declare const FIRE_SOOT_SCALE = 3;
840
843
  *
841
844
  * It lives beside the threshold because the two are one setting: bloom is a
842
845
  * multiplier on whatever clears the threshold, so its right value depends
843
- * entirely on how much fire is authored above it. 1.55 was tuned when the
844
- * flames were UNDER the threshold and bloom had almost nothing to work on —
845
- * measured, 0.07% of the frame. Once the flames cleared it, that same 1.55
846
- * tinted the entire black stage olive. Swept at 0.25, 0.6 and 1.2 against the
847
- * peak frame; past about 0.4 the stage stops being black.
846
+ * entirely on how much fire is authored above it. With a core authored to
847
+ * over-expose, 0.55 was the most the black stage could take; the flame of
848
+ * {@link FIRE_ZONES} (Noor, 2026-09-13) authors none of it past the
849
+ * threshold, and 1.25 is her tune for it. `pnpm test:fire-budget` holds the
850
+ * stage black and the paper unbloomed whatever this is set to.
848
851
  */
849
- declare const FX_BLOOM = 0.55;
852
+ declare const FX_BLOOM = 1.25;
850
853
  /**
851
854
  * A flame, in four zones — drawn the way the sheet's burn is (ember line, ash
852
855
  * lip, char, scorch): each part named for what it is, with its own controls.
@@ -854,11 +857,10 @@ declare const FX_BLOOM = 0.55;
854
857
  *
855
858
  * root the blue leading edge where fresh gas meets the air at the paper.
856
859
  * Its light comes from excited molecules, not soot, so it is faint
857
- * and blue — and it starts OFF, because blue light added over cream
858
- * paper reads lavender.
859
- * core the hottest gas, where soot forms densest and glows pale. The only
860
- * zone allowed to over-expose past the bloom threshold, and so the
861
- * one that blooms.
860
+ * and blue — and only ever a trace, because blue light added over
861
+ * cream paper reads lavender.
862
+ * core the hottest gas, where soot forms densest and glows pale the
863
+ * brightest zone, and the one the bloom finds first.
862
864
  * body the luminous bulk: soot glowing yellow-orange as it rises.
863
865
  * tip where soot cools and burns off at the outer edge — orange-red,
864
866
  * dimmer, tearing into tongues. What survives escapes as smoke.
@@ -876,12 +878,11 @@ declare const FX_BLOOM = 0.55;
876
878
  * multiples of paper white; colours are sRGB, the way a colour picker gives
877
879
  * them.
878
880
  *
879
- * The body is ABOVE paper white. It was once held below it, because under a
880
- * bright preset paper sits at the top of the tone curve and anything brighter
881
- * loses its colour and a flame dimmer than the paper behind it, drawn
882
- * opaque, is a yellow decal on the sheet, not light. Fire photographed in a
883
- * bright room DOES wash out; that is the lighting's to answer (the lab shoots
884
- * under `noir`, as the references were), not the flame's.
881
+ * How bright the body is against paper white is a judgement, and it has gone
882
+ * both ways. Above it, a flame keeps its light in a bright room but loses its
883
+ * colour to the tone curve; below it, the colour holds, and a flame drawn
884
+ * opaque risks reading as a decal on the sheet rather than as light. The
885
+ * defaults sit below it (Noor, 2026-09-13), with the bloom carrying the glow.
885
886
  */
886
887
  interface FireZones {
887
888
  root: {
@@ -912,10 +913,12 @@ type FireZonesInput = {
912
913
  [Z in keyof FireZones]?: Partial<FireZones[Z]>;
913
914
  };
914
915
  /**
915
- * The defaults reproduce the look arrived at by measurement against
916
- * Flame_base.png (yellow ~30%, orange ~40%, pale ~18% of a flame's pixels),
917
- * now expressed zone by zone. Each colour is the linear emission the previous
918
- * ramp used in that band, written as the sRGB a picker would show.
916
+ * Noor's tune in the lab on 2026-09-13, and the source of truth for how a
917
+ * flame looks: the lab starts from it and `/hands` inherits it. A gentler
918
+ * flame than the one it replaced every zone sits under the bloom threshold,
919
+ * so its glow comes from the bloom's strength ({@link FX_BLOOM}) spreading
920
+ * the brightest gas, not from a core authored to over-expose. The values it
921
+ * replaced, measured against Flame_base.png, are in the history of this file.
919
922
  */
920
923
  declare const FIRE_ZONES: FireZones;
921
924
  /** The defaults with `input` laid over them, zone by zone. */
@@ -1313,25 +1316,48 @@ declare class FireFluid {
1313
1316
  interface FxFireLightProps {
1314
1317
  field: DamageField;
1315
1318
  locate: SurfaceLocator;
1316
- /** How bright the fire is per unit of burning front. Tuned against Hero.png. */
1319
+ /** How bright the fire is per unit of burning front. */
1317
1320
  gain?: number;
1321
+ /**
1322
+ * The brightest cluster's light casts shadows: a curled edge, a lifted
1323
+ * plate and a hanging flap throw flickering shade across the sheet, which
1324
+ * is much of what makes firelight read as firelight. One shadow pass a
1325
+ * frame, so off unless asked for.
1326
+ */
1327
+ shadows?: boolean;
1318
1328
  }
1319
1329
  /**
1320
1330
  * Fire is a light source (§4.8, §7). A fire that changes nothing around it
1321
1331
  * looks pasted on.
1322
1332
  *
1323
- * A warm point light about 1900 K at the middle of the burning rim,
1324
- * raised by half a flame, as bright as the front is long and flickering on
1325
- * the very noise the flames puff on, so light and flame agree. It reaches the
1326
- * whole sheet: `decay` is held below the physical 2, because a sheet is a
1327
- * small thing next to a fire and Hero.png warms it to the top edge.
1328
- *
1329
- * And a dimmer twin on the FAR side of the sheet, for translucency: paper is
1330
- * thin, and a fire in front of it glows through to the back, warm and
1331
- * diffused (§7). A second light is the whole trick the sheet's own
1332
- * material already lights its back face from whatever is behind it.
1333
+ * One warm point light for each place the fire is gathered up to
1334
+ * {@link FIRE_CLUSTERS} of them, from {@link fireStateOf} standing at its
1335
+ * cluster raised by half a flame, as bright as its share of the fire and
1336
+ * flickering on its own tongues' puff. A tall tongue on the left lights the
1337
+ * left. It used to be one light at the middle of the whole rim, which lit the
1338
+ * sheet the same wherever the flames actually were.
1339
+ *
1340
+ * Each light's colour follows how hot the paper under its tongues is: the
1341
+ * pale peach of a fire at its height, deepening toward orange as it cools —
1342
+ * never toward red, which on white paper is the pink the spec forbids.
1343
+ * Together they give off what the single light did: the front's length times
1344
+ * `gain`, shared out.
1345
+ *
1346
+ * And one dimmer light on the FAR side of the sheet, at the fire as a whole,
1347
+ * for translucency: paper is thin, and a fire in front of it glows through to
1348
+ * the back (§7). The sheet's own material lights its back face from whatever
1349
+ * is behind it.
1350
+ *
1351
+ * Every light is mounted all the time and only its intensity changes, so a
1352
+ * fire gathering into more places or fewer never makes three recompile a
1353
+ * shader.
1354
+ */
1355
+ declare function FxFireLight({ field, locate, gain, shadows }: FxFireLightProps): react.JSX.Element;
1356
+ /**
1357
+ * Light per unit of front length; the front runs to ~0.045 at a burn's peak.
1358
+ * Noor's tune, 2026-09-13 (it was 42).
1333
1359
  */
1334
- declare function FxFireLight({ field, locate, gain }: FxFireLightProps): react.JSX.Element;
1360
+ declare const FIRE_LIGHT_GAIN = 26;
1335
1361
 
1336
1362
  /** What the page says about the match this frame. Written in place — never a prop change. */
1337
1363
  interface MatchFlameState {
@@ -1793,4 +1819,4 @@ declare class FireSound {
1793
1819
  private next;
1794
1820
  }
1795
1821
 
1796
- export { Afterglow, type AfterglowOptions, type AudioLike, type BiquadFilterLike, CHAR, DamageField, type DamageFieldOptions, DamageSource, FIELD_SIZE, FIRE_GLOW, FIRE_HEAT_SCALE, FIRE_SOOT_SCALE, FIRE_ZONES, FIXED_DT, FLAME_HEIGHT, FX_BLOOM, FX_BLOOM_THRESHOLD, FX_INITIAL_TIER, FX_TIER_ORDER, type FieldStats, FireEmitter, type FireEmitterOptions, FireFluid, type FireFluidParams, FireSound, type FireSoundOptions, type FireZones, type FireZonesInput, type FlameAnchor, type FluidGrid, FxAudio, type FxAudioOptions, type FxFilm, FxFireFluid, type FxFireFluidProps, FxFireLight, type FxFireLightProps, FxFlames, type FxFlamesProps, FxMatchFlame, type FxMatchFlameProps, FxParticles, type FxParticlesProps, FxPost, type FxPostProps, type FxQualityName, type FxQualitySettings, type FxQualityTier, FxWisps, type FxWispsProps, HEAT, type MatchFlameState, PAPER_WHITE, PRESENCE, type PannerLike, ParticlePool, type ParticlePreset, type ParticlePresetName, type ParticleTarget, SATURATION, type SolverUniforms, type SoundAt, type SurfaceLocator, type Voice, createAudioContext, emit, emitHex, fireEmitterDefaults, fireFluidControls, fireFluidDefaults, fireZones, flameAnchors, flamePuff, fxQualityFor, fxQualityNames, fxQualityTiers, hexToLinear, luminance, particlePresets, solverUniforms, srgbToLinear, timesPaperWhite };
1822
+ export { Afterglow, type AfterglowOptions, type AudioLike, type BiquadFilterLike, CHAR, DamageField, type DamageFieldOptions, DamageSource, FIELD_SIZE, FIRE_GLOW, FIRE_HEAT_SCALE, FIRE_LIGHT_GAIN, FIRE_SOOT_SCALE, FIRE_ZONES, FIXED_DT, FLAME_HEIGHT, FX_BLOOM, FX_BLOOM_THRESHOLD, FX_INITIAL_TIER, FX_TIER_ORDER, type FieldStats, FireEmitter, type FireEmitterOptions, FireFluid, type FireFluidParams, FireSound, type FireSoundOptions, type FireZones, type FireZonesInput, type FlameAnchor, type FluidGrid, FxAudio, type FxAudioOptions, type FxFilm, FxFireFluid, type FxFireFluidProps, FxFireLight, type FxFireLightProps, FxFlames, type FxFlamesProps, FxMatchFlame, type FxMatchFlameProps, FxParticles, type FxParticlesProps, FxPost, type FxPostProps, type FxQualityName, type FxQualitySettings, type FxQualityTier, FxWisps, type FxWispsProps, HEAT, type MatchFlameState, PAPER_WHITE, PRESENCE, type PannerLike, ParticlePool, type ParticlePreset, type ParticlePresetName, type ParticleTarget, SATURATION, type SolverUniforms, type SoundAt, type SurfaceLocator, type Voice, createAudioContext, emit, emitHex, fireEmitterDefaults, fireFluidControls, fireFluidDefaults, fireZones, flameAnchors, flamePuff, fxQualityFor, fxQualityNames, fxQualityTiers, hexToLinear, luminance, particlePresets, solverUniforms, srgbToLinear, timesPaperWhite };