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.
Files changed (35) hide show
  1. package/README.md +7 -5
  2. package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-V6YDK4H2.js} +23 -5
  3. package/dist/FxPostPass-V6YDK4H2.js.map +1 -0
  4. package/dist/chunk-232SU3AQ.js +34 -0
  5. package/dist/chunk-232SU3AQ.js.map +1 -0
  6. package/dist/{chunk-GB6BMHC3.js → chunk-7WOGU2D4.js} +7 -19
  7. package/dist/{chunk-GB6BMHC3.js.map → chunk-7WOGU2D4.js.map} +1 -1
  8. package/dist/{chunk-NTDVOOBA.js → chunk-SVWWC5UL.js} +372 -151
  9. package/dist/chunk-SVWWC5UL.js.map +1 -0
  10. package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-nF1fgK7P.d.cts} +31 -3
  11. package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-nF1fgK7P.d.ts} +31 -3
  12. package/dist/fx.cjs +320 -91
  13. package/dist/fx.cjs.map +1 -1
  14. package/dist/fx.d.cts +81 -55
  15. package/dist/fx.d.ts +81 -55
  16. package/dist/fx.js +278 -48
  17. package/dist/fx.js.map +1 -1
  18. package/dist/index.cjs +402 -172
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +13 -5
  21. package/dist/index.d.ts +13 -5
  22. package/dist/index.js +17 -4
  23. package/dist/index.js.map +1 -1
  24. package/dist/{slots-C5PqY0Q5.d.cts → slots-D_EE8GXn.d.cts} +12 -0
  25. package/dist/{slots-C5PqY0Q5.d.ts → slots-D_EE8GXn.d.ts} +12 -0
  26. package/dist/stage.cjs +396 -179
  27. package/dist/stage.cjs.map +1 -1
  28. package/dist/stage.d.cts +1 -1
  29. package/dist/stage.d.ts +1 -1
  30. package/dist/stage.js +2 -2
  31. package/package.json +1 -1
  32. package/dist/FxPostPass-M66QMVVC.js.map +0 -1
  33. package/dist/chunk-7TLIWPGM.js +0 -36
  34. package/dist/chunk-7TLIWPGM.js.map +0 -1
  35. package/dist/chunk-NTDVOOBA.js.map +0 -1
package/dist/fx.js CHANGED
@@ -31,11 +31,11 @@ import {
31
31
  luminance,
32
32
  srgbToLinear,
33
33
  timesPaperWhite
34
- } from "./chunk-GB6BMHC3.js";
34
+ } from "./chunk-7WOGU2D4.js";
35
35
  import {
36
36
  DAMAGE_CHANNELS,
37
37
  DAMAGE_LOOK_DEFAULTS
38
- } from "./chunk-7TLIWPGM.js";
38
+ } from "./chunk-232SU3AQ.js";
39
39
 
40
40
  // src/fx/particles.ts
41
41
  var particlePresets = {
@@ -90,7 +90,13 @@ var particlePresets = {
90
90
  * simulation, on purpose; see the fx plan.
91
91
  */
92
92
  smoke: {
93
- life: [1.8, 3.5],
93
+ // Shorter, and smaller below: these are the thread beside the
94
+ // simulator's smoke, and the simulator's is gone within a second or two
95
+ // of the last flame. Left to live three and a half seconds and grow to
96
+ // seven centimetres, the last few of them overlapped into a flat wash
97
+ // over the hole with nothing left to move them — the one moment of a
98
+ // burn Noor found the smoke wrong (2026-09-13).
99
+ life: [1.4, 2.6],
94
100
  speed: [0.05, 0.15],
95
101
  direction: [0, 1, 0],
96
102
  spread: 0.3,
@@ -98,13 +104,13 @@ var particlePresets = {
98
104
  drag: 0.9,
99
105
  jitter: 0.3,
100
106
  windCatch: 1,
101
- size: [0.05, 0.35],
107
+ size: [0.05, 0.18],
102
108
  // #6B6560 grey-brown, linear, and thin — the background stays clear.
103
109
  color: [
104
110
  [0.147, 0.13, 0.117],
105
111
  [0.2, 0.19, 0.18]
106
112
  ],
107
- alpha: [0.08, 0],
113
+ alpha: [0.07, 0],
108
114
  spin: 0.4,
109
115
  blend: "normal",
110
116
  shape: "soft",
@@ -116,7 +122,9 @@ var particlePresets = {
116
122
  * burn that proves paper was there.
117
123
  */
118
124
  ash: {
119
- life: [2.5, 5],
125
+ // A second or two: ash that hung in the air for five read as grey
126
+ // confetti over the hole, dust or stars rather than something falling.
127
+ life: [1.2, 2.6],
120
128
  // Thrown UP by the heat it came off, before it is heavier than the air.
121
129
  // It used to leave at almost nothing (0.02–0.1) with a lift of −0.35, so
122
130
  // every flake began falling the moment it was born — straight through the
@@ -124,7 +132,9 @@ var particlePresets = {
124
132
  speed: [0.14, 0.3],
125
133
  direction: [0, 1, 0],
126
134
  spread: 0.55,
127
- lift: -0.22,
135
+ // Heavier than the air once the throw is spent: most flakes tumble down,
136
+ // only the lightest ride the heat up a little first.
137
+ lift: -0.42,
128
138
  drag: 1.6,
129
139
  jitter: 1.2,
130
140
  windCatch: 1,
@@ -132,9 +142,11 @@ var particlePresets = {
132
142
  // 3.8 mm and there were far too many of them: a shower of specks rather
133
143
  // than a few flakes you would notice.
134
144
  size: [0.03, 0.026],
145
+ // Charred paper, near black: under a bright key the old value read as
146
+ // pale grey against the black stage, which is the confetti.
135
147
  color: [
136
- [0.07, 0.065, 0.06],
137
- [0.12, 0.115, 0.11]
148
+ [0.04, 0.037, 0.034],
149
+ [0.07, 0.066, 0.062]
138
150
  ],
139
151
  alpha: [0.95, 0],
140
152
  spin: 4,
@@ -358,13 +370,14 @@ var ParticlePool = class {
358
370
 
359
371
  // src/fx/fire.ts
360
372
  var fireEmitterDefaults = {
361
- embers: 0.35,
362
- smoke: 0,
363
- ash: 0.12,
373
+ embers: 0.39,
374
+ smoke: 0.135,
375
+ ash: 0.36,
364
376
  seed: 7,
365
377
  caps: {}
366
378
  };
367
379
  var PER_UPDATE = 24;
380
+ var CRACKLE_SPARKS = 0.06;
368
381
  var FireEmitter = class {
369
382
  field;
370
383
  pool;
@@ -401,7 +414,10 @@ var FireEmitter = class {
401
414
  heat /= front;
402
415
  const struggle = 0.35 + 1.6 * Math.max(0, 0.7 - heat) + air * 0.8;
403
416
  const oxygen = 1 + air * 1.5;
404
- this.emberDebt = Math.min(this.emberDebt + front * o.embers * oxygen * dt, PER_UPDATE);
417
+ this.emberDebt = Math.min(
418
+ this.emberDebt + front * o.embers * oxygen * dt + field.lastStats.charred * o.embers * CRACKLE_SPARKS,
419
+ PER_UPDATE
420
+ );
405
421
  this.smokeDebt = Math.min(this.smokeDebt + front * o.smoke * struggle * dt, PER_UPDATE);
406
422
  while (this.emberDebt >= 1) {
407
423
  this.emberDebt -= 1;
@@ -757,7 +773,7 @@ function FxParticles({ pool }) {
757
773
  import { Suspense, lazy } from "react";
758
774
  import { jsx as jsx2 } from "react/jsx-runtime";
759
775
  var Pass = lazy(
760
- () => import("./FxPostPass-M66QMVVC.js").then((m) => ({ default: m.FxPostPass })).catch((error) => {
776
+ () => import("./FxPostPass-V6YDK4H2.js").then((m) => ({ default: m.FxPostPass })).catch((error) => {
761
777
  warnOnce(error);
762
778
  return { default: (() => null) };
763
779
  })
@@ -1742,10 +1758,17 @@ void main() {
1742
1758
  float rootMask = smoothstep(mix(0.4, 0.02, uRootReach), mix(0.6, 0.12, uRootReach), fuel) * (1.0 - smoothstep(0.25, 0.6, t));
1743
1759
  fire += uRootColor * rootMask * uRootAmount * uPaperWhite * uGlow * 0.6;
1744
1760
 
1745
- // Smoke: warm grey-brown (a cool grey over cream reads lavender), lit by
1746
- // the fire under it.
1761
+ // Smoke: grey-brown, warm (a cool grey over cream reads lavender), and LIT
1762
+ // BY THE FIRE UNDER IT rather than by nothing.
1763
+ //
1764
+ // The base used to be bright enough to read on its own \u2014 beige \u2014 which is
1765
+ // right while flames are lighting it and wrong the moment they stop: the
1766
+ // last of the smoke hung over a black stage as flat beige slabs, the same
1767
+ // colour everywhere, which reads as a filled shape rather than as smoke
1768
+ // (Noor, 2026-09-13). Dim at the base and warm where the fire is: smoke by
1769
+ // the flames looks as it did, smoke after them goes to a dim grey.
1747
1770
  float smokeAlpha = 1.0 - exp(-smoke * uSmokeDensity);
1748
- vec3 smokeColor = vec3(0.16, 0.13, 0.10) + vec3(0.22, 0.09, 0.02) * smoothstep(0.15, 0.9, t);
1771
+ vec3 smokeColor = vec3(0.075, 0.068, 0.062) + vec3(0.3, 0.14, 0.045) * smoothstep(0.12, 0.9, t);
1749
1772
  float alpha = smokeAlpha + fireAlpha - smokeAlpha * fireAlpha;
1750
1773
  // No square edge: the domain fades out before its borders.
1751
1774
  float edge = smoothstep(0.0, 0.06, vUv.x) * smoothstep(1.0, 0.94, vUv.x) * smoothstep(0.0, 0.03, vUv.y) * smoothstep(1.0, 0.9, vUv.y);
@@ -2544,55 +2567,261 @@ function FxFireFluid({
2544
2567
  import * as THREE6 from "three";
2545
2568
  import { useFrame as useFrame5 } from "@react-three/fiber";
2546
2569
  import { useRef as useRef4 } from "react";
2547
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
2548
- function FxFireLight({ field, locate, gain = FIRE_LIGHT_GAIN }) {
2549
- const front = useRef4(null);
2550
- const through = useRef4(null);
2551
- const anchors = useRef4([]);
2552
- useFrame5(({ camera }) => {
2553
- const a = front.current;
2554
- const b = through.current;
2555
- if (!a || !b) return;
2556
- const n = flameAnchors(field, locate, 32, anchors.current);
2557
- if (n === 0) {
2558
- a.intensity = 0;
2559
- b.intensity = 0;
2560
- return;
2561
- }
2570
+
2571
+ // src/fx/fireState.ts
2572
+ var FLAMES = 32;
2573
+ var FIRE_CLUSTERS = 4;
2574
+ var CLUSTER_REACH = 25 / 210;
2575
+ var FireState = class {
2576
+ constructor(field, locate) {
2577
+ this.field = field;
2578
+ this.locate = locate;
2579
+ }
2580
+ field;
2581
+ locate;
2582
+ /** The flames it was taken over. Only the first {@link count} are current. */
2583
+ anchors = [];
2584
+ count = 0;
2585
+ /** Where the fire is, as a whole: its flames' mean. Zero with none. */
2586
+ x = 0;
2587
+ y = 0;
2588
+ z = 0;
2589
+ /** Its flames' mean height. */
2590
+ height = 0;
2591
+ /** Its flames' mean puff. */
2592
+ flicker = 0;
2593
+ /** Where it is gathered. Only the first {@link clusterCount} are current. */
2594
+ clusters = [];
2595
+ clusterCount = 0;
2596
+ frame = Number.NaN;
2597
+ order = [];
2598
+ root = new Float64Array(FIRE_CLUSTERS * 3);
2599
+ weight = new Float64Array(FIRE_CLUSTERS);
2600
+ /**
2601
+ * Bring it up to date for `frame` — any number that is the same for every
2602
+ * reader in one frame and different in the next; R3F's `clock.elapsedTime`
2603
+ * is the one in hand. Free the second time it is asked in a frame.
2604
+ */
2605
+ update(frame) {
2606
+ if (frame === this.frame) return this;
2607
+ this.frame = frame;
2608
+ const { field, anchors } = this;
2609
+ const n = flameAnchors(field, this.locate, FLAMES, anchors);
2610
+ this.count = n;
2562
2611
  let x = 0;
2563
2612
  let y = 0;
2564
2613
  let z = 0;
2565
2614
  let h = 0;
2566
2615
  let flicker = 0;
2567
2616
  for (let i = 0; i < n; i++) {
2568
- const f = anchors.current[i];
2617
+ const f = anchors[i];
2569
2618
  x += f.x;
2570
2619
  y += f.y;
2571
2620
  z += f.z;
2572
2621
  h += f.height;
2573
2622
  flicker += flamePuff(f.seed, field.time);
2574
2623
  }
2575
- x /= n;
2576
- y /= n;
2577
- z /= n;
2578
- h /= n;
2579
- flicker /= n;
2580
- const toCamera = new THREE6.Vector3(camera.position.x - x, 0, camera.position.z - z);
2581
- toCamera.normalize().multiplyScalar(0.035);
2582
- const lift = h * 0.5;
2583
- a.position.set(x + toCamera.x, y + lift, z + toCamera.z);
2584
- b.position.set(x - toCamera.x, y + lift, z - toCamera.z);
2585
- const level = field.lastStats.front * gain * flicker;
2586
- a.intensity = level;
2587
- b.intensity = level * 0.35;
2624
+ if (n > 0) {
2625
+ x /= n;
2626
+ y /= n;
2627
+ z /= n;
2628
+ h /= n;
2629
+ flicker /= n;
2630
+ }
2631
+ this.x = x;
2632
+ this.y = y;
2633
+ this.z = z;
2634
+ this.height = h;
2635
+ this.flicker = flicker;
2636
+ this.gather();
2637
+ return this;
2638
+ }
2639
+ /**
2640
+ * Group the flames into at most {@link FIRE_CLUSTERS} clusters.
2641
+ *
2642
+ * Tallest first, so each cluster is named by its tallest tongue and stands
2643
+ * where that one stands: a flame joins the nearest cluster within
2644
+ * {@link CLUSTER_REACH} of it, or starts one while there is room, or joins
2645
+ * the nearest once there is not. Pure arithmetic over the flames, so the
2646
+ * same burn gathers the same way every time.
2647
+ */
2648
+ gather() {
2649
+ const anchors = this.anchors;
2650
+ const clusters = this.clusters;
2651
+ const order = this.order;
2652
+ const root = this.root;
2653
+ const weight = this.weight;
2654
+ const n = this.count;
2655
+ order.length = 0;
2656
+ for (let i = 0; i < n; i++) order.push(i);
2657
+ order.sort((i, j) => anchors[j].height - anchors[i].height || i - j);
2658
+ let k = 0;
2659
+ for (const i of order) {
2660
+ const f = anchors[i];
2661
+ let best = -1;
2662
+ let near = Number.POSITIVE_INFINITY;
2663
+ for (let c = 0; c < k; c++) {
2664
+ const d = Math.hypot(f.x - root[c * 3], f.y - root[c * 3 + 1], f.z - root[c * 3 + 2]);
2665
+ if (d < near) {
2666
+ near = d;
2667
+ best = c;
2668
+ }
2669
+ }
2670
+ if (best < 0 || near > CLUSTER_REACH && k < FIRE_CLUSTERS) {
2671
+ best = k++;
2672
+ root[best * 3] = f.x;
2673
+ root[best * 3 + 1] = f.y;
2674
+ root[best * 3 + 2] = f.z;
2675
+ weight[best] = 0;
2676
+ let cluster2 = clusters[best];
2677
+ if (!cluster2) {
2678
+ cluster2 = { x: 0, y: 0, z: 0, height: 0, tallest: 0, flicker: 0, heat: 0, count: 0 };
2679
+ clusters[best] = cluster2;
2680
+ }
2681
+ cluster2.x = 0;
2682
+ cluster2.y = 0;
2683
+ cluster2.z = 0;
2684
+ cluster2.height = 0;
2685
+ cluster2.tallest = f.height;
2686
+ cluster2.flicker = 0;
2687
+ cluster2.heat = 0;
2688
+ cluster2.count = 0;
2689
+ }
2690
+ const cluster = clusters[best];
2691
+ const w = f.height;
2692
+ cluster.x += f.x * w;
2693
+ cluster.y += f.y * w;
2694
+ cluster.z += f.z * w;
2695
+ weight[best] = weight[best] + w;
2696
+ cluster.height += f.height;
2697
+ cluster.flicker += flamePuff(f.seed, this.field.time);
2698
+ cluster.heat += f.heat;
2699
+ cluster.count++;
2700
+ }
2701
+ for (let c = 0; c < k; c++) {
2702
+ const cluster = clusters[c];
2703
+ const w = weight[c];
2704
+ if (w > 0) {
2705
+ cluster.x /= w;
2706
+ cluster.y /= w;
2707
+ cluster.z /= w;
2708
+ } else {
2709
+ cluster.x = root[c * 3];
2710
+ cluster.y = root[c * 3 + 1];
2711
+ cluster.z = root[c * 3 + 2];
2712
+ }
2713
+ cluster.height /= cluster.count;
2714
+ cluster.flicker /= cluster.count;
2715
+ cluster.heat /= cluster.count;
2716
+ }
2717
+ this.clusterCount = k;
2718
+ }
2719
+ };
2720
+ var shared = /* @__PURE__ */ new WeakMap();
2721
+ function fireStateOf(field, locate) {
2722
+ let byLocator = shared.get(field);
2723
+ if (!byLocator) {
2724
+ byLocator = /* @__PURE__ */ new WeakMap();
2725
+ shared.set(field, byLocator);
2726
+ }
2727
+ let state = byLocator.get(locate);
2728
+ if (!state) {
2729
+ state = new FireState(field, locate);
2730
+ byLocator.set(locate, state);
2731
+ }
2732
+ return state;
2733
+ }
2734
+
2735
+ // src/fx/FxFireLight.tsx
2736
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs2 } from "react/jsx-runtime";
2737
+ function FxFireLight({ field, locate, gain = FIRE_LIGHT_GAIN, shadows = false }) {
2738
+ const front = useRef4([]);
2739
+ const through = useRef4(null);
2740
+ const spot = useRef4(null);
2741
+ useFrame5(({ camera, clock }) => {
2742
+ const b = through.current;
2743
+ if (!b) return;
2744
+ const fire = fireStateOf(field, locate).update(clock.elapsedTime);
2745
+ const level = field.lastStats.front * gain;
2746
+ let total = 0;
2747
+ let brightest = -1;
2748
+ let most = 0;
2749
+ for (let c = 0; c < fire.clusterCount; c++) {
2750
+ const cluster = fire.clusters[c];
2751
+ const share = cluster.count * cluster.height;
2752
+ total += share;
2753
+ if (share > most) {
2754
+ most = share;
2755
+ brightest = c;
2756
+ }
2757
+ }
2758
+ const s = shadows ? spot.current : null;
2759
+ if (s) s.intensity = 0;
2760
+ for (let i = 0; i < FIRE_CLUSTERS; i++) {
2761
+ const light = front.current[i];
2762
+ if (!light) continue;
2763
+ const cluster = i < fire.clusterCount ? fire.clusters[i] : null;
2764
+ if (!cluster || !(total > 0)) {
2765
+ light.intensity = 0;
2766
+ continue;
2767
+ }
2768
+ toward.set(camera.position.x - cluster.x, 0, camera.position.z - cluster.z).normalize().multiplyScalar(0.035);
2769
+ light.position.set(cluster.x + toward.x, cluster.y + cluster.height * 0.5, cluster.z + toward.z);
2770
+ light.intensity = level * cluster.flicker * (cluster.count * cluster.height / total);
2771
+ light.color.copy(COOLING).lerp(FIRE_COLOR, Math.min(1, Math.max(0, cluster.heat)));
2772
+ if (s && i === brightest) {
2773
+ s.position.copy(light.position);
2774
+ s.target.position.set(cluster.x, cluster.y, cluster.z);
2775
+ s.target.updateMatrixWorld();
2776
+ s.intensity = light.intensity;
2777
+ s.color.copy(light.color);
2778
+ light.intensity = 0;
2779
+ }
2780
+ }
2781
+ if (fire.count === 0) {
2782
+ b.intensity = 0;
2783
+ return;
2784
+ }
2785
+ toward.set(camera.position.x - fire.x, 0, camera.position.z - fire.z).normalize().multiplyScalar(0.035);
2786
+ b.position.set(fire.x - toward.x, fire.y + fire.height * 0.5, fire.z - toward.z);
2787
+ b.intensity = level * fire.flicker * 0.35;
2588
2788
  });
2589
2789
  return /* @__PURE__ */ jsxs2(Fragment3, { children: [
2590
- /* @__PURE__ */ jsx6("pointLight", { ref: front, color: FIRE_COLOR, intensity: 0, decay: 0.9, distance: 0 }),
2790
+ Array.from({ length: FIRE_CLUSTERS }, (_, i) => /* @__PURE__ */ jsx6(
2791
+ "pointLight",
2792
+ {
2793
+ ref: (light) => {
2794
+ front.current[i] = light;
2795
+ },
2796
+ color: FIRE_COLOR,
2797
+ intensity: 0,
2798
+ decay: 0.9,
2799
+ distance: 0
2800
+ },
2801
+ i
2802
+ )),
2803
+ shadows && /* @__PURE__ */ jsx6(
2804
+ "spotLight",
2805
+ {
2806
+ ref: spot,
2807
+ color: FIRE_COLOR,
2808
+ intensity: 0,
2809
+ decay: 0.9,
2810
+ distance: 0,
2811
+ angle: 1.3,
2812
+ penumbra: 0.8,
2813
+ castShadow: true,
2814
+ "shadow-mapSize": [512, 512],
2815
+ "shadow-bias": -4e-4
2816
+ }
2817
+ ),
2591
2818
  /* @__PURE__ */ jsx6("pointLight", { ref: through, color: FIRE_COLOR, intensity: 0, decay: 0.9, distance: 0 })
2592
2819
  ] });
2593
2820
  }
2821
+ var toward = new THREE6.Vector3();
2594
2822
  var FIRE_COLOR = new THREE6.Color("#ffa24c");
2595
- var FIRE_LIGHT_GAIN = 42;
2823
+ var COOLING = new THREE6.Color("#ff8a33");
2824
+ var FIRE_LIGHT_GAIN = 26;
2596
2825
 
2597
2826
  // src/fx/FxMatchFlame.tsx
2598
2827
  import * as THREE7 from "three";
@@ -3148,6 +3377,7 @@ export {
3148
3377
  FIELD_SIZE,
3149
3378
  FIRE_GLOW,
3150
3379
  FIRE_HEAT_SCALE,
3380
+ FIRE_LIGHT_GAIN,
3151
3381
  FIRE_SOOT_SCALE,
3152
3382
  FIRE_ZONES,
3153
3383
  FIXED_DT,