leafthrough 0.1.3 → 0.1.5

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.
@@ -32,7 +32,8 @@ function resolveSettings(options, layout, prefersReducedMotion) {
32
32
  metalness: paper.metalness ?? 0.17,
33
33
  backColor: paper.backColor ?? "#fcfcf9",
34
34
  wrinkles: paper.wrinkles ?? 1,
35
- bulge: paper.bulge ?? 1
35
+ bulge: paper.bulge ?? 1,
36
+ cutout: paper.cutout ?? false
36
37
  },
37
38
  lighting: {
38
39
  ambient: {
@@ -69,9 +70,46 @@ function resolveSettings(options, layout, prefersReducedMotion) {
69
70
  maxPixelRatio: options.maxPixelRatio ?? 3
70
71
  };
71
72
  }
73
+ var LIVE_OPTIONS = /* @__PURE__ */ new Set([
74
+ "onPageChange",
75
+ "onReady",
76
+ "onPageLoad",
77
+ "onError",
78
+ "formatPageLabel",
79
+ "label",
80
+ "startPage",
81
+ "preload",
82
+ "pages"
83
+ ]);
84
+ function preloadRadius(preload) {
85
+ const radius = preload ?? 6;
86
+ return Number.isNaN(radius) ? 6 : Math.max(0, radius);
87
+ }
88
+ function snapshot(value) {
89
+ if (typeof value !== "object" || value === null) return value;
90
+ if (Array.isArray(value)) return value.map(snapshot);
91
+ if (Object.getPrototypeOf(value) !== Object.prototype) return value;
92
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item)]));
93
+ }
94
+ function deepEqual(a, b) {
95
+ if (Object.is(a, b)) return true;
96
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
97
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
98
+ const protoA = Object.getPrototypeOf(a);
99
+ if (protoA !== Object.prototype && protoA !== Array.prototype) return false;
100
+ if (protoA !== Object.getPrototypeOf(b)) return false;
101
+ const keysA = Object.keys(a);
102
+ const keysB = Object.keys(b);
103
+ if (keysA.length !== keysB.length) return false;
104
+ return keysA.every((key) => deepEqual(a[key], b[key]));
105
+ }
106
+ function changedOptions(previous, next) {
107
+ const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(next)]);
108
+ return [...keys].filter((key) => !deepEqual(previous[key], next[key]));
109
+ }
72
110
 
73
111
  // src/LeafThrough.ts
74
- import * as THREE10 from "three";
112
+ import * as THREE13 from "three";
75
113
 
76
114
  // src/content.ts
77
115
  import * as THREE from "three";
@@ -226,6 +264,28 @@ function describe(element, alt, style) {
226
264
  return element;
227
265
  }
228
266
 
267
+ // src/engine.ts
268
+ import * as THREE2 from "three";
269
+ var floor = new THREE2.Plane(new THREE2.Vector3(0, 0, 1), 0);
270
+ var inverse = new THREE2.Matrix4();
271
+ var localRay = new THREE2.Ray();
272
+ function floorPoint(object, ray, target) {
273
+ object.updateWorldMatrix(true, false);
274
+ localRay.copy(ray).applyMatrix4(inverse.copy(object.matrixWorld).invert());
275
+ return localRay.intersectPlane(floor, target);
276
+ }
277
+ var raycaster = new THREE2.Raycaster();
278
+ var ndc = new THREE2.Vector2();
279
+ function rayThrough(camera, element, clientX, clientY, ray) {
280
+ const rect = element.getBoundingClientRect();
281
+ if (!rect.width || !rect.height) return false;
282
+ ndc.set((clientX - rect.left) / rect.width * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);
283
+ camera.updateWorldMatrix(true, false);
284
+ raycaster.setFromCamera(ndc, camera);
285
+ ray.copy(raycaster.ray);
286
+ return true;
287
+ }
288
+
229
289
  // src/framing.ts
230
290
  var REFERENCE_ASPECT = 1.377;
231
291
  var SPREAD_OVERSCAN = 0.27975;
@@ -286,12 +346,276 @@ function fit(framing, width, height, bleed) {
286
346
  return { scale, view: toPx(framing.view), box: toPx(framing.box), height: boxHeight };
287
347
  }
288
348
 
349
+ // src/navigation.ts
350
+ import * as THREE3 from "three";
351
+ function pageToPosition(page, layout, count) {
352
+ const p = THREE3.MathUtils.clamp(Math.round(page) || 0, 0, Math.max(count - 1, 0));
353
+ return layout === "spread" ? Math.ceil(p / 2) : p;
354
+ }
355
+ function positionToPage(position, layout) {
356
+ return layout === "spread" ? Math.max(0, 2 * position - 1) : position;
357
+ }
358
+ function pagesAt(position, layout, count) {
359
+ const candidates = layout === "spread" ? [2 * position - 1, 2 * position] : [position];
360
+ return candidates.filter((p) => p >= 0 && p < count);
361
+ }
362
+ function pageDetail(position, layout, count) {
363
+ const visiblePages = pagesAt(position, layout, count);
364
+ const page = visiblePages.length ? visiblePages[0] : Math.max(0, Math.min(positionToPage(position, layout), count - 1));
365
+ return { page, visiblePages, pageCount: count, layout };
366
+ }
367
+ var Riffle = class {
368
+ state = null;
369
+ get active() {
370
+ return this.state !== null;
371
+ }
372
+ start(target, speed) {
373
+ this.state = { target, interval: 140 / speed, last: -Infinity };
374
+ }
375
+ stop() {
376
+ this.state = null;
377
+ }
378
+ step(now, engine) {
379
+ const riffle = this.state;
380
+ if (!riffle || now - riffle.last < riffle.interval) return;
381
+ if (engine.position === riffle.target) {
382
+ this.state = null;
383
+ return;
384
+ }
385
+ riffle.last = now;
386
+ if (!engine.flip(engine.position < riffle.target)) this.state = null;
387
+ }
388
+ };
389
+ function bindKeyboard(element, mode, actions, signal) {
390
+ const handler = (e) => {
391
+ const current = mode();
392
+ if (!current || e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
393
+ if (current === "global") {
394
+ const target = e.target;
395
+ if (target?.closest?.('input, textarea, select, [contenteditable=""], [contenteditable="true"]')) return;
396
+ } else if (e.currentTarget !== element) return;
397
+ const keys = {
398
+ ArrowRight: actions.next,
399
+ PageDown: actions.next,
400
+ ArrowLeft: actions.prev,
401
+ PageUp: actions.prev,
402
+ Home: actions.first,
403
+ End: actions.last
404
+ };
405
+ const action = keys[e.key];
406
+ if (!action) return;
407
+ e.preventDefault();
408
+ action();
409
+ };
410
+ element.addEventListener("keydown", handler, { signal });
411
+ window.addEventListener(
412
+ "keydown",
413
+ (e) => {
414
+ if (mode() === "global" && !element.contains(e.target)) handler(e);
415
+ },
416
+ { signal }
417
+ );
418
+ }
419
+
420
+ // src/paper/paperTextures.ts
421
+ import * as THREE7 from "three";
422
+
423
+ // src/textures.ts
424
+ import * as THREE4 from "three";
425
+ var CONCURRENCY = 4;
426
+ var PageLoader = class {
427
+ constructor(sources, renderer, radius, events) {
428
+ this.sources = sources;
429
+ this.renderer = renderer;
430
+ this.radius = radius;
431
+ this.events = events;
432
+ this.textures = new Array(sources.length);
433
+ }
434
+ sources;
435
+ renderer;
436
+ radius;
437
+ events;
438
+ /** Loaded textures by page; missing pages are still loading, unloaded or failed. */
439
+ textures;
440
+ owned = /* @__PURE__ */ new Set();
441
+ inFlight = /* @__PURE__ */ new Map();
442
+ failed = /* @__PURE__ */ new Set();
443
+ // `first` pages load before the rest; the rest go nearest first
444
+ first = [];
445
+ // the pages in view, and the one being read, which loads first among them
446
+ from = 0;
447
+ to = 0;
448
+ around = 0;
449
+ // whether the magazine loops, putting the cover right after the last page
450
+ wrap = false;
451
+ waiting = /* @__PURE__ */ new Set();
452
+ settleFirst = () => {
453
+ };
454
+ disposed = false;
455
+ /**
456
+ * Starts loading around the visible pages `from`–`to`. Resolves once the `first` pages
457
+ * within reach are in (or failed).
458
+ */
459
+ start(from, to, first) {
460
+ this.from = from;
461
+ this.to = to;
462
+ this.first = first.filter((i) => i >= 0 && i < this.sources.length && this.distance(i) <= this.radius);
463
+ this.waiting = new Set(this.first);
464
+ const ready = new Promise((resolve) => this.settleFirst = resolve);
465
+ this.focus(from, to);
466
+ if (!this.waiting.size) this.settleFirst();
467
+ return ready;
468
+ }
469
+ /**
470
+ * Follows the reader: loads around the pages in view (`from`–`to`), the page being read
471
+ * (`around`) first, and unloads pages left far behind.
472
+ */
473
+ focus(from, to = from, around = from) {
474
+ if (this.disposed) return;
475
+ this.from = from;
476
+ this.to = to;
477
+ this.around = around;
478
+ const keep = 2 * this.radius;
479
+ for (const [i, request] of this.inFlight) {
480
+ if (this.distance(i) <= keep) continue;
481
+ request.abort();
482
+ this.inFlight.delete(i);
483
+ }
484
+ this.textures.forEach((texture, i) => {
485
+ if (!texture || this.distance(i) <= keep || !this.owned.has(texture)) return;
486
+ this.textures[i] = void 0;
487
+ this.events.onUnload(i);
488
+ this.owned.delete(texture);
489
+ texture.dispose();
490
+ });
491
+ this.pump();
492
+ }
493
+ setWrap(wrap) {
494
+ if (wrap === this.wrap) return;
495
+ this.wrap = wrap;
496
+ this.focus(this.from, this.to, this.around);
497
+ }
498
+ setRadius(radius) {
499
+ this.radius = radius;
500
+ this.focus(this.from, this.to, this.around);
501
+ }
502
+ /** Loads the pages that failed again. */
503
+ retry() {
504
+ this.failed.clear();
505
+ this.pump();
506
+ }
507
+ dispose() {
508
+ this.disposed = true;
509
+ for (const request of this.inFlight.values()) request.abort();
510
+ for (const texture of this.owned) texture.dispose();
511
+ this.owned.clear();
512
+ }
513
+ // pages away from the visible ones
514
+ distance(page) {
515
+ const straight = page < this.from ? this.from - page : page > this.to ? page - this.to : 0;
516
+ if (!this.wrap) return straight;
517
+ const count = this.sources.length;
518
+ return Math.min(straight, page + count - this.to, this.from + count - page);
519
+ }
520
+ // starts the most wanted pages while there's room
521
+ pump() {
522
+ 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;
524
+ const queue = [
525
+ ...this.first.filter((i) => this.waiting.has(i)),
526
+ ...this.sources.map((_, i) => i).sort((a, b) => this.distance(a) - this.distance(b) || Math.abs(a - this.around) - Math.abs(b - this.around))
527
+ ];
528
+ for (const page of queue) {
529
+ if (this.inFlight.size >= CONCURRENCY) return;
530
+ if (wanted(page)) this.load(page);
531
+ }
532
+ }
533
+ load(page) {
534
+ const request = new AbortController();
535
+ this.inFlight.set(page, request);
536
+ this.createTexture(this.sources[page], request.signal).then((texture) => {
537
+ if (this.disposed || request.signal.aborted) {
538
+ if (this.owned.delete(texture)) texture.dispose();
539
+ return;
540
+ }
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);
548
+ }).catch((error) => {
549
+ if (this.disposed || request.signal.aborted) return;
550
+ this.failed.add(page);
551
+ this.events.onError(page, error);
552
+ }).finally(() => {
553
+ if (this.inFlight.get(page) === request) this.inFlight.delete(page);
554
+ if (this.waiting.delete(page) && !this.waiting.size) this.settleFirst();
555
+ this.pump();
556
+ });
557
+ }
558
+ async createTexture(source, signal) {
559
+ if (source instanceof THREE4.Texture) return source;
560
+ const own = (texture) => (this.owned.add(texture), texture);
561
+ if (typeof source === "string") return own(new THREE4.Texture(await loadImage(source, signal)));
562
+ if (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) return own(new THREE4.CanvasTexture(source));
563
+ if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
564
+ if (!source.complete || !source.naturalWidth) await source.decode();
565
+ return own(new THREE4.Texture(source));
566
+ }
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));
572
+ }
573
+ };
574
+ function loadImage(url, signal) {
575
+ return new Promise((resolve, reject) => {
576
+ const img = new Image();
577
+ img.crossOrigin = "anonymous";
578
+ img.decoding = "async";
579
+ const onAbort = () => {
580
+ done();
581
+ img.src = "";
582
+ reject(new DOMException("Page no longer needed", "AbortError"));
583
+ };
584
+ const done = () => {
585
+ img.onload = img.onerror = null;
586
+ signal?.removeEventListener("abort", onAbort);
587
+ };
588
+ signal?.addEventListener("abort", onAbort, { once: true });
589
+ img.onload = () => {
590
+ done();
591
+ img.decode().then(() => resolve(img), () => resolve(img));
592
+ };
593
+ img.onerror = () => {
594
+ done();
595
+ reject(new Error(`Failed to load page image: ${url}`));
596
+ };
597
+ img.src = url;
598
+ });
599
+ }
600
+ async function loadGrain(source) {
601
+ let texture;
602
+ if (typeof source === "string") texture = new THREE4.Texture(await loadImage(source));
603
+ else if (source instanceof HTMLCanvasElement) texture = new THREE4.CanvasTexture(source);
604
+ else {
605
+ if (!source.complete || !source.naturalWidth) await source.decode();
606
+ texture = new THREE4.Texture(source);
607
+ }
608
+ texture.wrapS = texture.wrapT = THREE4.RepeatWrapping;
609
+ texture.needsUpdate = true;
610
+ return texture;
611
+ }
612
+
289
613
  // src/paper/paperMaterial.ts
290
- import * as THREE2 from "three";
614
+ import * as THREE5 from "three";
291
615
  var glslFloat = (n) => Number.isInteger(n) ? `${n}.0` : `${n}`;
292
616
  function createPaperMaterials(uniforms, shader, settings) {
293
617
  const { paper, pageAspect } = settings;
294
- const blank = new THREE2.Color(paper.backColor);
618
+ const blank = new THREE5.Color(paper.backColor);
295
619
  const constants = (
296
620
  /* glsl */
297
621
  `
@@ -308,8 +632,8 @@ function createPaperMaterials(uniforms, shader, settings) {
308
632
  `
309
633
  );
310
634
  const noiseAt = (component) => shader.noise ? `aNoise.${component}` : "0.0";
311
- const material = new THREE2.MeshStandardMaterial({
312
- side: THREE2.DoubleSide,
635
+ const material = new THREE5.MeshStandardMaterial({
636
+ side: THREE5.DoubleSide,
313
637
  metalness: paper.metalness,
314
638
  roughness: paper.roughness
315
639
  });
@@ -372,10 +696,15 @@ function createPaperMaterials(uniforms, shader, settings) {
372
696
  "#include <map_fragment>",
373
697
  `vec4 frontColor = texture2D(uFront, vSheetUv);
374
698
  vec4 backColor = texture2D(uBack, vec2(1.0 - vSheetUv.x, vSheetUv.y));
375
- // Transparent artwork is unprinted paper, not a hole in the sheet. Page images are
699
+ ${paper.cutout ? `// Cut to the front page: no paper where it's transparent. The edge that's left
700
+ // keeps its own colour (page images are premultiplied, so divide the alpha out) rather
701
+ // than fading to paper, which would leave a pale fringe.
702
+ if (frontColor.a < 0.5) discard;
703
+ frontColor = vec4(frontColor.rgb / frontColor.a, 1.0);
704
+ backColor = backColor.a > 0.01 ? vec4(backColor.rgb / backColor.a, 1.0) : vec4(BLANK_PAPER, 1.0);` : `// Transparent artwork is unprinted paper, not a hole in the sheet. Page images are
376
705
  // premultiplied, so this is "over" compositing onto the paper colour.
377
706
  frontColor = vec4(frontColor.rgb + BLANK_PAPER * (1.0 - frontColor.a), 1.0);
378
- backColor = vec4(backColor.rgb + BLANK_PAPER * (1.0 - backColor.a), 1.0);
707
+ backColor = vec4(backColor.rgb + BLANK_PAPER * (1.0 - backColor.a), 1.0);`}
379
708
  vec4 faceColor = gl_FrontFacing ? frontColor : backColor;
380
709
  vec4 otherColor = gl_FrontFacing ? backColor : frontColor;
381
710
  // thin paper lets some of the other side through
@@ -383,7 +712,7 @@ function createPaperMaterials(uniforms, shader, settings) {
383
712
  sheetColor.rgb *= mix(vec3(1.0), otherColor.rgb, SHOW_THROUGH);
384
713
  diffuseColor *= sheetColor;
385
714
  float inkAmount = 1.0 - dot(faceColor.rgb, vec3(0.299, 0.587, 0.114));`
386
- ).replace("#include <lights_fragment_begin>", withContactShadow(THREE2.ShaderChunk.lights_fragment_begin)).replace(
715
+ ).replace("#include <lights_fragment_begin>", withContactShadow(THREE5.ShaderChunk.lights_fragment_begin)).replace(
387
716
  "#include <roughnessmap_fragment>",
388
717
  `#include <roughnessmap_fragment>
389
718
  // ink sits glossier than bare paper; fibres are rougher
@@ -397,19 +726,38 @@ function createPaperMaterials(uniforms, shader, settings) {
397
726
  gl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(0.0), pattern * PATTERN_DARKEN);`
398
727
  );
399
728
  };
400
- const depthMaterial = new THREE2.MeshDepthMaterial({
401
- depthPacking: THREE2.RGBADepthPacking,
402
- side: THREE2.DoubleSide
729
+ const depthMaterial = new THREE5.MeshDepthMaterial({
730
+ depthPacking: THREE5.RGBADepthPacking,
731
+ side: THREE5.DoubleSide
403
732
  });
404
- depthMaterial.onBeforeCompile = (program) => {
733
+ const distanceMaterial = new THREE5.MeshDistanceMaterial({ side: THREE5.DoubleSide });
734
+ depthMaterial.onBeforeCompile = distanceMaterial.onBeforeCompile = (program) => {
405
735
  Object.assign(program.uniforms, uniforms);
406
- program.vertexShader = constants + shader.vertexChunk + program.vertexShader;
736
+ program.vertexShader = constants + shader.vertexChunk + "varying vec2 vSheetUv;\nvarying vec3 vSheetPosition;\n" + program.vertexShader;
407
737
  program.vertexShader = program.vertexShader.replace(
408
738
  "#include <begin_vertex>",
409
- `vec3 transformed = sheetPosition(uv, ${noiseAt("x")}) - vec3(SHADOW_OFFSET_X, 0.0, 0.0);`
739
+ `vSheetUv = uv;
740
+ vSheetPosition = sheetPosition(uv, ${noiseAt("x")});
741
+ vec3 transformed = vSheetPosition - vec3(SHADOW_OFFSET_X, 0.0, 0.0);`
742
+ );
743
+ program.fragmentShader = constants + `
744
+ uniform sampler2D uFront;
745
+ varying vec2 vSheetUv;
746
+ varying vec3 vSheetPosition;
747
+ ${shader.fragmentChunk ?? ""}
748
+ ` + program.fragmentShader;
749
+ program.fragmentShader = program.fragmentShader.replace(
750
+ "#include <clipping_planes_fragment>",
751
+ `#include <clipping_planes_fragment>
752
+ ${paper.cutout ? "if (texture2D(uFront, vSheetUv).a < 0.5) discard;" : ""}
753
+ ${shader.fragmentChunk ? "if (sheetHidden(vSheetPosition)) discard;" : ""}`
410
754
  );
411
755
  };
412
- return { material, depthMaterial };
756
+ const cacheKey = constants + shader.vertexChunk + (shader.fragmentChunk ?? "") + (paper.cutout ? "cutout" : "");
757
+ material.customProgramCacheKey = () => cacheKey;
758
+ depthMaterial.customProgramCacheKey = () => cacheKey;
759
+ distanceMaterial.customProgramCacheKey = () => cacheKey;
760
+ return { material, depthMaterial, distanceMaterial };
413
761
  }
414
762
  var DIRECTIONAL_SHADOW = "getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] )";
415
763
  function withContactShadow(chunk) {
@@ -420,9 +768,10 @@ function withContactShadow(chunk) {
420
768
  );
421
769
  }
422
770
  function createSheetMesh(geometry, uniforms, shader, settings) {
423
- const { material, depthMaterial } = createPaperMaterials(uniforms, shader, settings);
424
- const mesh = new THREE2.Mesh(geometry, material);
771
+ const { material, depthMaterial, distanceMaterial } = createPaperMaterials(uniforms, shader, settings);
772
+ const mesh = new THREE5.Mesh(geometry, material);
425
773
  mesh.customDepthMaterial = depthMaterial;
774
+ mesh.customDistanceMaterial = distanceMaterial;
426
775
  mesh.castShadow = settings.lighting.shadows;
427
776
  mesh.receiveShadow = settings.lighting.shadows;
428
777
  mesh.frustumCulled = false;
@@ -438,22 +787,23 @@ function layerSheet(mesh, depth, count, moving) {
438
787
  function disposeSheetMesh(mesh) {
439
788
  mesh.material.dispose();
440
789
  mesh.customDepthMaterial?.dispose();
790
+ mesh.customDistanceMaterial?.dispose();
441
791
  }
442
792
  function solidTexture(color) {
443
- const c = new THREE2.Color(color);
793
+ const c = new THREE5.Color(color);
444
794
  const srgb = c.clone().convertLinearToSRGB();
445
- const texture = new THREE2.DataTexture(
795
+ const texture = new THREE5.DataTexture(
446
796
  new Uint8Array([Math.round(srgb.r * 255), Math.round(srgb.g * 255), Math.round(srgb.b * 255), 255]),
447
797
  1,
448
798
  1
449
799
  );
450
- texture.colorSpace = THREE2.SRGBColorSpace;
800
+ texture.colorSpace = THREE5.SRGBColorSpace;
451
801
  texture.needsUpdate = true;
452
802
  return texture;
453
803
  }
454
804
 
455
805
  // src/paper/paperPattern.ts
456
- import * as THREE3 from "three";
806
+ import * as THREE6 from "three";
457
807
  var SIZE = 384;
458
808
  var KERNEL = [
459
809
  [1, 0.108, -0.049, 0.022],
@@ -546,19 +896,76 @@ function createPaperPattern(renderer) {
546
896
  image.data[o + 3] = 255;
547
897
  });
548
898
  ctx.putImageData(image, 0, 0);
549
- const texture = new THREE3.CanvasTexture(canvas);
550
- texture.wrapS = texture.wrapT = THREE3.RepeatWrapping;
899
+ const texture = new THREE6.CanvasTexture(canvas);
900
+ texture.wrapS = texture.wrapT = THREE6.RepeatWrapping;
551
901
  texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
552
902
  return texture;
553
903
  }
554
904
 
555
- // src/plain/PlainEngine.ts
556
- import * as THREE4 from "three";
557
- var SWIPE_PX = 30;
558
- var PlainEngine = class {
559
- group = new THREE4.Group();
560
- maxPosition;
561
- spread;
905
+ // src/paper/paperTextures.ts
906
+ var PaperTextures = class {
907
+ constructor(backColor, events) {
908
+ this.events = events;
909
+ this.backBlank = { texture: solidTexture(backColor), color: colorKey(backColor) };
910
+ }
911
+ events;
912
+ /** Plain paper, for missing pages and the spread's unprinted sides. */
913
+ blank = solidTexture("#fcfcf9");
914
+ backBlank;
915
+ pattern = null;
916
+ disposed = false;
917
+ /** The blank backs of single-layout pages, in `paper.backColor`. */
918
+ back(color) {
919
+ const key = colorKey(color);
920
+ if (key !== this.backBlank.color) {
921
+ this.backBlank.texture.dispose();
922
+ this.backBlank = { texture: solidTexture(color), color: key };
923
+ }
924
+ return this.backBlank.texture;
925
+ }
926
+ grain(settings, renderer) {
927
+ const grain = settings.paper.grain;
928
+ const key = grain === true || grain === void 0 ? "builtin" : grain === false ? "none" : grain;
929
+ 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");
936
+ loadGrain(key).then(
937
+ (loaded) => {
938
+ if (this.pattern?.key !== key || this.disposed) return loaded.dispose();
939
+ this.pattern.texture.dispose();
940
+ this.pattern.texture = loaded;
941
+ this.events.onGrain();
942
+ },
943
+ (error) => {
944
+ if (this.pattern?.key === key && !this.disposed) this.events.onError(error);
945
+ }
946
+ );
947
+ }
948
+ this.pattern = { texture, key };
949
+ return texture;
950
+ }
951
+ dispose() {
952
+ this.disposed = true;
953
+ this.pattern?.texture.dispose();
954
+ this.blank.dispose();
955
+ this.backBlank.texture.dispose();
956
+ }
957
+ };
958
+ function colorKey(color) {
959
+ return new THREE7.Color(color).getHexString();
960
+ }
961
+
962
+ // src/plain/PlainEngine.ts
963
+ import * as THREE8 from "three";
964
+ var SWIPE_PX = 30;
965
+ var PlainEngine = class {
966
+ group = new THREE8.Group();
967
+ maxPosition;
968
+ spread;
562
969
  settings;
563
970
  hitArea;
564
971
  abort = new AbortController();
@@ -663,10 +1070,10 @@ var PlainEngine = class {
663
1070
  };
664
1071
 
665
1072
  // src/single/SingleEngine.ts
666
- import * as THREE6 from "three";
1073
+ import * as THREE10 from "three";
667
1074
 
668
1075
  // src/single/singleSheet.ts
669
- import * as THREE5 from "three";
1076
+ import * as THREE9 from "three";
670
1077
  var NORMAL_EPSILON = 0.02;
671
1078
  var vertexChunk = (
672
1079
  /* glsl */
@@ -787,18 +1194,18 @@ var fragmentChunk = (
787
1194
  `
788
1195
  );
789
1196
  function createSingleGeometry(aspect) {
790
- return new THREE5.PlaneGeometry(1, aspect, 64, Math.round(64 * aspect));
1197
+ return new THREE9.PlaneGeometry(1, aspect, 64, Math.round(64 * aspect));
791
1198
  }
792
1199
  function createSingleSheet(geometry, front, back, pattern, settings, shading) {
793
1200
  const uniforms = {
794
- uPileLift: { value: new THREE5.Vector3(1, 0, 0) },
795
- uPileReach: { value: new THREE5.Vector2(1, 1) },
1201
+ uPileLift: { value: new THREE9.Vector3(1, 0, 0) },
1202
+ uPileReach: { value: new THREE9.Vector2(1, 1) },
796
1203
  uConeTaper: { value: 0 },
797
- uTailCurl: { value: new THREE5.Vector4(1, 0, 1, 1) },
1204
+ uTailCurl: { value: new THREE9.Vector4(1, 0, 1, 1) },
798
1205
  uTailBend: { value: 0 },
799
- uCone: { value: new THREE5.Vector3(0, 1, 0) },
800
- uFlipRotation: { value: new THREE5.Vector2(1, 0) },
801
- uTrim: { value: new THREE5.Vector3(0, 1, 1) },
1206
+ uCone: { value: new THREE9.Vector3(0, 1, 0) },
1207
+ uFlipRotation: { value: new THREE9.Vector2(1, 0) },
1208
+ uTrim: { value: new THREE9.Vector3(0, 1, 1) },
802
1209
  uFront: { value: front },
803
1210
  uBack: { value: back },
804
1211
  uPattern: { value: pattern },
@@ -867,7 +1274,7 @@ var PILE = {
867
1274
  var BOOK = { pushBack: 0.05, pushRight: 0.02 };
868
1275
  var CATCH_UP = { clump: 1.6, seed: 41 };
869
1276
  var TAU = Math.PI * 2;
870
- var clamp = THREE6.MathUtils.clamp;
1277
+ var clamp = THREE10.MathUtils.clamp;
871
1278
  var smooth = (x) => {
872
1279
  const t = clamp(x, 0, 1);
873
1280
  return t * t * (3 - 2 * t);
@@ -877,16 +1284,15 @@ var hash = (a, b) => {
877
1284
  return s - Math.floor(s);
878
1285
  };
879
1286
  var SingleEngine = class {
880
- group = new THREE6.Group();
1287
+ group = new THREE10.Group();
881
1288
  // the book, nudged back and right as you read
882
1289
  maxPosition;
883
1290
  settings;
884
1291
  halfHeight;
885
1292
  liftHeight;
886
- camera;
887
- canvas;
888
- hitArea;
1293
+ input;
889
1294
  metrics;
1295
+ requestFrame;
890
1296
  geometry;
891
1297
  sheets;
892
1298
  count;
@@ -906,19 +1312,15 @@ var SingleEngine = class {
906
1312
  touch = null;
907
1313
  dirty = true;
908
1314
  last = 0;
909
- raycaster = new THREE6.Raycaster();
910
- ndc = new THREE6.Vector2();
911
- floor = new THREE6.Plane(new THREE6.Vector3(0, 0, 1), 0);
912
- hit = new THREE6.Vector3();
913
- spine = new THREE6.Vector3();
1315
+ ray = new THREE10.Ray();
1316
+ hit = new THREE10.Vector3();
914
1317
  constructor(ctx) {
915
1318
  this.settings = ctx.settings;
916
1319
  this.halfHeight = ctx.settings.pageAspect / 2;
917
1320
  this.liftHeight = PILE.liftHeight * ctx.settings.paper.bulge;
918
- this.camera = ctx.camera;
919
- this.canvas = ctx.canvas;
920
- this.hitArea = ctx.hitArea;
1321
+ this.input = ctx.input;
921
1322
  this.metrics = ctx.metrics;
1323
+ this.requestFrame = ctx.requestFrame;
922
1324
  this.count = ctx.textures.length;
923
1325
  this.maxPosition = this.count;
924
1326
  this.current = clamp(Math.round(ctx.position), 0, Math.max(this.count - 1, 0));
@@ -989,7 +1391,13 @@ var SingleEngine = class {
989
1391
  const sheet = this.sheets[page];
990
1392
  if (!sheet) return;
991
1393
  sheet.uniforms.uFront.value = texture;
992
- this.dirty = true;
1394
+ this.wake();
1395
+ }
1396
+ cancelInput() {
1397
+ this.cancelHold();
1398
+ this.touch = null;
1399
+ if (this.press) this.release(new PointerEvent("pointercancel", { pointerId: this.press.id }));
1400
+ this.wake();
993
1401
  }
994
1402
  dispose() {
995
1403
  this.cancelHold();
@@ -1001,13 +1409,18 @@ var SingleEngine = class {
1001
1409
  randomWobble() {
1002
1410
  if (!this.settings.motion.jitter) return { tiltDeg: 0, flipTime: 1 };
1003
1411
  return {
1004
- tiltDeg: THREE6.MathUtils.lerp(-WOBBLE.tiltDeg, WOBBLE.tiltDeg, Math.random()),
1005
- flipTime: THREE6.MathUtils.lerp(WOBBLE.flipTimeMin, WOBBLE.flipTimeMax, Math.random())
1412
+ tiltDeg: THREE10.MathUtils.lerp(-WOBBLE.tiltDeg, WOBBLE.tiltDeg, Math.random()),
1413
+ flipTime: THREE10.MathUtils.lerp(WOBBLE.flipTimeMin, WOBBLE.flipTimeMax, Math.random())
1006
1414
  };
1007
1415
  }
1008
1416
  seconds(time) {
1009
1417
  return time / this.settings.motion.speed;
1010
1418
  }
1419
+ // something changed between frames
1420
+ wake() {
1421
+ this.dirty = true;
1422
+ this.requestFrame();
1423
+ }
1011
1424
  // pile lift for a sheet this many pages below the top
1012
1425
  pileHeight(depth) {
1013
1426
  return this.liftHeight / (1 + depth * PILE.liftDepthFalloff) * (1 - 2e-3 * depth);
@@ -1047,13 +1460,9 @@ var SingleEngine = class {
1047
1460
  return this.inFlight(sheet) || this.drag?.sheet === sheet;
1048
1461
  }
1049
1462
  // floor point under the pointer, relative to the book
1050
- floorPoint(clientX, clientY) {
1051
- const rect = this.canvas.getBoundingClientRect();
1052
- this.ndc.set((clientX - rect.left) / rect.width * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);
1053
- this.raycaster.setFromCamera(this.ndc, this.camera);
1054
- this.floor.constant = -this.group.position.z;
1055
- const hit = this.raycaster.ray.intersectPlane(this.floor, this.hit);
1056
- return hit ? { x: hit.x - this.group.position.x, y: hit.y - this.group.position.y } : null;
1463
+ pointAt(clientX, clientY) {
1464
+ if (!this.input.ray(clientX, clientY, this.ray)) return null;
1465
+ return floorPoint(this.group, this.ray, this.hit);
1057
1466
  }
1058
1467
  // where on the page the hand is, steering how the page tilts as it turns
1059
1468
  grab(sheet, point, snap) {
@@ -1070,11 +1479,10 @@ var SingleEngine = class {
1070
1479
  if (!forward) return this.current > 0;
1071
1480
  return this.current < (this.settings.loop ? this.count : this.count - 1);
1072
1481
  }
1073
- // which page a press at this x would pick up: right of the spine turns forward
1074
- sheetAt(clientX) {
1075
- const rect = this.canvas.getBoundingClientRect();
1076
- const spineX = this.spine.copy(this.group.position).project(this.camera).x;
1077
- const forward = clientX >= rect.left + (0.5 * spineX + 0.5) * rect.width;
1482
+ // which page a press here would pick up: right of the spine turns forward
1483
+ sheetAt(point) {
1484
+ if (!point) return null;
1485
+ const forward = point.x >= 0;
1078
1486
  if (!this.canTurn(forward)) return null;
1079
1487
  return { sheet: this.sheets[forward ? this.current : this.current - 1], forward };
1080
1488
  }
@@ -1082,7 +1490,7 @@ var SingleEngine = class {
1082
1490
  commit(forward, toProgress) {
1083
1491
  if (forward && toProgress === 1) this.current++;
1084
1492
  else if (!forward && toProgress === 0) this.current--;
1085
- this.dirty = true;
1493
+ this.wake();
1086
1494
  }
1087
1495
  fly(sheet, forward, toProgress, flipTime = CLICK.flipTime) {
1088
1496
  this.commit(forward, toProgress);
@@ -1110,11 +1518,11 @@ var SingleEngine = class {
1110
1518
  this.flights = [];
1111
1519
  this.current = clamp(Math.round(position), 0, Math.max(this.count - 1, 0));
1112
1520
  for (const sheet of this.sheets) sheet.progress = sheet.index < this.current ? 1 : 0;
1113
- this.dirty = true;
1521
+ this.wake();
1114
1522
  }
1115
1523
  // --- input ------------------------------------------------------------------
1116
1524
  bindInput() {
1117
- const el = this.hitArea;
1525
+ const el = this.input.element;
1118
1526
  const signal = this.abort.signal;
1119
1527
  const { interaction } = this.settings;
1120
1528
  el.addEventListener(
@@ -1123,10 +1531,10 @@ var SingleEngine = class {
1123
1531
  if (this.press && e.pointerId !== this.press.id) return;
1124
1532
  if (this.hold && this.moved(e, this.hold.fired ? HOLD.moveSlopPx : void 0)) this.cancelHold();
1125
1533
  if (!this.drag || !this.press || !interaction.drag) return;
1126
- const dx = e.clientX - this.press.x;
1534
+ const dx = this.metrics.dragDelta?.(e.clientX - this.press.x, e.clientY - this.press.y) ?? e.clientX - this.press.x;
1127
1535
  this.drag.progress = clamp(this.drag.base - dx / this.drag.rectWidth / this.settings.motion.dragDistance, 0, 1);
1128
- this.grab(this.drag.sheet, this.floorPoint(e.clientX, e.clientY), false);
1129
- this.dirty = true;
1536
+ this.grab(this.drag.sheet, this.pointAt(e.clientX, e.clientY), false);
1537
+ this.wake();
1130
1538
  },
1131
1539
  { signal }
1132
1540
  );
@@ -1135,11 +1543,13 @@ var SingleEngine = class {
1135
1543
  (e) => {
1136
1544
  if (this.press && !e.isPrimary) return;
1137
1545
  if (e.button !== 0 || !(interaction.drag || interaction.click || interaction.hold)) return;
1138
- const target = this.sheetAt(e.clientX);
1546
+ if (!this.input.covers(e.clientX, e.clientY)) return;
1547
+ const point = this.pointAt(e.clientX, e.clientY);
1548
+ const target = this.sheetAt(point);
1139
1549
  if (!target) return;
1140
1550
  const sheet = target.sheet;
1141
1551
  if (!this.inFlight(sheet)) sheet.wobble = this.randomWobble();
1142
- this.grab(sheet, this.floorPoint(e.clientX, e.clientY), true);
1552
+ this.grab(sheet, point, true);
1143
1553
  this.flights = this.flights.filter((f) => f.sheet !== sheet);
1144
1554
  this.press = { id: e.pointerId, x: e.clientX, y: e.clientY };
1145
1555
  this.drag = {
@@ -1165,7 +1575,7 @@ var SingleEngine = class {
1165
1575
  el.setPointerCapture(e.pointerId);
1166
1576
  } catch {
1167
1577
  }
1168
- this.dirty = true;
1578
+ this.wake();
1169
1579
  },
1170
1580
  { signal }
1171
1581
  );
@@ -1177,7 +1587,8 @@ var SingleEngine = class {
1177
1587
  "touchstart",
1178
1588
  (e) => {
1179
1589
  const t = e.touches[0];
1180
- this.touch = e.touches.length === 1 ? { x: t.clientX, y: t.clientY, claimed: false } : null;
1590
+ const single = e.touches.length === 1 && this.input.covers(t.clientX, t.clientY);
1591
+ this.touch = single ? { x: t.clientX, y: t.clientY, claimed: false } : null;
1181
1592
  },
1182
1593
  { passive: true, signal }
1183
1594
  );
@@ -1223,7 +1634,7 @@ var SingleEngine = class {
1223
1634
  const sheet = this.sheets[hold.forward ? this.current : this.current - 1];
1224
1635
  const ramp = (from, to) => from + (to - from) * Math.min(hold.count / HOLD.speedUpSheets, 1);
1225
1636
  if (!this.inFlight(sheet)) sheet.wobble = this.randomWobble();
1226
- this.grab(sheet, this.floorPoint(hold.x, hold.y), true);
1637
+ this.grab(sheet, this.pointAt(hold.x, hold.y), true);
1227
1638
  const flipTime = ramp(HOLD.firstFlipTime, HOLD.fastestFlipTime);
1228
1639
  this.fly(sheet, hold.forward, hold.forward ? 1 : 0, flipTime);
1229
1640
  hold.count++;
@@ -1242,9 +1653,10 @@ var SingleEngine = class {
1242
1653
  this.drag = null;
1243
1654
  this.press = null;
1244
1655
  try {
1245
- this.hitArea.releasePointerCapture(e.pointerId);
1656
+ this.input.element.releasePointerCapture(e.pointerId);
1246
1657
  } catch {
1247
1658
  }
1659
+ this.wake();
1248
1660
  if (!drag) return;
1249
1661
  const { interaction } = this.settings;
1250
1662
  if (!wasMoved || !interaction.drag) {
@@ -1278,6 +1690,7 @@ var SingleEngine = class {
1278
1690
  const to = drag.forward ? 0 : 1;
1279
1691
  if (drag.sheet.progress === to) return;
1280
1692
  this.flights.push({ sheet: drag.sheet, from: drag.sheet.progress, to, start: performance.now(), duration: 300 });
1693
+ this.wake();
1281
1694
  }
1282
1695
  // --- simulation ---------------------------------------------------------------
1283
1696
  update(now) {
@@ -1457,10 +1870,10 @@ var SingleEngine = class {
1457
1870
  };
1458
1871
 
1459
1872
  // src/spread/SpreadEngine.ts
1460
- import * as THREE8 from "three";
1873
+ import * as THREE12 from "three";
1461
1874
 
1462
1875
  // src/spread/spreadSheet.ts
1463
- import * as THREE7 from "three";
1876
+ import * as THREE11 from "three";
1464
1877
  var NORMAL_EPSILON2 = 0.03;
1465
1878
  var WRINKLE_SCALE = 1.2;
1466
1879
  var WRINKLE_SEED = 46;
@@ -1556,7 +1969,7 @@ function wrinkleNoise(u, v, aspect) {
1556
1969
  return value;
1557
1970
  }
1558
1971
  function createSpreadGeometry(aspect) {
1559
- const geometry = new THREE7.PlaneGeometry(1, aspect, 64, Math.round(64 * aspect));
1972
+ const geometry = new THREE11.PlaneGeometry(1, aspect, 64, Math.round(64 * aspect));
1560
1973
  const uv = geometry.attributes.uv;
1561
1974
  const noise = new Float32Array(uv.count * 3);
1562
1975
  for (let i = 0; i < uv.count; i++) {
@@ -1566,7 +1979,7 @@ function createSpreadGeometry(aspect) {
1566
1979
  noise[i * 3 + 1] = wrinkleNoise(u + NORMAL_EPSILON2, v, aspect);
1567
1980
  noise[i * 3 + 2] = wrinkleNoise(u, v + NORMAL_EPSILON2 / aspect, aspect);
1568
1981
  }
1569
- geometry.setAttribute("aNoise", new THREE7.BufferAttribute(noise, 3));
1982
+ geometry.setAttribute("aNoise", new THREE11.BufferAttribute(noise, 3));
1570
1983
  return geometry;
1571
1984
  }
1572
1985
  function createSpreadSheet(geometry, front, back, pattern, settings, shading) {
@@ -1574,9 +1987,9 @@ function createSpreadSheet(geometry, front, back, pattern, settings, shading) {
1574
1987
  uFlipProgress: { value: 0 },
1575
1988
  uWrinkleSide: { value: -1 },
1576
1989
  uBendAngle: { value: 0 },
1577
- uFold: { value: new THREE7.Vector2(1, 0) },
1578
- uCurl: { value: new THREE7.Vector2(0, 1) },
1579
- uFlipRotation: { value: new THREE7.Vector2(1, 0) },
1990
+ uFold: { value: new THREE11.Vector2(1, 0) },
1991
+ uCurl: { value: new THREE11.Vector2(0, 1) },
1992
+ uFlipRotation: { value: new THREE11.Vector2(1, 0) },
1580
1993
  uDirection: { value: 1 },
1581
1994
  uStackLift: { value: 1 },
1582
1995
  uFront: { value: front },
@@ -1618,21 +2031,20 @@ var HOLD2 = {
1618
2031
  };
1619
2032
  var DRAG2 = { progressSmoothTime: 0.9, fallTime: 1, fallTimeExponent: 0.5, landingSpeed: 0.5, clickSlopPx: 5 };
1620
2033
  var CLICK2 = { flipTime: 0.85, followArcGainMin: 1, followArcGainMax: 1.08 };
1621
- var clamp2 = THREE8.MathUtils.clamp;
2034
+ var clamp2 = THREE12.MathUtils.clamp;
1622
2035
  var approach = (smoothTime, dt) => 1 - Math.pow(1e-3, dt / smoothTime);
1623
2036
  function stackLift(index, count, progress) {
1624
2037
  if (count <= 1) return 1;
1625
2038
  return 0.31 + 0.69 * (index + (count - 1 - 2 * index) * (1 - progress)) / (count - 1);
1626
2039
  }
1627
2040
  var SpreadEngine = class {
1628
- group = new THREE8.Group();
2041
+ group = new THREE12.Group();
1629
2042
  maxPosition;
1630
2043
  settings;
1631
2044
  halfHeight;
1632
- camera;
1633
- canvas;
1634
- hitArea;
2045
+ input;
1635
2046
  metrics;
2047
+ requestFrame;
1636
2048
  geometry;
1637
2049
  sheets;
1638
2050
  abort = new AbortController();
@@ -1649,18 +2061,14 @@ var SpreadEngine = class {
1649
2061
  // the sheet most recently sent flying
1650
2062
  dirty = true;
1651
2063
  last = 0;
1652
- raycaster = new THREE8.Raycaster();
1653
- ndc = new THREE8.Vector2();
1654
- floor = new THREE8.Plane(new THREE8.Vector3(0, 0, 1), 0);
1655
- hit = new THREE8.Vector3();
1656
- spine = new THREE8.Vector3();
2064
+ ray = new THREE12.Ray();
2065
+ hit = new THREE12.Vector3();
1657
2066
  constructor(ctx) {
1658
2067
  this.settings = ctx.settings;
1659
2068
  this.halfHeight = ctx.settings.pageAspect / 2;
1660
- this.camera = ctx.camera;
1661
- this.canvas = ctx.canvas;
1662
- this.hitArea = ctx.hitArea;
2069
+ this.input = ctx.input;
1663
2070
  this.metrics = ctx.metrics;
2071
+ this.requestFrame = ctx.requestFrame;
1664
2072
  this.shading = ctx.shading;
1665
2073
  const count = Math.ceil(ctx.textures.length / 2);
1666
2074
  this.maxPosition = count;
@@ -1717,7 +2125,13 @@ var SpreadEngine = class {
1717
2125
  if (!sheet) return;
1718
2126
  if (page % 2 === 0) sheet.uniforms.uFront.value = texture;
1719
2127
  else sheet.uniforms.uBack.value = texture;
1720
- this.dirty = true;
2128
+ this.wake();
2129
+ }
2130
+ cancelInput() {
2131
+ this.cancelHold();
2132
+ this.hover.active = false;
2133
+ if (this.press) this.release(new PointerEvent("pointercancel", { pointerId: this.press.id }));
2134
+ this.wake();
1721
2135
  }
1722
2136
  dispose() {
1723
2137
  this.cancelHold();
@@ -1733,6 +2147,16 @@ var SpreadEngine = class {
1733
2147
  seconds(time) {
1734
2148
  return time / this.settings.motion.speed;
1735
2149
  }
2150
+ // something changed between frames
2151
+ wake() {
2152
+ this.dirty = true;
2153
+ this.requestFrame();
2154
+ }
2155
+ // the point on the floor under the pointer, the spine at x = 0
2156
+ pointAt(clientX, clientY) {
2157
+ if (!this.input.ray(clientX, clientY, this.ray)) return null;
2158
+ return floorPoint(this.group, this.ray, this.hit);
2159
+ }
1736
2160
  // how far a sheet may bend before its far corner rolls over itself
1737
2161
  arcLimit(angleDeg) {
1738
2162
  const t = Math.abs(angleDeg) * Math.PI / 180;
@@ -1745,11 +2169,11 @@ var SpreadEngine = class {
1745
2169
  inFlight(index) {
1746
2170
  return this.flights.some((f) => f.index === index);
1747
2171
  }
1748
- // which sheet a press at this x would pick up
1749
- sheetAt(clientX) {
1750
- const rect = this.canvas.getBoundingClientRect();
1751
- const spineX = this.spine.set(0, 0, 0).project(this.camera).x;
1752
- const forward = clientX >= rect.left + (0.5 * spineX + 0.5) * rect.width;
2172
+ // which sheet a press here would pick up
2173
+ sheetAt(clientX, clientY) {
2174
+ const point = this.pointAt(clientX, clientY);
2175
+ if (!point) return null;
2176
+ const forward = point.x >= 0;
1753
2177
  if (forward ? this.current >= this.sheets.length : this.current <= 0) return null;
1754
2178
  return { index: forward ? this.current : this.current - 1, forward };
1755
2179
  }
@@ -1758,10 +2182,7 @@ var SpreadEngine = class {
1758
2182
  curveFor(clientX, clientY, index) {
1759
2183
  const wobble = this.sheets[index].wobble;
1760
2184
  const maxTilt = this.settings.motion.maxTilt;
1761
- const rect = this.canvas.getBoundingClientRect();
1762
- this.ndc.set((clientX - rect.left) / rect.width * 2 - 1, -((clientY - rect.top) / rect.height) * 2 + 1);
1763
- this.raycaster.setFromCamera(this.ndc, this.camera);
1764
- const hit = this.raycaster.ray.intersectPlane(this.floor, this.hit);
2185
+ const hit = this.pointAt(clientX, clientY);
1765
2186
  let angleDeg = 0;
1766
2187
  if (hit) {
1767
2188
  let deg = Math.atan2(hit.y, hit.x) * 180 / Math.PI;
@@ -1779,7 +2200,7 @@ var SpreadEngine = class {
1779
2200
  if (!following) return { arc: this.restingArc(wobble, angleDeg), angleDeg };
1780
2201
  const lead = leader.curveTarget;
1781
2202
  angleDeg = clamp2(angleDeg, Math.min(0, lead.angleDeg), Math.max(0, lead.angleDeg));
1782
- const gain = THREE8.MathUtils.lerp(CLICK2.followArcGainMin, CLICK2.followArcGainMax, wobble.arc);
2203
+ const gain = THREE12.MathUtils.lerp(CLICK2.followArcGainMin, CLICK2.followArcGainMax, wobble.arc);
1783
2204
  return { arc: clamp2(lead.arc * gain, lead.arc, this.arcLimit(angleDeg)), angleDeg };
1784
2205
  }
1785
2206
  setCurve(sheet, curve, snap) {
@@ -1794,7 +2215,7 @@ var SpreadEngine = class {
1794
2215
  this.leader = index;
1795
2216
  this.sheets[index].wobble = this.randomWobble();
1796
2217
  this.current = toProgress === 1 ? index + 1 : index;
1797
- this.dirty = true;
2218
+ this.wake();
1798
2219
  }
1799
2220
  fly(index, forward, toProgress, flipTime = CLICK2.flipTime) {
1800
2221
  this.commit(index, toProgress);
@@ -1832,11 +2253,11 @@ var SpreadEngine = class {
1832
2253
  sheet.progress = sheet.index < this.current ? 1 : 0;
1833
2254
  sheet.direction = sheet.directionSmooth = 1;
1834
2255
  }
1835
- this.dirty = true;
2256
+ this.wake();
1836
2257
  }
1837
2258
  // --- input ------------------------------------------------------------------
1838
2259
  bindInput() {
1839
- const el = this.hitArea;
2260
+ const el = this.input.element;
1840
2261
  const signal = this.abort.signal;
1841
2262
  const { interaction } = this.settings;
1842
2263
  el.addEventListener(
@@ -1846,13 +2267,13 @@ var SpreadEngine = class {
1846
2267
  if (this.hold && this.moved(e, this.hold.fired ? HOLD2.moveSlopPx : void 0)) this.cancelHold();
1847
2268
  if (this.drag && this.press) {
1848
2269
  if (!interaction.drag) return;
1849
- const dx = e.clientX - this.press.x;
2270
+ const dx = this.metrics.dragDelta?.(e.clientX - this.press.x, e.clientY - this.press.y) ?? e.clientX - this.press.x;
1850
2271
  this.drag.progress = clamp2(this.drag.base - dx / this.drag.rectWidth / this.settings.motion.dragDistance, 0, 1);
1851
2272
  this.drag.targetCurve = this.curveFor(e.clientX, e.clientY, this.drag.index);
1852
2273
  } else if (e.pointerType === "mouse") {
1853
- this.hover = { active: true, x: e.clientX, y: e.clientY };
2274
+ this.hover = { active: this.input.covers(e.clientX, e.clientY), x: e.clientX, y: e.clientY };
1854
2275
  }
1855
- this.dirty = true;
2276
+ this.wake();
1856
2277
  },
1857
2278
  { signal }
1858
2279
  );
@@ -1860,7 +2281,7 @@ var SpreadEngine = class {
1860
2281
  "pointerleave",
1861
2282
  () => {
1862
2283
  this.hover.active = false;
1863
- this.dirty = true;
2284
+ this.wake();
1864
2285
  },
1865
2286
  { signal }
1866
2287
  );
@@ -1869,7 +2290,8 @@ var SpreadEngine = class {
1869
2290
  (e) => {
1870
2291
  if (this.press && !e.isPrimary) return;
1871
2292
  if (e.button !== 0 || !(interaction.drag || interaction.click || interaction.hold)) return;
1872
- const target = this.sheetAt(e.clientX);
2293
+ if (!this.input.covers(e.clientX, e.clientY)) return;
2294
+ const target = this.sheetAt(e.clientX, e.clientY);
1873
2295
  if (!target) return;
1874
2296
  const sheet = this.sheets[target.index];
1875
2297
  if (!this.inFlight(target.index)) this.setCurve(sheet, this.curveFor(e.clientX, e.clientY, target.index), true);
@@ -1900,7 +2322,7 @@ var SpreadEngine = class {
1900
2322
  el.setPointerCapture(e.pointerId);
1901
2323
  } catch {
1902
2324
  }
1903
- this.dirty = true;
2325
+ this.wake();
1904
2326
  },
1905
2327
  { signal }
1906
2328
  );
@@ -1947,9 +2369,10 @@ var SpreadEngine = class {
1947
2369
  this.drag = null;
1948
2370
  this.press = null;
1949
2371
  try {
1950
- this.hitArea.releasePointerCapture(e.pointerId);
2372
+ this.input.element.releasePointerCapture(e.pointerId);
1951
2373
  } catch {
1952
2374
  }
2375
+ this.wake();
1953
2376
  if (!drag) return;
1954
2377
  const { interaction } = this.settings;
1955
2378
  if (!wasMoved || !interaction.drag) {
@@ -1967,352 +2390,150 @@ var SpreadEngine = class {
1967
2390
  this.flights = this.flights.filter((f) => f.index !== drag.index);
1968
2391
  this.flights.push({
1969
2392
  index: drag.index,
1970
- from: sheet.progress,
1971
- to,
1972
- direction: drag.forward ? 1 : -1,
1973
- start: performance.now(),
1974
- duration: 1e3 * duration,
1975
- launch: clamp2(speed * duration / remaining, 0, 2)
1976
- });
1977
- }
1978
- // --- simulation ---------------------------------------------------------------
1979
- update(now) {
1980
- for (let i = this.flights.length - 1; i >= 0; i--) {
1981
- const f = this.flights[i];
1982
- const sheet = this.sheets[f.index];
1983
- const t = clamp2((now - f.start) / f.duration, 0, 1);
1984
- let k;
1985
- if (f.launch === void 0) {
1986
- k = 0.5 * (1 - Math.cos(t * Math.PI));
1987
- } else {
1988
- const t2 = t * t;
1989
- const t3 = t2 * t;
1990
- k = (t3 - 2 * t2 + t) * f.launch + (3 * t2 - 2 * t3) + (t3 - t2) * DRAG2.landingSpeed;
1991
- }
1992
- sheet.progress = f.from + (f.to - f.from) * k;
1993
- sheet.direction = f.direction;
1994
- if (now >= f.start + f.duration) {
1995
- this.flights.splice(i, 1);
1996
- this.dirty = true;
1997
- }
1998
- }
1999
- const dt = this.last ? Math.min((now - this.last) / 1e3, 0.1) : 1 / 60;
2000
- this.last = now;
2001
- let moving = this.flights.length > 0 || this.drag !== null;
2002
- if (this.drag) {
2003
- const sheet = this.sheets[this.drag.index];
2004
- const step = (this.drag.progress - sheet.progress) * (1 - Math.pow(0.01, dt / DRAG2.progressSmoothTime));
2005
- sheet.progress += step;
2006
- this.drag.speed = dt > 0 ? step / dt : 0;
2007
- this.setCurve(sheet, this.drag.targetCurve, false);
2008
- }
2009
- const { hoverLift } = this.settings.motion;
2010
- const previous = this.hovered;
2011
- const canHover = this.settings.interaction.hover && hoverLift > 0;
2012
- const target = canHover && this.hover.active && !this.drag && !this.hold ? this.sheetAt(this.hover.x) : null;
2013
- this.hovered = target && !this.inFlight(target.index) ? target.index : -1;
2014
- if (this.hovered !== -1) {
2015
- const sheet = this.sheets[this.hovered];
2016
- this.setCurve(sheet, this.curveFor(this.hover.x, this.hover.y, this.hovered), this.hovered !== previous);
2017
- }
2018
- const toward = approach(HOVER.smoothTime, dt);
2019
- const directionStep = approach(CURVE.directionSmoothTime, dt);
2020
- const curveStep = approach(CURVE.smoothTime, dt);
2021
- for (const sheet of this.sheets) {
2022
- const i = sheet.index;
2023
- if (!this.inFlight(i) && this.drag?.index !== i) {
2024
- const rest = i >= this.current ? 0 : 1;
2025
- const goal = i === this.hovered ? Math.abs(rest - hoverLift) : rest;
2026
- const diff = goal - sheet.progress;
2027
- if (Math.abs(diff) < CURVE.settleEpsilon) {
2028
- if (sheet.progress !== goal) {
2029
- sheet.progress = goal;
2030
- this.dirty = true;
2031
- }
2032
- } else {
2033
- sheet.progress += diff * toward;
2034
- moving = true;
2035
- }
2036
- const above = this.sheets[rest === 0 ? i - 1 : i + 1];
2037
- if (above) {
2038
- const bounded = rest === 0 ? Math.min(sheet.progress, Math.max(0, above.progress - HOVER.gapToSheetAbove)) : Math.max(sheet.progress, Math.min(1, above.progress + HOVER.gapToSheetAbove));
2039
- if (bounded !== sheet.progress) {
2040
- sheet.progress = bounded;
2041
- moving = true;
2042
- }
2043
- }
2044
- sheet.direction = rest === 1 && sheet.progress !== rest ? -1 : 1;
2045
- sheet.directionSmooth = sheet.direction;
2046
- }
2047
- const dd = sheet.direction - sheet.directionSmooth;
2048
- if (Math.abs(dd) < CURVE.settleEpsilon) {
2049
- if (sheet.directionSmooth !== sheet.direction) {
2050
- sheet.directionSmooth = sheet.direction;
2051
- this.dirty = true;
2052
- }
2053
- } else {
2054
- sheet.directionSmooth += dd * directionStep;
2055
- moving = true;
2056
- }
2057
- for (const key of ["arc", "angleDeg"]) {
2058
- const d = sheet.curveTarget[key] - sheet.curve[key];
2059
- if (Math.abs(d) < CURVE.settleEpsilon) sheet.curve[key] = sheet.curveTarget[key];
2060
- else {
2061
- sheet.curve[key] += d * curveStep;
2062
- moving = true;
2063
- }
2064
- }
2065
- this.applyUniforms(sheet);
2066
- }
2067
- if (!moving && !this.dirty) return false;
2068
- let contact = null;
2069
- let upright = 0;
2070
- for (const sheet of this.sheets) {
2071
- const i = sheet.index;
2072
- const depth = i >= this.current ? i - this.current : this.current - 1 - i;
2073
- const turning = this.inFlight(i) || this.drag?.index === i;
2074
- sheet.mesh.castShadow = this.settings.lighting.shadows && (depth < 3 || turning);
2075
- layerSheet(sheet.mesh, depth, this.sheets.length, turning);
2076
- const lift = Math.abs(sheet.uniforms.uFlipRotation.value.y);
2077
- if (turning && lift > upright) {
2078
- upright = lift;
2079
- contact = sheet;
2080
- }
2081
- }
2082
- const rotation = contact?.uniforms.uFlipRotation.value;
2083
- if (rotation) this.shading.spineContact.value.set(rotation.x, rotation.y, 1);
2084
- else this.shading.spineContact.value.set(1, 0, 0);
2085
- this.dirty = false;
2086
- return true;
2087
- }
2088
- applyUniforms(sheet) {
2089
- const u = sheet.uniforms;
2090
- const half = this.halfHeight;
2091
- const turn = sheet.progress * Math.PI;
2092
- const bend = Math.sin(turn) * Math.PI * sheet.curve.arc;
2093
- const tilt = sheet.curve.angleDeg * Math.PI / 180;
2094
- const cos = Math.cos(tilt);
2095
- const sin = Math.sin(tilt);
2096
- const curlLength = 1 - half * Math.sin(Math.abs(tilt));
2097
- const curlStart = 1 - curlLength;
2098
- const reach = Math.max(0, cos + sin * half * Math.sign(sin) - curlStart);
2099
- const tipAngle = reach / curlLength * bend;
2100
- const tipLift = reach * (Math.abs(tipAngle) < 1e-4 ? 0 : (1 - Math.cos(tipAngle)) / tipAngle);
2101
- const rotation = turn + 0.5 * sheet.directionSmooth * tipLift;
2102
- u.uFlipProgress.value = sheet.progress;
2103
- u.uWrinkleSide.value = -Math.cos(turn);
2104
- u.uDirection.value = sheet.directionSmooth;
2105
- u.uStackLift.value = sheet.liftBase + sheet.liftSpan * sheet.progress;
2106
- u.uBendAngle.value = bend;
2107
- u.uFold.value.set(cos, sin);
2108
- u.uCurl.value.set(curlStart, curlLength);
2109
- u.uFlipRotation.value.set(Math.cos(rotation), Math.sin(rotation));
2110
- }
2111
- };
2112
-
2113
- // src/textures.ts
2114
- import * as THREE9 from "three";
2115
- var CONCURRENCY = 4;
2116
- var PageLoader = class {
2117
- constructor(sources, renderer, radius, events) {
2118
- this.sources = sources;
2119
- this.renderer = renderer;
2120
- this.radius = radius;
2121
- this.events = events;
2122
- this.textures = new Array(sources.length);
2123
- }
2124
- sources;
2125
- renderer;
2126
- radius;
2127
- events;
2128
- /** Loaded textures by page; missing pages are still loading, unloaded or failed. */
2129
- textures;
2130
- owned = /* @__PURE__ */ new Set();
2131
- inFlight = /* @__PURE__ */ new Map();
2132
- failed = /* @__PURE__ */ new Set();
2133
- // `first` pages load before the rest; the rest go nearest first
2134
- first = [];
2135
- // the pages in view, and the one being read, which loads first among them
2136
- from = 0;
2137
- to = 0;
2138
- around = 0;
2139
- // whether the magazine loops, putting the cover right after the last page
2140
- wrap = false;
2141
- waiting = /* @__PURE__ */ new Set();
2142
- settleFirst = () => {
2143
- };
2144
- disposed = false;
2145
- /**
2146
- * Starts loading around the visible pages `from`–`to`. Resolves once the `first` pages
2147
- * within reach are in (or failed).
2148
- */
2149
- start(from, to, first) {
2150
- this.from = from;
2151
- this.to = to;
2152
- this.first = first.filter((i) => i >= 0 && i < this.sources.length && this.distance(i) <= this.radius);
2153
- this.waiting = new Set(this.first);
2154
- const ready = new Promise((resolve) => this.settleFirst = resolve);
2155
- this.focus(from, to);
2156
- if (!this.waiting.size) this.settleFirst();
2157
- return ready;
2158
- }
2159
- /**
2160
- * Follows the reader: loads around the pages in view (`from`–`to`), the page being read
2161
- * (`around`) first, and unloads pages left far behind.
2162
- */
2163
- focus(from, to = from, around = from) {
2164
- if (this.disposed) return;
2165
- this.from = from;
2166
- this.to = to;
2167
- this.around = around;
2168
- const keep = 2 * this.radius;
2169
- for (const [i, request] of this.inFlight) {
2170
- if (this.distance(i) <= keep) continue;
2171
- request.abort();
2172
- this.inFlight.delete(i);
2173
- }
2174
- this.textures.forEach((texture, i) => {
2175
- if (!texture || this.distance(i) <= keep || !this.owned.has(texture)) return;
2176
- this.textures[i] = void 0;
2177
- this.events.onUnload(i);
2178
- this.owned.delete(texture);
2179
- texture.dispose();
2180
- });
2181
- this.pump();
2182
- }
2183
- setWrap(wrap) {
2184
- if (wrap === this.wrap) return;
2185
- this.wrap = wrap;
2186
- this.focus(this.from, this.to, this.around);
2187
- }
2188
- setRadius(radius) {
2189
- this.radius = radius;
2190
- this.focus(this.from, this.to, this.around);
2191
- }
2192
- /** Loads the pages that failed again. */
2193
- retry() {
2194
- this.failed.clear();
2195
- this.pump();
2196
- }
2197
- dispose() {
2198
- this.disposed = true;
2199
- for (const request of this.inFlight.values()) request.abort();
2200
- for (const texture of this.owned) texture.dispose();
2201
- this.owned.clear();
2202
- }
2203
- // pages away from the visible ones
2204
- distance(page) {
2205
- const straight = page < this.from ? this.from - page : page > this.to ? page - this.to : 0;
2206
- if (!this.wrap) return straight;
2207
- const count = this.sources.length;
2208
- return Math.min(straight, page + count - this.to, this.from + count - page);
2209
- }
2210
- // starts the most wanted pages while there's room
2211
- pump() {
2212
- if (this.disposed || this.inFlight.size >= CONCURRENCY) return;
2213
- const wanted = (i) => !this.textures[i] && !this.inFlight.has(i) && !this.failed.has(i) && this.distance(i) <= this.radius;
2214
- const queue = [
2215
- ...this.first.filter((i) => this.waiting.has(i)),
2216
- ...this.sources.map((_, i) => i).sort((a, b) => this.distance(a) - this.distance(b) || Math.abs(a - this.around) - Math.abs(b - this.around))
2217
- ];
2218
- for (const page of queue) {
2219
- if (this.inFlight.size >= CONCURRENCY) return;
2220
- if (wanted(page)) this.load(page);
2221
- }
2222
- }
2223
- load(page) {
2224
- const request = new AbortController();
2225
- this.inFlight.set(page, request);
2226
- this.createTexture(this.sources[page], request.signal).then((texture) => {
2227
- if (this.disposed || request.signal.aborted) {
2228
- if (this.owned.delete(texture)) texture.dispose();
2229
- return;
2230
- }
2231
- texture.colorSpace = THREE9.SRGBColorSpace;
2232
- texture.premultiplyAlpha = true;
2233
- texture.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
2234
- texture.needsUpdate = true;
2235
- this.renderer.initTexture(texture);
2236
- this.textures[page] = texture;
2237
- this.events.onLoad(page, texture);
2238
- }).catch((error) => {
2239
- if (this.disposed || request.signal.aborted) return;
2240
- this.failed.add(page);
2241
- this.events.onError(page, error);
2242
- }).finally(() => {
2243
- if (this.inFlight.get(page) === request) this.inFlight.delete(page);
2244
- if (this.waiting.delete(page) && !this.waiting.size) this.settleFirst();
2245
- this.pump();
2393
+ from: sheet.progress,
2394
+ to,
2395
+ direction: drag.forward ? 1 : -1,
2396
+ start: performance.now(),
2397
+ duration: 1e3 * duration,
2398
+ launch: clamp2(speed * duration / remaining, 0, 2)
2246
2399
  });
2247
2400
  }
2248
- async createTexture(source, signal) {
2249
- if (source instanceof THREE9.Texture) return source;
2250
- const own = (texture) => (this.owned.add(texture), texture);
2251
- if (typeof source === "string") return own(new THREE9.Texture(await loadImage(source, signal)));
2252
- if (typeof HTMLCanvasElement !== "undefined" && source instanceof HTMLCanvasElement) return own(new THREE9.CanvasTexture(source));
2253
- if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
2254
- if (!source.complete || !source.naturalWidth) await source.decode();
2255
- return own(new THREE9.Texture(source));
2401
+ // --- simulation ---------------------------------------------------------------
2402
+ update(now) {
2403
+ for (let i = this.flights.length - 1; i >= 0; i--) {
2404
+ const f = this.flights[i];
2405
+ const sheet = this.sheets[f.index];
2406
+ const t = clamp2((now - f.start) / f.duration, 0, 1);
2407
+ let k;
2408
+ if (f.launch === void 0) {
2409
+ k = 0.5 * (1 - Math.cos(t * Math.PI));
2410
+ } else {
2411
+ const t2 = t * t;
2412
+ const t3 = t2 * t;
2413
+ k = (t3 - 2 * t2 + t) * f.launch + (3 * t2 - 2 * t3) + (t3 - t2) * DRAG2.landingSpeed;
2414
+ }
2415
+ sheet.progress = f.from + (f.to - f.from) * k;
2416
+ sheet.direction = f.direction;
2417
+ if (now >= f.start + f.duration) {
2418
+ this.flights.splice(i, 1);
2419
+ this.dirty = true;
2420
+ }
2256
2421
  }
2257
- const canvas = document.createElement("canvas");
2258
- canvas.width = source.width;
2259
- canvas.height = source.height;
2260
- canvas.getContext("2d").drawImage(source, 0, 0);
2261
- return own(new THREE9.CanvasTexture(canvas));
2422
+ const dt = this.last ? Math.min((now - this.last) / 1e3, 0.1) : 1 / 60;
2423
+ this.last = now;
2424
+ let moving = this.flights.length > 0 || this.drag !== null;
2425
+ if (this.drag) {
2426
+ const sheet = this.sheets[this.drag.index];
2427
+ const step = (this.drag.progress - sheet.progress) * (1 - Math.pow(0.01, dt / DRAG2.progressSmoothTime));
2428
+ sheet.progress += step;
2429
+ this.drag.speed = dt > 0 ? step / dt : 0;
2430
+ this.setCurve(sheet, this.drag.targetCurve, false);
2431
+ }
2432
+ const { hoverLift } = this.settings.motion;
2433
+ const previous = this.hovered;
2434
+ const canHover = this.settings.interaction.hover && hoverLift > 0;
2435
+ const target = canHover && this.hover.active && !this.drag && !this.hold ? this.sheetAt(this.hover.x, this.hover.y) : null;
2436
+ this.hovered = target && !this.inFlight(target.index) ? target.index : -1;
2437
+ if (this.hovered !== -1) {
2438
+ const sheet = this.sheets[this.hovered];
2439
+ this.setCurve(sheet, this.curveFor(this.hover.x, this.hover.y, this.hovered), this.hovered !== previous);
2440
+ }
2441
+ const toward = approach(HOVER.smoothTime, dt);
2442
+ const directionStep = approach(CURVE.directionSmoothTime, dt);
2443
+ const curveStep = approach(CURVE.smoothTime, dt);
2444
+ for (const sheet of this.sheets) {
2445
+ const i = sheet.index;
2446
+ if (!this.inFlight(i) && this.drag?.index !== i) {
2447
+ const rest = i >= this.current ? 0 : 1;
2448
+ const goal = i === this.hovered ? Math.abs(rest - hoverLift) : rest;
2449
+ const diff = goal - sheet.progress;
2450
+ if (Math.abs(diff) < CURVE.settleEpsilon) {
2451
+ if (sheet.progress !== goal) {
2452
+ sheet.progress = goal;
2453
+ this.dirty = true;
2454
+ }
2455
+ } else {
2456
+ sheet.progress += diff * toward;
2457
+ moving = true;
2458
+ }
2459
+ const above = this.sheets[rest === 0 ? i - 1 : i + 1];
2460
+ if (above) {
2461
+ const bounded = rest === 0 ? Math.min(sheet.progress, Math.max(0, above.progress - HOVER.gapToSheetAbove)) : Math.max(sheet.progress, Math.min(1, above.progress + HOVER.gapToSheetAbove));
2462
+ if (bounded !== sheet.progress) {
2463
+ sheet.progress = bounded;
2464
+ moving = true;
2465
+ }
2466
+ }
2467
+ sheet.direction = rest === 1 && sheet.progress !== rest ? -1 : 1;
2468
+ sheet.directionSmooth = sheet.direction;
2469
+ }
2470
+ const dd = sheet.direction - sheet.directionSmooth;
2471
+ if (Math.abs(dd) < CURVE.settleEpsilon) {
2472
+ if (sheet.directionSmooth !== sheet.direction) {
2473
+ sheet.directionSmooth = sheet.direction;
2474
+ this.dirty = true;
2475
+ }
2476
+ } else {
2477
+ sheet.directionSmooth += dd * directionStep;
2478
+ moving = true;
2479
+ }
2480
+ for (const key of ["arc", "angleDeg"]) {
2481
+ const d = sheet.curveTarget[key] - sheet.curve[key];
2482
+ if (Math.abs(d) < CURVE.settleEpsilon) sheet.curve[key] = sheet.curveTarget[key];
2483
+ else {
2484
+ sheet.curve[key] += d * curveStep;
2485
+ moving = true;
2486
+ }
2487
+ }
2488
+ this.applyUniforms(sheet);
2489
+ }
2490
+ if (!moving && !this.dirty) return false;
2491
+ let contact = null;
2492
+ let upright = 0;
2493
+ for (const sheet of this.sheets) {
2494
+ const i = sheet.index;
2495
+ const depth = i >= this.current ? i - this.current : this.current - 1 - i;
2496
+ const turning = this.inFlight(i) || this.drag?.index === i;
2497
+ sheet.mesh.castShadow = this.settings.lighting.shadows && (depth < 3 || turning);
2498
+ layerSheet(sheet.mesh, depth, this.sheets.length, turning);
2499
+ const lift = Math.abs(sheet.uniforms.uFlipRotation.value.y);
2500
+ if (turning && lift > upright) {
2501
+ upright = lift;
2502
+ contact = sheet;
2503
+ }
2504
+ }
2505
+ const rotation = contact?.uniforms.uFlipRotation.value;
2506
+ if (rotation) this.shading.spineContact.value.set(rotation.x, rotation.y, 1);
2507
+ else this.shading.spineContact.value.set(1, 0, 0);
2508
+ this.dirty = false;
2509
+ return true;
2262
2510
  }
2263
- };
2264
- function loadImage(url, signal) {
2265
- return new Promise((resolve, reject) => {
2266
- const img = new Image();
2267
- img.crossOrigin = "anonymous";
2268
- img.decoding = "async";
2269
- const onAbort = () => {
2270
- done();
2271
- img.src = "";
2272
- reject(new DOMException("Page no longer needed", "AbortError"));
2273
- };
2274
- const done = () => {
2275
- img.onload = img.onerror = null;
2276
- signal?.removeEventListener("abort", onAbort);
2277
- };
2278
- signal?.addEventListener("abort", onAbort, { once: true });
2279
- img.onload = () => {
2280
- done();
2281
- img.decode().then(() => resolve(img), () => resolve(img));
2282
- };
2283
- img.onerror = () => {
2284
- done();
2285
- reject(new Error(`Failed to load page image: ${url}`));
2286
- };
2287
- img.src = url;
2288
- });
2289
- }
2290
- async function loadGrain(source) {
2291
- let texture;
2292
- if (typeof source === "string") texture = new THREE9.Texture(await loadImage(source));
2293
- else if (source instanceof HTMLCanvasElement) texture = new THREE9.CanvasTexture(source);
2294
- else {
2295
- if (!source.complete || !source.naturalWidth) await source.decode();
2296
- texture = new THREE9.Texture(source);
2511
+ applyUniforms(sheet) {
2512
+ const u = sheet.uniforms;
2513
+ const half = this.halfHeight;
2514
+ const turn = sheet.progress * Math.PI;
2515
+ const bend = Math.sin(turn) * Math.PI * sheet.curve.arc;
2516
+ const tilt = sheet.curve.angleDeg * Math.PI / 180;
2517
+ const cos = Math.cos(tilt);
2518
+ const sin = Math.sin(tilt);
2519
+ const curlLength = 1 - half * Math.sin(Math.abs(tilt));
2520
+ const curlStart = 1 - curlLength;
2521
+ const reach = Math.max(0, cos + sin * half * Math.sign(sin) - curlStart);
2522
+ const tipAngle = reach / curlLength * bend;
2523
+ const tipLift = reach * (Math.abs(tipAngle) < 1e-4 ? 0 : (1 - Math.cos(tipAngle)) / tipAngle);
2524
+ const rotation = turn + 0.5 * sheet.directionSmooth * tipLift;
2525
+ u.uFlipProgress.value = sheet.progress;
2526
+ u.uWrinkleSide.value = -Math.cos(turn);
2527
+ u.uDirection.value = sheet.directionSmooth;
2528
+ u.uStackLift.value = sheet.liftBase + sheet.liftSpan * sheet.progress;
2529
+ u.uBendAngle.value = bend;
2530
+ u.uFold.value.set(cos, sin);
2531
+ u.uCurl.value.set(curlStart, curlLength);
2532
+ u.uFlipRotation.value.set(Math.cos(rotation), Math.sin(rotation));
2297
2533
  }
2298
- texture.wrapS = texture.wrapT = THREE9.RepeatWrapping;
2299
- texture.needsUpdate = true;
2300
- return texture;
2301
- }
2534
+ };
2302
2535
 
2303
2536
  // src/LeafThrough.ts
2304
- var LIVE_KEYS = /* @__PURE__ */ new Set([
2305
- "onPageChange",
2306
- "onReady",
2307
- "onPageLoad",
2308
- "onError",
2309
- "formatPageLabel",
2310
- "label",
2311
- "startPage",
2312
- "preload",
2313
- "pages"
2314
- ]);
2315
- var DEFAULT_PRELOAD = 6;
2316
2537
  var isClient = typeof window !== "undefined";
2317
2538
  var LeafThrough = class extends EventTarget {
2318
2539
  container;
@@ -2326,9 +2547,7 @@ var LeafThrough = class extends EventTarget {
2326
2547
  // null when the browser can't run WebGL; the magazine then shows plain pages
2327
2548
  renderer;
2328
2549
  frameRequest = 0;
2329
- blank;
2330
- backBlank;
2331
- pattern = null;
2550
+ paper;
2332
2551
  loader = null;
2333
2552
  view = null;
2334
2553
  spreadMedia = null;
@@ -2342,8 +2561,7 @@ var LeafThrough = class extends EventTarget {
2342
2561
  visible = true;
2343
2562
  needsRender = true;
2344
2563
  running = false;
2345
- // an animated goTo(): turn one page every `interval` ms until `target`
2346
- riffle = null;
2564
+ riffle = new Riffle();
2347
2565
  lastPagesKey = "";
2348
2566
  ready = false;
2349
2567
  disposed = false;
@@ -2380,9 +2598,10 @@ var LeafThrough = class extends EventTarget {
2380
2598
  element.append(this.canvas, this.content.element, this.hitArea, this.live);
2381
2599
  this.renderer = createRenderer(this.canvas);
2382
2600
  container.append(element);
2383
- this.blank = solidTexture("#fcfcf9");
2384
- const backColor = options.paper?.backColor ?? "#fcfcf9";
2385
- this.backBlank = { texture: solidTexture(backColor), color: colorKey(backColor) };
2601
+ this.paper = new PaperTextures(options.paper?.backColor ?? "#fcfcf9", {
2602
+ onGrain: () => this.remount(),
2603
+ onError: (error) => this.fail(error)
2604
+ });
2386
2605
  const signal = this.abort.signal;
2387
2606
  this.watchSpreadQuery();
2388
2607
  this.motionMedia = window.matchMedia("(prefers-reduced-motion: reduce)");
@@ -2431,18 +2650,18 @@ var LeafThrough = class extends EventTarget {
2431
2650
  }
2432
2651
  /** Turns to the next page. Returns false at the end. */
2433
2652
  next() {
2434
- this.stopRiffle();
2653
+ this.riffle.stop();
2435
2654
  return this.view?.engine.flip(true) ?? false;
2436
2655
  }
2437
2656
  /** Turns to the previous page. Returns false at the start. */
2438
2657
  prev() {
2439
- this.stopRiffle();
2658
+ this.riffle.stop();
2440
2659
  return this.view?.engine.flip(false) ?? false;
2441
2660
  }
2442
2661
  /** Shows `page` (0-based), jumping there or turning the pages one by one. */
2443
2662
  goTo(page, { animate = false } = {}) {
2444
2663
  if (!this.view || Number.isNaN(page)) return;
2445
- this.stopRiffle();
2664
+ this.riffle.stop();
2446
2665
  const engine = this.view.engine;
2447
2666
  const target = this.pageToPosition(page, this.view.settings.layout);
2448
2667
  if (!animate) {
@@ -2450,7 +2669,7 @@ var LeafThrough = class extends EventTarget {
2450
2669
  return;
2451
2670
  }
2452
2671
  if (engine.position === target) return;
2453
- this.riffle = { target, interval: 140 / this.view.settings.motion.speed, last: -Infinity };
2672
+ this.riffle.start(target, this.view.settings.motion.speed);
2454
2673
  this.needsRender = true;
2455
2674
  }
2456
2675
  /** Loads the pages that failed again. Also happens by itself when the browser comes back online. */
@@ -2474,20 +2693,19 @@ var LeafThrough = class extends EventTarget {
2474
2693
  if (this.disposed) return;
2475
2694
  const previous = this.options;
2476
2695
  const next = this.options = snapshot(options);
2477
- const keys = /* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(next)]);
2478
- const changed = [...keys].filter((key) => !deepEqual(previous[key], next[key]));
2696
+ const changed = changedOptions(previous, next);
2479
2697
  if (!changed.length) return;
2480
2698
  if (changed.includes("label") || changed.includes("formatPageLabel")) this.applyLabel();
2481
2699
  if (changed.includes("spreadQuery")) this.watchSpreadQuery();
2482
2700
  const page = this.page;
2483
- let rebuild = changed.some((key) => !LIVE_KEYS.has(key));
2701
+ let rebuild = changed.some((key) => !LIVE_OPTIONS.has(key));
2484
2702
  if (changed.includes("pages") && this.pagesKey(next) !== this.lastPagesKey) {
2485
2703
  this.loader?.dispose();
2486
2704
  this.loader = null;
2487
2705
  this.resetReady();
2488
2706
  rebuild = true;
2489
2707
  }
2490
- if (changed.includes("preload")) this.loader?.setRadius(this.preloadRadius());
2708
+ if (changed.includes("preload")) this.loader?.setRadius(preloadRadius(this.options.preload));
2491
2709
  if (rebuild) this.remount(page);
2492
2710
  }
2493
2711
  addEventListener(type, listener, options) {
@@ -2499,7 +2717,7 @@ var LeafThrough = class extends EventTarget {
2499
2717
  dispose() {
2500
2718
  if (this.disposed) return;
2501
2719
  this.disposed = true;
2502
- this.stopRiffle();
2720
+ this.riffle.stop();
2503
2721
  this.setLoop(null);
2504
2722
  this.abort.abort();
2505
2723
  this.spreadMedia?.abort.abort();
@@ -2508,9 +2726,7 @@ var LeafThrough = class extends EventTarget {
2508
2726
  this.view?.dispose();
2509
2727
  this.view = null;
2510
2728
  this.loader?.dispose();
2511
- this.pattern?.texture.dispose();
2512
- this.blank.dispose();
2513
- this.backBlank.texture.dispose();
2729
+ this.paper.dispose();
2514
2730
  this.renderer?.dispose();
2515
2731
  this.renderer?.forceContextLoss();
2516
2732
  this.element.remove();
@@ -2530,7 +2746,7 @@ var LeafThrough = class extends EventTarget {
2530
2746
  }
2531
2747
  remount(page = this.page) {
2532
2748
  if (this.disposed) return;
2533
- this.stopRiffle();
2749
+ this.riffle.stop();
2534
2750
  const wasPlain = this.view?.engine instanceof PlainEngine;
2535
2751
  this.view?.dispose();
2536
2752
  const layout = this.pickLayout();
@@ -2573,13 +2789,13 @@ var LeafThrough = class extends EventTarget {
2573
2789
  const framing = framingFor(layout, settings.pageAspect);
2574
2790
  const { drag, click, hold } = settings.interaction;
2575
2791
  this.hitArea.style.cursor = drag ? "grab" : click || hold ? "pointer" : "default";
2576
- const scene = new THREE10.Scene();
2577
- const camera = new THREE10.PerspectiveCamera(40, 1, Math.max(0.1, framing.distance - 1.8), framing.distance + 2);
2792
+ const scene = new THREE13.Scene();
2793
+ const camera = new THREE13.PerspectiveCamera(40, 1, Math.max(0.1, framing.distance - 1.8), framing.distance + 2);
2578
2794
  camera.position.set(framing.eye.x, framing.eye.y, framing.distance);
2579
2795
  camera.lookAt(framing.eye.x, framing.eye.y, 0);
2580
2796
  const { ambient, sun: sunOptions, shadows } = settings.lighting;
2581
- scene.add(new THREE10.HemisphereLight(ambient.sky, ambient.ground, ambient.intensity));
2582
- const sun = new THREE10.DirectionalLight(sunOptions.color, sunOptions.intensity);
2797
+ scene.add(new THREE13.HemisphereLight(ambient.sky, ambient.ground, ambient.intensity));
2798
+ const sun = new THREE13.DirectionalLight(sunOptions.color, sunOptions.intensity);
2583
2799
  sun.position.set(...sunOptions.position);
2584
2800
  sun.castShadow = shadows;
2585
2801
  if (layout === "single") sun.shadow.intensity = 0.4;
@@ -2589,35 +2805,35 @@ var LeafThrough = class extends EventTarget {
2589
2805
  sun.shadow.bias = -1e-3;
2590
2806
  scene.add(sun, sun.target);
2591
2807
  const shading = {
2592
- spineContact: { value: new THREE10.Vector3(1, 0, 0) },
2808
+ spineContact: { value: new THREE13.Vector3(1, 0, 0) },
2593
2809
  lightDirection: { value: sun.position.clone().sub(sun.target.position).normalize() }
2594
2810
  };
2595
- let floor = null;
2811
+ let floor2 = null;
2596
2812
  if (layout === "spread" && shadows && settings.lighting.floorShadow > 0) {
2597
- floor = new THREE10.Mesh(new THREE10.PlaneGeometry(8, 8), new THREE10.ShadowMaterial({ opacity: settings.lighting.floorShadow }));
2598
- floor.position.z = -1e-5;
2599
- floor.receiveShadow = true;
2600
- scene.add(floor);
2601
- }
2602
- const pages = this.options.pages;
2603
- const textures = pages.map((_, i) => this.loader?.textures[i] ?? this.blank);
2604
- const backColor = colorKey(settings.paper.backColor);
2605
- if (backColor !== this.backBlank.color) {
2606
- this.backBlank.texture.dispose();
2607
- this.backBlank.texture = solidTexture(settings.paper.backColor);
2608
- this.backBlank.color = backColor;
2813
+ floor2 = new THREE13.Mesh(new THREE13.PlaneGeometry(8, 8), new THREE13.ShadowMaterial({ opacity: settings.lighting.floorShadow }));
2814
+ floor2.position.z = -1e-5;
2815
+ floor2.receiveShadow = true;
2816
+ scene.add(floor2);
2609
2817
  }
2818
+ const blank = this.paper.blank;
2819
+ const textures = this.options.pages.map((_, i) => this.loader?.textures[i] ?? blank);
2610
2820
  const ctx = {
2611
- camera,
2612
- canvas: this.canvas,
2613
- hitArea: this.hitArea,
2821
+ input: {
2822
+ element: this.hitArea,
2823
+ ray: (x, y, ray) => rayThrough(camera, this.canvas, x, y, ray),
2824
+ // the hit area itself covers just the pages
2825
+ covers: () => true
2826
+ },
2614
2827
  settings,
2615
2828
  textures,
2616
- blank: layout === "single" ? this.backBlank.texture : this.blank,
2617
- pattern: this.patternFor(settings, renderer),
2829
+ blank: layout === "single" ? this.paper.back(settings.paper.backColor) : blank,
2830
+ pattern: this.paper.grain(settings, renderer),
2618
2831
  position: this.pageToPosition(page, layout),
2619
2832
  metrics: this.metrics,
2620
- shading
2833
+ shading,
2834
+ // the loop draws every frame the engine asks for while the magazine is on screen
2835
+ requestFrame: () => {
2836
+ }
2621
2837
  };
2622
2838
  const engine = layout === "spread" ? new SpreadEngine(ctx) : new SingleEngine(ctx);
2623
2839
  scene.add(engine.group);
@@ -2630,36 +2846,11 @@ var LeafThrough = class extends EventTarget {
2630
2846
  dispose: () => {
2631
2847
  engine.dispose();
2632
2848
  sun.shadow.dispose();
2633
- floor?.geometry.dispose();
2634
- floor?.material.dispose();
2849
+ floor2?.geometry.dispose();
2850
+ floor2?.material.dispose();
2635
2851
  }
2636
2852
  };
2637
2853
  }
2638
- patternFor(settings, renderer) {
2639
- const grain = settings.paper.grain;
2640
- const key = grain === true || grain === void 0 ? "builtin" : grain === false ? "none" : grain;
2641
- if (this.pattern?.key === key) return this.pattern.texture;
2642
- this.pattern?.texture.dispose();
2643
- let texture;
2644
- if (key === "builtin") texture = createPaperPattern(renderer);
2645
- else if (key === "none") texture = solidTexture("#000000");
2646
- else {
2647
- texture = solidTexture("#000000");
2648
- loadGrain(key).then(
2649
- (loaded) => {
2650
- if (this.pattern?.key !== key || this.disposed) return loaded.dispose();
2651
- this.pattern.texture.dispose();
2652
- this.pattern.texture = loaded;
2653
- this.remount();
2654
- },
2655
- (error) => {
2656
- if (this.pattern?.key === key && !this.disposed) this.fail(error);
2657
- }
2658
- );
2659
- }
2660
- this.pattern = { texture, key };
2661
- return texture;
2662
- }
2663
2854
  // sizes the canvas and hit area to the container
2664
2855
  layoutBox() {
2665
2856
  if (!this.view) return;
@@ -2724,13 +2915,9 @@ var LeafThrough = class extends EventTarget {
2724
2915
  pagesKey(options) {
2725
2916
  return options.pages.map((p) => sourceKey(pageImage(p))).join("\n");
2726
2917
  }
2727
- preloadRadius() {
2728
- const preload = this.options.preload ?? DEFAULT_PRELOAD;
2729
- return Number.isNaN(preload) ? DEFAULT_PRELOAD : Math.max(0, preload);
2730
- }
2731
2918
  loadPages(renderer, around, layout) {
2732
2919
  this.loader?.dispose();
2733
- const loader = new PageLoader(this.options.pages.map(pageImage), renderer, this.preloadRadius(), {
2920
+ const loader = new PageLoader(this.options.pages.map(pageImage), renderer, preloadRadius(this.options.preload), {
2734
2921
  onLoad: (page, texture) => {
2735
2922
  if (this.loader !== loader) return;
2736
2923
  this.view?.engine.setPageTexture(page, texture);
@@ -2739,7 +2926,7 @@ var LeafThrough = class extends EventTarget {
2739
2926
  },
2740
2927
  onUnload: (page) => {
2741
2928
  if (this.loader !== loader) return;
2742
- this.view?.engine.setPageTexture(page, this.blank);
2929
+ this.view?.engine.setPageTexture(page, this.paper.blank);
2743
2930
  this.needsRender = true;
2744
2931
  },
2745
2932
  onError: (page, error) => this.fail(error, page)
@@ -2781,25 +2968,14 @@ var LeafThrough = class extends EventTarget {
2781
2968
  }
2782
2969
  // --- pages ↔ engine positions -------------------------------------------------------
2783
2970
  pageToPosition(page, layout) {
2784
- const count = this.options.pages.length;
2785
- const p = THREE10.MathUtils.clamp(Math.round(page) || 0, 0, Math.max(count - 1, 0));
2786
- return layout === "spread" ? Math.ceil(p / 2) : p;
2971
+ return pageToPosition(page, layout, this.options.pages.length);
2787
2972
  }
2788
- positionToPage(position, layout) {
2789
- return layout === "spread" ? Math.max(0, 2 * position - 1) : position;
2790
- }
2791
- // the pages showing at an engine position
2792
2973
  pagesAt(position, layout) {
2793
- const candidates = layout === "spread" ? [2 * position - 1, 2 * position] : [position];
2794
- return candidates.filter((p) => p >= 0 && p < this.options.pages.length);
2974
+ return pagesAt(position, layout, this.options.pages.length);
2795
2975
  }
2796
2976
  detail() {
2797
- const count = this.options.pages.length;
2798
2977
  const layout = this.view?.settings.layout ?? this.pickLayout();
2799
- const position = this.view?.engine.position ?? 0;
2800
- const visiblePages = this.pagesAt(position, layout);
2801
- const page = visiblePages.length ? visiblePages[0] : Math.max(0, Math.min(this.positionToPage(position, layout), count - 1));
2802
- return { page, visiblePages, pageCount: count, layout };
2978
+ return pageDetail(this.view?.engine.position ?? 0, layout, this.options.pages.length);
2803
2979
  }
2804
2980
  lastDetailKey = "";
2805
2981
  emitPageChange() {
@@ -2832,49 +3008,13 @@ var LeafThrough = class extends EventTarget {
2832
3008
  }
2833
3009
  // --- input and loop -----------------------------------------------------------------
2834
3010
  bindKeyboard() {
2835
- const handler = (e) => {
2836
- const mode = this.view?.settings.interaction.keyboard;
2837
- if (!mode || e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey) return;
2838
- if (mode === "global") {
2839
- const target = e.target;
2840
- if (target?.closest?.('input, textarea, select, [contenteditable=""], [contenteditable="true"]')) return;
2841
- } else if (e.currentTarget !== this.element) return;
2842
- const last = this.pageCount - 1;
2843
- const actions = {
2844
- ArrowRight: () => this.next(),
2845
- PageDown: () => this.next(),
2846
- ArrowLeft: () => this.prev(),
2847
- PageUp: () => this.prev(),
2848
- Home: () => this.goTo(0),
2849
- End: () => this.goTo(last)
2850
- };
2851
- const action = actions[e.key];
2852
- if (!action) return;
2853
- e.preventDefault();
2854
- action();
2855
- };
2856
- this.element.addEventListener("keydown", handler, { signal: this.abort.signal });
2857
- window.addEventListener(
2858
- "keydown",
2859
- (e) => {
2860
- if (this.view?.settings.interaction.keyboard === "global" && !this.element.contains(e.target)) handler(e);
2861
- },
2862
- { signal: this.abort.signal }
3011
+ bindKeyboard(
3012
+ this.element,
3013
+ () => this.view?.settings.interaction.keyboard,
3014
+ { next: () => this.next(), prev: () => this.prev(), first: () => this.goTo(0), last: () => this.goTo(this.pageCount - 1) },
3015
+ this.abort.signal
2863
3016
  );
2864
3017
  }
2865
- stopRiffle() {
2866
- this.riffle = null;
2867
- }
2868
- stepRiffle(now, engine) {
2869
- const riffle = this.riffle;
2870
- if (!riffle || now - riffle.last < riffle.interval) return;
2871
- if (engine.position === riffle.target) {
2872
- this.riffle = null;
2873
- return;
2874
- }
2875
- riffle.last = now;
2876
- if (!engine.flip(engine.position < riffle.target)) this.riffle = null;
2877
- }
2878
3018
  // only animate while the magazine is on screen and the tab is visible
2879
3019
  syncLoop() {
2880
3020
  const run = this.visible && !document.hidden && !this.disposed;
@@ -2913,7 +3053,7 @@ var LeafThrough = class extends EventTarget {
2913
3053
  frame(now) {
2914
3054
  const view = this.view;
2915
3055
  if (!view) return;
2916
- this.stepRiffle(now, view.engine);
3056
+ this.riffle.step(now, view.engine);
2917
3057
  const moved = view.engine.update(now);
2918
3058
  this.followReader(view.engine, view.settings.layout);
2919
3059
  if (view.engine.activeSide !== this.hitSide) this.placeHitArea();
@@ -2934,45 +3074,44 @@ var LeafThrough = class extends EventTarget {
2934
3074
  this.syncContent(view);
2935
3075
  }
2936
3076
  };
2937
- function colorKey(color) {
2938
- return new THREE10.Color(color).getHexString();
2939
- }
2940
3077
  function createRenderer(canvas) {
2941
3078
  let renderer;
2942
3079
  try {
2943
- renderer = new THREE10.WebGLRenderer({ canvas, antialias: true, alpha: true });
3080
+ renderer = new THREE13.WebGLRenderer({ canvas, antialias: true, alpha: true });
2944
3081
  } catch {
2945
3082
  return null;
2946
3083
  }
2947
- renderer.outputColorSpace = THREE10.SRGBColorSpace;
3084
+ renderer.outputColorSpace = THREE13.SRGBColorSpace;
2948
3085
  renderer.shadowMap.enabled = true;
2949
- renderer.shadowMap.type = THREE10.PCFShadowMap;
3086
+ renderer.shadowMap.type = THREE13.PCFShadowMap;
2950
3087
  renderer.shadowMap.autoUpdate = false;
2951
3088
  return renderer;
2952
3089
  }
2953
- function snapshot(value) {
2954
- if (typeof value !== "object" || value === null) return value;
2955
- if (Array.isArray(value)) return value.map(snapshot);
2956
- if (Object.getPrototypeOf(value) !== Object.prototype) return value;
2957
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item)]));
2958
- }
2959
- function deepEqual(a, b) {
2960
- if (Object.is(a, b)) return true;
2961
- if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
2962
- if (Array.isArray(a) !== Array.isArray(b)) return false;
2963
- const protoA = Object.getPrototypeOf(a);
2964
- if (protoA !== Object.prototype && protoA !== Array.prototype) return false;
2965
- if (protoA !== Object.getPrototypeOf(b)) return false;
2966
- const keysA = Object.keys(a);
2967
- const keysB = Object.keys(b);
2968
- if (keysA.length !== keysB.length) return false;
2969
- return keysA.every((key) => deepEqual(a[key], b[key]));
2970
- }
2971
3090
 
2972
3091
  export {
2973
3092
  DEFAULT_SPREAD_QUERY,
2974
3093
  DEFAULT_PAGE_ASPECT,
2975
3094
  defaultPageLabel,
3095
+ pageImage,
3096
+ resolveSettings,
3097
+ LIVE_OPTIONS,
3098
+ preloadRadius,
3099
+ snapshot,
3100
+ changedOptions,
3101
+ sourceKey,
3102
+ floorPoint,
3103
+ rayThrough,
3104
+ framingFor,
3105
+ plainFramingFor,
3106
+ pageToPosition,
3107
+ pagesAt,
3108
+ pageDetail,
3109
+ Riffle,
3110
+ bindKeyboard,
3111
+ PageLoader,
3112
+ PaperTextures,
3113
+ SingleEngine,
3114
+ SpreadEngine,
2976
3115
  LeafThrough
2977
3116
  };
2978
- //# sourceMappingURL=chunk-XPIDTVB5.js.map
3117
+ //# sourceMappingURL=chunk-GYSQO7QZ.js.map