leafthrough 0.1.5 → 0.1.7

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.
@@ -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",
@@ -247,11 +250,11 @@ function displayImage(source, alt = "") {
247
250
  return img;
248
251
  }
249
252
  if (typeof HTMLCanvasElement !== "undefined" && (drawable instanceof HTMLCanvasElement || drawable instanceof ImageBitmap)) {
250
- const canvas = document.createElement("canvas");
251
- canvas.width = drawable.width;
252
- canvas.height = drawable.height;
253
- canvas.getContext("2d")?.drawImage(drawable, 0, 0);
254
- return describe(canvas, alt, style);
253
+ const canvas2 = document.createElement("canvas");
254
+ canvas2.width = drawable.width;
255
+ canvas2.height = drawable.height;
256
+ canvas2.getContext("2d")?.drawImage(drawable, 0, 0);
257
+ return describe(canvas2, alt, style);
255
258
  }
256
259
  return describe(document.createElement("div"), alt, style);
257
260
  }
@@ -423,6 +426,10 @@ import * as THREE7 from "three";
423
426
  // src/textures.ts
424
427
  import * as THREE4 from "three";
425
428
  var CONCURRENCY = 4;
429
+ var NEAR = 1;
430
+ var UPLOADS_PER_FRAME = 1;
431
+ var IDLE_SPARE = 8;
432
+ var UPLOAD_TIMEOUT = 100;
426
433
  var PageLoader = class {
427
434
  constructor(sources, renderer, radius, events) {
428
435
  this.sources = sources;
@@ -440,6 +447,9 @@ var PageLoader = class {
440
447
  owned = /* @__PURE__ */ new Set();
441
448
  inFlight = /* @__PURE__ */ new Map();
442
449
  failed = /* @__PURE__ */ new Set();
450
+ // decoded pages waiting for their turn on the GPU
451
+ pending = /* @__PURE__ */ new Map();
452
+ cancelUploads = null;
443
453
  // `first` pages load before the rest; the rest go nearest first
444
454
  first = [];
445
455
  // the pages in view, and the one being read, which loads first among them
@@ -488,6 +498,15 @@ var PageLoader = class {
488
498
  this.owned.delete(texture);
489
499
  texture.dispose();
490
500
  });
501
+ for (const [i, texture] of this.pending) {
502
+ if (this.distance(i) > keep) {
503
+ this.pending.delete(i);
504
+ if (this.owned.delete(texture)) texture.dispose();
505
+ } else if (this.distance(i) <= NEAR) {
506
+ this.pending.delete(i);
507
+ this.show(i, texture);
508
+ }
509
+ }
491
510
  this.pump();
492
511
  }
493
512
  setWrap(wrap) {
@@ -506,6 +525,9 @@ var PageLoader = class {
506
525
  }
507
526
  dispose() {
508
527
  this.disposed = true;
528
+ this.cancelUploads?.();
529
+ this.cancelUploads = null;
530
+ this.pending.clear();
509
531
  for (const request of this.inFlight.values()) request.abort();
510
532
  for (const texture of this.owned) texture.dispose();
511
533
  this.owned.clear();
@@ -520,7 +542,7 @@ var PageLoader = class {
520
542
  // starts the most wanted pages while there's room
521
543
  pump() {
522
544
  if (this.disposed || this.inFlight.size >= CONCURRENCY) return;
523
- const wanted = (i) => !this.textures[i] && !this.inFlight.has(i) && !this.failed.has(i) && this.distance(i) <= this.radius;
545
+ const wanted = (i) => !this.textures[i] && !this.inFlight.has(i) && !this.pending.has(i) && !this.failed.has(i) && this.distance(i) <= this.radius;
524
546
  const queue = [
525
547
  ...this.first.filter((i) => this.waiting.has(i)),
526
548
  ...this.sources.map((_, i) => i).sort((a, b) => this.distance(a) - this.distance(b) || Math.abs(a - this.around) - Math.abs(b - this.around))
@@ -538,13 +560,17 @@ var PageLoader = class {
538
560
  if (this.owned.delete(texture)) texture.dispose();
539
561
  return;
540
562
  }
541
- texture.colorSpace = THREE4.SRGBColorSpace;
542
- texture.premultiplyAlpha = true;
543
- texture.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
544
- texture.needsUpdate = true;
545
- this.renderer.initTexture(texture);
546
- this.textures[page] = texture;
547
- this.events.onLoad(page, texture);
563
+ if (this.owned.has(texture)) {
564
+ texture.colorSpace = THREE4.SRGBColorSpace;
565
+ texture.premultiplyAlpha = true;
566
+ texture.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
567
+ texture.needsUpdate = true;
568
+ }
569
+ if (this.waiting.has(page) || this.distance(page) <= NEAR) this.show(page, texture);
570
+ else {
571
+ this.pending.set(page, texture);
572
+ this.scheduleUploads();
573
+ }
548
574
  }).catch((error) => {
549
575
  if (this.disposed || request.signal.aborted) return;
550
576
  this.failed.add(page);
@@ -555,6 +581,41 @@ var PageLoader = class {
555
581
  this.pump();
556
582
  });
557
583
  }
584
+ // Puts the page's texture on the GPU and hands it over. A texture passed in is taken as it
585
+ // is, so one that's on the GPU already, or shared with the caller's own material, isn't
586
+ // uploaded again: initTexture only uploads what the renderer hasn't got.
587
+ show(page, texture) {
588
+ this.renderer.initTexture(texture);
589
+ this.textures[page] = texture;
590
+ this.events.onLoad(page, texture);
591
+ }
592
+ // the next pending pages, in idle time where the browser offers it, else before the next frame
593
+ scheduleUploads() {
594
+ if (this.cancelUploads || !this.pending.size) return;
595
+ if (typeof requestIdleCallback === "function") {
596
+ const id = requestIdleCallback((deadline) => this.uploadSome(deadline), { timeout: UPLOAD_TIMEOUT });
597
+ this.cancelUploads = () => cancelIdleCallback(id);
598
+ } else {
599
+ const id = requestAnimationFrame(() => this.uploadSome());
600
+ this.cancelUploads = () => cancelAnimationFrame(id);
601
+ }
602
+ }
603
+ // nearest the reader first
604
+ uploadSome(deadline) {
605
+ this.cancelUploads = null;
606
+ const queue = [...this.pending.keys()].sort(
607
+ (a, b) => this.distance(a) - this.distance(b) || Math.abs(a - this.around) - Math.abs(b - this.around)
608
+ );
609
+ let count = 0;
610
+ for (const page of queue) {
611
+ if (count >= UPLOADS_PER_FRAME && !(deadline && deadline.timeRemaining() > IDLE_SPARE)) break;
612
+ const texture = this.pending.get(page);
613
+ this.pending.delete(page);
614
+ this.show(page, texture);
615
+ count++;
616
+ }
617
+ this.scheduleUploads();
618
+ }
558
619
  async createTexture(source, signal) {
559
620
  if (source instanceof THREE4.Texture) return source;
560
621
  const own = (texture) => (this.owned.add(texture), texture);
@@ -564,11 +625,11 @@ var PageLoader = class {
564
625
  if (!source.complete || !source.naturalWidth) await source.decode();
565
626
  return own(new THREE4.Texture(source));
566
627
  }
567
- const canvas = document.createElement("canvas");
568
- canvas.width = source.width;
569
- canvas.height = source.height;
570
- canvas.getContext("2d").drawImage(source, 0, 0);
571
- return own(new THREE4.CanvasTexture(canvas));
628
+ const canvas2 = document.createElement("canvas");
629
+ canvas2.width = source.width;
630
+ canvas2.height = source.height;
631
+ canvas2.getContext("2d").drawImage(source, 0, 0);
632
+ return own(new THREE4.CanvasTexture(canvas2));
572
633
  }
573
634
  };
574
635
  function loadImage(url, signal) {
@@ -632,27 +693,37 @@ function createPaperMaterials(uniforms, shader, settings) {
632
693
  `
633
694
  );
634
695
  const noiseAt = (component) => shader.noise ? `aNoise.${component}` : "0.0";
696
+ const deform = settings.deform;
697
+ const deformChunk = deform ? `#include <common>
698
+ uniform vec3 uSheetOrigin;
699
+ ${deform.glsl}
700
+ vec3 sheetDeformed(vec3 p, vec2 uv) {
701
+ return deformSheet(p + uSheetOrigin, uv) - uSheetOrigin;
702
+ }` : "#include <common>";
703
+ const deformed = (p, uv) => deform ? `sheetDeformed(${p}, ${uv})` : p;
704
+ const deformUniforms = deform ? { ...deform.uniforms, uSheetOrigin: uniforms.uSheetOrigin } : {};
635
705
  const material = new THREE5.MeshStandardMaterial({
636
706
  side: THREE5.DoubleSide,
637
707
  metalness: paper.metalness,
638
708
  roughness: paper.roughness
639
709
  });
640
710
  material.onBeforeCompile = (program) => {
641
- Object.assign(program.uniforms, uniforms);
711
+ Object.assign(program.uniforms, uniforms, deformUniforms);
642
712
  program.vertexShader = constants + shader.vertexChunk + "varying vec2 vSheetUv;\nvarying vec3 vSheetPosition;\n" + program.vertexShader;
643
- program.vertexShader = program.vertexShader.replace(
713
+ program.vertexShader = program.vertexShader.replace("#include <common>", deformChunk).replace(
644
714
  "#include <beginnormal_vertex>",
645
715
  `#include <beginnormal_vertex>
646
716
  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")});
717
+ vec3 sheetShaped = sheetPosition(uv, ${noiseAt("x")});
718
+ vec3 sheetP = ${deformed("sheetShaped", "uv")};
719
+ vec3 sheetPx = ${deformed(`sheetPosition(uv + vec2(sheetStep.x, 0.0), ${noiseAt("y")})`, "uv + vec2(sheetStep.x, 0.0)")};
720
+ vec3 sheetPy = ${deformed(`sheetPosition(uv + vec2(0.0, sheetStep.y), ${noiseAt("z")})`, "uv + vec2(0.0, sheetStep.y)")};
650
721
  objectNormal = normalize(cross(sheetPx - sheetP, sheetPy - sheetP));`
651
722
  ).replace(
652
723
  "#include <begin_vertex>",
653
724
  `vec3 transformed = sheetP;
654
725
  vSheetUv = uv;
655
- vSheetPosition = sheetP;`
726
+ vSheetPosition = sheetShaped;`
656
727
  );
657
728
  program.fragmentShader = constants + `uniform sampler2D uFront;
658
729
  uniform sampler2D uBack;
@@ -732,13 +803,13 @@ function createPaperMaterials(uniforms, shader, settings) {
732
803
  });
733
804
  const distanceMaterial = new THREE5.MeshDistanceMaterial({ side: THREE5.DoubleSide });
734
805
  depthMaterial.onBeforeCompile = distanceMaterial.onBeforeCompile = (program) => {
735
- Object.assign(program.uniforms, uniforms);
806
+ Object.assign(program.uniforms, uniforms, deformUniforms);
736
807
  program.vertexShader = constants + shader.vertexChunk + "varying vec2 vSheetUv;\nvarying vec3 vSheetPosition;\n" + program.vertexShader;
737
- program.vertexShader = program.vertexShader.replace(
808
+ program.vertexShader = program.vertexShader.replace("#include <common>", deformChunk).replace(
738
809
  "#include <begin_vertex>",
739
810
  `vSheetUv = uv;
740
811
  vSheetPosition = sheetPosition(uv, ${noiseAt("x")});
741
- vec3 transformed = vSheetPosition - vec3(SHADOW_OFFSET_X, 0.0, 0.0);`
812
+ vec3 transformed = ${deformed("vSheetPosition - vec3(SHADOW_OFFSET_X, 0.0, 0.0)", "uv")};`
742
813
  );
743
814
  program.fragmentShader = constants + `
744
815
  uniform sampler2D uFront;
@@ -753,7 +824,7 @@ function createPaperMaterials(uniforms, shader, settings) {
753
824
  ${shader.fragmentChunk ? "if (sheetHidden(vSheetPosition)) discard;" : ""}`
754
825
  );
755
826
  };
756
- const cacheKey = constants + shader.vertexChunk + (shader.fragmentChunk ?? "") + (paper.cutout ? "cutout" : "");
827
+ const cacheKey = constants + shader.vertexChunk + (shader.fragmentChunk ?? "") + (paper.cutout ? "cutout" : "") + (deform?.glsl ?? "");
757
828
  material.customProgramCacheKey = () => cacheKey;
758
829
  depthMaterial.customProgramCacheKey = () => cacheKey;
759
830
  distanceMaterial.customProgramCacheKey = () => cacheKey;
@@ -777,11 +848,11 @@ function createSheetMesh(geometry, uniforms, shader, settings) {
777
848
  mesh.frustumCulled = false;
778
849
  return mesh;
779
850
  }
780
- function layerSheet(mesh, depth, count, moving) {
851
+ function layerSheet(mesh, depth, count, moving, bias) {
781
852
  const material = mesh.material;
782
- material.polygonOffset = true;
853
+ material.polygonOffset = bias;
783
854
  material.polygonOffsetFactor = 0;
784
- material.polygonOffsetUnits = moving ? -(count + 4) : -(count - Math.min(depth, count));
855
+ material.polygonOffsetUnits = bias ? moving ? -(count + 4) : -(count - Math.min(depth, count)) : 0;
785
856
  mesh.renderOrder = depth;
786
857
  }
787
858
  function disposeSheetMesh(mesh) {
@@ -802,6 +873,37 @@ function solidTexture(color) {
802
873
  return texture;
803
874
  }
804
875
 
876
+ // src/shared.ts
877
+ var SharedResources = class {
878
+ entries = /* @__PURE__ */ new Map();
879
+ /** The resource under `key`, made by `make` for the first user. Pair every call with `release`. */
880
+ acquire(key, make) {
881
+ let entry = this.entries.get(key);
882
+ if (!entry) {
883
+ entry = { value: make(), users: 0 };
884
+ this.entries.set(key, entry);
885
+ }
886
+ entry.users++;
887
+ return entry.value;
888
+ }
889
+ /** Lets go of `key`; the last user's release frees it. */
890
+ release(key) {
891
+ const entry = this.entries.get(key);
892
+ if (!entry || --entry.users > 0) return;
893
+ this.entries.delete(key);
894
+ entry.value.dispose();
895
+ }
896
+ };
897
+ var byRenderer = /* @__PURE__ */ new WeakMap();
898
+ function sharedResources(renderer) {
899
+ let shared = byRenderer.get(renderer);
900
+ if (!shared) {
901
+ shared = new SharedResources();
902
+ byRenderer.set(renderer, shared);
903
+ }
904
+ return shared;
905
+ }
906
+
805
907
  // src/paper/paperPattern.ts
806
908
  import * as THREE6 from "three";
807
909
  var SIZE = 384;
@@ -852,7 +954,9 @@ function spread(values) {
852
954
  for (const v of values) variance += (v - mean) ** 2;
853
955
  return Math.sqrt(variance / values.length) || 1;
854
956
  }
855
- function createPaperPattern(renderer) {
957
+ var canvas = null;
958
+ function paperPatternCanvas() {
959
+ if (canvas) return canvas;
856
960
  let s = 7;
857
961
  const random = () => (s = s * 1664525 + 1013904223 >>> 0) / 4294967296;
858
962
  const noise = new Float32Array(SIZE * SIZE);
@@ -880,7 +984,7 @@ function createPaperPattern(renderer) {
880
984
  const cloudScale = CLOUD * spread(grain) / spread(cloud);
881
985
  for (let i = 0; i < grain.length; i++) grain[i] += cloud[i] * cloudScale;
882
986
  const order = Array.from(grain.keys()).sort((a, b) => grain[a] - grain[b]);
883
- const canvas = document.createElement("canvas");
987
+ canvas = document.createElement("canvas");
884
988
  canvas.width = canvas.height = SIZE;
885
989
  const ctx = canvas.getContext("2d");
886
990
  const image = ctx.createImageData(SIZE, SIZE);
@@ -896,13 +1000,17 @@ function createPaperPattern(renderer) {
896
1000
  image.data[o + 3] = 255;
897
1001
  });
898
1002
  ctx.putImageData(image, 0, 0);
899
- const texture = new THREE6.CanvasTexture(canvas);
1003
+ return canvas;
1004
+ }
1005
+ function createPaperPattern(renderer) {
1006
+ const texture = new THREE6.CanvasTexture(paperPatternCanvas());
900
1007
  texture.wrapS = texture.wrapT = THREE6.RepeatWrapping;
901
1008
  texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
902
1009
  return texture;
903
1010
  }
904
1011
 
905
1012
  // src/paper/paperTextures.ts
1013
+ var BUILTIN_PATTERN = "paper-pattern";
906
1014
  var PaperTextures = class {
907
1015
  constructor(backColor, events) {
908
1016
  this.events = events;
@@ -927,30 +1035,34 @@ var PaperTextures = class {
927
1035
  const grain = settings.paper.grain;
928
1036
  const key = grain === true || grain === void 0 ? "builtin" : grain === false ? "none" : grain;
929
1037
  if (this.pattern?.key === key) return this.pattern.texture;
930
- this.pattern?.texture.dispose();
931
- let texture;
932
- if (key === "builtin") texture = createPaperPattern(renderer);
933
- else if (key === "none") texture = solidTexture("#000000");
934
- else {
935
- texture = solidTexture("#000000");
1038
+ this.pattern?.release();
1039
+ if (key === "builtin") {
1040
+ const shared = sharedResources(renderer);
1041
+ const texture = shared.acquire(BUILTIN_PATTERN, () => createPaperPattern(renderer));
1042
+ this.pattern = { texture, key, release: () => shared.release(BUILTIN_PATTERN) };
1043
+ return texture;
1044
+ }
1045
+ const pattern = { texture: solidTexture("#000000"), key, release: () => pattern.texture.dispose() };
1046
+ this.pattern = pattern;
1047
+ if (key !== "none") {
936
1048
  loadGrain(key).then(
937
1049
  (loaded) => {
938
- if (this.pattern?.key !== key || this.disposed) return loaded.dispose();
939
- this.pattern.texture.dispose();
940
- this.pattern.texture = loaded;
1050
+ if (this.pattern !== pattern || this.disposed) return loaded.dispose();
1051
+ pattern.texture.dispose();
1052
+ pattern.texture = loaded;
941
1053
  this.events.onGrain();
942
1054
  },
943
1055
  (error) => {
944
- if (this.pattern?.key === key && !this.disposed) this.events.onError(error);
1056
+ if (this.pattern === pattern && !this.disposed) this.events.onError(error);
945
1057
  }
946
1058
  );
947
1059
  }
948
- this.pattern = { texture, key };
949
- return texture;
1060
+ return pattern.texture;
950
1061
  }
951
1062
  dispose() {
952
1063
  this.disposed = true;
953
- this.pattern?.texture.dispose();
1064
+ this.pattern?.release();
1065
+ this.pattern = null;
954
1066
  this.blank.dispose();
955
1067
  this.backBlank.texture.dispose();
956
1068
  }
@@ -1085,6 +1197,7 @@ var vertexChunk = (
1085
1197
  uniform float uTailBend; // how far the free end bows back
1086
1198
  uniform vec3 uCone; // radius change per unit of y, radius at the top, how far to wrap
1087
1199
  uniform vec2 uFlipRotation;
1200
+ uniform float uStackGap; // how far the sheet stands off the spine, along its own normal
1088
1201
 
1089
1202
  float sheetPileLift(float x) {
1090
1203
  float t = 1.0 - min(x / uPileLift.x, 1.0);
@@ -1170,7 +1283,7 @@ var vertexChunk = (
1170
1283
  }
1171
1284
 
1172
1285
  // spin about the spine
1173
- float z = shape.z + pile.z;
1286
+ float z = shape.z + pile.z + uStackGap;
1174
1287
  return vec3(
1175
1288
  shape.x * uFlipRotation.x - z * uFlipRotation.y,
1176
1289
  shape.y,
@@ -1196,6 +1309,10 @@ var fragmentChunk = (
1196
1309
  function createSingleGeometry(aspect) {
1197
1310
  return new THREE9.PlaneGeometry(1, aspect, 64, Math.round(64 * aspect));
1198
1311
  }
1312
+ function sharedSingleGeometry(shared, aspect) {
1313
+ const key = `single-geometry:${aspect}`;
1314
+ return { geometry: shared.acquire(key, () => createSingleGeometry(aspect)), release: () => shared.release(key) };
1315
+ }
1199
1316
  function createSingleSheet(geometry, front, back, pattern, settings, shading) {
1200
1317
  const uniforms = {
1201
1318
  uPileLift: { value: new THREE9.Vector3(1, 0, 0) },
@@ -1206,11 +1323,13 @@ function createSingleSheet(geometry, front, back, pattern, settings, shading) {
1206
1323
  uCone: { value: new THREE9.Vector3(0, 1, 0) },
1207
1324
  uFlipRotation: { value: new THREE9.Vector2(1, 0) },
1208
1325
  uTrim: { value: new THREE9.Vector3(0, 1, 1) },
1326
+ uStackGap: { value: 0 },
1209
1327
  uFront: { value: front },
1210
1328
  uBack: { value: back },
1211
1329
  uPattern: { value: pattern },
1212
1330
  uSpineContact: shading.spineContact,
1213
- uLightDirection: shading.lightDirection
1331
+ uLightDirection: shading.lightDirection,
1332
+ uSheetOrigin: shading.origin
1214
1333
  };
1215
1334
  const mesh = createSheetMesh(
1216
1335
  geometry,
@@ -1294,6 +1413,7 @@ var SingleEngine = class {
1294
1413
  metrics;
1295
1414
  requestFrame;
1296
1415
  geometry;
1416
+ releaseGeometry;
1297
1417
  sheets;
1298
1418
  count;
1299
1419
  radiusSpacing;
@@ -1301,6 +1421,7 @@ var SingleEngine = class {
1301
1421
  fallDelay;
1302
1422
  fallDuration;
1303
1423
  abort = new AbortController();
1424
+ shading;
1304
1425
  current;
1305
1426
  turned = 0;
1306
1427
  // sum of every sheet's progress, as of the last update
@@ -1321,10 +1442,13 @@ var SingleEngine = class {
1321
1442
  this.input = ctx.input;
1322
1443
  this.metrics = ctx.metrics;
1323
1444
  this.requestFrame = ctx.requestFrame;
1445
+ this.shading = ctx.shading;
1324
1446
  this.count = ctx.textures.length;
1325
1447
  this.maxPosition = this.count;
1326
1448
  this.current = clamp(Math.round(ctx.position), 0, Math.max(this.count - 1, 0));
1327
- this.geometry = createSingleGeometry(ctx.settings.pageAspect);
1449
+ const { geometry, release } = sharedSingleGeometry(ctx.shared, ctx.settings.pageAspect);
1450
+ this.geometry = geometry;
1451
+ this.releaseGeometry = release;
1328
1452
  this.sheets = ctx.textures.map((front, index) => {
1329
1453
  const { mesh, uniforms } = createSingleSheet(this.geometry, front, ctx.blank, ctx.pattern, ctx.settings, ctx.shading);
1330
1454
  this.group.add(mesh);
@@ -1399,11 +1523,16 @@ var SingleEngine = class {
1399
1523
  if (this.press) this.release(new PointerEvent("pointercancel", { pointerId: this.press.id }));
1400
1524
  this.wake();
1401
1525
  }
1526
+ setShadows(on) {
1527
+ this.settings.lighting.shadows = on;
1528
+ for (const sheet of this.sheets) sheet.mesh.castShadow = sheet.mesh.receiveShadow = on;
1529
+ this.wake();
1530
+ }
1402
1531
  dispose() {
1403
1532
  this.cancelHold();
1404
1533
  this.abort.abort();
1405
1534
  for (const sheet of this.sheets) disposeSheetMesh(sheet.mesh);
1406
- this.geometry.dispose();
1535
+ this.releaseGeometry();
1407
1536
  }
1408
1537
  // --- helpers ----------------------------------------------------------------
1409
1538
  randomWobble() {
@@ -1453,6 +1582,10 @@ var SingleEngine = class {
1453
1582
  shutting(turned) {
1454
1583
  return smooth((this.closing(turned) - 0.6) / 0.4);
1455
1584
  }
1585
+ // how far a read page has settled flat under the pile: 0 still on the roll, 1 flat
1586
+ fallOf(sheet, paced, shut) {
1587
+ return Math.max(smooth((paced - (sheet.index + 1) - this.fallDelay) / this.fallDuration), shut);
1588
+ }
1456
1589
  inFlight(sheet) {
1457
1590
  return this.flights.some((f) => f.sheet === sheet);
1458
1591
  }
@@ -1755,15 +1888,19 @@ var SingleEngine = class {
1755
1888
  sheet.uniforms.uTrim.value.set(sheet === top || this.busy(sheet) ? 0 : 1, edge.bottom, edge.top);
1756
1889
  }
1757
1890
  const closing = this.closing(turned) > 0;
1891
+ const paced = this.paced(turned);
1892
+ const shut = this.shutting(turned);
1758
1893
  const pileFrom = Math.max(this.current, top?.index ?? this.current);
1759
1894
  for (const sheet of this.sheets) {
1760
1895
  const i = sheet.index;
1761
1896
  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));
1897
+ const depth = i >= this.current ? i - this.current : this.fallOf(sheet, paced, shut) >= 1 ? this.count - this.current + i : this.current - 1 - i;
1898
+ layerSheet(sheet.mesh, depth, this.count, this.busy(sheet), this.settings.paper.depthBias);
1763
1899
  }
1764
1900
  const read = smooth(this.count > 1 ? this.paced(turned) / (this.count - 1) : 0) * (1 - this.shutting(turned));
1765
1901
  this.group.position.x = BOOK.pushRight * read;
1766
1902
  this.group.position.z = -BOOK.pushBack * read;
1903
+ this.shading.origin.value.copy(this.group.position);
1767
1904
  this.dirty = false;
1768
1905
  return true;
1769
1906
  }
@@ -1790,10 +1927,7 @@ var SingleEngine = class {
1790
1927
  const spineOpen = (ROLL.spineOpenStartDeg + (ROLL.spineOpenEndDeg - ROLL.spineOpenStartDeg) * Math.pow(openFraction, ROLL.spineOpenPower)) * Math.PI / 180;
1791
1928
  const shut = this.shutting(turned);
1792
1929
  const closed = this.closing(turned) >= 1 ? 1 : 0;
1793
- const fall = Math.max(
1794
- smooth((paced - (sheet.index + 1) - this.fallDelay) / this.fallDuration),
1795
- shut
1796
- );
1930
+ const fall = this.fallOf(sheet, paced, shut);
1797
1931
  const spacing = this.radiusSpacing[sheet.index] ?? 0;
1798
1932
  const topR = ROLL.coverTopRadius + (ROLL.backTopRadius - ROLL.coverTopRadius) * spacing;
1799
1933
  const bottomR = ROLL.coverBottomRadius + (ROLL.backBottomRadius - ROLL.coverBottomRadius) * spacing;
@@ -1833,6 +1967,11 @@ var SingleEngine = class {
1833
1967
  topReach = Math.min(topReach, edge.top);
1834
1968
  }
1835
1969
  u.uPileReach.value.set(bottomReach, topReach);
1970
+ const under = P - 1 - sheet.index;
1971
+ const before = Math.max(0, turned - 1 - sheet.index);
1972
+ const around = smooth(T);
1973
+ const offset = fallen ? -(sheet.index + 1) : under + (before - under) * around;
1974
+ u.uStackGap.value = this.settings.paper.sheetGap * (offset + (under - offset) * shut);
1836
1975
  }
1837
1976
  // a turning page: swing round the spine up to the open angle, then roll round the cone
1838
1977
  shapeTurn(sheet, T, spineOpen, radii, tilt) {
@@ -1891,6 +2030,7 @@ var vertexChunk2 = (settings) => (
1891
2030
 
1892
2031
  const float BULGE = ${settings.paper.bulge.toFixed(4)};
1893
2032
  const float WRINKLE = ${(0.13 * settings.paper.wrinkles).toFixed(4)};
2033
+ const bool NESTED = ${settings.paper.depthBias ? "false" : "true"};
1894
2034
 
1895
2035
  uniform float uFlipProgress;
1896
2036
  uniform float uWrinkleSide;
@@ -1900,6 +2040,7 @@ var vertexChunk2 = (settings) => (
1900
2040
  uniform vec2 uFlipRotation;
1901
2041
  uniform float uDirection;
1902
2042
  uniform float uStackLift;
2043
+ uniform float uStackGap;
1903
2044
 
1904
2045
  attribute vec3 aNoise;
1905
2046
 
@@ -1916,8 +2057,11 @@ var vertexChunk2 = (settings) => (
1916
2057
  float y = (uv.y - 0.5) * SHEET_ASPECT;
1917
2058
 
1918
2059
  // 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;
2060
+ // so stacked sheets nest inside each other instead of crossing. Where a wrinkle dips, a
2061
+ // bigger one dips below the sheets under it, which only the depth bias hides; without it
2062
+ // the dips are the same on every sheet.
2063
+ float wrinkle = uWrinkleSide * (mix(noise, 1.0 - noise, uFlipProgress) - 0.5) * smoothstep(0.0, 0.35, x) * WRINKLE;
2064
+ wrinkle *= NESTED && wrinkle < 0.0 ? 1.0 : uStackLift;
1921
2065
  float lift = sheetRestingLift(x) * BULGE * uStackLift;
1922
2066
 
1923
2067
  // bend with constant curvature past the fold line, trailing against the turn
@@ -1935,7 +2079,7 @@ var vertexChunk2 = (settings) => (
1935
2079
  // turn about the spine
1936
2080
  p = vec3(p.x * uFlipRotation.x - p.z * uFlipRotation.y, p.y, p.x * uFlipRotation.y + p.z * uFlipRotation.x);
1937
2081
 
1938
- p.z += lift + uWrinkleSide * wrinkle;
2082
+ p.z += lift + wrinkle + uStackGap;
1939
2083
  return p;
1940
2084
  }
1941
2085
  `
@@ -1968,20 +2112,29 @@ function wrinkleNoise(u, v, aspect) {
1968
2112
  }
1969
2113
  return value;
1970
2114
  }
2115
+ var noiseByAspect = /* @__PURE__ */ new Map();
1971
2116
  function createSpreadGeometry(aspect) {
1972
2117
  const geometry = new THREE11.PlaneGeometry(1, aspect, 64, Math.round(64 * aspect));
1973
- const uv = geometry.attributes.uv;
1974
- const noise = new Float32Array(uv.count * 3);
1975
- for (let i = 0; i < uv.count; i++) {
1976
- const u = uv.getX(i);
1977
- const v = uv.getY(i);
1978
- noise[i * 3] = wrinkleNoise(u, v, aspect);
1979
- noise[i * 3 + 1] = wrinkleNoise(u + NORMAL_EPSILON2, v, aspect);
1980
- noise[i * 3 + 2] = wrinkleNoise(u, v + NORMAL_EPSILON2 / aspect, aspect);
2118
+ let noise = noiseByAspect.get(aspect);
2119
+ if (!noise) {
2120
+ const uv = geometry.attributes.uv;
2121
+ noise = new Float32Array(uv.count * 3);
2122
+ for (let i = 0; i < uv.count; i++) {
2123
+ const u = uv.getX(i);
2124
+ const v = uv.getY(i);
2125
+ noise[i * 3] = wrinkleNoise(u, v, aspect);
2126
+ noise[i * 3 + 1] = wrinkleNoise(u + NORMAL_EPSILON2, v, aspect);
2127
+ noise[i * 3 + 2] = wrinkleNoise(u, v + NORMAL_EPSILON2 / aspect, aspect);
2128
+ }
2129
+ noiseByAspect.set(aspect, noise);
1981
2130
  }
1982
2131
  geometry.setAttribute("aNoise", new THREE11.BufferAttribute(noise, 3));
1983
2132
  return geometry;
1984
2133
  }
2134
+ function sharedSpreadGeometry(shared, aspect) {
2135
+ const key = `spread-geometry:${aspect}`;
2136
+ return { geometry: shared.acquire(key, () => createSpreadGeometry(aspect)), release: () => shared.release(key) };
2137
+ }
1985
2138
  function createSpreadSheet(geometry, front, back, pattern, settings, shading) {
1986
2139
  const uniforms = {
1987
2140
  uFlipProgress: { value: 0 },
@@ -1992,11 +2145,13 @@ function createSpreadSheet(geometry, front, back, pattern, settings, shading) {
1992
2145
  uFlipRotation: { value: new THREE11.Vector2(1, 0) },
1993
2146
  uDirection: { value: 1 },
1994
2147
  uStackLift: { value: 1 },
2148
+ uStackGap: { value: 0 },
1995
2149
  uFront: { value: front },
1996
2150
  uBack: { value: back },
1997
2151
  uPattern: { value: pattern },
1998
2152
  uSpineContact: shading.spineContact,
1999
- uLightDirection: shading.lightDirection
2153
+ uLightDirection: shading.lightDirection,
2154
+ uSheetOrigin: shading.origin
2000
2155
  };
2001
2156
  const mesh = createSheetMesh(
2002
2157
  geometry,
@@ -2037,6 +2192,10 @@ function stackLift(index, count, progress) {
2037
2192
  if (count <= 1) return 1;
2038
2193
  return 0.31 + 0.69 * (index + (count - 1 - 2 * index) * (1 - progress)) / (count - 1);
2039
2194
  }
2195
+ function sheetsUnder(index, count, progress) {
2196
+ const t = clamp2((progress - 0.25) / 0.5, 0, 1);
2197
+ return count - 1 - index + (2 * index - count + 1) * t * t * (3 - 2 * t);
2198
+ }
2040
2199
  var SpreadEngine = class {
2041
2200
  group = new THREE12.Group();
2042
2201
  maxPosition;
@@ -2046,6 +2205,7 @@ var SpreadEngine = class {
2046
2205
  metrics;
2047
2206
  requestFrame;
2048
2207
  geometry;
2208
+ releaseGeometry;
2049
2209
  sheets;
2050
2210
  abort = new AbortController();
2051
2211
  shading;
@@ -2073,7 +2233,9 @@ var SpreadEngine = class {
2073
2233
  const count = Math.ceil(ctx.textures.length / 2);
2074
2234
  this.maxPosition = count;
2075
2235
  this.current = clamp2(Math.round(ctx.position), 0, count);
2076
- this.geometry = createSpreadGeometry(ctx.settings.pageAspect);
2236
+ const { geometry, release } = sharedSpreadGeometry(ctx.shared, ctx.settings.pageAspect);
2237
+ this.geometry = geometry;
2238
+ this.releaseGeometry = release;
2077
2239
  this.sheets = [];
2078
2240
  for (let i = 0; i < count; i++) {
2079
2241
  const front = ctx.textures[2 * i];
@@ -2133,11 +2295,16 @@ var SpreadEngine = class {
2133
2295
  if (this.press) this.release(new PointerEvent("pointercancel", { pointerId: this.press.id }));
2134
2296
  this.wake();
2135
2297
  }
2298
+ setShadows(on) {
2299
+ this.settings.lighting.shadows = on;
2300
+ for (const sheet of this.sheets) sheet.mesh.receiveShadow = on;
2301
+ this.wake();
2302
+ }
2136
2303
  dispose() {
2137
2304
  this.cancelHold();
2138
2305
  this.abort.abort();
2139
2306
  for (const sheet of this.sheets) disposeSheetMesh(sheet.mesh);
2140
- this.geometry.dispose();
2307
+ this.releaseGeometry();
2141
2308
  }
2142
2309
  // --- helpers ----------------------------------------------------------------
2143
2310
  randomWobble() {
@@ -2495,7 +2662,7 @@ var SpreadEngine = class {
2495
2662
  const depth = i >= this.current ? i - this.current : this.current - 1 - i;
2496
2663
  const turning = this.inFlight(i) || this.drag?.index === i;
2497
2664
  sheet.mesh.castShadow = this.settings.lighting.shadows && (depth < 3 || turning);
2498
- layerSheet(sheet.mesh, depth, this.sheets.length, turning);
2665
+ layerSheet(sheet.mesh, depth, this.sheets.length, turning, this.settings.paper.depthBias);
2499
2666
  const lift = Math.abs(sheet.uniforms.uFlipRotation.value.y);
2500
2667
  if (turning && lift > upright) {
2501
2668
  upright = lift;
@@ -2526,6 +2693,7 @@ var SpreadEngine = class {
2526
2693
  u.uWrinkleSide.value = -Math.cos(turn);
2527
2694
  u.uDirection.value = sheet.directionSmooth;
2528
2695
  u.uStackLift.value = sheet.liftBase + sheet.liftSpan * sheet.progress;
2696
+ u.uStackGap.value = this.settings.paper.sheetGap * sheetsUnder(sheet.index, this.sheets.length, sheet.progress);
2529
2697
  u.uBendAngle.value = bend;
2530
2698
  u.uFold.value.set(cos, sin);
2531
2699
  u.uCurl.value.set(curlStart, curlLength);
@@ -2806,7 +2974,8 @@ var LeafThrough = class extends EventTarget {
2806
2974
  scene.add(sun, sun.target);
2807
2975
  const shading = {
2808
2976
  spineContact: { value: new THREE13.Vector3(1, 0, 0) },
2809
- lightDirection: { value: sun.position.clone().sub(sun.target.position).normalize() }
2977
+ lightDirection: { value: sun.position.clone().sub(sun.target.position).normalize() },
2978
+ origin: { value: new THREE13.Vector3() }
2810
2979
  };
2811
2980
  let floor2 = null;
2812
2981
  if (layout === "spread" && shadows && settings.lighting.floorShadow > 0) {
@@ -2828,6 +2997,7 @@ var LeafThrough = class extends EventTarget {
2828
2997
  textures,
2829
2998
  blank: layout === "single" ? this.paper.back(settings.paper.backColor) : blank,
2830
2999
  pattern: this.paper.grain(settings, renderer),
3000
+ shared: sharedResources(renderer),
2831
3001
  position: this.pageToPosition(page, layout),
2832
3002
  metrics: this.metrics,
2833
3003
  shading,
@@ -2880,18 +3050,18 @@ var LeafThrough = class extends EventTarget {
2880
3050
  const right = Math.min(placed.view.x + placed.view.width, (documentWidth - pageLeft) / scaleX);
2881
3051
  const top = Math.max(placed.view.y, -pageTop / scaleY);
2882
3052
  const bottom = placed.view.y + placed.view.height;
2883
- const canvas = { x: left, y: top, width: Math.max(1, right - left), height: Math.max(1, bottom - top) };
3053
+ const canvas2 = { x: left, y: top, width: Math.max(1, right - left), height: Math.max(1, bottom - top) };
2884
3054
  const px = (n) => `${Math.round(n * 100) / 100}px`;
2885
3055
  Object.assign(this.canvas.style, {
2886
- left: px(canvas.x),
2887
- top: px(canvas.y),
2888
- width: px(canvas.width),
2889
- height: px(canvas.height)
3056
+ left: px(canvas2.x),
3057
+ top: px(canvas2.y),
3058
+ width: px(canvas2.width),
3059
+ height: px(canvas2.height)
2890
3060
  });
2891
3061
  const maxRatio = Math.min(devicePixelRatio, settings.maxPixelRatio);
2892
- const budget = Math.sqrt(15e6 / Math.max(1, canvas.width * canvas.height));
3062
+ const budget = Math.sqrt(15e6 / Math.max(1, canvas2.width * canvas2.height));
2893
3063
  this.renderer.setPixelRatio(Math.min(maxRatio, budget));
2894
- this.renderer.setSize(Math.max(1, Math.round(canvas.width)), Math.max(1, Math.round(canvas.height)), false);
3064
+ this.renderer.setSize(Math.max(1, Math.round(canvas2.width)), Math.max(1, Math.round(canvas2.height)), false);
2895
3065
  const { eye, view, distance } = framing;
2896
3066
  const halfWidth = Math.max(eye.x - view.x, view.x + view.width - eye.x);
2897
3067
  const halfHeight = Math.max(eye.y - view.y, view.y + view.height - eye.y);
@@ -2903,10 +3073,10 @@ var LeafThrough = class extends EventTarget {
2903
3073
  camera.setViewOffset(
2904
3074
  2 * halfWidth * scale,
2905
3075
  2 * halfHeight * scale,
2906
- canvas.x - frustumLeft,
2907
- canvas.y - frustumTop,
2908
- canvas.width,
2909
- canvas.height
3076
+ canvas2.x - frustumLeft,
3077
+ canvas2.y - frustumTop,
3078
+ canvas2.width,
3079
+ canvas2.height
2910
3080
  );
2911
3081
  camera.updateProjectionMatrix();
2912
3082
  }
@@ -3074,10 +3244,10 @@ var LeafThrough = class extends EventTarget {
3074
3244
  this.syncContent(view);
3075
3245
  }
3076
3246
  };
3077
- function createRenderer(canvas) {
3247
+ function createRenderer(canvas2) {
3078
3248
  let renderer;
3079
3249
  try {
3080
- renderer = new THREE13.WebGLRenderer({ canvas, antialias: true, alpha: true });
3250
+ renderer = new THREE13.WebGLRenderer({ canvas: canvas2, antialias: true, alpha: true });
3081
3251
  } catch {
3082
3252
  return null;
3083
3253
  }
@@ -3097,6 +3267,7 @@ export {
3097
3267
  LIVE_OPTIONS,
3098
3268
  preloadRadius,
3099
3269
  snapshot,
3270
+ deepEqual,
3100
3271
  changedOptions,
3101
3272
  sourceKey,
3102
3273
  floorPoint,
@@ -3109,9 +3280,10 @@ export {
3109
3280
  Riffle,
3110
3281
  bindKeyboard,
3111
3282
  PageLoader,
3283
+ sharedResources,
3112
3284
  PaperTextures,
3113
3285
  SingleEngine,
3114
3286
  SpreadEngine,
3115
3287
  LeafThrough
3116
3288
  };
3117
- //# sourceMappingURL=chunk-GYSQO7QZ.js.map
3289
+ //# sourceMappingURL=chunk-KCIOFDSW.js.map