postext-folio 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bookGeometry.d.ts +2 -1
- package/dist/bookGeometry.d.ts.map +1 -1
- package/dist/bookGeometry.js +10 -4
- package/dist/bookGeometry.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/pageFlip.d.ts +98 -0
- package/dist/pageFlip.d.ts.map +1 -1
- package/dist/pageFlip.js +322 -53
- package/dist/pageFlip.js.map +1 -1
- package/dist/pageFlipFlat.d.ts +1 -1
- package/dist/pageFlipFlat.d.ts.map +1 -1
- package/dist/pageFlipFlat.js +8 -0
- package/dist/pageFlipFlat.js.map +1 -1
- package/dist/postext.d.ts +17 -0
- package/dist/postext.d.ts.map +1 -1
- package/dist/postext.js +113 -9
- package/dist/postext.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +21 -0
- package/dist/styles.js.map +1 -1
- package/dist/viewer.d.ts +31 -0
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +88 -3
- package/dist/viewer.js.map +1 -1
- package/package.json +3 -3
package/dist/pageFlip.js
CHANGED
|
@@ -16,6 +16,13 @@ const FOV = 24;
|
|
|
16
16
|
const ROLL = 0.11;
|
|
17
17
|
/** Milliseconds one leaf takes to turn. */
|
|
18
18
|
const DURATION = 1000;
|
|
19
|
+
/** A jump of more pages than this is made in one move: the block of
|
|
20
|
+
* leaves in between is lifted together and laid over on the other side,
|
|
21
|
+
* instead of every leaf turning on its own. */
|
|
22
|
+
export const BLOCK_PAGES = 10;
|
|
23
|
+
/** How long a block of leaves takes to go over (ms), a little longer for
|
|
24
|
+
* a thicker one. */
|
|
25
|
+
const BLOCK_DURATION = 1150;
|
|
19
26
|
/** A leaf follows the one before it once that one is this far through its turn. */
|
|
20
27
|
const GAP = 0.14;
|
|
21
28
|
// Ambient occlusion from the book's own shape: each frame the book is
|
|
@@ -274,24 +281,92 @@ function pageMaterial(mirror, sign) {
|
|
|
274
281
|
};
|
|
275
282
|
return m;
|
|
276
283
|
}
|
|
284
|
+
/** The uniforms every page block's edge material shares: book units per
|
|
285
|
+
* mm, and how strongly the cut face is relieved (uncoated stock shows
|
|
286
|
+
* its fibres; a coated sheet is cut cleaner). */
|
|
287
|
+
const edgeUniforms = {
|
|
288
|
+
uEdgeK: { value: 1 },
|
|
289
|
+
uEdgeRelief: { value: 1 },
|
|
290
|
+
};
|
|
291
|
+
// The cut face of a page block, as a height field in mm over (leaf, mm
|
|
292
|
+
// along the cut): every leaf a rounded ridge standing a hair proud of or
|
|
293
|
+
// behind its neighbours (sheets are never quite in line), the sections
|
|
294
|
+
// and the bundles the knife took together a little out of line with the
|
|
295
|
+
// next, and the faint streaks the guillotine drags across the block. Each
|
|
296
|
+
// scale fades out where it is finer than a pixel, so the face never
|
|
297
|
+
// shimmers. The relief bends the normal (bump mapping from the screen-
|
|
298
|
+
// space derivatives of the field), so the face catches a grazing light.
|
|
299
|
+
const EDGE_RELIEF = /* glsl */ `
|
|
300
|
+
uniform float uEdgeK;
|
|
301
|
+
uniform float uEdgeRelief;
|
|
302
|
+
varying float vRun;
|
|
303
|
+
float eHash(float n) { return fract(sin(n * 127.1) * 43758.5453); }
|
|
304
|
+
float eHash2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
|
305
|
+
float eNoise(vec2 p) {
|
|
306
|
+
vec2 i = floor(p);
|
|
307
|
+
vec2 f = fract(p);
|
|
308
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
309
|
+
return mix(mix(eHash2(i), eHash2(i + vec2(1.0, 0.0)), u.x), mix(eHash2(i + vec2(0.0, 1.0)), eHash2(i + vec2(1.0, 1.0)), u.x), u.y);
|
|
310
|
+
}
|
|
311
|
+
// fade: x = the leaves, y = the bundles, z = the fine knife marks.
|
|
312
|
+
float edgeHeight(vec2 lv, vec3 fade) {
|
|
313
|
+
float id = floor(lv.x);
|
|
314
|
+
float ridge = sin(fract(lv.x) * PI);
|
|
315
|
+
float proud = eHash(id) - 0.5 + 0.8 * (eNoise(vec2(id * 0.37, lv.y * 0.12)) - 0.5);
|
|
316
|
+
float leaf = (0.45 * ridge + proud) * 0.04 * fade.x;
|
|
317
|
+
float bundle = (eNoise(vec2(lv.x / 5.0, lv.y * 0.03)) - 0.5) * 0.12 * fade.y
|
|
318
|
+
+ (eNoise(vec2(lv.x / 24.0 + 7.0, lv.y * 0.02)) - 0.5) * 0.18;
|
|
319
|
+
float knife = (eNoise(vec2(lv.y * 0.7, lv.x / 90.0)) - 0.5) * 0.035
|
|
320
|
+
+ (eNoise(vec2(lv.y * 3.0, lv.x / 40.0)) - 0.5) * 0.006 * fade.z;
|
|
321
|
+
return leaf + bundle + knife;
|
|
322
|
+
}
|
|
323
|
+
vec3 edgeNormal(vec3 surfPos, vec3 n, vec2 dH, float faceDir) {
|
|
324
|
+
vec3 sx = dFdx(surfPos);
|
|
325
|
+
vec3 sy = dFdy(surfPos);
|
|
326
|
+
vec3 r1 = cross(sy, n);
|
|
327
|
+
vec3 r2 = cross(n, sx);
|
|
328
|
+
float det = dot(sx, r1) * faceDir;
|
|
329
|
+
vec3 grad = sign(det) * (dH.x * r1 + dH.y * r2);
|
|
330
|
+
return normalize(abs(det) * n - grad);
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
277
333
|
/** The page block's edges: paper with a fine line between leaves (fading
|
|
278
|
-
* to their average where the lines are finer than a pixel)
|
|
279
|
-
|
|
334
|
+
* to their average where the lines are finer than a pixel), its cut face
|
|
335
|
+
* in relief. */
|
|
336
|
+
function edgeMaterial(sign, bands, paper) {
|
|
280
337
|
const m = new MeshStandardMaterial({ side: DoubleSide, roughness: 0.92 });
|
|
338
|
+
if (paper)
|
|
339
|
+
m.color.copy(paper);
|
|
281
340
|
const band = { uBoardLo: { value: bands?.lo ?? -1 }, uBoardHi: { value: bands?.hi ?? 2 }, uBoardColor: { value: bands?.color ?? new Color(0, 0, 0) } };
|
|
282
341
|
m.onBeforeCompile = (shader) => {
|
|
283
|
-
Object.assign(shader.uniforms, occlusionUniforms, band, { uSign: { value: sign } });
|
|
342
|
+
Object.assign(shader.uniforms, occlusionUniforms, edgeUniforms, band, { uSign: { value: sign } });
|
|
284
343
|
shader.vertexShader = shader.vertexShader
|
|
285
|
-
.replace("#include <common>", `#include <common>\nattribute float layer;\nattribute float lambda;\nvarying float vLayer;\nvarying float vLambda;\n${BOOK_POS_PARS}`)
|
|
286
|
-
.replace("#include <begin_vertex>", `#include <begin_vertex>\nvLayer = layer;\nvLambda = lambda;\n${BOOK_POS}`);
|
|
344
|
+
.replace("#include <common>", `#include <common>\nattribute float layer;\nattribute float lambda;\nattribute float run;\nvarying float vLayer;\nvarying float vLambda;\nvarying float vRun;\n${BOOK_POS_PARS}`)
|
|
345
|
+
.replace("#include <begin_vertex>", `#include <begin_vertex>\nvLayer = layer;\nvLambda = lambda;\nvRun = run;\n${BOOK_POS}`);
|
|
287
346
|
shader.fragmentShader = keyShadowed(shader.fragmentShader)
|
|
288
|
-
.replace("#include <common>", `#include <common>\nuniform float uBoardLo;\nuniform float uBoardHi;\nuniform vec3 uBoardColor;\nvarying float vLayer;\nvarying float vLambda;\nvarying vec3 vBookPos;\n${OCCLUSION}`)
|
|
347
|
+
.replace("#include <common>", `#include <common>\nuniform float uBoardLo;\nuniform float uBoardHi;\nuniform vec3 uBoardColor;\nvarying float vLayer;\nvarying float vLambda;\nvarying vec3 vBookPos;\n${OCCLUSION}\n${EDGE_RELIEF}`)
|
|
348
|
+
.replace("#include <normal_fragment_maps>", `{
|
|
349
|
+
vec2 lv = vec2(vLayer, vRun / uEdgeK);
|
|
350
|
+
vec2 dx = dFdx(lv);
|
|
351
|
+
vec2 dy = dFdy(lv);
|
|
352
|
+
float wl = fwidth(vLayer);
|
|
353
|
+
vec3 fade = vec3(
|
|
354
|
+
1.0 - smoothstep(0.3, 0.8, wl),
|
|
355
|
+
1.0 - smoothstep(2.0, 5.0, wl),
|
|
356
|
+
1.0 - smoothstep(0.05, 0.15, fwidth(lv.y))
|
|
357
|
+
);
|
|
358
|
+
float h0 = edgeHeight(lv, fade);
|
|
359
|
+
vec2 dH = vec2(edgeHeight(lv + dx, fade) - h0, edgeHeight(lv + dy, fade) - h0) * uEdgeK * uEdgeRelief;
|
|
360
|
+
if (vLambda >= uBoardLo && vLambda <= uBoardHi) normal = edgeNormal(-vViewPosition, normal, dH, faceDirection);
|
|
361
|
+
}`)
|
|
289
362
|
.replace("#include <map_fragment>", `{
|
|
290
363
|
float w = fwidth(vLayer);
|
|
291
364
|
float d = abs(fract(vLayer) - 0.5) * 2.0;
|
|
292
365
|
float line = smoothstep(0.55 - w, 0.95 + w, d);
|
|
293
366
|
float fine = clamp(w * 1.5, 0.0, 1.0);
|
|
294
367
|
diffuseColor.rgb *= 1.0 - mix(0.22 * line, 0.08, fine);
|
|
368
|
+
// The bundles' sheets are never quite one shade.
|
|
369
|
+
diffuseColor.rgb *= 1.0 + 0.06 * uEdgeRelief * (eNoise(vec2(vLayer / 9.0, 3.0)) - 0.5);
|
|
295
370
|
// A board lying in the block (the book's own cover) shows its
|
|
296
371
|
// own edge, not paper.
|
|
297
372
|
if (vLambda < uBoardLo || vLambda > uBoardHi) diffuseColor.rgb = uBoardColor;
|
|
@@ -389,7 +464,7 @@ function surfaceAt(book, signed) {
|
|
|
389
464
|
* page lifts and shrinks to nothing as it lands on the other side; a
|
|
390
465
|
* stiffer paper (`roll`) bends in a wider roll.
|
|
391
466
|
*/
|
|
392
|
-
function foldOf(G, P, W, H, roll = 1) {
|
|
467
|
+
export function foldOf(G, P, W, H, roll = 1) {
|
|
393
468
|
const du = G.u - P.u;
|
|
394
469
|
const dv = G.v - P.v;
|
|
395
470
|
const D = Math.hypot(du, dv);
|
|
@@ -416,7 +491,7 @@ function foldOf(G, P, W, H, roll = 1) {
|
|
|
416
491
|
}
|
|
417
492
|
/** How far through its turn a leaf is: the point taken, from where it
|
|
418
493
|
* was to its mirror image across the spine. */
|
|
419
|
-
function progressFrom(G, P) {
|
|
494
|
+
export function progressFrom(G, P) {
|
|
420
495
|
return Math.min(1, Math.max(0, (G.u - P.u) / (2 * G.u)));
|
|
421
496
|
}
|
|
422
497
|
/** The hand can carry the point taken no farther from the spine's ends
|
|
@@ -442,7 +517,7 @@ function reach(G, P, H) {
|
|
|
442
517
|
* the spine the left one's. A stiff leaf (`rigidity`) turns more as a
|
|
443
518
|
* plate on its hinge: a board does not bend at all.
|
|
444
519
|
*/
|
|
445
|
-
function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q, flutter = 0, time = 0, board) {
|
|
520
|
+
export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q, flutter = 0, time = 0, board) {
|
|
446
521
|
const pos = geometry.attributes.position;
|
|
447
522
|
const sx = forward ? 1 : -1;
|
|
448
523
|
const [xr, zr] = along(book.right, W);
|
|
@@ -495,6 +570,25 @@ function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q, flutter
|
|
|
495
570
|
// it leans off that slope, not off a level plane over it.
|
|
496
571
|
const chordR = (zr - zr0) / (xr || 1);
|
|
497
572
|
const chordL = (zl - zl0) / (xl || 1);
|
|
573
|
+
// Onto the book. At rest (and as it lands) the leaf lies on the open
|
|
574
|
+
// book's surface, into the gutter and out. In the air it is lifted off
|
|
575
|
+
// at the spine: its own plane rises from the gutter's floor at a small
|
|
576
|
+
// angle, and it touches the pages only where they stand higher (the
|
|
577
|
+
// gutter's walls), joined smoothly. `z`: how far over that it stands.
|
|
578
|
+
const onBook = (s, z) => {
|
|
579
|
+
const [rx, rz] = surfaceAt(book, s);
|
|
580
|
+
let px = rx;
|
|
581
|
+
let pz = rz + z + lift;
|
|
582
|
+
if (inAir > 0) {
|
|
583
|
+
const fx = (s >= 0 ? 1 : -1) * Math.abs(s) * cosA;
|
|
584
|
+
const plane = (s >= 0 ? zr0 + s * chordR : zl0 - s * chordL) + Math.abs(s) * sinA + z + lift;
|
|
585
|
+
const under = surfaceAt(book, fx)[1] + lift + 0.6;
|
|
586
|
+
const flying = smoothMax(plane, under, 0.006 * W);
|
|
587
|
+
px += (fx - px) * inAir;
|
|
588
|
+
pz += (flying - pz) * inAir;
|
|
589
|
+
}
|
|
590
|
+
return [px, pz];
|
|
591
|
+
};
|
|
498
592
|
for (let iy = 0; iy <= NY; iy++) {
|
|
499
593
|
const v = (0.5 - iy / NY) * H;
|
|
500
594
|
for (let ix = 0; ix <= NX; ix++) {
|
|
@@ -502,13 +596,14 @@ function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q, flutter
|
|
|
502
596
|
let x = u;
|
|
503
597
|
let y = v;
|
|
504
598
|
let z = 0;
|
|
599
|
+
let d = 0;
|
|
600
|
+
let dd = 0;
|
|
505
601
|
if (fold) {
|
|
506
602
|
const { F, n, R } = fold;
|
|
507
|
-
|
|
603
|
+
d = (u - F.u) * n.u + (v - F.v) * n.v;
|
|
508
604
|
if (d > 0) {
|
|
509
605
|
// Round the roll, then straight on at the fold's angle.
|
|
510
606
|
const theta = fold.theta;
|
|
511
|
-
let dd;
|
|
512
607
|
if (R > 1e-3 && d < R * theta) {
|
|
513
608
|
dd = R * Math.sin(d / R);
|
|
514
609
|
z = R * (1 - Math.cos(d / R));
|
|
@@ -522,22 +617,17 @@ function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q, flutter
|
|
|
522
617
|
y = v - n.v * (d - dd);
|
|
523
618
|
}
|
|
524
619
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const plane = (s >= 0 ? zr0 + s * chordR : zl0 - s * chordL) + Math.abs(s) * sinA + z + lift;
|
|
537
|
-
const under = surfaceAt(book, fx)[1] + lift + 0.6;
|
|
538
|
-
const flying = smoothMax(plane, under, 0.006 * W);
|
|
539
|
-
px += (fx - px) * inAir;
|
|
540
|
-
pz += (flying - pz) * inAir;
|
|
620
|
+
let [px, pz] = onBook(sx * x, z);
|
|
621
|
+
if (fold && d > 0) {
|
|
622
|
+
// Paper up in the air is not pressed onto the pages under it: wrapped
|
|
623
|
+
// onto them it dipped into the gutter and was squeezed across it (a
|
|
624
|
+
// crease where the curl passed over the spine). It goes on from the
|
|
625
|
+
// foot of its fold, level, as the fold has it, and only comes down
|
|
626
|
+
// onto the pages as it does (lying back over itself, landing).
|
|
627
|
+
const [fx, fz] = onBook(sx * (u - fold.n.u * d), 0);
|
|
628
|
+
const w = smooth(0, 0.05 * W, z);
|
|
629
|
+
px += (fx + sx * fold.n.u * dd - px) * w;
|
|
630
|
+
pz += (fz + z - pz) * w;
|
|
541
631
|
}
|
|
542
632
|
if (r > 0) {
|
|
543
633
|
px += (u * Math.cos(phi) - offset * Math.sin(phi) - px) * r;
|
|
@@ -632,6 +722,10 @@ export class PageFlipper {
|
|
|
632
722
|
/** Leaf k lies on the left. */
|
|
633
723
|
turned;
|
|
634
724
|
turns = new Map();
|
|
725
|
+
/** A long jump in progress: leaves lo … hi − 1 turning over as one
|
|
726
|
+
* block (a rigid slab as thick as they are together). */
|
|
727
|
+
block = null;
|
|
728
|
+
blockMesh = null;
|
|
635
729
|
target;
|
|
636
730
|
raf = 0;
|
|
637
731
|
lastFrame = 0;
|
|
@@ -774,9 +868,10 @@ export class PageFlipper {
|
|
|
774
868
|
paperSpec({ ...r.paper, grammage: Math.min(350, Math.round(r.paper.grammage * 1.5)), showThrough: false })
|
|
775
869
|
: {
|
|
776
870
|
...paperSpec({ ...r.paper, type: "board", grammage: 1250, bulk: 1.6, finish: "silk", texture: "smooth", shade: { hex: "#ffffff", model: "hex" }, showThrough: false }),
|
|
777
|
-
// The book's own printed covers: a
|
|
778
|
-
//
|
|
779
|
-
|
|
871
|
+
// The book's own printed covers: in a case binding a printed
|
|
872
|
+
// (litho-laminated) case, on the case's 2–3 mm board; on a
|
|
873
|
+
// paperback a laminated cover card, about 0.3 mm.
|
|
874
|
+
caliperMm: BINDINGS[r.binding.type].boardMm || 0.45,
|
|
780
875
|
// Board does not bend: it turns on its joint as a plate.
|
|
781
876
|
rigidity: 1,
|
|
782
877
|
roughness: 0.45,
|
|
@@ -854,6 +949,7 @@ export class PageFlipper {
|
|
|
854
949
|
this.key.shadow.intensity = 0.55 + 0.25 * (1 - env.softness);
|
|
855
950
|
this.renderer.toneMappingExposure = env.exposure * r.lighting.intensity;
|
|
856
951
|
this.edges.color.set(r.paper.shade.hex);
|
|
952
|
+
edgeUniforms.uEdgeRelief.value = r.paper.finish === "uncoated" ? 1 : r.paper.finish === "matte" ? 0.7 : 0.5;
|
|
857
953
|
this.surfaceKey = "";
|
|
858
954
|
for (const mesh of [this.left, this.right, ...this.leaves.values()])
|
|
859
955
|
mesh.material.userData.spec = null;
|
|
@@ -922,8 +1018,13 @@ export class PageFlipper {
|
|
|
922
1018
|
/** The paper of leaf k (outside the pages shown: the book's). */
|
|
923
1019
|
specOf(k) {
|
|
924
1020
|
const covers = this.appearance.coverLeaves;
|
|
925
|
-
if (this.coverSpec && (k === covers?.front || k === covers?.back))
|
|
926
|
-
|
|
1021
|
+
if (this.coverSpec && (k === covers?.front || k === covers?.back)) {
|
|
1022
|
+
// A stapled cover printed on a stock of its own (a `:::paper` card
|
|
1023
|
+
// round newsprint pages) is that sheet; a board is a board whatever
|
|
1024
|
+
// is printed on it.
|
|
1025
|
+
const own = this.resolved.binding.type === "saddleStitch" && this.appearance.leafPapers?.[k];
|
|
1026
|
+
return own ? this.leafSpecs[k] : this.coverSpec;
|
|
1027
|
+
}
|
|
927
1028
|
return this.leafSpecs[k] ?? this.bookSpec;
|
|
928
1029
|
}
|
|
929
1030
|
dressPage(mesh, spec) {
|
|
@@ -959,11 +1060,20 @@ export class PageFlipper {
|
|
|
959
1060
|
const left = [];
|
|
960
1061
|
const right = [];
|
|
961
1062
|
this.turned.forEach((t, k) => {
|
|
962
|
-
if (!this.turns.has(k))
|
|
1063
|
+
if (!this.turns.has(k) && !this.inBlock(k))
|
|
963
1064
|
(t ? left : right).push(k);
|
|
964
1065
|
});
|
|
965
1066
|
return { left, right };
|
|
966
1067
|
}
|
|
1068
|
+
/** Leaf k is part of the block in the air. */
|
|
1069
|
+
inBlock(k) {
|
|
1070
|
+
return !!this.block && k >= this.block.lo && k < this.block.hi;
|
|
1071
|
+
}
|
|
1072
|
+
/** Pages a leaf carries: two, or one when the book shows a page at a
|
|
1073
|
+
* time (each sheet's back is blank). */
|
|
1074
|
+
pagesPerLeaf() {
|
|
1075
|
+
return this.appearance.singlePage ? 1 : 2;
|
|
1076
|
+
}
|
|
967
1077
|
/** Loads the spine picture, then rebuilds the book to print it. */
|
|
968
1078
|
async loadSpine(src) {
|
|
969
1079
|
let tex = null;
|
|
@@ -1050,6 +1160,7 @@ export class PageFlipper {
|
|
|
1050
1160
|
return;
|
|
1051
1161
|
this.surfaceKey = key;
|
|
1052
1162
|
const k = this.pxPerMm();
|
|
1163
|
+
edgeUniforms.uEdgeK.value = k;
|
|
1053
1164
|
const half = this.appearance.singlePage ? 0.5 : 1;
|
|
1054
1165
|
const thick = (leaves, more) => (leaves.reduce((s, i) => s + this.specOf(i).caliperMm, 0) * half + more * this.bookSpec.caliperMm) * k;
|
|
1055
1166
|
const tL = thick(left, extra.before);
|
|
@@ -1117,7 +1228,7 @@ export class PageFlipper {
|
|
|
1117
1228
|
color: this.boardColor(),
|
|
1118
1229
|
}
|
|
1119
1230
|
: undefined;
|
|
1120
|
-
const mesh = new Mesh(stackGeometry(p, side, W, H, Math.round(n * half)), bands ? edgeMaterial(this.sign, bands) : this.edges);
|
|
1231
|
+
const mesh = new Mesh(stackGeometry(p, side, W, H, Math.round(n * half)), bands ? edgeMaterial(this.sign, bands, this.edges.color) : this.edges);
|
|
1121
1232
|
mesh.castShadow = mesh.receiveShadow = true;
|
|
1122
1233
|
this.stacks.push(mesh);
|
|
1123
1234
|
this.stage.add(mesh);
|
|
@@ -1385,6 +1496,17 @@ export class PageFlipper {
|
|
|
1385
1496
|
mesh.material.userData.uniforms.uPaper.value.setRGB(r, g, b, SRGBColorSpace);
|
|
1386
1497
|
this.redraw();
|
|
1387
1498
|
}
|
|
1499
|
+
/** A page source drawn again in place (a canvas repainted): its
|
|
1500
|
+
* texture is uploaded again and the book redrawn. */
|
|
1501
|
+
touch(src) {
|
|
1502
|
+
if (!src || typeof src === "string")
|
|
1503
|
+
return;
|
|
1504
|
+
const tex = this.ready.get(src);
|
|
1505
|
+
if (!tex)
|
|
1506
|
+
return;
|
|
1507
|
+
tex.needsUpdate = true;
|
|
1508
|
+
this.redraw();
|
|
1509
|
+
}
|
|
1388
1510
|
/** Starts loading the pages of these spreads as textures. */
|
|
1389
1511
|
preload(indexes) {
|
|
1390
1512
|
for (const i of indexes)
|
|
@@ -1433,6 +1555,7 @@ export class PageFlipper {
|
|
|
1433
1555
|
cancelAnimationFrame(this.raf);
|
|
1434
1556
|
this.raf = 0;
|
|
1435
1557
|
this.turns.clear();
|
|
1558
|
+
this.block = null;
|
|
1436
1559
|
for (const mesh of this.leaves.values()) {
|
|
1437
1560
|
mesh.removeFromParent();
|
|
1438
1561
|
mesh.userData.caster.removeFromParent();
|
|
@@ -1492,17 +1615,7 @@ export class PageFlipper {
|
|
|
1492
1615
|
leaf(k) {
|
|
1493
1616
|
let mesh = this.leaves.get(k);
|
|
1494
1617
|
if (!mesh) {
|
|
1495
|
-
|
|
1496
|
-
spreadColumns(geometry, NX);
|
|
1497
|
-
geometry.setAttribute("ao", new BufferAttribute(new Float32Array(geometry.attributes.position.count).fill(1), 1));
|
|
1498
|
-
mesh = new Mesh(geometry, pageMaterial(this.sign < 0, this.sign));
|
|
1499
|
-
mesh.material.userData.uniforms.uPaper.value.setRGB(...this.paper, SRGBColorSpace);
|
|
1500
|
-
mesh.castShadow = mesh.receiveShadow = true;
|
|
1501
|
-
mesh.frustumCulled = false;
|
|
1502
|
-
const caster = new Mesh(mesh.geometry, this.leafCaster);
|
|
1503
|
-
caster.frustumCulled = false;
|
|
1504
|
-
this.shadowScene.add(caster);
|
|
1505
|
-
mesh.userData.caster = caster;
|
|
1618
|
+
mesh = this.leafMesh();
|
|
1506
1619
|
this.assign(mesh, "Front", this.book[k]?.[1] ?? null);
|
|
1507
1620
|
this.assign(mesh, "Back", this.book[k + 1]?.[0] ?? null);
|
|
1508
1621
|
this.leaves.set(k, mesh);
|
|
@@ -1510,6 +1623,39 @@ export class PageFlipper {
|
|
|
1510
1623
|
this.dressPage(mesh, this.specOf(k));
|
|
1511
1624
|
return mesh;
|
|
1512
1625
|
}
|
|
1626
|
+
/** A leaf's mesh, its pages not yet assigned. */
|
|
1627
|
+
leafMesh() {
|
|
1628
|
+
const geometry = new PlaneGeometry(1, 1, NX, NY);
|
|
1629
|
+
spreadColumns(geometry, NX);
|
|
1630
|
+
geometry.setAttribute("ao", new BufferAttribute(new Float32Array(geometry.attributes.position.count).fill(1), 1));
|
|
1631
|
+
const mesh = new Mesh(geometry, pageMaterial(this.sign < 0, this.sign));
|
|
1632
|
+
mesh.material.userData.uniforms.uPaper.value.setRGB(...this.paper, SRGBColorSpace);
|
|
1633
|
+
mesh.castShadow = mesh.receiveShadow = true;
|
|
1634
|
+
mesh.frustumCulled = false;
|
|
1635
|
+
const caster = new Mesh(mesh.geometry, this.leafCaster);
|
|
1636
|
+
caster.frustumCulled = false;
|
|
1637
|
+
caster.visible = false;
|
|
1638
|
+
this.shadowScene.add(caster);
|
|
1639
|
+
mesh.userData.caster = caster;
|
|
1640
|
+
return mesh;
|
|
1641
|
+
}
|
|
1642
|
+
/** The block in the air: its top page (the first leaf's front) and its
|
|
1643
|
+
* bottom one (the last leaf's back), and the stock of its first leaf. */
|
|
1644
|
+
blockLeaf(lo, hi) {
|
|
1645
|
+
this.blockMesh ??= this.leafMesh();
|
|
1646
|
+
const mesh = this.blockMesh;
|
|
1647
|
+
this.assign(mesh, "Front", this.book[lo]?.[1] ?? null);
|
|
1648
|
+
this.assign(mesh, "Back", this.book[hi]?.[0] ?? null);
|
|
1649
|
+
this.dressPage(mesh, this.specOf(lo));
|
|
1650
|
+
return mesh;
|
|
1651
|
+
}
|
|
1652
|
+
/** How thick the leaves lo … hi − 1 are together (book units). */
|
|
1653
|
+
blockThickness(lo, hi) {
|
|
1654
|
+
let mm = 0;
|
|
1655
|
+
for (let k = lo; k < hi; k++)
|
|
1656
|
+
mm += this.specOf(k).caliperMm;
|
|
1657
|
+
return Math.max(0.5, mm * (this.appearance.singlePage ? 0.5 : 1) * this.pxPerMm());
|
|
1658
|
+
}
|
|
1513
1659
|
/** A board leaf's other face and edges, carried with its mesh. */
|
|
1514
1660
|
boardParts(mesh) {
|
|
1515
1661
|
let parts = mesh.userData.board;
|
|
@@ -1540,11 +1686,15 @@ export class PageFlipper {
|
|
|
1540
1686
|
// The first leaves' pages are warm; wait a moment for cold ones.
|
|
1541
1687
|
this.starting = true;
|
|
1542
1688
|
const wanted = [];
|
|
1543
|
-
|
|
1544
|
-
|
|
1689
|
+
// A long jump goes over in one block: only the pages it lands on (and
|
|
1690
|
+
// the block's two faces) show, not the ones in between.
|
|
1691
|
+
const blockwise = (hi - lo) * this.pagesPerLeaf() > BLOCK_PAGES;
|
|
1692
|
+
const ends = blockwise ? [lo, hi] : Array.from({ length: Math.min(hi, lo + 2) - lo + 1 }, (_, i) => lo + i);
|
|
1693
|
+
for (const i of ends)
|
|
1694
|
+
for (const src of this.book[i] ?? [])
|
|
1545
1695
|
if (src)
|
|
1546
1696
|
wanted.push(src);
|
|
1547
|
-
this.preload(Array.from({ length: hi - lo + 1 }, (_, i) => lo + i));
|
|
1697
|
+
this.preload(blockwise ? [lo, hi] : Array.from({ length: hi - lo + 1 }, (_, i) => lo + i));
|
|
1548
1698
|
void Promise.race([Promise.all(wanted.map((s) => this.texture(s))), new Promise((r) => setTimeout(r, 700))]).then(() => {
|
|
1549
1699
|
this.starting = false;
|
|
1550
1700
|
if (this.disposed)
|
|
@@ -1611,6 +1761,31 @@ export class PageFlipper {
|
|
|
1611
1761
|
}
|
|
1612
1762
|
}
|
|
1613
1763
|
}
|
|
1764
|
+
// The block in the air: carried over, and laid down once it lands.
|
|
1765
|
+
if (this.block) {
|
|
1766
|
+
const { turn, lo, hi } = this.block;
|
|
1767
|
+
const anim = turn.anim;
|
|
1768
|
+
const t = Math.min(1, (now - anim.start) / anim.duration);
|
|
1769
|
+
turn.P = { u: anim.from.u + (anim.to.u - anim.from.u) * ease(t), v: anim.from.v };
|
|
1770
|
+
if (t >= 1) {
|
|
1771
|
+
for (let k = lo; k < hi; k++)
|
|
1772
|
+
this.turned[k] = turn.forward;
|
|
1773
|
+
this.block = null;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
// A long way to go with no leaf in the air: the leaves in between go
|
|
1777
|
+
// over in one block. (Leaves already turning land first: a run of
|
|
1778
|
+
// clicks that has grown long carries on as a block.)
|
|
1779
|
+
const at = this.settledAt();
|
|
1780
|
+
const away = Math.abs(this.target - at);
|
|
1781
|
+
if (!this.block && this.turns.size === 0 && away > 1 && away * this.pagesPerLeaf() > BLOCK_PAGES) {
|
|
1782
|
+
const forward = this.target > at;
|
|
1783
|
+
const lo = forward ? at : this.target;
|
|
1784
|
+
const hi = forward ? this.target : at;
|
|
1785
|
+
const G = { u: this.W, v: 0 };
|
|
1786
|
+
const duration = BLOCK_DURATION * (1 + 0.25 * smooth(BLOCK_PAGES, 400, away * this.pagesPerLeaf()));
|
|
1787
|
+
this.block = { lo, hi, turn: { forward, rigid: true, G, P: { ...G }, anim: { start: now, duration, from: G, to: { u: -G.u, v: 0 }, arc: 0, lands: true } } };
|
|
1788
|
+
}
|
|
1614
1789
|
// Lift the next ones: each follows its neighbour a little behind, so a
|
|
1615
1790
|
// jump of several spreads turns every page in between.
|
|
1616
1791
|
const pending = Math.abs(this.target - this.settledAt()) || 1;
|
|
@@ -1621,14 +1796,14 @@ export class PageFlipper {
|
|
|
1621
1796
|
return forward ? this.turned[k] : !this.turned[k];
|
|
1622
1797
|
return turn.forward === forward && !turn.held && progress(turn) >= gap;
|
|
1623
1798
|
};
|
|
1624
|
-
for (let k = 0; k < this.target && k < n; k++) {
|
|
1799
|
+
for (let k = 0; k < this.target && k < n && !this.block; k++) {
|
|
1625
1800
|
if (this.turned[k] || this.turns.has(k))
|
|
1626
1801
|
continue;
|
|
1627
1802
|
if (k > 0 && !follows(k - 1, true))
|
|
1628
1803
|
break;
|
|
1629
1804
|
this.turns.set(k, { ...this.lift(true, now), rigid: this.rigidOf(k) });
|
|
1630
1805
|
}
|
|
1631
|
-
for (let k = n - 1; k >= this.target; k--) {
|
|
1806
|
+
for (let k = n - 1; k >= this.target && !this.block; k--) {
|
|
1632
1807
|
if (!this.turned[k] || this.turns.has(k))
|
|
1633
1808
|
continue;
|
|
1634
1809
|
if (k < n - 1 && !follows(k + 1, false))
|
|
@@ -1637,7 +1812,7 @@ export class PageFlipper {
|
|
|
1637
1812
|
}
|
|
1638
1813
|
this.draw();
|
|
1639
1814
|
this.spread.classList.add("is-turning");
|
|
1640
|
-
if (this.turns.size > 0 || this.settledAt() !== this.target)
|
|
1815
|
+
if (this.turns.size > 0 || this.block || this.settledAt() !== this.target)
|
|
1641
1816
|
this.raf = requestAnimationFrame(this.frame);
|
|
1642
1817
|
else
|
|
1643
1818
|
this.settle();
|
|
@@ -1672,6 +1847,70 @@ export class PageFlipper {
|
|
|
1672
1847
|
return { x: 0, y: 1e9 };
|
|
1673
1848
|
return { x: this.sign * p.x, y: p.y };
|
|
1674
1849
|
}
|
|
1850
|
+
/** The pointer's ray, from the eye through the pointer. */
|
|
1851
|
+
rayAt(event) {
|
|
1852
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1853
|
+
const ndc = new Vector2(((event.clientX - rect.left) / rect.width) * 2 - 1, -(((event.clientY - rect.top) / rect.height) * 2 - 1));
|
|
1854
|
+
this.scene.updateMatrixWorld();
|
|
1855
|
+
this.camera.updateMatrixWorld();
|
|
1856
|
+
this.raycaster.setFromCamera(ndc, this.camera);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* The open page under the pointer, as the book is seen (tilted, orbited,
|
|
1860
|
+
* curving into the gutter): its slot in the spread (0 the verso, 1 the
|
|
1861
|
+
* recto) and where on the printed page, as fractions of its width and
|
|
1862
|
+
* height from the top left. Null off the open pages, or where a leaf in
|
|
1863
|
+
* the air hides them.
|
|
1864
|
+
*/
|
|
1865
|
+
pagePoint(event) {
|
|
1866
|
+
if (!this.surfaces)
|
|
1867
|
+
this.layout();
|
|
1868
|
+
this.rayAt(event);
|
|
1869
|
+
const open = [this.left, this.right].filter((m) => m.visible);
|
|
1870
|
+
const air = [...this.leaves.values(), ...(this.blockMesh ? [this.blockMesh] : [])].filter((m) => m.visible);
|
|
1871
|
+
for (const m of air)
|
|
1872
|
+
m.geometry.computeBoundingSphere();
|
|
1873
|
+
const hit = this.raycaster.intersectObjects([...open, ...air], false)[0];
|
|
1874
|
+
if (!hit || !hit.uv || (hit.object !== this.left && hit.object !== this.right))
|
|
1875
|
+
return null;
|
|
1876
|
+
// uv.x runs from the spine (the shader mirrors it for a right-bound
|
|
1877
|
+
// book); the recto shows its front, the verso its back (mirrored).
|
|
1878
|
+
const u = this.sign < 0 ? 1 - hit.uv.x : hit.uv.x;
|
|
1879
|
+
const recto = hit.object === this.right;
|
|
1880
|
+
return { side: recto ? 1 : 0, x: recto ? u : 1 - u, y: 1 - hit.uv.y };
|
|
1881
|
+
}
|
|
1882
|
+
/** Where a point of an open page (`pagePoint`'s terms) lies on screen. */
|
|
1883
|
+
screenPoint(side, x, y) {
|
|
1884
|
+
if (!this.surfaces)
|
|
1885
|
+
this.layout();
|
|
1886
|
+
const mesh = side === 1 ? this.right : this.left;
|
|
1887
|
+
if (!mesh.visible)
|
|
1888
|
+
return null;
|
|
1889
|
+
const u0 = side === 1 ? x : 1 - x;
|
|
1890
|
+
const t = Math.min(1, Math.max(0, this.sign < 0 ? 1 - u0 : u0));
|
|
1891
|
+
// The column the point falls in (the columns are closer near the spine).
|
|
1892
|
+
const pos = mesh.geometry.attributes.position;
|
|
1893
|
+
const uv = mesh.geometry.attributes.uv;
|
|
1894
|
+
const cols = NX_OPEN + 1;
|
|
1895
|
+
let ix = 0;
|
|
1896
|
+
while (ix < NX_OPEN - 1 && uv.getX(ix + 1) < t)
|
|
1897
|
+
ix++;
|
|
1898
|
+
const a = uv.getX(ix);
|
|
1899
|
+
const b = uv.getX(ix + 1);
|
|
1900
|
+
const f = b > a ? Math.min(1, Math.max(0, (t - a) / (b - a))) : 0;
|
|
1901
|
+
const lerp = (i, j, k) => {
|
|
1902
|
+
const p = new Vector3(pos.getX(i), pos.getY(i), pos.getZ(i));
|
|
1903
|
+
return p.lerp(new Vector3(pos.getX(j), pos.getY(j), pos.getZ(j)), k);
|
|
1904
|
+
};
|
|
1905
|
+
const top = lerp(ix, ix + 1, f);
|
|
1906
|
+
const bottom = lerp(cols + ix, cols + ix + 1, f);
|
|
1907
|
+
const p = top.lerp(bottom, y);
|
|
1908
|
+
this.scene.updateMatrixWorld();
|
|
1909
|
+
this.camera.updateMatrixWorld();
|
|
1910
|
+
mesh.localToWorld(p).project(this.camera);
|
|
1911
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1912
|
+
return { x: rect.left + ((p.x + 1) / 2) * rect.width, y: rect.top + ((1 - p.y) / 2) * rect.height };
|
|
1913
|
+
}
|
|
1675
1914
|
/** A board turns on the joint at the top of the spine; other leaves on
|
|
1676
1915
|
* the gutter. */
|
|
1677
1916
|
rigidOf(k) {
|
|
@@ -1870,14 +2109,42 @@ export class PageFlipper {
|
|
|
1870
2109
|
if (!mesh.parent)
|
|
1871
2110
|
this.stage.add(mesh);
|
|
1872
2111
|
});
|
|
2112
|
+
// The block in the air: a slab as thick as its leaves, turning on the
|
|
2113
|
+
// joint at the top of the spine like a board, its first page on top
|
|
2114
|
+
// and its last underneath.
|
|
2115
|
+
if (this.blockMesh) {
|
|
2116
|
+
const caster = this.blockMesh.userData.caster;
|
|
2117
|
+
this.blockMesh.visible = caster.visible = false;
|
|
2118
|
+
const parts = this.blockMesh.userData.board;
|
|
2119
|
+
if (parts)
|
|
2120
|
+
parts.back.visible = parts.slab.visible = false;
|
|
2121
|
+
}
|
|
2122
|
+
if (this.block) {
|
|
2123
|
+
const { lo, hi, turn } = this.block;
|
|
2124
|
+
const mesh = this.blockLeaf(lo, hi);
|
|
2125
|
+
const q = progressFrom(turn.G, turn.P);
|
|
2126
|
+
const thick = this.blockThickness(lo, hi);
|
|
2127
|
+
const parts = this.boardParts(mesh);
|
|
2128
|
+
const lay = (geometry, face) => layLeaf(geometry, null, turn.forward, W, H, 0.5, book, 1, q, 0, 0, { thick, face });
|
|
2129
|
+
const { phi, pivot } = lay(mesh.geometry, 1);
|
|
2130
|
+
lay(parts.back.geometry, -1);
|
|
2131
|
+
parts.slab.position.set((W / 2) * Math.cos(phi), 0, pivot + (W / 2) * Math.sin(phi));
|
|
2132
|
+
parts.slab.rotation.set(0, -phi, 0);
|
|
2133
|
+
parts.slab.scale.set(W, H, thick * 0.97);
|
|
2134
|
+
const spec = this.specOf(lo);
|
|
2135
|
+
parts.slab.material.color.copy(spec === this.coverSpec ? this.boardColor() : new Color(spec.paper.shade.hex));
|
|
2136
|
+
mesh.visible = parts.back.visible = parts.slab.visible = mesh.userData.caster.visible = true;
|
|
2137
|
+
if (!mesh.parent)
|
|
2138
|
+
this.stage.add(mesh);
|
|
2139
|
+
}
|
|
1873
2140
|
// The pages lying open: the top leaf of each stack.
|
|
1874
2141
|
let l = -1;
|
|
1875
2142
|
for (let j = 0; j < n; j++)
|
|
1876
|
-
if (this.turned[j] && !this.turns.has(j))
|
|
2143
|
+
if (this.turned[j] && !this.turns.has(j) && !this.inBlock(j))
|
|
1877
2144
|
l = j;
|
|
1878
2145
|
let r = n;
|
|
1879
2146
|
for (let j = n - 1; j >= 0; j--)
|
|
1880
|
-
if (!this.turned[j] && !this.turns.has(j))
|
|
2147
|
+
if (!this.turned[j] && !this.turns.has(j) && !this.inBlock(j))
|
|
1881
2148
|
r = j;
|
|
1882
2149
|
const leftSrc = this.book[l + 1]?.[0] ?? null;
|
|
1883
2150
|
const rightSrc = this.book[r]?.[1] ?? null;
|
|
@@ -1942,6 +2209,8 @@ export class PageFlipper {
|
|
|
1942
2209
|
this.pmrem?.dispose();
|
|
1943
2210
|
for (const mesh of this.leaves.values())
|
|
1944
2211
|
disposeLeaf(mesh);
|
|
2212
|
+
if (this.blockMesh)
|
|
2213
|
+
disposeLeaf(this.blockMesh);
|
|
1945
2214
|
for (const mesh of [this.left, this.right, this.desk, ...this.stacks]) {
|
|
1946
2215
|
mesh.geometry.dispose();
|
|
1947
2216
|
mesh.material.dispose();
|