leafthrough 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -4
- package/dist/LeafThrough.d.ts +2 -9
- package/dist/LeafThroughObject.d.ts +151 -0
- package/dist/{chunk-XPIDTVB5.js → chunk-GYSQO7QZ.js} +758 -619
- package/dist/chunk-GYSQO7QZ.js.map +1 -0
- package/dist/element.js +1 -1
- package/dist/engine.d.ts +25 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +367 -2
- package/dist/index.js.map +1 -1
- package/dist/navigation.d.ts +26 -0
- package/dist/options.d.ts +14 -0
- package/dist/paper/paperTextures.d.ts +25 -0
- package/dist/react.js +1 -1
- package/dist/single/SingleEngine.d.ts +7 -9
- package/dist/spread/SpreadEngine.d.ts +7 -8
- package/dist/svelte.js +1 -1
- package/dist/vue.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-XPIDTVB5.js.map +0 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
312
|
-
side:
|
|
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
|
-
|
|
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(
|
|
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
|
|
401
|
-
depthPacking:
|
|
402
|
-
side:
|
|
729
|
+
const depthMaterial = new THREE5.MeshDepthMaterial({
|
|
730
|
+
depthPacking: THREE5.RGBADepthPacking,
|
|
731
|
+
side: THREE5.DoubleSide
|
|
403
732
|
});
|
|
404
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
|
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
|
|
793
|
+
const c = new THREE5.Color(color);
|
|
444
794
|
const srgb = c.clone().convertLinearToSRGB();
|
|
445
|
-
const texture = new
|
|
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 =
|
|
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
|
|
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
|
|
550
|
-
texture.wrapS = texture.wrapT =
|
|
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/
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
1073
|
+
import * as THREE10 from "three";
|
|
667
1074
|
|
|
668
1075
|
// src/single/singleSheet.ts
|
|
669
|
-
import * as
|
|
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
|
|
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
|
|
795
|
-
uPileReach: { value: new
|
|
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
|
|
1204
|
+
uTailCurl: { value: new THREE9.Vector4(1, 0, 1, 1) },
|
|
798
1205
|
uTailBend: { value: 0 },
|
|
799
|
-
uCone: { value: new
|
|
800
|
-
uFlipRotation: { value: new
|
|
801
|
-
uTrim: { value: new
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
910
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
1005
|
-
flipTime:
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
this.
|
|
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
|
|
1074
|
-
sheetAt(
|
|
1075
|
-
|
|
1076
|
-
const
|
|
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.
|
|
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.
|
|
1521
|
+
this.wake();
|
|
1114
1522
|
}
|
|
1115
1523
|
// --- input ------------------------------------------------------------------
|
|
1116
1524
|
bindInput() {
|
|
1117
|
-
const el = this.
|
|
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.
|
|
1129
|
-
this.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1873
|
+
import * as THREE12 from "three";
|
|
1461
1874
|
|
|
1462
1875
|
// src/spread/spreadSheet.ts
|
|
1463
|
-
import * as
|
|
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
|
|
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
|
|
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
|
|
1578
|
-
uCurl: { value: new
|
|
1579
|
-
uFlipRotation: { value: new
|
|
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 =
|
|
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
|
|
2041
|
+
group = new THREE12.Group();
|
|
1629
2042
|
maxPosition;
|
|
1630
2043
|
settings;
|
|
1631
2044
|
halfHeight;
|
|
1632
|
-
|
|
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
|
-
|
|
1653
|
-
|
|
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.
|
|
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.
|
|
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
|
|
1749
|
-
sheetAt(clientX) {
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
const forward =
|
|
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
|
|
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 =
|
|
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.
|
|
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.
|
|
2256
|
+
this.wake();
|
|
1836
2257
|
}
|
|
1837
2258
|
// --- input ------------------------------------------------------------------
|
|
1838
2259
|
bindInput() {
|
|
1839
|
-
const el = this.
|
|
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:
|
|
2274
|
+
this.hover = { active: this.input.covers(e.clientX, e.clientY), x: e.clientX, y: e.clientY };
|
|
1854
2275
|
}
|
|
1855
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
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
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
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
|
-
|
|
2265
|
-
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
const
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2384
|
-
|
|
2385
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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) => !
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
2577
|
-
const camera = new
|
|
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
|
|
2582
|
-
const sun = new
|
|
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
|
|
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
|
|
2811
|
+
let floor2 = null;
|
|
2596
2812
|
if (layout === "spread" && shadows && settings.lighting.floorShadow > 0) {
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
scene.add(
|
|
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
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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.
|
|
2617
|
-
pattern: this.
|
|
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
|
-
|
|
2634
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
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.
|
|
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
|
|
3080
|
+
renderer = new THREE13.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
2944
3081
|
} catch {
|
|
2945
3082
|
return null;
|
|
2946
3083
|
}
|
|
2947
|
-
renderer.outputColorSpace =
|
|
3084
|
+
renderer.outputColorSpace = THREE13.SRGBColorSpace;
|
|
2948
3085
|
renderer.shadowMap.enabled = true;
|
|
2949
|
-
renderer.shadowMap.type =
|
|
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-
|
|
3117
|
+
//# sourceMappingURL=chunk-GYSQO7QZ.js.map
|