paperlab 0.7.1 → 0.8.1
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 +57 -28
- package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-ACFSHY3S.js} +24 -6
- package/dist/FxPostPass-ACFSHY3S.js.map +1 -0
- package/dist/chunk-BIFUJCUA.js +34 -0
- package/dist/chunk-BIFUJCUA.js.map +1 -0
- package/dist/{chunk-NTDVOOBA.js → chunk-LUKNPQ3S.js} +385 -165
- package/dist/chunk-LUKNPQ3S.js.map +1 -0
- package/dist/{chunk-GB6BMHC3.js → chunk-S74YZAXE.js} +7 -19
- package/dist/chunk-S74YZAXE.js.map +1 -0
- package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-9RC4egJg.d.cts} +31 -3
- package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-9RC4egJg.d.ts} +31 -3
- package/dist/fx.cjs +336 -107
- package/dist/fx.cjs.map +1 -1
- package/dist/fx.d.cts +113 -89
- package/dist/fx.d.ts +113 -89
- package/dist/fx.js +293 -63
- package/dist/fx.js.map +1 -1
- package/dist/index.cjs +422 -193
- 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 +21 -8
- package/dist/index.js.map +1 -1
- package/dist/{slots-C5PqY0Q5.d.cts → slots-DbJQfwVd.d.cts} +13 -1
- package/dist/{slots-C5PqY0Q5.d.ts → slots-DbJQfwVd.d.ts} +13 -1
- package/dist/stage.cjs +417 -201
- 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 +12 -12
- package/dist/stage.js.map +1 -1
- package/package.json +5 -5
- 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-GB6BMHC3.js.map +0 -1
- package/dist/chunk-NTDVOOBA.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
[](https://github.com/NourMtir0722/Paperlab/actions/workflows/ci.yml)
|
|
7
7
|
[](https://github.com/NourMtir0722/Paperlab/blob/main/LICENSE)
|
|
8
8
|
|
|
9
|
-
A hero image that peels, a receipt that unrolls, a letter that folds, a poster rippling in wind, a gallery ring of prints. A sheet is real 3D geometry, not a CSS trick and not a video — content is a texture on a mesh that genuinely bends, so text and imagery curl with perfect continuity.
|
|
9
|
+
A hero image that peels, a receipt that unrolls, a letter that folds, a poster rippling in wind, a gallery ring of prints — and a sheet that burns. A sheet is real 3D geometry, not a CSS trick and not a video — content is a texture on a mesh that genuinely bends, so text and imagery curl with perfect continuity.
|
|
10
10
|
|
|
11
|
-
**[Try it →](https://paperlab.nawwara.studio/)** · [the editor](https://paperlab.nawwara.studio/editor/) (desktop) · [the reference](https://paperlab.nawwara.studio/docs/) · [with your hands](https://paperlab.nawwara.studio/hands/) (webcam) · [for coding agents](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md)
|
|
11
|
+
**[Try it →](https://paperlab.nawwara.studio/)** · [the editor](https://paperlab.nawwara.studio/editor/) (desktop) · [the reference](https://paperlab.nawwara.studio/docs/) · [with your hands](https://paperlab.nawwara.studio/hands/) (webcam) · [the FX Lab](https://paperlab.nawwara.studio/fx-lab/) · [for coding agents](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md)
|
|
12
12
|
|
|
13
13
|
| | |
|
|
14
14
|
|---|---|
|
|
@@ -27,15 +27,21 @@ Requires React ≥ 19 and three ≥ 0.162. TypeScript types ship with the packag
|
|
|
27
27
|
|
|
28
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
29
|
|
|
30
|
-
|
|
30
|
+
Paperlab has three entry points, and a bundle only resolves the ones it imports:
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `paperlab` | `<Paper>`, `<PaperMesh>` and `<PaperField>` — the sheet, and many sheets |
|
|
35
|
+
| `paperlab/stage` | `<PaperStage>` — paper as architecture |
|
|
36
|
+
| `paperlab/fx` | what happens **to** the paper — the fire, and everything it throws off |
|
|
37
|
+
|
|
38
|
+
Stage mode and the fire's glow need two more peers:
|
|
31
39
|
|
|
32
40
|
```sh
|
|
33
41
|
npm i @react-three/postprocessing postprocessing
|
|
34
42
|
```
|
|
35
43
|
|
|
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
|
|
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.
|
|
44
|
+
They are genuinely optional, in both directions. `<Paper>` and `<PaperField>` never reach for them, so a bundle that does not import `paperlab/stage` or `paperlab/fx` contains none of it — the subpath keeps the import specifier itself out of your build graph. And both load their print pass on demand rather than at module scope, so they render without the peers too: you lose the tone curve, bloom, vignette and grain, and the console says so once.
|
|
39
45
|
|
|
40
46
|
## Quick start
|
|
41
47
|
|
|
@@ -112,6 +118,21 @@ A fold that closes past 45° at a line that stays put leaves a crease behind at
|
|
|
112
118
|
<Paper preset="letter-fold" memory={{ set: 0.6 }} onCrease={save} />
|
|
113
119
|
```
|
|
114
120
|
|
|
121
|
+
### Fire — paper that burns
|
|
122
|
+
|
|
123
|
+
`<Paper damage={source}>` hands a sheet a grid of char, water, heat and missing paper over its own UV, and the sheet draws it without knowing what caused it — the same split `content` already has. So a live simulation, a baked texture and a recorded burn played back all work the same way.
|
|
124
|
+
|
|
125
|
+
A burnt edge is drawn in the layers a real one has, measured in millimetres from the cut: a pale ash lip, a beaded ember line (the only part of the sheet that gives off light), a black char band, and a scorch with a steep edge into clean paper. On a cloth sheet the same grid reaches the physics too — char shrinks the paper and curls it toward the flame, wet paper gets heavier, and paper that has burnt away leaves the simulation, so a piece the fire cuts loose falls away with its own burnt edge.
|
|
126
|
+
|
|
127
|
+
What *causes* the damage lives in `paperlab/fx`:
|
|
128
|
+
|
|
129
|
+
- **`DamageField`** — heat, water, char and presence, simulated on the CPU with the paper's grain and fibre direction in it. It is untiered, so the same seed burns the same way on any device, and it burns at the speed real paper does: a few millimetres a second.
|
|
130
|
+
- **`FxFireFluid`** — the flames: a small GPU fire simulation fed from the hot rim in uneven clusters. Fuel burns where air reaches it, so a tongue has a pale core, a yellow body and a torn tip, and every zone of it is an option.
|
|
131
|
+
- **`FxFireLight`** — the burn is a light source. It lights the sheet, glows through it, and through `DamageSource.firelight` the room it burns in dims around it.
|
|
132
|
+
- **The rest of a fire** — embers, smoke and ash (`FxParticles`), a match held in a hand (`FxMatchFlame`), the thread of smoke from a smouldering bead (`FxWisps`), the embers left after the flames (`Afterglow`), and the bloom and heat haze (`FxPost`).
|
|
133
|
+
|
|
134
|
+
Every default is a tune made in the [FX Lab](https://paperlab.nawwara.studio/fx-lab/), and every one of them is an option you can set. See [AGENTS.md](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md) for the wiring.
|
|
135
|
+
|
|
115
136
|
### Layouts — 12
|
|
116
137
|
|
|
117
138
|

|
|
@@ -156,19 +177,20 @@ And it is navigable rather than a video. It drifts on its own until you touch it
|
|
|
156
177
|
|
|
157
178
|
- **Content** — `blank`, `image`, `text`, `card` and `receipt`, any of which can also sit on the reverse of the sheet via `content.back`. Text is measured and wrapped with real tracking applied *before* measurement, so the painted line matches the line it was broken to.
|
|
158
179
|
- **Physics** — curated idle motion (`float`, `tumble`, `dangle`, `taped`, `breeze`) that composes with behaviors, and a verlet **cloth** mode: pin the top edge, add wind, grab the sheet and pull. Cloth and behaviors are mutually exclusive by schema — cloth owns the vertices.
|
|
180
|
+
- **A floor** — `scene.floor` puts dark, matte ground under the paper. The contact shadow, a simulated sheet and anything the paper lets go of all meet at the same height, so a falling piece lands on the thing casting its shadow instead of through it.
|
|
159
181
|
- **Interaction states** — a preset can carry `states`: overrides-on-base diffs keyed `rest` / `hover` / `pressed` / `picked` / `placed`, with the triggers built in. Drag a stamp past its threshold and it tears off its sheet (the perforation edges facing its neighbours flip to torn), release it over a `<DropZone>` and it settles, release it anywhere else and it flutters home. The whole flow is reachable from the keyboard: focus a paper, Enter picks, arrows move between zones, Enter places, Escape returns it.
|
|
160
182
|
- **Hardware that holds the paper up** — thread to the ceiling or a rod across the top edge, gripped by a clip or a peg. A hung thing that shows what holds it stops reading as a rectangle that happens to float.
|
|
161
183
|
- **Presets** — 18 paper presets and 6 stage presets, and everything serializes to `.paper` JSON validated by a zod schema. Diffable, forkable, shareable.
|
|
162
|
-
- **Agent-first export** — the editor's **Copy for AI**
|
|
163
|
-
- **Accessible by default** — `prefers-reduced-motion` freezes behaviors at their pose and disables physics and
|
|
184
|
+
- **Agent-first export** — the editor's **Copy for AI** produces a self-contained brief you paste into a coding agent: install line, inlined component, placement contract, and a verification step the agent can self-check. See [AGENTS.md](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md) and [docs/llms.txt](https://github.com/NourMtir0722/Paperlab/blob/main/docs/llms.txt).
|
|
185
|
+
- **Accessible by default** — `prefers-reduced-motion` freezes behaviors at their pose and disables physics, entrances and the fire's simulations, a hidden DOM mirror carries the content for screen readers and find-in-page, and a flat DOM fallback renders when WebGL isn't available.
|
|
164
186
|
|
|
165
|
-
The zod schema in `config/schema.ts` is the single source of truth: it validates the API, generates the editor's controls, defines the file format and feeds the docs. If a feature can't serialize into a preset, it doesn't ship.
|
|
187
|
+
The zod schema in `config/schema.ts` is the single source of truth: it validates the API, generates the editor's controls, defines the file format and feeds the docs. If a feature can't serialize into a preset, it doesn't ship. (Damage is the one deliberate exception: it is live state, so it is a prop and never part of a preset or a share link.)
|
|
166
188
|
|
|
167
189
|
## Papers are made to be passed around
|
|
168
190
|
|
|
169
191
|
A paper is data — a `.paper` JSON object validated by a zod schema — so it travels without asking anyone's permission. **You do not need to fork this repo to share one.**
|
|
170
192
|
|
|
171
|
-
**Sending one.** Sculpt a paper in the [editor](https://paperlab.nawwara.studio/editor/) and
|
|
193
|
+
**Sending one.** Sculpt a paper in the [editor](https://paperlab.nawwara.studio/editor/) and pick **Export → Copy share link**: you get a link with the whole paper packed into it. Anyone who opens that link lands in their own editor with your paper loaded and *editable* — a fork, not a read-only view. (Uploaded images are too big for a URL; use the ⬇ download and send the `.paper` file instead.)
|
|
172
194
|
|
|
173
195
|
**Receiving one.** Open the link, or drag a `.paper` file onto the preset panel. Either way it lands in your library next to the built-ins.
|
|
174
196
|
|
|
@@ -193,11 +215,11 @@ That's the whole loop: **make → send → remix → ship.** If you'd rather you
|
|
|
193
215
|
|
|
194
216
|
## The apps
|
|
195
217
|
|
|
196
|
-
|
|
218
|
+
Three sites ship alongside the library, all built on its public API only.
|
|
197
219
|
|
|
198
220
|
**[The playground](https://paperlab.nawwara.studio/playground/)** — one input, one scene, shareable by link. Type a sentence and it builds you a room out of it. Built for a phone.
|
|
199
221
|
|
|
200
|
-
**[The editor](https://paperlab.nawwara.studio/editor/)** — a three-rail canvas tool: presets on the left, sculpt on canvas, inspector on the right, transport at the bottom (space = play/pause), undo and redo on ⌘Z.
|
|
222
|
+
**[The editor](https://paperlab.nawwara.studio/editor/)** — a three-rail canvas tool: presets on the left, sculpt on canvas, inspector on the right, transport at the bottom (space = play/pause), undo and redo on ⌘Z. One switch along the top moves between five surfaces that all wear that same frame — **Paper · Field · Stage · Hands · FX Lab** — and the top-right holds a single button, **Export**.
|
|
201
223
|
|
|
202
224
|

|
|
203
225
|
|
|
@@ -205,13 +227,14 @@ The inspector is generated from the zod schema, so it can never drift from the A
|
|
|
205
227
|
|
|
206
228
|

|
|
207
229
|
|
|
208
|
-
Field mode composes galleries against the same panel — swap the layout, watch fourteen papers rearrange in one draw call. **Export
|
|
230
|
+
Field mode composes galleries against the same panel — swap the layout, watch fourteen papers rearrange in one draw call. **Export** ends the session wherever you are going next: a share link, an image framed for a post, a story or a slide, the component or its JSON for your codebase, or **Copy for AI** for a coding agent. It wants a real screen: under about 900px it says so and points you at the playground.
|
|
209
231
|
|
|
210
|
-
**[
|
|
232
|
+
- **[FX Lab](https://paperlab.nawwara.studio/fx-lab/)** — every knob behind the burn, around the sheet it is burning: the flame's zones, the fluid, the bloom, the light it throws, the ash lip and the char, how fast it eats and how much of the sheet it takes. The library's defaults are a tune made here. Scrub the burn, turn something, and **Export** copies the tune out as JSON to pass to `<Paper>`.
|
|
233
|
+
- **[Hands](https://paperlab.nawwara.studio/hands/)** — set fire to the paper with a real flame. Hold a lighter up to the camera and the sheet catches where the flame is; with no lighter, pinch and hold still and you are holding a match; with no camera, there is a button. The page draws your hand as it tracks it and boxes the flame when it finds one, and once a flame touches the paper the fire runs from there until the whole sheet has burnt. Blow to put it out. It burns the way the FX Lab burns, from the same defaults, and the page is a hundred percent public API — `packages/paperlab` doesn't know it exists.
|
|
211
234
|
|
|
212
|
-
|
|
235
|
+
The flame detector is arithmetic over the camera's pixels, not a model: a flame is bright, warm in the order red-green-blue, and never still, and the last of those three is what keeps a desk lamp from lighting your paper. The hand tracking is [MediaPipe](https://ai.google.dev/edge/mediapipe) (`@mediapipe/tasks-vision`, Apache-2.0) and it runs entirely in your browser: the models download from Google once, and after that no video and no measurement taken from it leaves the device. There is no server to send it to, and a `connect-src` CSP on the page makes that enforceable rather than a promise — including against MediaPipe's own usage telemetry, which the page blocks. Needs a camera, and asks before it takes one.
|
|
213
236
|
|
|
214
|
-
The
|
|
237
|
+
**[The reference](https://paperlab.nawwara.studio/docs/)** — the whole catalogue with every behavior, deformer, layout, stock and surface rendering live. The catalogue is generated from the registries, so it cannot advertise something the library doesn't have.
|
|
215
238
|
|
|
216
239
|
## Development
|
|
217
240
|
|
|
@@ -219,36 +242,41 @@ pnpm + Turborepo, Node 22, [Biome](https://biomejs.dev) for lint and format.
|
|
|
219
242
|
|
|
220
243
|
```sh
|
|
221
244
|
pnpm install
|
|
245
|
+
pnpm hands:setup # once — vendors the hand tracker's wasm and models (35 MB, not committed)
|
|
222
246
|
pnpm dev # the editor at localhost:5173
|
|
223
247
|
```
|
|
224
248
|
|
|
225
249
|
| | |
|
|
226
250
|
|---|---|
|
|
227
251
|
| [`packages/paperlab`](https://github.com/NourMtir0722/Paperlab/blob/main/packages/paperlab/) | the npm library — the only published artifact |
|
|
228
|
-
| [`apps/editor`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/editor/) | the editor —
|
|
252
|
+
| [`apps/editor`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/editor/) | the editor — Paper, Field and Stage, plus Hands (`/hands`) and the FX Lab (`/fx-lab`), each built from the same app in a pass of its own |
|
|
229
253
|
| [`apps/playground`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/playground/) | the playground — one input, one scene, shareable by link |
|
|
230
254
|
| [`apps/docs`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/docs/) | the reference site, with every behavior running live |
|
|
231
|
-
| [`tools/`](https://github.com/NourMtir0722/Paperlab/blob/main/tools/) | browser harnesses — parity, perf, screenshots, the README's motion |
|
|
255
|
+
| [`tools/`](https://github.com/NourMtir0722/Paperlab/blob/main/tools/) | browser harnesses — parity, perf, screenshots, the fire's checks, the README's motion |
|
|
232
256
|
| [`AGENTS.md`](https://github.com/NourMtir0722/Paperlab/blob/main/AGENTS.md) · [`docs/llms.txt`](https://github.com/NourMtir0722/Paperlab/blob/main/docs/llms.txt) | the agent-readable API reference |
|
|
233
|
-
| [`docs/design.md`](https://github.com/NourMtir0722/Paperlab/blob/main/docs/design.md) | the design language the
|
|
257
|
+
| [`docs/design.md`](https://github.com/NourMtir0722/Paperlab/blob/main/docs/design.md) | the design language the apps share, enforced by a test |
|
|
234
258
|
|
|
235
259
|
### Checks
|
|
236
260
|
|
|
237
261
|
```sh
|
|
238
|
-
pnpm test
|
|
239
|
-
pnpm test:parity
|
|
240
|
-
pnpm test:drive
|
|
241
|
-
pnpm test:share
|
|
242
|
-
pnpm test:dropdown
|
|
243
|
-
pnpm test:route
|
|
244
|
-
pnpm test:
|
|
262
|
+
pnpm test # 1,100+ unit tests — deformer math, schema, cloth, layouts, the fire, exports
|
|
263
|
+
pnpm test:parity # 37 golden-vector cases: every deformer's GLSL twin vs its JS twin
|
|
264
|
+
pnpm test:drive # the stage really walks when you drag, wheel or arrow it
|
|
265
|
+
pnpm test:share # sculpt → link → a browser that has never seen the paper
|
|
266
|
+
pnpm test:dropdown # every dropdown option is reachable, including below the fold
|
|
267
|
+
pnpm test:route # the site root routes by device, and links every route it deploys
|
|
268
|
+
pnpm test:consumer # installs the packed library into an empty project and imports it
|
|
269
|
+
pnpm test:damage # an untouched damage field changes not one pixel
|
|
270
|
+
pnpm test:fire-budget # what a burn must measure in the frame — the stage stays black, paper never blooms
|
|
271
|
+
pnpm test:hands # the ways to light the paper really light it (needs a camera-less Chromium)
|
|
272
|
+
pnpm test:fire-look # the burn, moment by moment, beside the stills it is judged against
|
|
245
273
|
pnpm typecheck
|
|
246
274
|
pnpm lint
|
|
247
|
-
pnpm knip
|
|
275
|
+
pnpm knip # dead code and unused exports
|
|
248
276
|
pnpm build
|
|
249
277
|
```
|
|
250
278
|
|
|
251
|
-
Anything that needs a real GPU, real pointer events or a second browser profile is a browser harness in `tools/` rather than a unit test. All of them but `test:hands` are CI gates, along with `publint` and `are-the-types-wrong` on the published package. `test:hands` runs in [its own workflow](https://github.com/NourMtir0722/Paperlab/blob/main/.github/workflows/hands.yml) instead — on the paths that can break it, and weekly — because it fetches its models from Google and a required gate would let someone else's CDN block every unrelated PR. A red X you have to read, rather than a veto.
|
|
279
|
+
Anything that needs a real GPU, real pointer events or a second browser profile is a browser harness in `tools/` rather than a unit test. All of them but `test:hands` and `test:fire-look` are CI gates, along with `publint` and `are-the-types-wrong` on the published package. `test:hands` runs in [its own workflow](https://github.com/NourMtir0722/Paperlab/blob/main/.github/workflows/hands.yml) instead — on the paths that can break it, and weekly — because it fetches its models from Google and a required gate would let someone else's CDN block every unrelated PR. A red X you have to read, rather than a veto. `test:fire-look` is run by hand: it lays each render beside a reference still, those stills are kept out of the repo, and passing it still takes someone looking.
|
|
252
280
|
|
|
253
281
|
### Measurement
|
|
254
282
|
|
|
@@ -260,6 +288,7 @@ pnpm shot:light # every lighting rig, one frame each
|
|
|
260
288
|
pnpm shot:catalogue # sweep any axis — --vary=layout|stock|lighting --all
|
|
261
289
|
pnpm sheet # compose those frames into a labelled contact sheet
|
|
262
290
|
pnpm media # the README's GIFs and MP4s, stepped frame-exact (needs ffmpeg)
|
|
291
|
+
pnpm film # the burn playing, first contact to cold, one video per origin (needs ffmpeg)
|
|
263
292
|
```
|
|
264
293
|
|
|
265
294
|
## Contributing
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
PAPER_WHITE,
|
|
5
5
|
flameAnchors,
|
|
6
6
|
fxQualityFor
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-S74YZAXE.js";
|
|
8
|
+
import "./chunk-BIFUJCUA.js";
|
|
9
9
|
|
|
10
10
|
// src/fx/FxPostPass.tsx
|
|
11
|
-
import { Bloom, EffectComposer, ToneMapping } from "@react-three/postprocessing";
|
|
11
|
+
import { Bloom, DepthOfField, EffectComposer, ToneMapping } from "@react-three/postprocessing";
|
|
12
12
|
import { useFrame } from "@react-three/fiber";
|
|
13
13
|
import { ToneMappingMode } from "postprocessing";
|
|
14
14
|
import { useMemo, useRef } from "react";
|
|
@@ -61,7 +61,7 @@ void mainUv(inout vec2 uv) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// The grade: the whole frame warms as the fire grows \u2014 a few percent, no
|
|
64
|
-
// more
|
|
64
|
+
// more. A multiply, so black stays black.
|
|
65
65
|
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
|
|
66
66
|
outputColor = vec4(inputColor.rgb * mix(vec3(1.0), uWarm, uGrade), inputColor.a);
|
|
67
67
|
}
|
|
@@ -117,12 +117,14 @@ function FxPostPass({
|
|
|
117
117
|
threshold = FX_BLOOM_THRESHOLD,
|
|
118
118
|
field,
|
|
119
119
|
locate,
|
|
120
|
-
haze: hazeOverride
|
|
120
|
+
haze: hazeOverride,
|
|
121
|
+
focus = 0
|
|
121
122
|
}) {
|
|
122
123
|
const { bloomScale, haze: tierHaze } = fxQualityFor(quality);
|
|
123
124
|
const hazePx = hazeOverride ?? tierHaze;
|
|
124
125
|
const haze = useMemo(() => new HazeGradeEffect(), []);
|
|
125
126
|
const anchors = useRef([]);
|
|
127
|
+
const focusTarget = useRef(new THREE2.Vector3());
|
|
126
128
|
useFrame(({ camera }) => {
|
|
127
129
|
if (!field || !locate) {
|
|
128
130
|
haze.count = 0;
|
|
@@ -137,6 +139,14 @@ function FxPostPass({
|
|
|
137
139
|
top.set(a.x, a.y + a.height, a.z).project(camera);
|
|
138
140
|
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
141
|
}
|
|
142
|
+
if (focus > 0 && n > 0) {
|
|
143
|
+
focusTarget.current.set(0, 0, 0);
|
|
144
|
+
for (let i = 0; i < n; i++) {
|
|
145
|
+
const a = anchors.current[i];
|
|
146
|
+
focusTarget.current.add(root.set(a.x, a.y, a.z));
|
|
147
|
+
}
|
|
148
|
+
focusTarget.current.multiplyScalar(1 / n);
|
|
149
|
+
}
|
|
140
150
|
haze.count = n;
|
|
141
151
|
haze.time = field.time;
|
|
142
152
|
haze.amount = hazePx;
|
|
@@ -154,6 +164,14 @@ function FxPostPass({
|
|
|
154
164
|
}
|
|
155
165
|
) : null,
|
|
156
166
|
/* @__PURE__ */ jsx("primitive", { object: haze }),
|
|
167
|
+
focus > 0 ? /* @__PURE__ */ jsx(
|
|
168
|
+
DepthOfField,
|
|
169
|
+
{
|
|
170
|
+
target: focusTarget.current,
|
|
171
|
+
focalLength: 0.06 - 0.045 * focus,
|
|
172
|
+
bokehScale: 1 + 3 * focus
|
|
173
|
+
}
|
|
174
|
+
) : null,
|
|
157
175
|
/* @__PURE__ */ jsx(ToneMapping, { mode: modes[film] })
|
|
158
176
|
] });
|
|
159
177
|
}
|
|
@@ -163,4 +181,4 @@ export {
|
|
|
163
181
|
FxPostPass,
|
|
164
182
|
PAPER_WHITE
|
|
165
183
|
};
|
|
166
|
-
//# sourceMappingURL=FxPostPass-
|
|
184
|
+
//# sourceMappingURL=FxPostPass-ACFSHY3S.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fx/FxPostPass.tsx","../src/fx/haze.ts"],"sourcesContent":["import { Bloom, DepthOfField, 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: 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 * test for this pass is that it does not: with no fire in frame,\n * post on and post off are the same picture (`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 focus = 0,\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 /** Where the fire is, for the focus to sit on it. */\n const focusTarget = useRef(new THREE.Vector3())\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 // Focus on the fire itself, as a point in the room rather than a\n // fraction of the far plane: the far plane is a thousand times the size\n // of a sheet, and a focus expressed against it lands the plane a metre\n // away and blurs everything, the rim included.\n if (focus > 0 && n > 0) {\n focusTarget.current.set(0, 0, 0)\n for (let i = 0; i < n; i++) {\n const a = anchors.current[i]!\n focusTarget.current.add(root.set(a.x, a.y, a.z))\n }\n focusTarget.current.multiplyScalar(1 / n)\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.\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, 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 {focus > 0 ? (\n <DepthOfField\n target={focusTarget.current}\n // A hand's width of sheet in focus at the gentlest, a few\n // centimetres at the strongest — the macro look of the reference\n // crops, without losing the rim the shot is about.\n focalLength={0.06 - 0.045 * focus}\n bokehScale={1 + 3 * focus}\n />\n ) : null}\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. 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, 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":";;;;;;;;;;AAAA,SAAS,OAAO,cAAc,gBAAgB,mBAAmB;AACjE,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAChC,SAAS,SAAS,cAAc;AAChC,YAAYA,YAAW;;;ACJvB,YAAY,WAAW;AACvB,SAAS,eAAe,cAAc;AAG/B,IAAM,eAAe;AAE5B,IAAM;AAAA;AAAA,EAAsB;AAAA,wBACJ,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAoBZ,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkC7B,IAAM,kBAAN,cAA8B,OAAO;AAAA,EAC1C,cAAc;AACZ,UAAM,mBAAmB,UAAU;AAAA,MACjC,eAAe,cAAc;AAAA,MAC7B,UAAU,oBAAI,IAA2B;AAAA,QACvC,CAAC,YAAY,IAAU,cAAQ,MAAM,KAAK,EAAE,QAAQ,aAAa,GAAG,MAAM,IAAU,cAAQ,CAAC,CAAC,CAAC;AAAA,QAC/F,CAAC,UAAU,IAAU,cAAQ,CAAC,CAAC;AAAA,QAC/B,CAAC,SAAS,IAAU,cAAQ,CAAC,CAAC;AAAA;AAAA,QAE9B,CAAC,WAAW,IAAU,cAAQ,MAAM,IAAI,CAAC;AAAA,QACzC,CAAC,SAAS,IAAU,cAAQ,IAAU,cAAQ,MAAM,GAAK,IAAI,CAAC,CAAC;AAAA,QAC/D,CAAC,UAAU,IAAU,cAAQ,CAAC,CAAC;AAAA,MACjC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAA2B;AAC7B,WAAO,KAAK,SAAS,IAAI,UAAU,EAAG;AAAA,EACxC;AAAA,EAEA,IAAI,MAAM,GAAW;AACnB,SAAK,SAAS,IAAI,QAAQ,EAAG,QAAQ;AAAA,EACvC;AAAA,EAEA,IAAI,KAAK,GAAW;AAClB,SAAK,SAAS,IAAI,OAAO,EAAG,QAAQ;AAAA,EACtC;AAAA;AAAA,EAGA,IAAI,OAAO,QAAgB;AACzB,SAAK,SAAS,IAAI,SAAS,EAAG,QAAQ,SAAS;AAAA,EACjD;AAAA;AAAA,EAGA,IAAI,MAAM,GAAW;AACnB,SAAK,SAAS,IAAI,QAAQ,EAAG,QAAQ;AAAA,EACvC;AACF;;;ADMI,SAEI,KAFJ;AA7DJ,IAAM,QAAQ;AAAA,EACZ,KAAK,gBAAgB;AAAA,EACrB,SAAS,gBAAgB;AAAA,EACzB,QAAQ,gBAAgB;AAC1B;AAEA,IAAM,OAAO,IAAU,eAAQ;AAC/B,IAAM,MAAM,IAAU,eAAQ;AAEvB,SAAS,WAAW;AAAA,EACzB,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,QAAQ;AACV,GAAgB;AACd,QAAM,EAAE,YAAY,MAAM,SAAS,IAAI,aAAa,OAAO;AAC3D,QAAM,SAAS,gBAAgB;AAC/B,QAAM,OAAO,QAAQ,MAAM,IAAI,gBAAgB,GAAG,CAAC,CAAC;AACpD,QAAM,UAAU,OAAsB,CAAC,CAAC;AAExC,QAAM,cAAc,OAAO,IAAU,eAAQ,CAAC;AAG9C,WAAS,CAAC,EAAE,OAAO,MAAM;AACvB,QAAI,CAAC,SAAS,CAAC,QAAQ;AACrB,WAAK,QAAQ;AACb,WAAK,QAAQ;AACb;AAAA,IACF;AACA,UAAM,IAAI,SAAS,IAAI,aAAa,OAAO,QAAQ,cAAc,QAAQ,OAAO,IAAI;AACpF,UAAM,UAAU,KAAK;AACrB,aAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,YAAM,IAAI,QAAQ,QAAQ,CAAC;AAC3B,WAAK,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,MAAM;AACtC,UAAI,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,MAAM;AAChD,cAAQ,CAAC,EAAG,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,GAAG,GAAG,EAAE,IAAI;AAAA,IACrG;AAKA,QAAI,QAAQ,KAAK,IAAI,GAAG;AACtB,kBAAY,QAAQ,IAAI,GAAG,GAAG,CAAC;AAC/B,eAAS,IAAI,GAAG,IAAI,GAAG,KAAK;AAC1B,cAAM,IAAI,QAAQ,QAAQ,CAAC;AAC3B,oBAAY,QAAQ,IAAI,KAAK,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;AAAA,MACjD;AACA,kBAAY,QAAQ,eAAe,IAAI,CAAC;AAAA,IAC1C;AACA,SAAK,QAAQ;AACb,SAAK,OAAO,MAAM;AAClB,SAAK,SAAS;AAEd,SAAK,QAAQ,KAAK,IAAI,GAAG,MAAM,UAAU,QAAQ,IAAI;AAAA,EACvD,CAAC;AAED,SACE,qBAAC,kBACE;AAAA,YAAQ,IACP;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,QACX,oBAAoB;AAAA,QAIpB,oBAAoB;AAAA,QACpB,YAAU;AAAA,QACV,iBAAiB;AAAA;AAAA,IACnB,IACE;AAAA,IACJ,oBAAC,eAAU,QAAQ,MAAM;AAAA,IACxB,QAAQ,IACP;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,YAAY;AAAA,QAIpB,aAAa,OAAO,QAAQ;AAAA,QAC5B,YAAY,IAAI,IAAI;AAAA;AAAA,IACtB,IACE;AAAA,IACJ,oBAAC,eAAY,MAAM,MAAM,IAAI,GAAG;AAAA,KAClC;AAEJ;","names":["THREE"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/surface/damageContract.ts
|
|
2
|
+
function roomLight(source) {
|
|
3
|
+
const room = source?.firelight?.room;
|
|
4
|
+
if (room === void 0 || !(room >= 0)) return 1;
|
|
5
|
+
return Math.min(1, room);
|
|
6
|
+
}
|
|
7
|
+
var DAMAGE_LOOK_DEFAULTS = {
|
|
8
|
+
emberWidth: 1.25,
|
|
9
|
+
emberIntensity: 1.05,
|
|
10
|
+
emberCoverage: 0.6,
|
|
11
|
+
emberFlicker: 1.65,
|
|
12
|
+
emberGlow: 1.25,
|
|
13
|
+
sparkle: 0.5,
|
|
14
|
+
// As wide as the char band: the lip has to be seen. At
|
|
15
|
+
// 0.95 mm it was a hairline tracing the edge.
|
|
16
|
+
lipWidth: 3.5,
|
|
17
|
+
charWidth: 3.5,
|
|
18
|
+
lipBrightness: 0.76,
|
|
19
|
+
charWarmth: 0.32,
|
|
20
|
+
charCracks: 0.45,
|
|
21
|
+
scorchReach: 9,
|
|
22
|
+
scorchDarkness: 0.86,
|
|
23
|
+
fingers: 1.95,
|
|
24
|
+
edgeWave: 13,
|
|
25
|
+
edgeBite: 6
|
|
26
|
+
};
|
|
27
|
+
var DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
roomLight,
|
|
31
|
+
DAMAGE_LOOK_DEFAULTS,
|
|
32
|
+
DAMAGE_CHANNELS
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=chunk-BIFUJCUA.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 * The light the damage gives off, as the room around the sheet should feel\n * it. Presentation only; the physics never sees it. Omitted, the room is\n * lit exactly as its lighting says.\n */\n readonly firelight?: DamageFirelight\n}\n\n/**\n * What a burning sheet does to the light around it.\n *\n * A fire big enough to see by is the key light while it burns, and a room's\n * own light yields to it — a sheet burning under an unchanged studio key\n * looks like a flame pasted onto a photograph. The source says by how much,\n * because only the source knows how big its fire is; the lighting does the\n * dimming, because only the lighting knows what its lights are.\n */\nexport interface DamageFirelight {\n /**\n * How much of the room's own light is left, 0..1; omitted means 1. The key,\n * the ambient fill and the studio light are all scaled by it, every frame,\n * without rebuilding anything.\n */\n readonly room?: number\n}\n\n/**\n * How much of the room's light `source` leaves, 0..1 — the one reading of\n * {@link DamageFirelight.room}, so every lighting rig reads it alike. Anything\n * that is not a number in range is the room untouched: a firelight is a\n * dimmer, and the worst a bad one may do is nothing.\n */\nexport function roomLight(source: DamageSource | null | undefined): number {\n const room = source?.firelight?.room\n if (room === undefined || !(room >= 0)) return 1\n return Math.min(1, room)\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 /** How wide the pale ash lip is, from the cut out to the ember line, mm. */\n lipWidth?: number\n /** How wide the black char band is, past the ember line, mm. */\n charWidth?: 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 tune made in the\n * lab's sidebar, and the source of truth for how a burn looks:\n * the lab starts from it, `/hands` inherits it, and a user changes it through\n * `look`. The values it replaced, and why each of those had moved, are in the\n * history of this file.\n */\nexport const DAMAGE_LOOK_DEFAULTS: Required<DamageLook> = {\n emberWidth: 1.25,\n emberIntensity: 1.05,\n emberCoverage: 0.6,\n emberFlicker: 1.65,\n emberGlow: 1.25,\n sparkle: 0.5,\n // As wide as the char band: the lip has to be seen. At\n // 0.95 mm it was a hairline tracing the edge.\n lipWidth: 3.5,\n charWidth: 3.5,\n lipBrightness: 0.76,\n charWarmth: 0.32,\n charCracks: 0.45,\n scorchReach: 9,\n scorchDarkness: 0.86,\n fingers: 1.95,\n edgeWave: 13,\n edgeBite: 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":";AAwGO,SAAS,UAAU,QAAiD;AACzE,QAAM,OAAO,QAAQ,WAAW;AAChC,MAAI,SAAS,UAAa,EAAE,QAAQ,GAAI,QAAO;AAC/C,SAAO,KAAK,IAAI,GAAG,IAAI;AACzB;AAgDO,IAAM,uBAA6C;AAAA,EACxD,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,WAAW;AAAA,EACX,SAAS;AAAA;AAAA;AAAA,EAGT,UAAU;AAAA,EACV,WAAW;AAAA,EACX,eAAe;AAAA,EACf,YAAY;AAAA,EACZ,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":[]}
|