paperlab 0.6.0 → 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 +5 -1
- 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-E22ILEDO.js → chunk-MZCAN3AR.js} +1264 -197
- 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 +2652 -914
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/stage.cjs +2864 -1087
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.js +31 -51
- package/dist/stage.js.map +1 -1
- package/package.json +16 -3
- package/dist/chunk-E22ILEDO.js.map +0 -1
package/README.md
CHANGED
|
@@ -25,13 +25,17 @@ npm i paperlab three @react-three/fiber gsap
|
|
|
25
25
|
|
|
26
26
|
Requires React ≥ 19 and three ≥ 0.162. TypeScript types ship with the package; both ESM and CJS builds are published.
|
|
27
27
|
|
|
28
|
+
> **npm users:** `@react-three/fiber@9.7.0` caps React at `>=19 <19.3`, and npm installs React 19.3 by default, so the line above can fail with `ERESOLVE`. Ask for a React the renderer accepts — `npm i paperlab three @react-three/fiber gsap react@19.2 react-dom@19.2` — until fiber widens its range. pnpm and yarn resolve it without complaint. This is upstream's ceiling, not Paperlab's: the library itself supports any React ≥ 19.
|
|
29
|
+
|
|
28
30
|
Stage mode lives at the `paperlab/stage` subpath and needs two more peers:
|
|
29
31
|
|
|
30
32
|
```sh
|
|
31
33
|
npm i @react-three/postprocessing postprocessing
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
They are optional. `<Paper>` and `<PaperField>` never reach for them, so a bundle that does not import `paperlab/stage` contains none of it —
|
|
36
|
+
They are genuinely optional, in both directions. `<Paper>` and `<PaperField>` never reach for them, so a bundle that does not import `paperlab/stage` contains none of it — the subpath keeps the import specifier itself out of your build graph. And `paperlab/stage` loads the print pass on demand rather than at module scope, so a stage renders without them too: you lose the tone curve, bloom, vignette and grain, and the console says so once.
|
|
37
|
+
|
|
38
|
+
`paperlab/fx` is the third entry point: what happens **to** the paper. A sheet takes `<Paper damage={source}>` — char, water, heat and missing paper over its own UV — and draws it, cuts its shadow with it, and on a simulated sheet shrinks, curls, weighs down and breaks the paper it describes. What *causes* damage lives behind the subpath: a field that burns, soaks and erodes; the embers, smoke and ash a burn throws off; and a fire synthesised from the same numbers the picture is drawn from. A `<Paper>`-only bundle resolves none of it.
|
|
35
39
|
|
|
36
40
|
## Quick start
|
|
37
41
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FX_BLOOM,
|
|
3
|
+
FX_BLOOM_THRESHOLD,
|
|
4
|
+
PAPER_WHITE,
|
|
5
|
+
flameAnchors,
|
|
6
|
+
fxQualityFor
|
|
7
|
+
} from "./chunk-GB6BMHC3.js";
|
|
8
|
+
import "./chunk-7TLIWPGM.js";
|
|
9
|
+
|
|
10
|
+
// src/fx/FxPostPass.tsx
|
|
11
|
+
import { Bloom, EffectComposer, ToneMapping } from "@react-three/postprocessing";
|
|
12
|
+
import { useFrame } from "@react-three/fiber";
|
|
13
|
+
import { ToneMappingMode } from "postprocessing";
|
|
14
|
+
import { useMemo, useRef } from "react";
|
|
15
|
+
import * as THREE2 from "three";
|
|
16
|
+
|
|
17
|
+
// src/fx/haze.ts
|
|
18
|
+
import * as THREE from "three";
|
|
19
|
+
import { BlendFunction, Effect } from "postprocessing";
|
|
20
|
+
var HAZE_SOURCES = 16;
|
|
21
|
+
var FRAGMENT = (
|
|
22
|
+
/* glsl */
|
|
23
|
+
`
|
|
24
|
+
uniform vec4 uSources[${HAZE_SOURCES}];
|
|
25
|
+
uniform int uCount;
|
|
26
|
+
uniform float uTime;
|
|
27
|
+
uniform float uAmount;
|
|
28
|
+
uniform vec3 uWarm;
|
|
29
|
+
uniform float uGrade;
|
|
30
|
+
|
|
31
|
+
float hzHash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
|
|
32
|
+
float hzNoise(vec2 p) {
|
|
33
|
+
vec2 i = floor(p);
|
|
34
|
+
vec2 f = fract(p);
|
|
35
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
36
|
+
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);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Heat haze: the air above a flame is hotter than the air beside it, and
|
|
40
|
+
// light bends through the difference. A few pixels of upward-scrolling
|
|
41
|
+
// displacement, only in the column above each flame, fading with height.
|
|
42
|
+
void mainUv(inout vec2 uv) {
|
|
43
|
+
vec2 offset = vec2(0.0);
|
|
44
|
+
for (int i = 0; i < ${HAZE_SOURCES}; i++) {
|
|
45
|
+
if (i >= uCount) break;
|
|
46
|
+
vec4 s = uSources[i];
|
|
47
|
+
vec2 d = uv - s.xy;
|
|
48
|
+
float h = max(s.z, 1e-4);
|
|
49
|
+
float above = d.y / h;
|
|
50
|
+
if (above < -0.1 || above > 3.2) continue;
|
|
51
|
+
float across = abs(d.x) / (h * 0.55 + 1e-4);
|
|
52
|
+
// Around the flame's own boundary as well as above it: gentle inside the
|
|
53
|
+
// body, strongest in the hot column over it, gone by three flame heights.
|
|
54
|
+
float column = smoothstep(0.3, 0.9, above) * (1.0 - smoothstep(1.4, 3.2, above));
|
|
55
|
+
float around = smoothstep(-0.1, 0.15, above) * (1.0 - smoothstep(0.8, 1.2, above)) * 0.45;
|
|
56
|
+
float m = (1.0 - smoothstep(0.45, 1.15, across)) * max(column, around) * s.w;
|
|
57
|
+
vec2 q = vec2(uv.x * 140.0, uv.y * 90.0 - uTime * 3.0);
|
|
58
|
+
offset += (vec2(hzNoise(q), hzNoise(q + 31.7)) - 0.5) * m;
|
|
59
|
+
}
|
|
60
|
+
uv += offset * uAmount;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// The grade: the whole frame warms as the fire grows \u2014 a few percent, no
|
|
64
|
+
// more (spec \xA77). A multiply, so black stays black.
|
|
65
|
+
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
|
|
66
|
+
outputColor = vec4(inputColor.rgb * mix(vec3(1.0), uWarm, uGrade), inputColor.a);
|
|
67
|
+
}
|
|
68
|
+
`
|
|
69
|
+
);
|
|
70
|
+
var HazeGradeEffect = class extends Effect {
|
|
71
|
+
constructor() {
|
|
72
|
+
super("HazeGradeEffect", FRAGMENT, {
|
|
73
|
+
blendFunction: BlendFunction.NORMAL,
|
|
74
|
+
uniforms: /* @__PURE__ */ new Map([
|
|
75
|
+
["uSources", new THREE.Uniform(Array.from({ length: HAZE_SOURCES }, () => new THREE.Vector4()))],
|
|
76
|
+
["uCount", new THREE.Uniform(0)],
|
|
77
|
+
["uTime", new THREE.Uniform(0)],
|
|
78
|
+
// 1–3 px at 1080p, as a fraction of the frame.
|
|
79
|
+
["uAmount", new THREE.Uniform(2.2 / 1080)],
|
|
80
|
+
["uWarm", new THREE.Uniform(new THREE.Vector3(1.04, 1, 0.94))],
|
|
81
|
+
["uGrade", new THREE.Uniform(0)]
|
|
82
|
+
])
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
get sources() {
|
|
86
|
+
return this.uniforms.get("uSources").value;
|
|
87
|
+
}
|
|
88
|
+
set count(n) {
|
|
89
|
+
this.uniforms.get("uCount").value = n;
|
|
90
|
+
}
|
|
91
|
+
set time(t) {
|
|
92
|
+
this.uniforms.get("uTime").value = t;
|
|
93
|
+
}
|
|
94
|
+
/** 0 turns the shimmer off (the low tier) and keeps the grade. */
|
|
95
|
+
set amount(px1080) {
|
|
96
|
+
this.uniforms.get("uAmount").value = px1080 / 1080;
|
|
97
|
+
}
|
|
98
|
+
/** 0..1 — how much of the warm grade to apply. */
|
|
99
|
+
set grade(g) {
|
|
100
|
+
this.uniforms.get("uGrade").value = g;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/fx/FxPostPass.tsx
|
|
105
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
106
|
+
var modes = {
|
|
107
|
+
agx: ToneMappingMode.AGX,
|
|
108
|
+
neutral: ToneMappingMode.NEUTRAL,
|
|
109
|
+
filmic: ToneMappingMode.ACES_FILMIC
|
|
110
|
+
};
|
|
111
|
+
var root = new THREE2.Vector3();
|
|
112
|
+
var top = new THREE2.Vector3();
|
|
113
|
+
function FxPostPass({
|
|
114
|
+
film = "neutral",
|
|
115
|
+
quality = "medium",
|
|
116
|
+
bloom = FX_BLOOM,
|
|
117
|
+
threshold = FX_BLOOM_THRESHOLD,
|
|
118
|
+
field,
|
|
119
|
+
locate,
|
|
120
|
+
haze: hazeOverride
|
|
121
|
+
}) {
|
|
122
|
+
const { bloomScale, haze: tierHaze } = fxQualityFor(quality);
|
|
123
|
+
const hazePx = hazeOverride ?? tierHaze;
|
|
124
|
+
const haze = useMemo(() => new HazeGradeEffect(), []);
|
|
125
|
+
const anchors = useRef([]);
|
|
126
|
+
useFrame(({ camera }) => {
|
|
127
|
+
if (!field || !locate) {
|
|
128
|
+
haze.count = 0;
|
|
129
|
+
haze.grade = 0;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const n = hazePx > 0 ? flameAnchors(field, locate, HAZE_SOURCES, anchors.current) : 0;
|
|
133
|
+
const sources = haze.sources;
|
|
134
|
+
for (let i = 0; i < n; i++) {
|
|
135
|
+
const a = anchors.current[i];
|
|
136
|
+
root.set(a.x, a.y, a.z).project(camera);
|
|
137
|
+
top.set(a.x, a.y + a.height, a.z).project(camera);
|
|
138
|
+
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);
|
|
139
|
+
}
|
|
140
|
+
haze.count = n;
|
|
141
|
+
haze.time = field.time;
|
|
142
|
+
haze.amount = hazePx;
|
|
143
|
+
haze.grade = Math.min(1, field.lastStats.front / 0.03);
|
|
144
|
+
});
|
|
145
|
+
return /* @__PURE__ */ jsxs(EffectComposer, { children: [
|
|
146
|
+
bloom > 0 ? /* @__PURE__ */ jsx(
|
|
147
|
+
Bloom,
|
|
148
|
+
{
|
|
149
|
+
intensity: bloom,
|
|
150
|
+
luminanceThreshold: threshold,
|
|
151
|
+
luminanceSmoothing: 0.02,
|
|
152
|
+
mipmapBlur: true,
|
|
153
|
+
resolutionScale: bloomScale
|
|
154
|
+
}
|
|
155
|
+
) : null,
|
|
156
|
+
/* @__PURE__ */ jsx("primitive", { object: haze }),
|
|
157
|
+
/* @__PURE__ */ jsx(ToneMapping, { mode: modes[film] })
|
|
158
|
+
] });
|
|
159
|
+
}
|
|
160
|
+
export {
|
|
161
|
+
FX_BLOOM,
|
|
162
|
+
FX_BLOOM_THRESHOLD,
|
|
163
|
+
FxPostPass,
|
|
164
|
+
PAPER_WHITE
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=FxPostPass-M66QMVVC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fx/FxPostPass.tsx","../src/fx/haze.ts"],"sourcesContent":["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","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"],"mappings":"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","names":["THREE"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/stage/Grade.tsx
|
|
2
|
+
import {
|
|
3
|
+
EffectComposer,
|
|
4
|
+
Bloom,
|
|
5
|
+
DepthOfField,
|
|
6
|
+
Vignette,
|
|
7
|
+
Noise,
|
|
8
|
+
ToneMapping
|
|
9
|
+
} from "@react-three/postprocessing";
|
|
10
|
+
import { BlendFunction, ToneMappingMode } from "postprocessing";
|
|
11
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
|
+
var toneMappingModes = {
|
|
13
|
+
agx: ToneMappingMode.AGX,
|
|
14
|
+
neutral: ToneMappingMode.NEUTRAL,
|
|
15
|
+
filmic: ToneMappingMode.ACES_FILMIC
|
|
16
|
+
};
|
|
17
|
+
function Grade({ grade, film }) {
|
|
18
|
+
const bloom = grade.bloom > 0;
|
|
19
|
+
const depth = grade.depth > 0;
|
|
20
|
+
const vignette = grade.vignette > 0;
|
|
21
|
+
const grain = grade.grain > 0;
|
|
22
|
+
if (!bloom && !depth && !vignette && !grain) return null;
|
|
23
|
+
return /* @__PURE__ */ jsxs(EffectComposer, { children: [
|
|
24
|
+
bloom ? /* @__PURE__ */ jsx(
|
|
25
|
+
Bloom,
|
|
26
|
+
{
|
|
27
|
+
intensity: grade.bloom,
|
|
28
|
+
luminanceThreshold: grade.threshold,
|
|
29
|
+
luminanceSmoothing: 0.22,
|
|
30
|
+
mipmapBlur: true
|
|
31
|
+
}
|
|
32
|
+
) : null,
|
|
33
|
+
depth ? /* @__PURE__ */ jsx(
|
|
34
|
+
DepthOfField,
|
|
35
|
+
{
|
|
36
|
+
focusDistance: 8e-3,
|
|
37
|
+
focalLength: 0.02 + grade.depth * 0.04,
|
|
38
|
+
bokehScale: grade.depth * 2.5
|
|
39
|
+
}
|
|
40
|
+
) : null,
|
|
41
|
+
/* @__PURE__ */ jsx(ToneMapping, { mode: toneMappingModes[film] }),
|
|
42
|
+
vignette ? /* @__PURE__ */ jsx(Vignette, { offset: 0.32, darkness: grade.vignette }) : null,
|
|
43
|
+
grain ? /* @__PURE__ */ jsx(Noise, { opacity: grade.grain, blendFunction: BlendFunction.OVERLAY }) : null
|
|
44
|
+
] });
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
Grade
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=Grade-SBGYELKV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stage/Grade.tsx"],"sourcesContent":["import {\n EffectComposer,\n Bloom,\n DepthOfField,\n Vignette,\n Noise,\n ToneMapping,\n} from '@react-three/postprocessing'\nimport { BlendFunction, ToneMappingMode } from 'postprocessing'\nimport type { FilmName } from '../config/schema'\nimport type { StageGradeConfig } from './schema'\n\n/**\n * The print pass: tone curve, bloom, vignette, grain.\n *\n * Kept in its own module for one reason — it is the ONLY file in the library\n * that imports `@react-three/postprocessing`, and nothing imports it\n * statically. `GradeLazy` reaches it through a dynamic import, which is what\n * lets the two peers stay genuinely optional; importing this module directly\n * from anywhere would put the specifier back into stage's module graph and\n * undo that. Adding an import here is fine. Importing this from anywhere but\n * `GradeLazy` is not.\n *\n * **The composer takes the tone curve away from the renderer, so this file\n * has to give it back.** `<EffectComposer>` sets `gl.toneMapping =\n * NoToneMapping` for as long as it is mounted — it has to, because tone\n * mapping belongs at the END of a post chain rather than at the end of the\n * scene pass, and a frame mapped twice is wrong twice. What that means here\n * is that mounting a composer without a `<ToneMapping>` effect silently\n * throws away `light.film` entirely: the stage's own grade would have been\n * the one thing capable of un-doing the AgX curve everything else reads.\n *\n * Why the rest of it is needed, in the order it shows up in a frame:\n *\n * - **Bloom.** The source at the end of the walk is a `meshBasicMaterial`\n * with `toneMapped: false`, deliberately, because it is light rather than\n * an object. Nothing rolls it off, so without bloom it clips to a flat\n * shape with a boundary — a lit panel hanging in the room. `Surround`\n * already spends a seven-stop alpha ramp fighting that in geometry, which\n * is the wrong layer to fight it in.\n * - **Vignette.** A frame with no edge reads as a viewport.\n * - **Grain.** The one texture the render and the subject have in common.\n */\n\n/** The rig's film, as a postprocessing mode. Mirrors `toneMappings` in PaperLighting. */\nconst toneMappingModes: Record<FilmName, ToneMappingMode> = {\n agx: ToneMappingMode.AGX,\n neutral: ToneMappingMode.NEUTRAL,\n filmic: ToneMappingMode.ACES_FILMIC,\n}\n\nexport function Grade({ grade, film }: { grade: StageGradeConfig; film: FilmName }) {\n const bloom = grade.bloom > 0\n const depth = grade.depth > 0\n const vignette = grade.vignette > 0\n const grain = grade.grain > 0\n\n // A composer is a full-screen render target and a second pass over every\n // pixel. A stage graded to nothing should not pay for one — and crucially,\n // must not MOUNT one, because an empty composer would still take the tone\n // curve off the renderer and hand back nothing.\n if (!bloom && !depth && !vignette && !grain) return null\n\n return (\n <EffectComposer>\n {/*\n Order is the whole correctness argument here.\n\n Bloom reads the scene while it is still HDR — that is what lets a\n threshold near 1.0 mean \"brighter than paper\" rather than \"brighter\n than whatever the curve happened to flatten paper to\". Tone mapping\n then lands the result in display range, and vignette and grain come\n after it because both are darkroom moves on a finished print, not\n light in the room.\n */}\n {bloom ? (\n <Bloom\n intensity={grade.bloom}\n luminanceThreshold={grade.threshold}\n luminanceSmoothing={0.22}\n mipmapBlur\n />\n ) : null}\n {/*\n Depth goes with bloom on the HDR side, before the curve, because a\n blur of tone-mapped pixels averages DISPLAY values and a blur of\n scene values averages light. Only the second one puts a bright\n highlight's glow into the soft region, which is the entire reason a\n real lens's out-of-focus areas look the way they do.\n\n `focusDistance` is normalized against the camera's far plane, and the\n stage's camera stands ON the walk looking down it — so the focal\n plane sits a little ahead of the viewer and both ends fall away.\n */}\n {depth ? (\n <DepthOfField\n // Normalized against the camera's far plane, which this scene sets\n // to 400 — so 0.008 is roughly three units out, which is where the\n // banner you are standing in front of actually is. The first pass\n // at this focused eight units away and put the focal plane in the\n // empty air past the paper, so nothing in frame was sharp.\n focusDistance={0.008}\n focalLength={0.02 + grade.depth * 0.04}\n bokehScale={grade.depth * 2.5}\n />\n ) : null}\n <ToneMapping mode={toneMappingModes[film]} />\n {vignette ? <Vignette offset={0.32} darkness={grade.vignette} /> : null}\n {/*\n OVERLAY rather than NORMAL: grain added flat lifts the blacks and\n turns a dark hall grey. Overlay leaves them where they are and puts\n the texture into the midtones, which is where film grain lives.\n */}\n {grain ? <Noise opacity={grade.grain} blendFunction={BlendFunction.OVERLAY} /> : null}\n </EffectComposer>\n )\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,eAAe,uBAAuB;AAwD3C,SAYI,KAZJ;AAnBJ,IAAM,mBAAsD;AAAA,EAC1D,KAAK,gBAAgB;AAAA,EACrB,SAAS,gBAAgB;AAAA,EACzB,QAAQ,gBAAgB;AAC1B;AAEO,SAAS,MAAM,EAAE,OAAO,KAAK,GAAgD;AAClF,QAAM,QAAQ,MAAM,QAAQ;AAC5B,QAAM,QAAQ,MAAM,QAAQ;AAC5B,QAAM,WAAW,MAAM,WAAW;AAClC,QAAM,QAAQ,MAAM,QAAQ;AAM5B,MAAI,CAAC,SAAS,CAAC,SAAS,CAAC,YAAY,CAAC,MAAO,QAAO;AAEpD,SACE,qBAAC,kBAWE;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,MAAM;AAAA,QACjB,oBAAoB,MAAM;AAAA,QAC1B,oBAAoB;AAAA,QACpB,YAAU;AAAA;AAAA,IACZ,IACE;AAAA,IAYH,QACC;AAAA,MAAC;AAAA;AAAA,QAMC,eAAe;AAAA,QACf,aAAa,OAAO,MAAM,QAAQ;AAAA,QAClC,YAAY,MAAM,QAAQ;AAAA;AAAA,IAC5B,IACE;AAAA,IACJ,oBAAC,eAAY,MAAM,iBAAiB,IAAI,GAAG;AAAA,IAC1C,WAAW,oBAAC,YAAS,QAAQ,MAAM,UAAU,MAAM,UAAU,IAAK;AAAA,IAMlE,QAAQ,oBAAC,SAAM,SAAS,MAAM,OAAO,eAAe,cAAc,SAAS,IAAK;AAAA,KACnF;AAEJ;","names":[]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/surface/damageContract.ts
|
|
2
|
+
var DAMAGE_LOOK_DEFAULTS = {
|
|
3
|
+
// §5 asks for 0.3–1 mm. 1.8 was outside it, and outside the slider's range
|
|
4
|
+
// it was tuned in — a control at its limit is a report that something
|
|
5
|
+
// underneath is wrong, which in this case was a fire nothing could see.
|
|
6
|
+
emberWidth: 0.9,
|
|
7
|
+
emberIntensity: 1.45,
|
|
8
|
+
emberCoverage: 0.6,
|
|
9
|
+
emberFlicker: 1.65,
|
|
10
|
+
emberGlow: 1.25,
|
|
11
|
+
sparkle: 0.5,
|
|
12
|
+
lipWidth: 1.2,
|
|
13
|
+
// Was 1.5, its slider's ceiling, which made the ash lip brighter than the
|
|
14
|
+
// paper it sits on. Ash is pale GREY; the reference's lip is dimmer than
|
|
15
|
+
// the sheet, not a highlight drawn on it.
|
|
16
|
+
lipBrightness: 0.85,
|
|
17
|
+
// Was 1, also a ceiling. At full warmth the char is milk chocolate —
|
|
18
|
+
// closer to cardboard than to charcoal (§5). Burnt paper keeps a little
|
|
19
|
+
// warmth in the plates and reads near black in a frame with a fire in it.
|
|
20
|
+
charWarmth: 0.3,
|
|
21
|
+
// Was 1, also a ceiling. The cracks are drawn as thin polygon outlines, so
|
|
22
|
+
// at full strength the char reads as a mosaic rather than as broken plates.
|
|
23
|
+
charCracks: 0.55,
|
|
24
|
+
scorchReach: 30,
|
|
25
|
+
scorchDarkness: 1.17,
|
|
26
|
+
fingers: 1.25,
|
|
27
|
+
edgeWave: 8.5,
|
|
28
|
+
edgeBite: 3.6
|
|
29
|
+
};
|
|
30
|
+
var DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
DAMAGE_LOOK_DEFAULTS,
|
|
34
|
+
DAMAGE_CHANNELS
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=chunk-7TLIWPGM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/surface/damageContract.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"],"mappings":";AAmHO,IAAM,uBAA6C;AAAA;AAAA;AAAA;AAAA,EAIxD,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,WAAW;AAAA,EACX,SAAS;AAAA,EACT,UAAU;AAAA;AAAA;AAAA;AAAA,EAIV,eAAe;AAAA;AAAA;AAAA;AAAA,EAIf,YAAY;AAAA;AAAA;AAAA,EAGZ,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT,UAAU;AAAA,EACV,UAAU;AACZ;AAUO,IAAM,kBAAkB,EAAE,MAAM,GAAG,YAAY,GAAG,MAAM,GAAG,UAAU,EAAE;","names":[]}
|