paperlab 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-V6YDK4H2.js} +23 -5
- package/dist/FxPostPass-V6YDK4H2.js.map +1 -0
- package/dist/chunk-232SU3AQ.js +34 -0
- package/dist/chunk-232SU3AQ.js.map +1 -0
- package/dist/{chunk-GB6BMHC3.js → chunk-7WOGU2D4.js} +7 -19
- package/dist/{chunk-GB6BMHC3.js.map → chunk-7WOGU2D4.js.map} +1 -1
- package/dist/{chunk-MZCAN3AR.js → chunk-SVWWC5UL.js} +576 -160
- package/dist/chunk-SVWWC5UL.js.map +1 -0
- package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-nF1fgK7P.d.cts} +31 -3
- package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-nF1fgK7P.d.ts} +31 -3
- package/dist/fx.cjs +320 -91
- package/dist/fx.cjs.map +1 -1
- package/dist/fx.d.cts +81 -55
- package/dist/fx.d.ts +81 -55
- package/dist/fx.js +278 -48
- package/dist/fx.js.map +1 -1
- package/dist/index.cjs +606 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/{slots-C5PqY0Q5.d.cts → slots-D_EE8GXn.d.cts} +12 -0
- package/dist/{slots-C5PqY0Q5.d.ts → slots-D_EE8GXn.d.ts} +12 -0
- package/dist/stage.cjs +600 -188
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.d.cts +1 -1
- package/dist/stage.d.ts +1 -1
- package/dist/stage.js +2 -2
- package/package.json +1 -1
- package/dist/FxPostPass-M66QMVVC.js.map +0 -1
- package/dist/chunk-7TLIWPGM.js +0 -36
- package/dist/chunk-7TLIWPGM.js.map +0 -1
- package/dist/chunk-MZCAN3AR.js.map +0 -1
|
@@ -69,6 +69,29 @@ interface DamageSource {
|
|
|
69
69
|
* Anything left out takes {@link DAMAGE_LOOK_DEFAULTS}.
|
|
70
70
|
*/
|
|
71
71
|
readonly look?: DamageLook;
|
|
72
|
+
/**
|
|
73
|
+
* The light the damage gives off, as the room around the sheet should feel
|
|
74
|
+
* it. Presentation only; the physics never sees it. Omitted, the room is
|
|
75
|
+
* lit exactly as its lighting says.
|
|
76
|
+
*/
|
|
77
|
+
readonly firelight?: DamageFirelight;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* What a burning sheet does to the light around it.
|
|
81
|
+
*
|
|
82
|
+
* A fire big enough to see by is the key light while it burns, and a room's
|
|
83
|
+
* own light yields to it — a sheet burning under an unchanged studio key
|
|
84
|
+
* looks like a flame pasted onto a photograph. The source says by how much,
|
|
85
|
+
* because only the source knows how big its fire is; the lighting does the
|
|
86
|
+
* dimming, because only the lighting knows what its lights are.
|
|
87
|
+
*/
|
|
88
|
+
interface DamageFirelight {
|
|
89
|
+
/**
|
|
90
|
+
* How much of the room's own light is left, 0..1; omitted means 1. The key,
|
|
91
|
+
* the ambient fill and the studio light are all scaled by it, every frame,
|
|
92
|
+
* without rebuilding anything.
|
|
93
|
+
*/
|
|
94
|
+
readonly room?: number;
|
|
72
95
|
}
|
|
73
96
|
/**
|
|
74
97
|
* The knobs on what a burn looks like, in the units a person tunes by —
|
|
@@ -87,8 +110,10 @@ interface DamageLook {
|
|
|
87
110
|
emberGlow?: number;
|
|
88
111
|
/** Specks of glowing fibre along the edge, 0..2. */
|
|
89
112
|
sparkle?: number;
|
|
90
|
-
/**
|
|
113
|
+
/** How wide the pale ash lip is, from the cut out to the ember line, mm. */
|
|
91
114
|
lipWidth?: number;
|
|
115
|
+
/** How wide the black char band is, past the ember line, mm. */
|
|
116
|
+
charWidth?: number;
|
|
92
117
|
/** How pale the ash lip is, × the sampled grey. */
|
|
93
118
|
lipBrightness?: number;
|
|
94
119
|
/** 0 is grey char, 1 is dark orange to deep brown. */
|
|
@@ -107,8 +132,11 @@ interface DamageLook {
|
|
|
107
132
|
edgeBite?: number;
|
|
108
133
|
}
|
|
109
134
|
/**
|
|
110
|
-
* What every burn is drawn with unless told otherwise —
|
|
111
|
-
*
|
|
135
|
+
* What every burn is drawn with unless told otherwise — Noor's tune in the
|
|
136
|
+
* lab's sidebar on 2026-09-13, and the source of truth for how a burn looks:
|
|
137
|
+
* the lab starts from it, `/hands` inherits it, and a user changes it through
|
|
138
|
+
* `look`. The values it replaced, and why each of those had moved, are in the
|
|
139
|
+
* history of this file.
|
|
112
140
|
*/
|
|
113
141
|
declare const DAMAGE_LOOK_DEFAULTS: Required<DamageLook>;
|
|
114
142
|
/**
|
|
@@ -69,6 +69,29 @@ interface DamageSource {
|
|
|
69
69
|
* Anything left out takes {@link DAMAGE_LOOK_DEFAULTS}.
|
|
70
70
|
*/
|
|
71
71
|
readonly look?: DamageLook;
|
|
72
|
+
/**
|
|
73
|
+
* The light the damage gives off, as the room around the sheet should feel
|
|
74
|
+
* it. Presentation only; the physics never sees it. Omitted, the room is
|
|
75
|
+
* lit exactly as its lighting says.
|
|
76
|
+
*/
|
|
77
|
+
readonly firelight?: DamageFirelight;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* What a burning sheet does to the light around it.
|
|
81
|
+
*
|
|
82
|
+
* A fire big enough to see by is the key light while it burns, and a room's
|
|
83
|
+
* own light yields to it — a sheet burning under an unchanged studio key
|
|
84
|
+
* looks like a flame pasted onto a photograph. The source says by how much,
|
|
85
|
+
* because only the source knows how big its fire is; the lighting does the
|
|
86
|
+
* dimming, because only the lighting knows what its lights are.
|
|
87
|
+
*/
|
|
88
|
+
interface DamageFirelight {
|
|
89
|
+
/**
|
|
90
|
+
* How much of the room's own light is left, 0..1; omitted means 1. The key,
|
|
91
|
+
* the ambient fill and the studio light are all scaled by it, every frame,
|
|
92
|
+
* without rebuilding anything.
|
|
93
|
+
*/
|
|
94
|
+
readonly room?: number;
|
|
72
95
|
}
|
|
73
96
|
/**
|
|
74
97
|
* The knobs on what a burn looks like, in the units a person tunes by —
|
|
@@ -87,8 +110,10 @@ interface DamageLook {
|
|
|
87
110
|
emberGlow?: number;
|
|
88
111
|
/** Specks of glowing fibre along the edge, 0..2. */
|
|
89
112
|
sparkle?: number;
|
|
90
|
-
/**
|
|
113
|
+
/** How wide the pale ash lip is, from the cut out to the ember line, mm. */
|
|
91
114
|
lipWidth?: number;
|
|
115
|
+
/** How wide the black char band is, past the ember line, mm. */
|
|
116
|
+
charWidth?: number;
|
|
92
117
|
/** How pale the ash lip is, × the sampled grey. */
|
|
93
118
|
lipBrightness?: number;
|
|
94
119
|
/** 0 is grey char, 1 is dark orange to deep brown. */
|
|
@@ -107,8 +132,11 @@ interface DamageLook {
|
|
|
107
132
|
edgeBite?: number;
|
|
108
133
|
}
|
|
109
134
|
/**
|
|
110
|
-
* What every burn is drawn with unless told otherwise —
|
|
111
|
-
*
|
|
135
|
+
* What every burn is drawn with unless told otherwise — Noor's tune in the
|
|
136
|
+
* lab's sidebar on 2026-09-13, and the source of truth for how a burn looks:
|
|
137
|
+
* the lab starts from it, `/hands` inherits it, and a user changes it through
|
|
138
|
+
* `look`. The values it replaced, and why each of those had moved, are in the
|
|
139
|
+
* history of this file.
|
|
112
140
|
*/
|
|
113
141
|
declare const DAMAGE_LOOK_DEFAULTS: Required<DamageLook>;
|
|
114
142
|
/**
|
package/dist/fx.cjs
CHANGED
|
@@ -41,32 +41,24 @@ var init_damageContract = __esm({
|
|
|
41
41
|
"src/surface/damageContract.ts"() {
|
|
42
42
|
"use strict";
|
|
43
43
|
DAMAGE_LOOK_DEFAULTS = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// underneath is wrong, which in this case was a fire nothing could see.
|
|
47
|
-
emberWidth: 0.9,
|
|
48
|
-
emberIntensity: 1.45,
|
|
44
|
+
emberWidth: 1.25,
|
|
45
|
+
emberIntensity: 1.05,
|
|
49
46
|
emberCoverage: 0.6,
|
|
50
47
|
emberFlicker: 1.65,
|
|
51
48
|
emberGlow: 1.25,
|
|
52
49
|
sparkle: 0.5,
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
lipBrightness: 0.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
scorchReach: 30,
|
|
66
|
-
scorchDarkness: 1.17,
|
|
67
|
-
fingers: 1.25,
|
|
68
|
-
edgeWave: 8.5,
|
|
69
|
-
edgeBite: 3.6
|
|
50
|
+
// As wide as the char band (Noor, 2026-09-13): the lip has to be seen. At
|
|
51
|
+
// 0.95 mm it was a hairline tracing the edge.
|
|
52
|
+
lipWidth: 3.5,
|
|
53
|
+
charWidth: 3.5,
|
|
54
|
+
lipBrightness: 0.76,
|
|
55
|
+
charWarmth: 0.32,
|
|
56
|
+
charCracks: 0.45,
|
|
57
|
+
scorchReach: 9,
|
|
58
|
+
scorchDarkness: 0.86,
|
|
59
|
+
fingers: 1.95,
|
|
60
|
+
edgeWave: 13,
|
|
61
|
+
edgeBite: 6
|
|
70
62
|
};
|
|
71
63
|
DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
72
64
|
}
|
|
@@ -613,27 +605,15 @@ var init_emission = __esm({
|
|
|
613
605
|
FIRE_GLOW = [4, 8];
|
|
614
606
|
FIRE_HEAT_SCALE = 0.65;
|
|
615
607
|
FIRE_SOOT_SCALE = 3;
|
|
616
|
-
FX_BLOOM =
|
|
608
|
+
FX_BLOOM = 1.25;
|
|
617
609
|
FIRE_CONTRAST = 0.9;
|
|
618
610
|
FIRE_OPACITY = 5;
|
|
619
611
|
FIRE_THIN = 0.55;
|
|
620
612
|
FIRE_ZONES = {
|
|
621
|
-
root: { color: "#3b6bff", amount: 0, reach: 0.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
// budget. Brighter keeps the same small area: 1.47% bloom, near-white 0.8%.
|
|
626
|
-
// Starting it lower (from 0.45) bloomed 6.4% but put near-white back at 4.7%.
|
|
627
|
-
core: { color: "#fff7d4", glow: 6, from: 0.55 },
|
|
628
|
-
// Saturated, because the tone curve takes saturation away from anything
|
|
629
|
-
// above paper white: #ffdd7c at this glow came out pale yellow (s ~0.4),
|
|
630
|
-
// and measured 7.5% yellow against Flame_base.png's 29%.
|
|
631
|
-
body: { color: "#ffcf3a", glow: 1.3 },
|
|
632
|
-
// to: 0.4, measured against Flame_base.png with bloom off (bloom's halo
|
|
633
|
-
// counts as flame in any diff): near-white / pale / yellow / orange came out
|
|
634
|
-
// 0.7 / 21 / 34 / 44% against the reference's 0.9 / 17 / 29 / 43. At 0.32
|
|
635
|
-
// the body took half the flame and it was 51% yellow; at 0.45, 64% orange.
|
|
636
|
-
tip: { color: "#ff9e2c", glow: 0.9, from: 0.08, to: 0.4, softness: 0.15, tearing: 0.35 }
|
|
613
|
+
root: { color: "#3b6bff", amount: 0.01, reach: 0.39 },
|
|
614
|
+
core: { color: "#fff7d4", glow: 2.1, from: 0.63 },
|
|
615
|
+
body: { color: "#ffcf3a", glow: 0.8 },
|
|
616
|
+
tip: { color: "#ff9e2c", glow: 0.21, from: 0, to: 0.5, softness: 0.02, tearing: 0.64 }
|
|
637
617
|
};
|
|
638
618
|
}
|
|
639
619
|
});
|
|
@@ -711,16 +691,16 @@ function flameAnchors(field, locate, max, out, time = field.time) {
|
|
|
711
691
|
const rx = at.x;
|
|
712
692
|
const ry = at.y;
|
|
713
693
|
const rz = at.z;
|
|
714
|
-
const
|
|
694
|
+
const toward2 = locate(u + gx / gl / last, v + gy / gl / last);
|
|
715
695
|
let upper = 0;
|
|
716
696
|
scratchA.x = 0;
|
|
717
697
|
scratchA.y = 0;
|
|
718
698
|
scratchA.z = 0;
|
|
719
|
-
if (
|
|
720
|
-
const l = Math.hypot(
|
|
721
|
-
scratchA.x = (
|
|
722
|
-
scratchA.y = (
|
|
723
|
-
scratchA.z = (
|
|
699
|
+
if (toward2) {
|
|
700
|
+
const l = Math.hypot(toward2.x - rx, toward2.y - ry, toward2.z - rz) || 1;
|
|
701
|
+
scratchA.x = (toward2.x - rx) / l;
|
|
702
|
+
scratchA.y = (toward2.y - ry) / l;
|
|
703
|
+
scratchA.z = (toward2.z - rz) / l;
|
|
724
704
|
upper = scratchA.y;
|
|
725
705
|
}
|
|
726
706
|
const heat = data[cell * 4 + HEAT];
|
|
@@ -976,12 +956,14 @@ function FxPostPass({
|
|
|
976
956
|
threshold = FX_BLOOM_THRESHOLD,
|
|
977
957
|
field,
|
|
978
958
|
locate,
|
|
979
|
-
haze: hazeOverride
|
|
959
|
+
haze: hazeOverride,
|
|
960
|
+
focus = 0
|
|
980
961
|
}) {
|
|
981
962
|
const { bloomScale, haze: tierHaze } = fxQualityFor(quality);
|
|
982
963
|
const hazePx = hazeOverride ?? tierHaze;
|
|
983
964
|
const haze = (0, import_react2.useMemo)(() => new HazeGradeEffect(), []);
|
|
984
965
|
const anchors = (0, import_react2.useRef)([]);
|
|
966
|
+
const focusTarget = (0, import_react2.useRef)(new THREE3.Vector3());
|
|
985
967
|
(0, import_fiber2.useFrame)(({ camera }) => {
|
|
986
968
|
if (!field || !locate) {
|
|
987
969
|
haze.count = 0;
|
|
@@ -996,6 +978,14 @@ function FxPostPass({
|
|
|
996
978
|
top.set(a.x, a.y + a.height, a.z).project(camera);
|
|
997
979
|
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);
|
|
998
980
|
}
|
|
981
|
+
if (focus > 0 && n > 0) {
|
|
982
|
+
focusTarget.current.set(0, 0, 0);
|
|
983
|
+
for (let i = 0; i < n; i++) {
|
|
984
|
+
const a = anchors.current[i];
|
|
985
|
+
focusTarget.current.add(root.set(a.x, a.y, a.z));
|
|
986
|
+
}
|
|
987
|
+
focusTarget.current.multiplyScalar(1 / n);
|
|
988
|
+
}
|
|
999
989
|
haze.count = n;
|
|
1000
990
|
haze.time = field.time;
|
|
1001
991
|
haze.amount = hazePx;
|
|
@@ -1013,6 +1003,14 @@ function FxPostPass({
|
|
|
1013
1003
|
}
|
|
1014
1004
|
) : null,
|
|
1015
1005
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("primitive", { object: haze }),
|
|
1006
|
+
focus > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1007
|
+
import_postprocessing2.DepthOfField,
|
|
1008
|
+
{
|
|
1009
|
+
target: focusTarget.current,
|
|
1010
|
+
focalLength: 0.06 - 0.045 * focus,
|
|
1011
|
+
bokehScale: 1 + 3 * focus
|
|
1012
|
+
}
|
|
1013
|
+
) : null,
|
|
1016
1014
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_postprocessing2.ToneMapping, { mode: modes[film] })
|
|
1017
1015
|
] });
|
|
1018
1016
|
}
|
|
@@ -1051,6 +1049,7 @@ __export(fx_exports, {
|
|
|
1051
1049
|
FIELD_SIZE: () => FIELD_SIZE,
|
|
1052
1050
|
FIRE_GLOW: () => FIRE_GLOW,
|
|
1053
1051
|
FIRE_HEAT_SCALE: () => FIRE_HEAT_SCALE,
|
|
1052
|
+
FIRE_LIGHT_GAIN: () => FIRE_LIGHT_GAIN,
|
|
1054
1053
|
FIRE_SOOT_SCALE: () => FIRE_SOOT_SCALE,
|
|
1055
1054
|
FIRE_ZONES: () => FIRE_ZONES,
|
|
1056
1055
|
FIXED_DT: () => FIXED_DT,
|
|
@@ -1152,7 +1151,13 @@ var particlePresets = {
|
|
|
1152
1151
|
* simulation, on purpose; see the fx plan.
|
|
1153
1152
|
*/
|
|
1154
1153
|
smoke: {
|
|
1155
|
-
|
|
1154
|
+
// Shorter, and smaller below: these are the thread beside the
|
|
1155
|
+
// simulator's smoke, and the simulator's is gone within a second or two
|
|
1156
|
+
// of the last flame. Left to live three and a half seconds and grow to
|
|
1157
|
+
// seven centimetres, the last few of them overlapped into a flat wash
|
|
1158
|
+
// over the hole with nothing left to move them — the one moment of a
|
|
1159
|
+
// burn Noor found the smoke wrong (2026-09-13).
|
|
1160
|
+
life: [1.4, 2.6],
|
|
1156
1161
|
speed: [0.05, 0.15],
|
|
1157
1162
|
direction: [0, 1, 0],
|
|
1158
1163
|
spread: 0.3,
|
|
@@ -1160,13 +1165,13 @@ var particlePresets = {
|
|
|
1160
1165
|
drag: 0.9,
|
|
1161
1166
|
jitter: 0.3,
|
|
1162
1167
|
windCatch: 1,
|
|
1163
|
-
size: [0.05, 0.
|
|
1168
|
+
size: [0.05, 0.18],
|
|
1164
1169
|
// #6B6560 grey-brown, linear, and thin — the background stays clear.
|
|
1165
1170
|
color: [
|
|
1166
1171
|
[0.147, 0.13, 0.117],
|
|
1167
1172
|
[0.2, 0.19, 0.18]
|
|
1168
1173
|
],
|
|
1169
|
-
alpha: [0.
|
|
1174
|
+
alpha: [0.07, 0],
|
|
1170
1175
|
spin: 0.4,
|
|
1171
1176
|
blend: "normal",
|
|
1172
1177
|
shape: "soft",
|
|
@@ -1178,7 +1183,9 @@ var particlePresets = {
|
|
|
1178
1183
|
* burn that proves paper was there.
|
|
1179
1184
|
*/
|
|
1180
1185
|
ash: {
|
|
1181
|
-
|
|
1186
|
+
// A second or two: ash that hung in the air for five read as grey
|
|
1187
|
+
// confetti over the hole, dust or stars rather than something falling.
|
|
1188
|
+
life: [1.2, 2.6],
|
|
1182
1189
|
// Thrown UP by the heat it came off, before it is heavier than the air.
|
|
1183
1190
|
// It used to leave at almost nothing (0.02–0.1) with a lift of −0.35, so
|
|
1184
1191
|
// every flake began falling the moment it was born — straight through the
|
|
@@ -1186,7 +1193,9 @@ var particlePresets = {
|
|
|
1186
1193
|
speed: [0.14, 0.3],
|
|
1187
1194
|
direction: [0, 1, 0],
|
|
1188
1195
|
spread: 0.55,
|
|
1189
|
-
|
|
1196
|
+
// Heavier than the air once the throw is spent: most flakes tumble down,
|
|
1197
|
+
// only the lightest ride the heat up a little first.
|
|
1198
|
+
lift: -0.42,
|
|
1190
1199
|
drag: 1.6,
|
|
1191
1200
|
jitter: 1.2,
|
|
1192
1201
|
windCatch: 1,
|
|
@@ -1194,9 +1203,11 @@ var particlePresets = {
|
|
|
1194
1203
|
// 3.8 mm and there were far too many of them: a shower of specks rather
|
|
1195
1204
|
// than a few flakes you would notice.
|
|
1196
1205
|
size: [0.03, 0.026],
|
|
1206
|
+
// Charred paper, near black: under a bright key the old value read as
|
|
1207
|
+
// pale grey against the black stage, which is the confetti.
|
|
1197
1208
|
color: [
|
|
1198
|
-
[0.
|
|
1199
|
-
[0.
|
|
1209
|
+
[0.04, 0.037, 0.034],
|
|
1210
|
+
[0.07, 0.066, 0.062]
|
|
1200
1211
|
],
|
|
1201
1212
|
alpha: [0.95, 0],
|
|
1202
1213
|
spin: 4,
|
|
@@ -1421,13 +1432,14 @@ var ParticlePool = class {
|
|
|
1421
1432
|
// src/fx/fire.ts
|
|
1422
1433
|
init_field();
|
|
1423
1434
|
var fireEmitterDefaults = {
|
|
1424
|
-
embers: 0.
|
|
1425
|
-
smoke: 0,
|
|
1426
|
-
ash: 0.
|
|
1435
|
+
embers: 0.39,
|
|
1436
|
+
smoke: 0.135,
|
|
1437
|
+
ash: 0.36,
|
|
1427
1438
|
seed: 7,
|
|
1428
1439
|
caps: {}
|
|
1429
1440
|
};
|
|
1430
1441
|
var PER_UPDATE = 24;
|
|
1442
|
+
var CRACKLE_SPARKS = 0.06;
|
|
1431
1443
|
var FireEmitter = class {
|
|
1432
1444
|
field;
|
|
1433
1445
|
pool;
|
|
@@ -1464,7 +1476,10 @@ var FireEmitter = class {
|
|
|
1464
1476
|
heat /= front;
|
|
1465
1477
|
const struggle = 0.35 + 1.6 * Math.max(0, 0.7 - heat) + air * 0.8;
|
|
1466
1478
|
const oxygen = 1 + air * 1.5;
|
|
1467
|
-
this.emberDebt = Math.min(
|
|
1479
|
+
this.emberDebt = Math.min(
|
|
1480
|
+
this.emberDebt + front * o.embers * oxygen * dt + field.lastStats.charred * o.embers * CRACKLE_SPARKS,
|
|
1481
|
+
PER_UPDATE
|
|
1482
|
+
);
|
|
1468
1483
|
this.smokeDebt = Math.min(this.smokeDebt + front * o.smoke * struggle * dt, PER_UPDATE);
|
|
1469
1484
|
while (this.emberDebt >= 1) {
|
|
1470
1485
|
this.emberDebt -= 1;
|
|
@@ -2814,10 +2829,17 @@ void main() {
|
|
|
2814
2829
|
float rootMask = smoothstep(mix(0.4, 0.02, uRootReach), mix(0.6, 0.12, uRootReach), fuel) * (1.0 - smoothstep(0.25, 0.6, t));
|
|
2815
2830
|
fire += uRootColor * rootMask * uRootAmount * uPaperWhite * uGlow * 0.6;
|
|
2816
2831
|
|
|
2817
|
-
// Smoke:
|
|
2818
|
-
//
|
|
2832
|
+
// Smoke: grey-brown, warm (a cool grey over cream reads lavender), and LIT
|
|
2833
|
+
// BY THE FIRE UNDER IT rather than by nothing.
|
|
2834
|
+
//
|
|
2835
|
+
// The base used to be bright enough to read on its own \u2014 beige \u2014 which is
|
|
2836
|
+
// right while flames are lighting it and wrong the moment they stop: the
|
|
2837
|
+
// last of the smoke hung over a black stage as flat beige slabs, the same
|
|
2838
|
+
// colour everywhere, which reads as a filled shape rather than as smoke
|
|
2839
|
+
// (Noor, 2026-09-13). Dim at the base and warm where the fire is: smoke by
|
|
2840
|
+
// the flames looks as it did, smoke after them goes to a dim grey.
|
|
2819
2841
|
float smokeAlpha = 1.0 - exp(-smoke * uSmokeDensity);
|
|
2820
|
-
vec3 smokeColor = vec3(0.
|
|
2842
|
+
vec3 smokeColor = vec3(0.075, 0.068, 0.062) + vec3(0.3, 0.14, 0.045) * smoothstep(0.12, 0.9, t);
|
|
2821
2843
|
float alpha = smokeAlpha + fireAlpha - smokeAlpha * fireAlpha;
|
|
2822
2844
|
// No square edge: the domain fades out before its borders.
|
|
2823
2845
|
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);
|
|
@@ -3617,56 +3639,262 @@ function FxFireFluid({
|
|
|
3617
3639
|
var THREE8 = __toESM(require("three"), 1);
|
|
3618
3640
|
var import_fiber6 = require("@react-three/fiber");
|
|
3619
3641
|
var import_react7 = require("react");
|
|
3642
|
+
|
|
3643
|
+
// src/fx/fireState.ts
|
|
3620
3644
|
init_flames();
|
|
3621
|
-
var
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3645
|
+
var FLAMES = 32;
|
|
3646
|
+
var FIRE_CLUSTERS = 4;
|
|
3647
|
+
var CLUSTER_REACH = 25 / 210;
|
|
3648
|
+
var FireState = class {
|
|
3649
|
+
constructor(field, locate) {
|
|
3650
|
+
this.field = field;
|
|
3651
|
+
this.locate = locate;
|
|
3652
|
+
}
|
|
3653
|
+
field;
|
|
3654
|
+
locate;
|
|
3655
|
+
/** The flames it was taken over. Only the first {@link count} are current. */
|
|
3656
|
+
anchors = [];
|
|
3657
|
+
count = 0;
|
|
3658
|
+
/** Where the fire is, as a whole: its flames' mean. Zero with none. */
|
|
3659
|
+
x = 0;
|
|
3660
|
+
y = 0;
|
|
3661
|
+
z = 0;
|
|
3662
|
+
/** Its flames' mean height. */
|
|
3663
|
+
height = 0;
|
|
3664
|
+
/** Its flames' mean puff. */
|
|
3665
|
+
flicker = 0;
|
|
3666
|
+
/** Where it is gathered. Only the first {@link clusterCount} are current. */
|
|
3667
|
+
clusters = [];
|
|
3668
|
+
clusterCount = 0;
|
|
3669
|
+
frame = Number.NaN;
|
|
3670
|
+
order = [];
|
|
3671
|
+
root = new Float64Array(FIRE_CLUSTERS * 3);
|
|
3672
|
+
weight = new Float64Array(FIRE_CLUSTERS);
|
|
3673
|
+
/**
|
|
3674
|
+
* Bring it up to date for `frame` — any number that is the same for every
|
|
3675
|
+
* reader in one frame and different in the next; R3F's `clock.elapsedTime`
|
|
3676
|
+
* is the one in hand. Free the second time it is asked in a frame.
|
|
3677
|
+
*/
|
|
3678
|
+
update(frame) {
|
|
3679
|
+
if (frame === this.frame) return this;
|
|
3680
|
+
this.frame = frame;
|
|
3681
|
+
const { field, anchors } = this;
|
|
3682
|
+
const n = flameAnchors(field, this.locate, FLAMES, anchors);
|
|
3683
|
+
this.count = n;
|
|
3636
3684
|
let x = 0;
|
|
3637
3685
|
let y = 0;
|
|
3638
3686
|
let z = 0;
|
|
3639
3687
|
let h = 0;
|
|
3640
3688
|
let flicker = 0;
|
|
3641
3689
|
for (let i = 0; i < n; i++) {
|
|
3642
|
-
const f = anchors
|
|
3690
|
+
const f = anchors[i];
|
|
3643
3691
|
x += f.x;
|
|
3644
3692
|
y += f.y;
|
|
3645
3693
|
z += f.z;
|
|
3646
3694
|
h += f.height;
|
|
3647
3695
|
flicker += flamePuff(f.seed, field.time);
|
|
3648
3696
|
}
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3697
|
+
if (n > 0) {
|
|
3698
|
+
x /= n;
|
|
3699
|
+
y /= n;
|
|
3700
|
+
z /= n;
|
|
3701
|
+
h /= n;
|
|
3702
|
+
flicker /= n;
|
|
3703
|
+
}
|
|
3704
|
+
this.x = x;
|
|
3705
|
+
this.y = y;
|
|
3706
|
+
this.z = z;
|
|
3707
|
+
this.height = h;
|
|
3708
|
+
this.flicker = flicker;
|
|
3709
|
+
this.gather();
|
|
3710
|
+
return this;
|
|
3711
|
+
}
|
|
3712
|
+
/**
|
|
3713
|
+
* Group the flames into at most {@link FIRE_CLUSTERS} clusters.
|
|
3714
|
+
*
|
|
3715
|
+
* Tallest first, so each cluster is named by its tallest tongue and stands
|
|
3716
|
+
* where that one stands: a flame joins the nearest cluster within
|
|
3717
|
+
* {@link CLUSTER_REACH} of it, or starts one while there is room, or joins
|
|
3718
|
+
* the nearest once there is not. Pure arithmetic over the flames, so the
|
|
3719
|
+
* same burn gathers the same way every time.
|
|
3720
|
+
*/
|
|
3721
|
+
gather() {
|
|
3722
|
+
const anchors = this.anchors;
|
|
3723
|
+
const clusters = this.clusters;
|
|
3724
|
+
const order = this.order;
|
|
3725
|
+
const root2 = this.root;
|
|
3726
|
+
const weight = this.weight;
|
|
3727
|
+
const n = this.count;
|
|
3728
|
+
order.length = 0;
|
|
3729
|
+
for (let i = 0; i < n; i++) order.push(i);
|
|
3730
|
+
order.sort((i, j) => anchors[j].height - anchors[i].height || i - j);
|
|
3731
|
+
let k = 0;
|
|
3732
|
+
for (const i of order) {
|
|
3733
|
+
const f = anchors[i];
|
|
3734
|
+
let best = -1;
|
|
3735
|
+
let near = Number.POSITIVE_INFINITY;
|
|
3736
|
+
for (let c = 0; c < k; c++) {
|
|
3737
|
+
const d = Math.hypot(f.x - root2[c * 3], f.y - root2[c * 3 + 1], f.z - root2[c * 3 + 2]);
|
|
3738
|
+
if (d < near) {
|
|
3739
|
+
near = d;
|
|
3740
|
+
best = c;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
if (best < 0 || near > CLUSTER_REACH && k < FIRE_CLUSTERS) {
|
|
3744
|
+
best = k++;
|
|
3745
|
+
root2[best * 3] = f.x;
|
|
3746
|
+
root2[best * 3 + 1] = f.y;
|
|
3747
|
+
root2[best * 3 + 2] = f.z;
|
|
3748
|
+
weight[best] = 0;
|
|
3749
|
+
let cluster2 = clusters[best];
|
|
3750
|
+
if (!cluster2) {
|
|
3751
|
+
cluster2 = { x: 0, y: 0, z: 0, height: 0, tallest: 0, flicker: 0, heat: 0, count: 0 };
|
|
3752
|
+
clusters[best] = cluster2;
|
|
3753
|
+
}
|
|
3754
|
+
cluster2.x = 0;
|
|
3755
|
+
cluster2.y = 0;
|
|
3756
|
+
cluster2.z = 0;
|
|
3757
|
+
cluster2.height = 0;
|
|
3758
|
+
cluster2.tallest = f.height;
|
|
3759
|
+
cluster2.flicker = 0;
|
|
3760
|
+
cluster2.heat = 0;
|
|
3761
|
+
cluster2.count = 0;
|
|
3762
|
+
}
|
|
3763
|
+
const cluster = clusters[best];
|
|
3764
|
+
const w = f.height;
|
|
3765
|
+
cluster.x += f.x * w;
|
|
3766
|
+
cluster.y += f.y * w;
|
|
3767
|
+
cluster.z += f.z * w;
|
|
3768
|
+
weight[best] = weight[best] + w;
|
|
3769
|
+
cluster.height += f.height;
|
|
3770
|
+
cluster.flicker += flamePuff(f.seed, this.field.time);
|
|
3771
|
+
cluster.heat += f.heat;
|
|
3772
|
+
cluster.count++;
|
|
3773
|
+
}
|
|
3774
|
+
for (let c = 0; c < k; c++) {
|
|
3775
|
+
const cluster = clusters[c];
|
|
3776
|
+
const w = weight[c];
|
|
3777
|
+
if (w > 0) {
|
|
3778
|
+
cluster.x /= w;
|
|
3779
|
+
cluster.y /= w;
|
|
3780
|
+
cluster.z /= w;
|
|
3781
|
+
} else {
|
|
3782
|
+
cluster.x = root2[c * 3];
|
|
3783
|
+
cluster.y = root2[c * 3 + 1];
|
|
3784
|
+
cluster.z = root2[c * 3 + 2];
|
|
3785
|
+
}
|
|
3786
|
+
cluster.height /= cluster.count;
|
|
3787
|
+
cluster.flicker /= cluster.count;
|
|
3788
|
+
cluster.heat /= cluster.count;
|
|
3789
|
+
}
|
|
3790
|
+
this.clusterCount = k;
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
var shared = /* @__PURE__ */ new WeakMap();
|
|
3794
|
+
function fireStateOf(field, locate) {
|
|
3795
|
+
let byLocator = shared.get(field);
|
|
3796
|
+
if (!byLocator) {
|
|
3797
|
+
byLocator = /* @__PURE__ */ new WeakMap();
|
|
3798
|
+
shared.set(field, byLocator);
|
|
3799
|
+
}
|
|
3800
|
+
let state = byLocator.get(locate);
|
|
3801
|
+
if (!state) {
|
|
3802
|
+
state = new FireState(field, locate);
|
|
3803
|
+
byLocator.set(locate, state);
|
|
3804
|
+
}
|
|
3805
|
+
return state;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
// src/fx/FxFireLight.tsx
|
|
3809
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
3810
|
+
function FxFireLight({ field, locate, gain = FIRE_LIGHT_GAIN, shadows = false }) {
|
|
3811
|
+
const front = (0, import_react7.useRef)([]);
|
|
3812
|
+
const through = (0, import_react7.useRef)(null);
|
|
3813
|
+
const spot = (0, import_react7.useRef)(null);
|
|
3814
|
+
(0, import_fiber6.useFrame)(({ camera, clock }) => {
|
|
3815
|
+
const b = through.current;
|
|
3816
|
+
if (!b) return;
|
|
3817
|
+
const fire = fireStateOf(field, locate).update(clock.elapsedTime);
|
|
3818
|
+
const level = field.lastStats.front * gain;
|
|
3819
|
+
let total = 0;
|
|
3820
|
+
let brightest = -1;
|
|
3821
|
+
let most = 0;
|
|
3822
|
+
for (let c = 0; c < fire.clusterCount; c++) {
|
|
3823
|
+
const cluster = fire.clusters[c];
|
|
3824
|
+
const share = cluster.count * cluster.height;
|
|
3825
|
+
total += share;
|
|
3826
|
+
if (share > most) {
|
|
3827
|
+
most = share;
|
|
3828
|
+
brightest = c;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
const s = shadows ? spot.current : null;
|
|
3832
|
+
if (s) s.intensity = 0;
|
|
3833
|
+
for (let i = 0; i < FIRE_CLUSTERS; i++) {
|
|
3834
|
+
const light = front.current[i];
|
|
3835
|
+
if (!light) continue;
|
|
3836
|
+
const cluster = i < fire.clusterCount ? fire.clusters[i] : null;
|
|
3837
|
+
if (!cluster || !(total > 0)) {
|
|
3838
|
+
light.intensity = 0;
|
|
3839
|
+
continue;
|
|
3840
|
+
}
|
|
3841
|
+
toward.set(camera.position.x - cluster.x, 0, camera.position.z - cluster.z).normalize().multiplyScalar(0.035);
|
|
3842
|
+
light.position.set(cluster.x + toward.x, cluster.y + cluster.height * 0.5, cluster.z + toward.z);
|
|
3843
|
+
light.intensity = level * cluster.flicker * (cluster.count * cluster.height / total);
|
|
3844
|
+
light.color.copy(COOLING).lerp(FIRE_COLOR, Math.min(1, Math.max(0, cluster.heat)));
|
|
3845
|
+
if (s && i === brightest) {
|
|
3846
|
+
s.position.copy(light.position);
|
|
3847
|
+
s.target.position.set(cluster.x, cluster.y, cluster.z);
|
|
3848
|
+
s.target.updateMatrixWorld();
|
|
3849
|
+
s.intensity = light.intensity;
|
|
3850
|
+
s.color.copy(light.color);
|
|
3851
|
+
light.intensity = 0;
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
if (fire.count === 0) {
|
|
3855
|
+
b.intensity = 0;
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
toward.set(camera.position.x - fire.x, 0, camera.position.z - fire.z).normalize().multiplyScalar(0.035);
|
|
3859
|
+
b.position.set(fire.x - toward.x, fire.y + fire.height * 0.5, fire.z - toward.z);
|
|
3860
|
+
b.intensity = level * fire.flicker * 0.35;
|
|
3662
3861
|
});
|
|
3663
3862
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
3664
|
-
|
|
3863
|
+
Array.from({ length: FIRE_CLUSTERS }, (_, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
3864
|
+
"pointLight",
|
|
3865
|
+
{
|
|
3866
|
+
ref: (light) => {
|
|
3867
|
+
front.current[i] = light;
|
|
3868
|
+
},
|
|
3869
|
+
color: FIRE_COLOR,
|
|
3870
|
+
intensity: 0,
|
|
3871
|
+
decay: 0.9,
|
|
3872
|
+
distance: 0
|
|
3873
|
+
},
|
|
3874
|
+
i
|
|
3875
|
+
)),
|
|
3876
|
+
shadows && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
3877
|
+
"spotLight",
|
|
3878
|
+
{
|
|
3879
|
+
ref: spot,
|
|
3880
|
+
color: FIRE_COLOR,
|
|
3881
|
+
intensity: 0,
|
|
3882
|
+
decay: 0.9,
|
|
3883
|
+
distance: 0,
|
|
3884
|
+
angle: 1.3,
|
|
3885
|
+
penumbra: 0.8,
|
|
3886
|
+
castShadow: true,
|
|
3887
|
+
"shadow-mapSize": [512, 512],
|
|
3888
|
+
"shadow-bias": -4e-4
|
|
3889
|
+
}
|
|
3890
|
+
),
|
|
3665
3891
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("pointLight", { ref: through, color: FIRE_COLOR, intensity: 0, decay: 0.9, distance: 0 })
|
|
3666
3892
|
] });
|
|
3667
3893
|
}
|
|
3894
|
+
var toward = new THREE8.Vector3();
|
|
3668
3895
|
var FIRE_COLOR = new THREE8.Color("#ffa24c");
|
|
3669
|
-
var
|
|
3896
|
+
var COOLING = new THREE8.Color("#ff8a33");
|
|
3897
|
+
var FIRE_LIGHT_GAIN = 26;
|
|
3670
3898
|
|
|
3671
3899
|
// src/fx/FxMatchFlame.tsx
|
|
3672
3900
|
var THREE9 = __toESM(require("three"), 1);
|
|
@@ -4230,6 +4458,7 @@ init_quality();
|
|
|
4230
4458
|
FIELD_SIZE,
|
|
4231
4459
|
FIRE_GLOW,
|
|
4232
4460
|
FIRE_HEAT_SCALE,
|
|
4461
|
+
FIRE_LIGHT_GAIN,
|
|
4233
4462
|
FIRE_SOOT_SCALE,
|
|
4234
4463
|
FIRE_ZONES,
|
|
4235
4464
|
FIXED_DT,
|