paperlab 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-V6YDK4H2.js} +23 -5
- package/dist/FxPostPass-V6YDK4H2.js.map +1 -0
- package/dist/chunk-232SU3AQ.js +34 -0
- package/dist/chunk-232SU3AQ.js.map +1 -0
- package/dist/{chunk-GB6BMHC3.js → chunk-7WOGU2D4.js} +7 -19
- package/dist/{chunk-GB6BMHC3.js.map → chunk-7WOGU2D4.js.map} +1 -1
- package/dist/{chunk-NTDVOOBA.js → chunk-SVWWC5UL.js} +372 -151
- package/dist/chunk-SVWWC5UL.js.map +1 -0
- package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-nF1fgK7P.d.cts} +31 -3
- package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-nF1fgK7P.d.ts} +31 -3
- package/dist/fx.cjs +320 -91
- package/dist/fx.cjs.map +1 -1
- package/dist/fx.d.cts +81 -55
- package/dist/fx.d.ts +81 -55
- package/dist/fx.js +278 -48
- package/dist/fx.js.map +1 -1
- package/dist/index.cjs +402 -172
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/{slots-C5PqY0Q5.d.cts → slots-D_EE8GXn.d.cts} +12 -0
- package/dist/{slots-C5PqY0Q5.d.ts → slots-D_EE8GXn.d.ts} +12 -0
- package/dist/stage.cjs +396 -179
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.d.cts +1 -1
- package/dist/stage.d.ts +1 -1
- package/dist/stage.js +2 -2
- package/package.json +1 -1
- package/dist/FxPostPass-M66QMVVC.js.map +0 -1
- package/dist/chunk-7TLIWPGM.js +0 -36
- package/dist/chunk-7TLIWPGM.js.map +0 -1
- package/dist/chunk-NTDVOOBA.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
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.
|
|
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 fire 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
|
|---|---|
|
|
@@ -207,11 +207,13 @@ The inspector is generated from the zod schema, so it can never drift from the A
|
|
|
207
207
|
|
|
208
208
|
Field mode composes galleries against the same panel — swap the layout, watch fourteen papers rearrange in one draw call. **Export code** ends the session in your codebase, and **Copy for AI** ends it in a coding agent's. It wants a real screen: under about 900px it says so and points you at the playground.
|
|
209
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>`.
|
|
211
|
+
|
|
210
212
|
**[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.
|
|
211
213
|
|
|
212
|
-
**[Your hands](https://paperlab.nawwara.studio/hands/)** — the
|
|
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.
|
|
213
215
|
|
|
214
|
-
The 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.
|
|
216
|
+
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.
|
|
215
217
|
|
|
216
218
|
## Development
|
|
217
219
|
|
|
@@ -225,7 +227,7 @@ pnpm dev # the editor at localhost:5173
|
|
|
225
227
|
| | |
|
|
226
228
|
|---|---|
|
|
227
229
|
| [`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 — every knob, and the export. Also
|
|
230
|
+
| [`apps/editor`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/editor/) | the editor — every knob, and the export. Also `/hands` and `/fx-lab`, built from the same app in passes of their own |
|
|
229
231
|
| [`apps/playground`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/playground/) | the playground — one input, one scene, shareable by link |
|
|
230
232
|
| [`apps/docs`](https://github.com/NourMtir0722/Paperlab/blob/main/apps/docs/) | the reference site, with every behavior running live |
|
|
231
233
|
| [`tools/`](https://github.com/NourMtir0722/Paperlab/blob/main/tools/) | browser harnesses — parity, perf, screenshots, the README's motion |
|
|
@@ -241,7 +243,7 @@ pnpm test:drive # the stage really walks when you drag, wheel or arrow it
|
|
|
241
243
|
pnpm test:share # sculpt → link → a browser that has never seen the paper
|
|
242
244
|
pnpm test:dropdown # every dropdown option is reachable, including below the fold
|
|
243
245
|
pnpm test:route # the site root routes by device, and links every route it deploys
|
|
244
|
-
pnpm test:hands #
|
|
246
|
+
pnpm test:hands # the three ways to light the paper really light it (needs a camera-less Chromium)
|
|
245
247
|
pnpm typecheck
|
|
246
248
|
pnpm lint
|
|
247
249
|
pnpm knip # dead code and unused exports
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
PAPER_WHITE,
|
|
5
5
|
flameAnchors,
|
|
6
6
|
fxQualityFor
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-7WOGU2D4.js";
|
|
8
|
+
import "./chunk-232SU3AQ.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";
|
|
@@ -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-V6YDK4H2.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, in the words of `paperlab-fx-fire-spec.md` §0 and §7: the\n * first fire's glow was PAINT — a warm colour added to `csm_Emissive` over\n * paper that had not burnt — and red added to white makes pink. Real glow is\n * light too bright for the film: an ember is brighter than the whitest paper\n * in the frame, the lens spreads it, and the curve rolls it off to white.\n * Nothing on the sheet paints glow any more; anything that wants to glow\n * emits past paper white and this makes it bloom.\n *\n * Order is the whole argument: bloom reads the frame while it is still HDR —\n * which is what lets a threshold mean \"brighter than paper\" — the haze bends\n * the light above the flames and the grade warms the frame, and the curve\n * lands the result last.\n *\n * **The composer takes the tone curve off the renderer, so this gives it\n * back.** `<EffectComposer>` sets `gl.toneMapping = NoToneMapping` while it is\n * mounted; without the `<ToneMapping>` below, mounting this pass would\n * silently throw away the rig's film and the unburnt sheet would change. The\n * spec's own test for this pass is that it does not: with no fire in frame,\n * post on and post off are the same picture (§14.3, `pnpm test:fire-look`).\n */\n\nconst modes = {\n agx: ToneMappingMode.AGX,\n neutral: ToneMappingMode.NEUTRAL,\n filmic: ToneMappingMode.ACES_FILMIC,\n} as const\n\nconst root = new THREE.Vector3()\nconst top = new THREE.Vector3()\n\nexport function FxPostPass({\n film = 'neutral',\n quality = 'medium',\n bloom = FX_BLOOM,\n threshold = FX_BLOOM_THRESHOLD,\n field,\n locate,\n haze: hazeOverride,\n 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 (§7).\n haze.grade = Math.min(1, field.lastStats.front / 0.03)\n })\n\n return (\n <EffectComposer>\n {bloom > 0 ? (\n <Bloom\n intensity={bloom}\n luminanceThreshold={threshold}\n // Nearly a hard knee. A wide one reaches BELOW the threshold, and\n // below it is paper: the whole point of the threshold is that paper\n // never blooms (§7), and a soft knee would bloom it a little.\n luminanceSmoothing={0.02}\n mipmapBlur\n resolutionScale={bloomScale}\n />\n ) : null}\n <primitive object={haze} />\n {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 (spec §7). A multiply, so black stays black.\nvoid mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {\n outputColor = vec4(inputColor.rgb * mix(vec3(1.0), uWarm, uGrade), inputColor.a);\n}\n`\n\n/**\n * Heat haze above the flames, and the warm grade of a frame with a fire in\n * it — spec §7's last two items, as one pass because both are cheap and both\n * run on the HDR frame before the tone curve.\n *\n * Fed each frame by `FxPostPass`: `sources` holds up to {@link HAZE_SOURCES}\n * flames as (screen u, screen v, flame height in screen v, strength).\n */\nexport class HazeGradeEffect extends Effect {\n constructor() {\n super('HazeGradeEffect', FRAGMENT, {\n blendFunction: BlendFunction.NORMAL,\n uniforms: new Map<string, THREE.Uniform>([\n ['uSources', new THREE.Uniform(Array.from({ length: HAZE_SOURCES }, () => new THREE.Vector4()))],\n ['uCount', new THREE.Uniform(0)],\n ['uTime', new THREE.Uniform(0)],\n // 1–3 px at 1080p, as a fraction of the frame.\n ['uAmount', new THREE.Uniform(2.2 / 1080)],\n ['uWarm', new THREE.Uniform(new THREE.Vector3(1.04, 1.0, 0.94))],\n ['uGrade', new THREE.Uniform(0)],\n ]),\n })\n }\n\n get sources(): THREE.Vector4[] {\n return this.uniforms.get('uSources')!.value as THREE.Vector4[]\n }\n\n set count(n: number) {\n this.uniforms.get('uCount')!.value = n\n }\n\n set time(t: number) {\n this.uniforms.get('uTime')!.value = t\n }\n\n /** 0 turns the shimmer off (the low tier) and keeps the grade. */\n set amount(px1080: number) {\n this.uniforms.get('uAmount')!.value = px1080 / 1080\n }\n\n /** 0..1 — how much of the warm grade to apply. */\n set grade(g: number) {\n this.uniforms.get('uGrade')!.value = g\n }\n}\n"],"mappings":"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","names":["THREE"]}
|
|
@@ -0,0 +1,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 (Noor, 2026-09-13): 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-232SU3AQ.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 — Noor's tune in the\n * lab's sidebar on 2026-09-13, 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 (Noor, 2026-09-13): 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":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DAMAGE_CHANNELS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-232SU3AQ.js";
|
|
4
4
|
|
|
5
5
|
// src/fx/field.ts
|
|
6
6
|
var CHAR = DAMAGE_CHANNELS.char;
|
|
@@ -516,27 +516,15 @@ function emitHex(hex, times) {
|
|
|
516
516
|
}
|
|
517
517
|
var FIRE_HEAT_SCALE = 0.65;
|
|
518
518
|
var FIRE_SOOT_SCALE = 3;
|
|
519
|
-
var FX_BLOOM =
|
|
519
|
+
var FX_BLOOM = 1.25;
|
|
520
520
|
var FIRE_CONTRAST = 0.9;
|
|
521
521
|
var FIRE_OPACITY = 5;
|
|
522
522
|
var FIRE_THIN = 0.55;
|
|
523
523
|
var FIRE_ZONES = {
|
|
524
|
-
root: { color: "#3b6bff", amount: 0, reach: 0.
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// budget. Brighter keeps the same small area: 1.47% bloom, near-white 0.8%.
|
|
529
|
-
// Starting it lower (from 0.45) bloomed 6.4% but put near-white back at 4.7%.
|
|
530
|
-
core: { color: "#fff7d4", glow: 6, from: 0.55 },
|
|
531
|
-
// Saturated, because the tone curve takes saturation away from anything
|
|
532
|
-
// above paper white: #ffdd7c at this glow came out pale yellow (s ~0.4),
|
|
533
|
-
// and measured 7.5% yellow against Flame_base.png's 29%.
|
|
534
|
-
body: { color: "#ffcf3a", glow: 1.3 },
|
|
535
|
-
// to: 0.4, measured against Flame_base.png with bloom off (bloom's halo
|
|
536
|
-
// counts as flame in any diff): near-white / pale / yellow / orange came out
|
|
537
|
-
// 0.7 / 21 / 34 / 44% against the reference's 0.9 / 17 / 29 / 43. At 0.32
|
|
538
|
-
// the body took half the flame and it was 51% yellow; at 0.45, 64% orange.
|
|
539
|
-
tip: { color: "#ff9e2c", glow: 0.9, from: 0.08, to: 0.4, softness: 0.15, tearing: 0.35 }
|
|
524
|
+
root: { color: "#3b6bff", amount: 0.01, reach: 0.39 },
|
|
525
|
+
core: { color: "#fff7d4", glow: 2.1, from: 0.63 },
|
|
526
|
+
body: { color: "#ffcf3a", glow: 0.8 },
|
|
527
|
+
tip: { color: "#ff9e2c", glow: 0.21, from: 0, to: 0.5, softness: 0.02, tearing: 0.64 }
|
|
540
528
|
};
|
|
541
529
|
function fireZones(input) {
|
|
542
530
|
return {
|
|
@@ -808,4 +796,4 @@ export {
|
|
|
808
796
|
FX_TIER_ORDER,
|
|
809
797
|
fxQualityFor
|
|
810
798
|
};
|
|
811
|
-
//# sourceMappingURL=chunk-
|
|
799
|
+
//# sourceMappingURL=chunk-7WOGU2D4.js.map
|