leafthrough 0.1.5 → 0.1.6

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 CHANGED
@@ -145,7 +145,9 @@ draws with your renderer and takes pointer input through your camera, so it turn
145
145
  it lies, however it's moved, turned or scaled. Every option, method, property and event of
146
146
  `LeafThrough` works the same, apart from those about its own canvas and DOM: the options
147
147
  `bleed`, `maxPixelRatio`, `plain`, `label` and `formatPageLabel`, and the properties
148
- `plain` and `element`.
148
+ `plain` and `element`. It has one option of its own, `deform`
149
+ ([Reshaping the paper](#reshaping-the-paper)), and its sheets stand apart by default
150
+ ([Depth](#depth)).
149
151
 
150
152
  ```ts
151
153
  import { LeafThroughObject } from 'leafthrough'
@@ -183,7 +185,11 @@ renderer.setAnimationLoop((now) => {
183
185
  sheets cast and receive shadows when your renderer draws them, and `sun.position`
184
186
  should point towards your shadow-casting directional light, in the magazine's own
185
187
  frame (only the direction counts). It places the shadow a turning sheet casts right by
186
- the spine, where the shadow map can't.
188
+ the spine, where the shadow map can't. `update({ lighting: { shadows } })` switches the
189
+ sheets' shadows without a rebuild. Use it rather than setting `castShadow` or
190
+ `receiveShadow` on the sheets, which the magazine sets itself.
191
+ - **Depth.** See [Depth](#depth): the sheets stand a little apart, which needs enough depth
192
+ precision from your camera.
187
193
  - **Rendering on demand.** If you only draw when something changes, listen for `change`:
188
194
  it fires when input, loading or a method call needs a frame. Then keep drawing while
189
195
  `tick()` returns true.
@@ -197,11 +203,72 @@ magazine.covers(e.clientX, e.clientY) // whether the magazine takes a press t
197
203
  magazine.bounds // { pages, reach }, in the magazine's own units
198
204
  magazine.camera = otherCamera // cast input from another camera
199
205
  magazine.enabled = false // cancel active gestures and stop taking pointer/key input
206
+ magazine.update({ lighting: { shadows: false } }) // sheets stop casting and receiving, no rebuild
200
207
  magazine.addEventListener('change', () => requestRender())
201
208
  magazine.addEventListener('pagechange', (e) => e.detail) // also ready, pageload, error
202
209
  magazine.dispose() // free everything and leave the scene
203
210
  ```
204
211
 
212
+ ### Depth
213
+
214
+ In a `LeafThroughObject` the sheets stand apart, `paper.sheetGap` `0.0005` page widths
215
+ (0.1 mm paper on a 210 mm page), and sort by their real depth, so they stay in order from the front, from
216
+ behind and at any angle. Your camera has to resolve that gap. A 24-bit depth buffer tells
217
+ apart points roughly `distance² / (near × 16,777,216)` apart, in your scene's units, and that
218
+ must be well under the gap in the same units. Take a booklet 120 mm wide in a scene in
219
+ metres, seen from 4.4 m: its gap is 0.06 mm. With the camera's `near` at 0.1 the buffer
220
+ resolves about 0.012 mm, which is enough; at 0.01 it's about 0.12 mm, and hidden pages
221
+ flicker through. So keep `near` as large as your scene allows; `far` matters much less.
222
+ The magazine never changes your camera. With a coarse depth buffer you can widen the gap.
223
+
224
+ The standalone magazine instead draws sheets that touch at the spine and lets
225
+ `paper.depthBias` sort them: a polygon offset brings each sheet nearer the camera than
226
+ those under it. You can turn that on here too (`paper: { sheetGap: 0, depthBias: true }`),
227
+ but it has two limits in a scene of your own. It assumes the magazine is seen from the
228
+ front, so from behind it brings the wrong sheets forward. And it moves sheets by steps of
229
+ the depth buffer, so how far depends on `near`, `far` and distance: with a small `near`
230
+ it can pull pages through whatever lies just in front of the magazine.
231
+
232
+ ### Reshaping the paper
233
+
234
+ Experimental: this may change in a patch release. `deform` reshapes the paper with your
235
+ own GLSL, after the magazine has shaped it: bend a booklet coming out of its case, squeeze
236
+ it, roll it. It applies wherever the paper is drawn, in colour and in the shadow passes,
237
+ and lighting follows, because normals come from the deformed shape.
238
+
239
+ ```ts
240
+ const bend = { value: 0 }
241
+ const magazine = new LeafThroughObject({ renderer, camera }, {
242
+ pages,
243
+ deform: {
244
+ uniforms: { uBend: bend },
245
+ glsl: /* glsl */ `
246
+ uniform float uBend;
247
+ vec3 deformSheet(vec3 p, vec2 uv) {
248
+ // curl the pages up along x, around a cylinder of radius 1 / uBend
249
+ if (uBend < 1e-4) return p;
250
+ float a = p.x * uBend;
251
+ float r = 1.0 / uBend - p.z;
252
+ return vec3(r * sin(a), p.y, 1.0 / uBend - r * cos(a));
253
+ }`,
254
+ },
255
+ })
256
+
257
+ bend.value = 1.2 // read on the next frame: no rebuild
258
+ ```
259
+
260
+ - `deformSheet` gets `p`, a point of a sheet in the magazine's own units (see Units
261
+ above), and `uv`, where it is on that sheet: x from the spine (0) to the outer edge (1),
262
+ y from bottom to top. It returns where to draw that point, in the same units.
263
+ - three.js's common GLSL (`PI`, `saturate` and so on) is available. Name your uniforms
264
+ with a prefix of your own, as the magazine's start with `u`.
265
+ - Uniforms are read every frame: set `value` on your own objects, or pass new objects
266
+ under the same names through `update()`. Changing the GLSL, or which uniforms there are,
267
+ rebuilds the pages.
268
+ - It changes only what's drawn. Pointer input, `covers()`, `bounds` and the shadow a
269
+ turning sheet casts by the spine still work from the undeformed pages, so turn input off
270
+ (`enabled = false`) while the paper is far from its own shape.
271
+
205
272
  The [three.js demo](https://dmtrbrl.github.io/leafthrough/three.html) takes a magazine
206
273
  out of a portrait paperboard mailer on a table; its source is
207
274
  [`demo/main-three.ts`](https://github.com/dmtrbrl/leafthrough/blob/main/demo/main-three.ts),
@@ -261,6 +328,8 @@ The magazine fills the element you give it and keeps its proportions, centred.
261
328
  | `wrinkles` | `1` | Waviness of resting sheets; `0` lays them flat. |
262
329
  | `bulge` | `1` | How much pages rise off the spine. |
263
330
  | `cutout` | `false` | Cuts each sheet to the shape of its front page: transparent areas are no paper at all, shadow included. |
331
+ | `sheetGap` | `0` (`0.0005` in a scene) | Space between stacked sheets, in page widths. 0.1 mm paper on a 120 mm page is `0.1 / 120`. |
332
+ | `depthBias` | `true` (`false` in a scene) | Draws each sheet nearer the camera than those under it, so sheets touching at the spine don't flicker. See [Depth](#depth). |
264
333
 
265
334
  **`lighting`**: `ambient: { sky, ground, intensity }`, `sun: { color, intensity, position }`,
266
335
  `shadows` (default `true`), `shadowMapSize` (default `2048`), and `floorShadow`. That last
@@ -311,7 +380,8 @@ magazine.addEventListener('error', (e) => e.detail) // { error, page }
311
380
 
312
381
  Both compare options by value. Changing callbacks or the label is free. Changing `pages`,
313
382
  `layout`, paper, lighting, motion or interaction rebuilds the scene (a few milliseconds)
314
- and keeps the reader on the same page. `startPage` only applies at creation; use `goTo()`
383
+ and keeps the reader on the same page. In a `LeafThroughObject`, `lighting.shadows` and
384
+ new values for `deform`'s uniforms apply without a rebuild. `startPage` only applies at creation; use `goTo()`
315
385
  after that. The React, Vue and Svelte bindings and the element use `setOptions()`, so
316
386
  removing a prop or attribute restores its default. Options are copied on every call, so an
317
387
  object or array changed in place and passed again (as Vue's reactive props are) counts as
@@ -402,7 +472,10 @@ How it works:
402
472
  - `content.ts` keeps the visible pages' content as DOM: hidden for screen readers in 3D,
403
473
  the pages themselves in the plain view.
404
474
  - `textures.ts` loads, prioritises and unloads page textures.
405
- - `src/paper` holds the shared paper material and the procedural grain.
475
+ - `src/paper` holds the shared paper material and the procedural grain. Sheets keep their
476
+ order by real depth, each lifted by `paper.sheetGap` for the sheets under it, or by a
477
+ polygon offset (`layerSheet`) where they touch. `deform` wraps the layout's sheet shape
478
+ in every pass.
406
479
  - `LeafThrough.ts` handles the DOM, sizing, loading, layout switching, keyboard and
407
480
  events. `LeafThroughObject.ts` does the same inside someone else's scene, without the DOM.
408
481
  - Engines take input through `EngineInput` (`engine.ts`): an element to listen on, a ray
@@ -410,6 +483,8 @@ How it works:
410
483
  their own floor plane, so the same code serves the magazine's own camera and any scene's.
411
484
  - `navigation.ts` maps pages to engine positions, riffles an animated `goTo()` and binds
412
485
  the keyboard, for both.
486
+ - `test/depth.test.ts` prints the pages that should be hidden pure red and checks that
487
+ none shows, across layouts, views, paper shapes, turns and camera depth ranges.
413
488
 
414
489
  ## Credits
415
490
 
@@ -1,14 +1,21 @@
1
1
  import * as THREE from 'three';
2
2
  import { type Rect } from './framing.js';
3
3
  import type { GoToOptions } from './LeafThrough.js';
4
- import { type LeafThroughOptions, type Layout, type PageChangeDetail } from './options.js';
4
+ import { type LeafThroughOptions, type Layout, type PageChangeDetail, type PaperDeform } from './options.js';
5
5
  /**
6
6
  * The magazine's options, less those about its own canvas and DOM. Of `lighting`, only
7
7
  * `shadows` (whether sheets cast and receive them) and `sun.position` apply: the lights are
8
8
  * your scene's. Point `sun.position` towards your shadow-casting directional light, in the
9
9
  * magazine's own frame; it places the shadow a turning sheet casts right by the spine.
10
+ * `lighting.shadows` changes without a rebuild.
11
+ *
12
+ * Its sheets stand apart by default (`paper.sheetGap` `0.0005`, about 0.1 mm paper on a
13
+ * 210 mm page) and `paper.depthBias` is off, so they sort by real depth from any side.
10
14
  */
11
- export type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'>;
15
+ export type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'> & {
16
+ /** Reshapes the paper in your own GLSL. Experimental. */
17
+ deform?: PaperDeform;
18
+ };
12
19
  /** What the magazine draws with, from your scene. */
13
20
  export interface LeafThroughObjectContext {
14
21
  /** The renderer drawing your scene. Pages load into it. */
@@ -138,6 +145,7 @@ export declare class LeafThroughObject extends THREE.Group<LeafThroughObjectEven
138
145
  private pickLayout;
139
146
  private watchSpreadQuery;
140
147
  private remount;
148
+ private resolve;
141
149
  private readonly axis;
142
150
  private pageAxis;
143
151
  private dragWidth;
@@ -33,8 +33,11 @@ function resolveSettings(options, layout, prefersReducedMotion) {
33
33
  backColor: paper.backColor ?? "#fcfcf9",
34
34
  wrinkles: paper.wrinkles ?? 1,
35
35
  bulge: paper.bulge ?? 1,
36
- cutout: paper.cutout ?? false
36
+ cutout: paper.cutout ?? false,
37
+ sheetGap: Math.max(0, paper.sheetGap ?? 0),
38
+ depthBias: paper.depthBias ?? true
37
39
  },
40
+ deform: null,
38
41
  lighting: {
39
42
  ambient: {
40
43
  sky: lighting.ambient?.sky ?? "#ffffff",
@@ -632,27 +635,37 @@ function createPaperMaterials(uniforms, shader, settings) {
632
635
  `
633
636
  );
634
637
  const noiseAt = (component) => shader.noise ? `aNoise.${component}` : "0.0";
638
+ const deform = settings.deform;
639
+ const deformChunk = deform ? `#include <common>
640
+ uniform vec3 uSheetOrigin;
641
+ ${deform.glsl}
642
+ vec3 sheetDeformed(vec3 p, vec2 uv) {
643
+ return deformSheet(p + uSheetOrigin, uv) - uSheetOrigin;
644
+ }` : "#include <common>";
645
+ const deformed = (p, uv) => deform ? `sheetDeformed(${p}, ${uv})` : p;
646
+ const deformUniforms = deform ? { ...deform.uniforms, uSheetOrigin: uniforms.uSheetOrigin } : {};
635
647
  const material = new THREE5.MeshStandardMaterial({
636
648
  side: THREE5.DoubleSide,
637
649
  metalness: paper.metalness,
638
650
  roughness: paper.roughness
639
651
  });
640
652
  material.onBeforeCompile = (program) => {
641
- Object.assign(program.uniforms, uniforms);
653
+ Object.assign(program.uniforms, uniforms, deformUniforms);
642
654
  program.vertexShader = constants + shader.vertexChunk + "varying vec2 vSheetUv;\nvarying vec3 vSheetPosition;\n" + program.vertexShader;
643
- program.vertexShader = program.vertexShader.replace(
655
+ program.vertexShader = program.vertexShader.replace("#include <common>", deformChunk).replace(
644
656
  "#include <beginnormal_vertex>",
645
657
  `#include <beginnormal_vertex>
646
658
  vec2 sheetStep = vec2(NORMAL_EPSILON, NORMAL_EPSILON / SHEET_ASPECT);
647
- vec3 sheetP = sheetPosition(uv, ${noiseAt("x")});
648
- vec3 sheetPx = sheetPosition(uv + vec2(sheetStep.x, 0.0), ${noiseAt("y")});
649
- vec3 sheetPy = sheetPosition(uv + vec2(0.0, sheetStep.y), ${noiseAt("z")});
659
+ vec3 sheetShaped = sheetPosition(uv, ${noiseAt("x")});
660
+ vec3 sheetP = ${deformed("sheetShaped", "uv")};
661
+ vec3 sheetPx = ${deformed(`sheetPosition(uv + vec2(sheetStep.x, 0.0), ${noiseAt("y")})`, "uv + vec2(sheetStep.x, 0.0)")};
662
+ vec3 sheetPy = ${deformed(`sheetPosition(uv + vec2(0.0, sheetStep.y), ${noiseAt("z")})`, "uv + vec2(0.0, sheetStep.y)")};
650
663
  objectNormal = normalize(cross(sheetPx - sheetP, sheetPy - sheetP));`
651
664
  ).replace(
652
665
  "#include <begin_vertex>",
653
666
  `vec3 transformed = sheetP;
654
667
  vSheetUv = uv;
655
- vSheetPosition = sheetP;`
668
+ vSheetPosition = sheetShaped;`
656
669
  );
657
670
  program.fragmentShader = constants + `uniform sampler2D uFront;
658
671
  uniform sampler2D uBack;
@@ -732,13 +745,13 @@ function createPaperMaterials(uniforms, shader, settings) {
732
745
  });
733
746
  const distanceMaterial = new THREE5.MeshDistanceMaterial({ side: THREE5.DoubleSide });
734
747
  depthMaterial.onBeforeCompile = distanceMaterial.onBeforeCompile = (program) => {
735
- Object.assign(program.uniforms, uniforms);
748
+ Object.assign(program.uniforms, uniforms, deformUniforms);
736
749
  program.vertexShader = constants + shader.vertexChunk + "varying vec2 vSheetUv;\nvarying vec3 vSheetPosition;\n" + program.vertexShader;
737
- program.vertexShader = program.vertexShader.replace(
750
+ program.vertexShader = program.vertexShader.replace("#include <common>", deformChunk).replace(
738
751
  "#include <begin_vertex>",
739
752
  `vSheetUv = uv;
740
753
  vSheetPosition = sheetPosition(uv, ${noiseAt("x")});
741
- vec3 transformed = vSheetPosition - vec3(SHADOW_OFFSET_X, 0.0, 0.0);`
754
+ vec3 transformed = ${deformed("vSheetPosition - vec3(SHADOW_OFFSET_X, 0.0, 0.0)", "uv")};`
742
755
  );
743
756
  program.fragmentShader = constants + `
744
757
  uniform sampler2D uFront;
@@ -753,7 +766,7 @@ function createPaperMaterials(uniforms, shader, settings) {
753
766
  ${shader.fragmentChunk ? "if (sheetHidden(vSheetPosition)) discard;" : ""}`
754
767
  );
755
768
  };
756
- const cacheKey = constants + shader.vertexChunk + (shader.fragmentChunk ?? "") + (paper.cutout ? "cutout" : "");
769
+ const cacheKey = constants + shader.vertexChunk + (shader.fragmentChunk ?? "") + (paper.cutout ? "cutout" : "") + (deform?.glsl ?? "");
757
770
  material.customProgramCacheKey = () => cacheKey;
758
771
  depthMaterial.customProgramCacheKey = () => cacheKey;
759
772
  distanceMaterial.customProgramCacheKey = () => cacheKey;
@@ -777,11 +790,11 @@ function createSheetMesh(geometry, uniforms, shader, settings) {
777
790
  mesh.frustumCulled = false;
778
791
  return mesh;
779
792
  }
780
- function layerSheet(mesh, depth, count, moving) {
793
+ function layerSheet(mesh, depth, count, moving, bias) {
781
794
  const material = mesh.material;
782
- material.polygonOffset = true;
795
+ material.polygonOffset = bias;
783
796
  material.polygonOffsetFactor = 0;
784
- material.polygonOffsetUnits = moving ? -(count + 4) : -(count - Math.min(depth, count));
797
+ material.polygonOffsetUnits = bias ? moving ? -(count + 4) : -(count - Math.min(depth, count)) : 0;
785
798
  mesh.renderOrder = depth;
786
799
  }
787
800
  function disposeSheetMesh(mesh) {
@@ -1085,6 +1098,7 @@ var vertexChunk = (
1085
1098
  uniform float uTailBend; // how far the free end bows back
1086
1099
  uniform vec3 uCone; // radius change per unit of y, radius at the top, how far to wrap
1087
1100
  uniform vec2 uFlipRotation;
1101
+ uniform float uStackGap; // how far the sheet stands off the spine, along its own normal
1088
1102
 
1089
1103
  float sheetPileLift(float x) {
1090
1104
  float t = 1.0 - min(x / uPileLift.x, 1.0);
@@ -1170,7 +1184,7 @@ var vertexChunk = (
1170
1184
  }
1171
1185
 
1172
1186
  // spin about the spine
1173
- float z = shape.z + pile.z;
1187
+ float z = shape.z + pile.z + uStackGap;
1174
1188
  return vec3(
1175
1189
  shape.x * uFlipRotation.x - z * uFlipRotation.y,
1176
1190
  shape.y,
@@ -1206,11 +1220,13 @@ function createSingleSheet(geometry, front, back, pattern, settings, shading) {
1206
1220
  uCone: { value: new THREE9.Vector3(0, 1, 0) },
1207
1221
  uFlipRotation: { value: new THREE9.Vector2(1, 0) },
1208
1222
  uTrim: { value: new THREE9.Vector3(0, 1, 1) },
1223
+ uStackGap: { value: 0 },
1209
1224
  uFront: { value: front },
1210
1225
  uBack: { value: back },
1211
1226
  uPattern: { value: pattern },
1212
1227
  uSpineContact: shading.spineContact,
1213
- uLightDirection: shading.lightDirection
1228
+ uLightDirection: shading.lightDirection,
1229
+ uSheetOrigin: shading.origin
1214
1230
  };
1215
1231
  const mesh = createSheetMesh(
1216
1232
  geometry,
@@ -1301,6 +1317,7 @@ var SingleEngine = class {
1301
1317
  fallDelay;
1302
1318
  fallDuration;
1303
1319
  abort = new AbortController();
1320
+ shading;
1304
1321
  current;
1305
1322
  turned = 0;
1306
1323
  // sum of every sheet's progress, as of the last update
@@ -1321,6 +1338,7 @@ var SingleEngine = class {
1321
1338
  this.input = ctx.input;
1322
1339
  this.metrics = ctx.metrics;
1323
1340
  this.requestFrame = ctx.requestFrame;
1341
+ this.shading = ctx.shading;
1324
1342
  this.count = ctx.textures.length;
1325
1343
  this.maxPosition = this.count;
1326
1344
  this.current = clamp(Math.round(ctx.position), 0, Math.max(this.count - 1, 0));
@@ -1399,6 +1417,11 @@ var SingleEngine = class {
1399
1417
  if (this.press) this.release(new PointerEvent("pointercancel", { pointerId: this.press.id }));
1400
1418
  this.wake();
1401
1419
  }
1420
+ setShadows(on) {
1421
+ this.settings.lighting.shadows = on;
1422
+ for (const sheet of this.sheets) sheet.mesh.castShadow = sheet.mesh.receiveShadow = on;
1423
+ this.wake();
1424
+ }
1402
1425
  dispose() {
1403
1426
  this.cancelHold();
1404
1427
  this.abort.abort();
@@ -1759,11 +1782,13 @@ var SingleEngine = class {
1759
1782
  for (const sheet of this.sheets) {
1760
1783
  const i = sheet.index;
1761
1784
  sheet.mesh.visible = closing || i <= pileFrom + PILE.renderDepth || this.busy(sheet);
1762
- layerSheet(sheet.mesh, i < this.current ? this.current - 1 - i : i - this.current, this.count, this.busy(sheet));
1785
+ const depth = i < this.current ? this.current - 1 - i : i - this.current;
1786
+ layerSheet(sheet.mesh, depth, this.count, this.busy(sheet), this.settings.paper.depthBias);
1763
1787
  }
1764
1788
  const read = smooth(this.count > 1 ? this.paced(turned) / (this.count - 1) : 0) * (1 - this.shutting(turned));
1765
1789
  this.group.position.x = BOOK.pushRight * read;
1766
1790
  this.group.position.z = -BOOK.pushBack * read;
1791
+ this.shading.origin.value.copy(this.group.position);
1767
1792
  this.dirty = false;
1768
1793
  return true;
1769
1794
  }
@@ -1833,6 +1858,11 @@ var SingleEngine = class {
1833
1858
  topReach = Math.min(topReach, edge.top);
1834
1859
  }
1835
1860
  u.uPileReach.value.set(bottomReach, topReach);
1861
+ const under = P - 1 - sheet.index;
1862
+ const before = Math.max(0, turned - 1 - sheet.index);
1863
+ const around = smooth(T);
1864
+ const offset = fallen ? before * Math.cos(spineOpen) : under + (before - under) * around;
1865
+ u.uStackGap.value = this.settings.paper.sheetGap * (offset + (under - offset) * shut);
1836
1866
  }
1837
1867
  // a turning page: swing round the spine up to the open angle, then roll round the cone
1838
1868
  shapeTurn(sheet, T, spineOpen, radii, tilt) {
@@ -1891,6 +1921,7 @@ var vertexChunk2 = (settings) => (
1891
1921
 
1892
1922
  const float BULGE = ${settings.paper.bulge.toFixed(4)};
1893
1923
  const float WRINKLE = ${(0.13 * settings.paper.wrinkles).toFixed(4)};
1924
+ const bool NESTED = ${settings.paper.depthBias ? "false" : "true"};
1894
1925
 
1895
1926
  uniform float uFlipProgress;
1896
1927
  uniform float uWrinkleSide;
@@ -1900,6 +1931,7 @@ var vertexChunk2 = (settings) => (
1900
1931
  uniform vec2 uFlipRotation;
1901
1932
  uniform float uDirection;
1902
1933
  uniform float uStackLift;
1934
+ uniform float uStackGap;
1903
1935
 
1904
1936
  attribute vec3 aNoise;
1905
1937
 
@@ -1916,8 +1948,11 @@ var vertexChunk2 = (settings) => (
1916
1948
  float y = (uv.y - 0.5) * SHEET_ASPECT;
1917
1949
 
1918
1950
  // Every sheet shares the same wrinkles and bulge, scaled by its place in the stack,
1919
- // so stacked sheets nest inside each other instead of crossing.
1920
- float wrinkle = (mix(noise, 1.0 - noise, uFlipProgress) - 0.5) * smoothstep(0.0, 0.35, x) * WRINKLE * uStackLift;
1951
+ // so stacked sheets nest inside each other instead of crossing. Where a wrinkle dips, a
1952
+ // bigger one dips below the sheets under it, which only the depth bias hides; without it
1953
+ // the dips are the same on every sheet.
1954
+ float wrinkle = uWrinkleSide * (mix(noise, 1.0 - noise, uFlipProgress) - 0.5) * smoothstep(0.0, 0.35, x) * WRINKLE;
1955
+ wrinkle *= NESTED && wrinkle < 0.0 ? 1.0 : uStackLift;
1921
1956
  float lift = sheetRestingLift(x) * BULGE * uStackLift;
1922
1957
 
1923
1958
  // bend with constant curvature past the fold line, trailing against the turn
@@ -1935,7 +1970,7 @@ var vertexChunk2 = (settings) => (
1935
1970
  // turn about the spine
1936
1971
  p = vec3(p.x * uFlipRotation.x - p.z * uFlipRotation.y, p.y, p.x * uFlipRotation.y + p.z * uFlipRotation.x);
1937
1972
 
1938
- p.z += lift + uWrinkleSide * wrinkle;
1973
+ p.z += lift + wrinkle + uStackGap;
1939
1974
  return p;
1940
1975
  }
1941
1976
  `
@@ -1992,11 +2027,13 @@ function createSpreadSheet(geometry, front, back, pattern, settings, shading) {
1992
2027
  uFlipRotation: { value: new THREE11.Vector2(1, 0) },
1993
2028
  uDirection: { value: 1 },
1994
2029
  uStackLift: { value: 1 },
2030
+ uStackGap: { value: 0 },
1995
2031
  uFront: { value: front },
1996
2032
  uBack: { value: back },
1997
2033
  uPattern: { value: pattern },
1998
2034
  uSpineContact: shading.spineContact,
1999
- uLightDirection: shading.lightDirection
2035
+ uLightDirection: shading.lightDirection,
2036
+ uSheetOrigin: shading.origin
2000
2037
  };
2001
2038
  const mesh = createSheetMesh(
2002
2039
  geometry,
@@ -2037,6 +2074,10 @@ function stackLift(index, count, progress) {
2037
2074
  if (count <= 1) return 1;
2038
2075
  return 0.31 + 0.69 * (index + (count - 1 - 2 * index) * (1 - progress)) / (count - 1);
2039
2076
  }
2077
+ function sheetsUnder(index, count, progress) {
2078
+ const t = clamp2((progress - 0.25) / 0.5, 0, 1);
2079
+ return count - 1 - index + (2 * index - count + 1) * t * t * (3 - 2 * t);
2080
+ }
2040
2081
  var SpreadEngine = class {
2041
2082
  group = new THREE12.Group();
2042
2083
  maxPosition;
@@ -2133,6 +2174,11 @@ var SpreadEngine = class {
2133
2174
  if (this.press) this.release(new PointerEvent("pointercancel", { pointerId: this.press.id }));
2134
2175
  this.wake();
2135
2176
  }
2177
+ setShadows(on) {
2178
+ this.settings.lighting.shadows = on;
2179
+ for (const sheet of this.sheets) sheet.mesh.receiveShadow = on;
2180
+ this.wake();
2181
+ }
2136
2182
  dispose() {
2137
2183
  this.cancelHold();
2138
2184
  this.abort.abort();
@@ -2495,7 +2541,7 @@ var SpreadEngine = class {
2495
2541
  const depth = i >= this.current ? i - this.current : this.current - 1 - i;
2496
2542
  const turning = this.inFlight(i) || this.drag?.index === i;
2497
2543
  sheet.mesh.castShadow = this.settings.lighting.shadows && (depth < 3 || turning);
2498
- layerSheet(sheet.mesh, depth, this.sheets.length, turning);
2544
+ layerSheet(sheet.mesh, depth, this.sheets.length, turning, this.settings.paper.depthBias);
2499
2545
  const lift = Math.abs(sheet.uniforms.uFlipRotation.value.y);
2500
2546
  if (turning && lift > upright) {
2501
2547
  upright = lift;
@@ -2526,6 +2572,7 @@ var SpreadEngine = class {
2526
2572
  u.uWrinkleSide.value = -Math.cos(turn);
2527
2573
  u.uDirection.value = sheet.directionSmooth;
2528
2574
  u.uStackLift.value = sheet.liftBase + sheet.liftSpan * sheet.progress;
2575
+ u.uStackGap.value = this.settings.paper.sheetGap * sheetsUnder(sheet.index, this.sheets.length, sheet.progress);
2529
2576
  u.uBendAngle.value = bend;
2530
2577
  u.uFold.value.set(cos, sin);
2531
2578
  u.uCurl.value.set(curlStart, curlLength);
@@ -2806,7 +2853,8 @@ var LeafThrough = class extends EventTarget {
2806
2853
  scene.add(sun, sun.target);
2807
2854
  const shading = {
2808
2855
  spineContact: { value: new THREE13.Vector3(1, 0, 0) },
2809
- lightDirection: { value: sun.position.clone().sub(sun.target.position).normalize() }
2856
+ lightDirection: { value: sun.position.clone().sub(sun.target.position).normalize() },
2857
+ origin: { value: new THREE13.Vector3() }
2810
2858
  };
2811
2859
  let floor2 = null;
2812
2860
  if (layout === "spread" && shadows && settings.lighting.floorShadow > 0) {
@@ -3097,6 +3145,7 @@ export {
3097
3145
  LIVE_OPTIONS,
3098
3146
  preloadRadius,
3099
3147
  snapshot,
3148
+ deepEqual,
3100
3149
  changedOptions,
3101
3150
  sourceKey,
3102
3151
  floorPoint,
@@ -3114,4 +3163,4 @@ export {
3114
3163
  SpreadEngine,
3115
3164
  LeafThrough
3116
3165
  };
3117
- //# sourceMappingURL=chunk-GYSQO7QZ.js.map
3166
+ //# sourceMappingURL=chunk-QFOGCGJJ.js.map