paperlab 0.8.0 → 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 -30
- package/dist/{FxPostPass-V6YDK4H2.js → FxPostPass-ACFSHY3S.js} +4 -4
- package/dist/FxPostPass-ACFSHY3S.js.map +1 -0
- package/dist/{chunk-232SU3AQ.js → chunk-BIFUJCUA.js} +2 -2
- package/dist/{chunk-232SU3AQ.js.map → chunk-BIFUJCUA.js.map} +1 -1
- package/dist/{chunk-SVWWC5UL.js → chunk-LUKNPQ3S.js} +16 -17
- package/dist/chunk-LUKNPQ3S.js.map +1 -0
- package/dist/{chunk-7WOGU2D4.js → chunk-S74YZAXE.js} +2 -2
- package/dist/chunk-S74YZAXE.js.map +1 -0
- package/dist/{damageContract-nF1fgK7P.d.cts → damageContract-9RC4egJg.d.cts} +2 -2
- package/dist/{damageContract-nF1fgK7P.d.ts → damageContract-9RC4egJg.d.ts} +2 -2
- package/dist/fx.cjs +20 -20
- package/dist/fx.cjs.map +1 -1
- package/dist/fx.d.cts +42 -44
- package/dist/fx.d.ts +42 -44
- package/dist/fx.js +21 -21
- package/dist/fx.js.map +1 -1
- package/dist/index.cjs +22 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/{slots-D_EE8GXn.d.cts → slots-DbJQfwVd.d.cts} +1 -1
- package/dist/{slots-D_EE8GXn.d.ts → slots-DbJQfwVd.d.ts} +1 -1
- package/dist/stage.cjs +23 -24
- 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-V6YDK4H2.js.map +0 -1
- package/dist/chunk-7WOGU2D4.js.map +0 -1
- package/dist/chunk-SVWWC5UL.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) · [the
|
|
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,52 +227,56 @@ 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
|
|
209
|
-
|
|
210
|
-
**[The fire lab](https://paperlab.nawwara.studio/fx-lab/)** — every knob behind the burn, on one page: the flame's zones, the fluid, the bloom, the light it throws, the ash lip and the char, how fast it eats. The library's defaults are a tune made here. Scrub the burn, turn something, copy the tune out as JSON and pass it to `<Paper>`.
|
|
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.
|
|
211
231
|
|
|
212
|
-
**[
|
|
213
|
-
|
|
214
|
-
**[Your 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. 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. One feature, done properly: it burns the way the fire lab burns, from the same defaults, and the page is a hundred percent public API — `packages/paperlab` doesn't know it exists.
|
|
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.
|
|
215
234
|
|
|
216
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.
|
|
217
236
|
|
|
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.
|
|
238
|
+
|
|
218
239
|
## Development
|
|
219
240
|
|
|
220
241
|
pnpm + Turborepo, Node 22, [Biome](https://biomejs.dev) for lint and format.
|
|
221
242
|
|
|
222
243
|
```sh
|
|
223
244
|
pnpm install
|
|
245
|
+
pnpm hands:setup # once — vendors the hand tracker's wasm and models (35 MB, not committed)
|
|
224
246
|
pnpm dev # the editor at localhost:5173
|
|
225
247
|
```
|
|
226
248
|
|
|
227
249
|
| | |
|
|
228
250
|
|---|---|
|
|
229
251
|
| [`packages/paperlab`](https://github.com/NourMtir0722/Paperlab/blob/main/packages/paperlab/) | the npm library — the only published artifact |
|
|
230
|
-
| [`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 |
|
|
231
253
|
| [`apps/playground`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/playground/) | the playground — one input, one scene, shareable by link |
|
|
232
254
|
| [`apps/docs`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/docs/) | the reference site, with every behavior running live |
|
|
233
|
-
| [`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 |
|
|
234
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 |
|
|
235
|
-
| [`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 |
|
|
236
258
|
|
|
237
259
|
### Checks
|
|
238
260
|
|
|
239
261
|
```sh
|
|
240
|
-
pnpm test
|
|
241
|
-
pnpm test:parity
|
|
242
|
-
pnpm test:drive
|
|
243
|
-
pnpm test:share
|
|
244
|
-
pnpm test:dropdown
|
|
245
|
-
pnpm test:route
|
|
246
|
-
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
|
|
247
273
|
pnpm typecheck
|
|
248
274
|
pnpm lint
|
|
249
|
-
pnpm knip
|
|
275
|
+
pnpm knip # dead code and unused exports
|
|
250
276
|
pnpm build
|
|
251
277
|
```
|
|
252
278
|
|
|
253
|
-
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.
|
|
254
280
|
|
|
255
281
|
### Measurement
|
|
256
282
|
|
|
@@ -262,6 +288,7 @@ pnpm shot:light # every lighting rig, one frame each
|
|
|
262
288
|
pnpm shot:catalogue # sweep any axis — --vary=layout|stock|lighting --all
|
|
263
289
|
pnpm sheet # compose those frames into a labelled contact sheet
|
|
264
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)
|
|
265
292
|
```
|
|
266
293
|
|
|
267
294
|
## Contributing
|
|
@@ -4,8 +4,8 @@ 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
11
|
import { Bloom, DepthOfField, EffectComposer, ToneMapping } from "@react-three/postprocessing";
|
|
@@ -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
|
}
|
|
@@ -181,4 +181,4 @@ export {
|
|
|
181
181
|
FxPostPass,
|
|
182
182
|
PAPER_WHITE
|
|
183
183
|
};
|
|
184
|
-
//# 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":"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","names":["THREE"]}
|
|
@@ -11,7 +11,7 @@ var DAMAGE_LOOK_DEFAULTS = {
|
|
|
11
11
|
emberFlicker: 1.65,
|
|
12
12
|
emberGlow: 1.25,
|
|
13
13
|
sparkle: 0.5,
|
|
14
|
-
// As wide as the char band
|
|
14
|
+
// As wide as the char band: the lip has to be seen. At
|
|
15
15
|
// 0.95 mm it was a hairline tracing the edge.
|
|
16
16
|
lipWidth: 3.5,
|
|
17
17
|
charWidth: 3.5,
|
|
@@ -31,4 +31,4 @@ export {
|
|
|
31
31
|
DAMAGE_LOOK_DEFAULTS,
|
|
32
32
|
DAMAGE_CHANNELS
|
|
33
33
|
};
|
|
34
|
-
//# sourceMappingURL=chunk-
|
|
34
|
+
//# sourceMappingURL=chunk-BIFUJCUA.js.map
|
|
@@ -1 +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 —
|
|
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":[]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
DAMAGE_CHANNELS,
|
|
3
3
|
DAMAGE_LOOK_DEFAULTS,
|
|
4
4
|
roomLight
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-BIFUJCUA.js";
|
|
6
6
|
|
|
7
7
|
// src/config/merge.ts
|
|
8
8
|
function mergeConfig(base, override) {
|
|
@@ -1549,7 +1549,7 @@ var paperConfigSchema = z15.object({
|
|
|
1549
1549
|
ctx.addIssue({
|
|
1550
1550
|
code: z15.ZodIssueCode.custom,
|
|
1551
1551
|
path: ["physics"],
|
|
1552
|
-
message: "the strip simulation and behavior/deformers
|
|
1552
|
+
message: "the strip simulation and behavior/deformers cannot be used together. The roll owns the vertices, and its rows are chain nodes rather than the sheet's own grid (pick Shape OR Strip)"
|
|
1553
1553
|
});
|
|
1554
1554
|
}
|
|
1555
1555
|
if (config.states) {
|
|
@@ -1571,7 +1571,7 @@ var paperConfigSchema = z15.object({
|
|
|
1571
1571
|
ctx.addIssue({
|
|
1572
1572
|
code: z15.ZodIssueCode.custom,
|
|
1573
1573
|
path: ["states", "states", name, "overrides"],
|
|
1574
|
-
message: `state "${name}" overrides don't validate against the paper schema: ${first ? `${first.path.join(".")}
|
|
1574
|
+
message: `state "${name}" overrides don't validate against the paper schema: ${first ? `${first.path.join(".")}: ${first.message}` : "invalid"}`
|
|
1575
1575
|
});
|
|
1576
1576
|
}
|
|
1577
1577
|
}
|
|
@@ -2162,7 +2162,7 @@ function getPreset(name) {
|
|
|
2162
2162
|
}
|
|
2163
2163
|
function registerPreset(name, input) {
|
|
2164
2164
|
if (name in builtins) {
|
|
2165
|
-
throw new Error(`[paperlab] "${name}" is a built-in preset
|
|
2165
|
+
throw new Error(`[paperlab] "${name}" is a built-in preset. Pick another name.`);
|
|
2166
2166
|
}
|
|
2167
2167
|
paperConfigSchema.parse(input);
|
|
2168
2168
|
userPresets.set(name, input);
|
|
@@ -2711,7 +2711,7 @@ function resolveDeformerStack(raw) {
|
|
|
2711
2711
|
if (!parsed.success) {
|
|
2712
2712
|
const issue = parsed.error.issues[0];
|
|
2713
2713
|
throw new Error(
|
|
2714
|
-
`[paperlab] deformers[${i}] ("${instance.type}"): ${issue ? `${issue.path.join(".") || "options"}
|
|
2714
|
+
`[paperlab] deformers[${i}] ("${instance.type}"): ${issue ? `${issue.path.join(".") || "options"}: ${issue.message}` : "invalid options"}`
|
|
2715
2715
|
);
|
|
2716
2716
|
}
|
|
2717
2717
|
return {
|
|
@@ -6699,7 +6699,7 @@ vec3 plScorchTint(float t) {
|
|
|
6699
6699
|
return min(c / paper, vec3(1.0));
|
|
6700
6700
|
}
|
|
6701
6701
|
|
|
6702
|
-
/** The ember ramp
|
|
6702
|
+
/** The ember ramp: off \u2192 #870E03 \u2192 #A12108 \u2192 #F77E14 \u2192 #FEFBE0, hottest last. */
|
|
6703
6703
|
vec3 plEmberRamp(float t) {
|
|
6704
6704
|
vec3 c0 = plLinear(vec3(0.529, 0.055, 0.012));
|
|
6705
6705
|
vec3 c1 = plLinear(vec3(0.631, 0.129, 0.031));
|
|
@@ -6729,7 +6729,7 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6729
6729
|
vec2 gradC = vec2(e.r - w.r, n.r - s.r) * 0.5 * perWorld;
|
|
6730
6730
|
// Scorch reaches ahead of the char, and further UP than anywhere else:
|
|
6731
6731
|
// the char a centimetre below this point, and the char a few millimetres
|
|
6732
|
-
// around it, each lent to the scorch at a discount
|
|
6732
|
+
// around it, each lent to the scorch at a discount.
|
|
6733
6733
|
vec2 mmUv = vec2(PL_MM) / uSheetSize;
|
|
6734
6734
|
float charBelow = max(texture2D(uDamage, uv - upUv * uLook2.y * mmUv).r, texture2D(uDamage, uv - upUv * 0.5 * uLook2.y * mmUv).r);
|
|
6735
6735
|
// A ring of eight, averaged: four diagonal maxima drew the grid back in as
|
|
@@ -6788,7 +6788,7 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6788
6788
|
float lipEdge = uLook1.y * mix(0.7, 1.1, plFbm(vec2(dot(p, tangentR) / (5.0 * PL_MM), 1.7)));
|
|
6789
6789
|
float charEnd = lipEdge + uLook0.x + uLook3.w * mix(0.8, 1.2, plFbm(vec2(dot(p, tangentR) / (7.0 * PL_MM), 4.3)));
|
|
6790
6790
|
// Further above the burn than below it \u2014 hot gas rises and cooks the paper
|
|
6791
|
-
// over it
|
|
6791
|
+
// over it \u2014 out to the scorch's full reach straight up, and about
|
|
6792
6792
|
// half of it below and beside.
|
|
6793
6793
|
vec2 upMm = normalize(upUv * uSheetSize);
|
|
6794
6794
|
float above = length(toward) > 1e-3 ? max(0.0, dot(-normalize(toward), upMm)) : 0.0;
|
|
@@ -9212,7 +9212,7 @@ function buildDisplacementGLSL(stack, sheet2) {
|
|
|
9212
9212
|
const deformer = getDeformer(instance.type);
|
|
9213
9213
|
if (!deformer.glsl) {
|
|
9214
9214
|
throw new Error(
|
|
9215
|
-
`[paperlab] Deformer "${instance.type}" has no GLSL implementation
|
|
9215
|
+
`[paperlab] Deformer "${instance.type}" has no GLSL implementation, so it can't run in field mode.`
|
|
9216
9216
|
);
|
|
9217
9217
|
}
|
|
9218
9218
|
const ns = `u${cap(instance.type)}${i}_`;
|
|
@@ -10120,10 +10120,9 @@ function FieldKeyboardMirror({
|
|
|
10120
10120
|
children: [
|
|
10121
10121
|
paperLabel(slot, i),
|
|
10122
10122
|
carrying?.slot === i && /* @__PURE__ */ jsxs7("span", { "aria-live": "polite", children: [
|
|
10123
|
-
" ",
|
|
10124
|
-
"\u2014 carrying; zone ",
|
|
10123
|
+
", carrying. Zone ",
|
|
10125
10124
|
controller.current?.zoneIds()[carrying.zoneIndex] ?? "none",
|
|
10126
|
-
"
|
|
10125
|
+
". Enter places it, Escape puts it back."
|
|
10127
10126
|
] })
|
|
10128
10127
|
]
|
|
10129
10128
|
},
|
|
@@ -10452,7 +10451,7 @@ function withoutUploads(value) {
|
|
|
10452
10451
|
};
|
|
10453
10452
|
return { value: walk(value), replaced };
|
|
10454
10453
|
}
|
|
10455
|
-
var UPLOAD_NOTE = "// Uploaded pictures cannot travel in a snippet
|
|
10454
|
+
var UPLOAD_NOTE = "// Uploaded pictures cannot travel in a snippet, so the paths below are\n// stand-ins, in order. Point them at your own files.";
|
|
10456
10455
|
function buildJsxSnippet(config) {
|
|
10457
10456
|
const diffed = diffConfig(config);
|
|
10458
10457
|
delete diffed.meta;
|
|
@@ -10480,7 +10479,7 @@ var BEHAVIOR_PHRASES = {
|
|
|
10480
10479
|
settle: (o) => o.relax > 0.7 ? "lying where it landed, flat but for one corner the stiffness kept" : "just come to rest, still holding a little of the shape it fell in",
|
|
10481
10480
|
carry: () => "drooping from a pinched corner, fluttering as if being carried",
|
|
10482
10481
|
flight: (o) => o.path === "loop" ? "tumbling through a seamless airborne loop" : "tumbling across the scene on the wind",
|
|
10483
|
-
crumple: (o) => o.progress < 0.3 ? "lightly handled
|
|
10482
|
+
crumple: (o) => o.progress < 0.3 ? "lightly handled, with a few soft creases across it" : o.progress < 0.7 ? "crushed into irregular creased facets, as if screwed up and flattened out again" : "balled up in a fist"
|
|
10484
10483
|
};
|
|
10485
10484
|
function describeConfig(config) {
|
|
10486
10485
|
const stock = getStock(config.stock);
|
|
@@ -10541,8 +10540,8 @@ export function ${name}() {
|
|
|
10541
10540
|
give the parent an explicit height.
|
|
10542
10541
|
|
|
10543
10542
|
4. Verify: run the dev server. You should see ${describeConfig(config)}.
|
|
10544
|
-
If the canvas is blank, the parent container almost certainly has no height
|
|
10545
|
-
|
|
10543
|
+
If the canvas is blank, the parent container almost certainly has no height.
|
|
10544
|
+
Give it one (this is the classic React Three Fiber integration bug, not a
|
|
10546
10545
|
paperlab bug).
|
|
10547
10546
|
|
|
10548
10547
|
Constraints: don't modify the preset values; three >= 0.160 and React 19 are
|
|
@@ -10636,4 +10635,4 @@ export {
|
|
|
10636
10635
|
describeConfig,
|
|
10637
10636
|
buildAgentPayload
|
|
10638
10637
|
};
|
|
10639
|
-
//# sourceMappingURL=chunk-
|
|
10638
|
+
//# sourceMappingURL=chunk-LUKNPQ3S.js.map
|