postext-folio 0.2.4 → 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 +45 -0
- package/dist/pageFlip.d.ts.map +1 -1
- package/dist/pageFlip.js +286 -32
- 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;
|
|
@@ -647,6 +722,10 @@ export class PageFlipper {
|
|
|
647
722
|
/** Leaf k lies on the left. */
|
|
648
723
|
turned;
|
|
649
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;
|
|
650
729
|
target;
|
|
651
730
|
raf = 0;
|
|
652
731
|
lastFrame = 0;
|
|
@@ -789,9 +868,10 @@ export class PageFlipper {
|
|
|
789
868
|
paperSpec({ ...r.paper, grammage: Math.min(350, Math.round(r.paper.grammage * 1.5)), showThrough: false })
|
|
790
869
|
: {
|
|
791
870
|
...paperSpec({ ...r.paper, type: "board", grammage: 1250, bulk: 1.6, finish: "silk", texture: "smooth", shade: { hex: "#ffffff", model: "hex" }, showThrough: false }),
|
|
792
|
-
// The book's own printed covers: a
|
|
793
|
-
//
|
|
794
|
-
|
|
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,
|
|
795
875
|
// Board does not bend: it turns on its joint as a plate.
|
|
796
876
|
rigidity: 1,
|
|
797
877
|
roughness: 0.45,
|
|
@@ -869,6 +949,7 @@ export class PageFlipper {
|
|
|
869
949
|
this.key.shadow.intensity = 0.55 + 0.25 * (1 - env.softness);
|
|
870
950
|
this.renderer.toneMappingExposure = env.exposure * r.lighting.intensity;
|
|
871
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;
|
|
872
953
|
this.surfaceKey = "";
|
|
873
954
|
for (const mesh of [this.left, this.right, ...this.leaves.values()])
|
|
874
955
|
mesh.material.userData.spec = null;
|
|
@@ -937,8 +1018,13 @@ export class PageFlipper {
|
|
|
937
1018
|
/** The paper of leaf k (outside the pages shown: the book's). */
|
|
938
1019
|
specOf(k) {
|
|
939
1020
|
const covers = this.appearance.coverLeaves;
|
|
940
|
-
if (this.coverSpec && (k === covers?.front || k === covers?.back))
|
|
941
|
-
|
|
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
|
+
}
|
|
942
1028
|
return this.leafSpecs[k] ?? this.bookSpec;
|
|
943
1029
|
}
|
|
944
1030
|
dressPage(mesh, spec) {
|
|
@@ -974,11 +1060,20 @@ export class PageFlipper {
|
|
|
974
1060
|
const left = [];
|
|
975
1061
|
const right = [];
|
|
976
1062
|
this.turned.forEach((t, k) => {
|
|
977
|
-
if (!this.turns.has(k))
|
|
1063
|
+
if (!this.turns.has(k) && !this.inBlock(k))
|
|
978
1064
|
(t ? left : right).push(k);
|
|
979
1065
|
});
|
|
980
1066
|
return { left, right };
|
|
981
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
|
+
}
|
|
982
1077
|
/** Loads the spine picture, then rebuilds the book to print it. */
|
|
983
1078
|
async loadSpine(src) {
|
|
984
1079
|
let tex = null;
|
|
@@ -1065,6 +1160,7 @@ export class PageFlipper {
|
|
|
1065
1160
|
return;
|
|
1066
1161
|
this.surfaceKey = key;
|
|
1067
1162
|
const k = this.pxPerMm();
|
|
1163
|
+
edgeUniforms.uEdgeK.value = k;
|
|
1068
1164
|
const half = this.appearance.singlePage ? 0.5 : 1;
|
|
1069
1165
|
const thick = (leaves, more) => (leaves.reduce((s, i) => s + this.specOf(i).caliperMm, 0) * half + more * this.bookSpec.caliperMm) * k;
|
|
1070
1166
|
const tL = thick(left, extra.before);
|
|
@@ -1132,7 +1228,7 @@ export class PageFlipper {
|
|
|
1132
1228
|
color: this.boardColor(),
|
|
1133
1229
|
}
|
|
1134
1230
|
: undefined;
|
|
1135
|
-
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);
|
|
1136
1232
|
mesh.castShadow = mesh.receiveShadow = true;
|
|
1137
1233
|
this.stacks.push(mesh);
|
|
1138
1234
|
this.stage.add(mesh);
|
|
@@ -1400,6 +1496,17 @@ export class PageFlipper {
|
|
|
1400
1496
|
mesh.material.userData.uniforms.uPaper.value.setRGB(r, g, b, SRGBColorSpace);
|
|
1401
1497
|
this.redraw();
|
|
1402
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
|
+
}
|
|
1403
1510
|
/** Starts loading the pages of these spreads as textures. */
|
|
1404
1511
|
preload(indexes) {
|
|
1405
1512
|
for (const i of indexes)
|
|
@@ -1448,6 +1555,7 @@ export class PageFlipper {
|
|
|
1448
1555
|
cancelAnimationFrame(this.raf);
|
|
1449
1556
|
this.raf = 0;
|
|
1450
1557
|
this.turns.clear();
|
|
1558
|
+
this.block = null;
|
|
1451
1559
|
for (const mesh of this.leaves.values()) {
|
|
1452
1560
|
mesh.removeFromParent();
|
|
1453
1561
|
mesh.userData.caster.removeFromParent();
|
|
@@ -1507,17 +1615,7 @@ export class PageFlipper {
|
|
|
1507
1615
|
leaf(k) {
|
|
1508
1616
|
let mesh = this.leaves.get(k);
|
|
1509
1617
|
if (!mesh) {
|
|
1510
|
-
|
|
1511
|
-
spreadColumns(geometry, NX);
|
|
1512
|
-
geometry.setAttribute("ao", new BufferAttribute(new Float32Array(geometry.attributes.position.count).fill(1), 1));
|
|
1513
|
-
mesh = new Mesh(geometry, pageMaterial(this.sign < 0, this.sign));
|
|
1514
|
-
mesh.material.userData.uniforms.uPaper.value.setRGB(...this.paper, SRGBColorSpace);
|
|
1515
|
-
mesh.castShadow = mesh.receiveShadow = true;
|
|
1516
|
-
mesh.frustumCulled = false;
|
|
1517
|
-
const caster = new Mesh(mesh.geometry, this.leafCaster);
|
|
1518
|
-
caster.frustumCulled = false;
|
|
1519
|
-
this.shadowScene.add(caster);
|
|
1520
|
-
mesh.userData.caster = caster;
|
|
1618
|
+
mesh = this.leafMesh();
|
|
1521
1619
|
this.assign(mesh, "Front", this.book[k]?.[1] ?? null);
|
|
1522
1620
|
this.assign(mesh, "Back", this.book[k + 1]?.[0] ?? null);
|
|
1523
1621
|
this.leaves.set(k, mesh);
|
|
@@ -1525,6 +1623,39 @@ export class PageFlipper {
|
|
|
1525
1623
|
this.dressPage(mesh, this.specOf(k));
|
|
1526
1624
|
return mesh;
|
|
1527
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
|
+
}
|
|
1528
1659
|
/** A board leaf's other face and edges, carried with its mesh. */
|
|
1529
1660
|
boardParts(mesh) {
|
|
1530
1661
|
let parts = mesh.userData.board;
|
|
@@ -1555,11 +1686,15 @@ export class PageFlipper {
|
|
|
1555
1686
|
// The first leaves' pages are warm; wait a moment for cold ones.
|
|
1556
1687
|
this.starting = true;
|
|
1557
1688
|
const wanted = [];
|
|
1558
|
-
|
|
1559
|
-
|
|
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] ?? [])
|
|
1560
1695
|
if (src)
|
|
1561
1696
|
wanted.push(src);
|
|
1562
|
-
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));
|
|
1563
1698
|
void Promise.race([Promise.all(wanted.map((s) => this.texture(s))), new Promise((r) => setTimeout(r, 700))]).then(() => {
|
|
1564
1699
|
this.starting = false;
|
|
1565
1700
|
if (this.disposed)
|
|
@@ -1626,6 +1761,31 @@ export class PageFlipper {
|
|
|
1626
1761
|
}
|
|
1627
1762
|
}
|
|
1628
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
|
+
}
|
|
1629
1789
|
// Lift the next ones: each follows its neighbour a little behind, so a
|
|
1630
1790
|
// jump of several spreads turns every page in between.
|
|
1631
1791
|
const pending = Math.abs(this.target - this.settledAt()) || 1;
|
|
@@ -1636,14 +1796,14 @@ export class PageFlipper {
|
|
|
1636
1796
|
return forward ? this.turned[k] : !this.turned[k];
|
|
1637
1797
|
return turn.forward === forward && !turn.held && progress(turn) >= gap;
|
|
1638
1798
|
};
|
|
1639
|
-
for (let k = 0; k < this.target && k < n; k++) {
|
|
1799
|
+
for (let k = 0; k < this.target && k < n && !this.block; k++) {
|
|
1640
1800
|
if (this.turned[k] || this.turns.has(k))
|
|
1641
1801
|
continue;
|
|
1642
1802
|
if (k > 0 && !follows(k - 1, true))
|
|
1643
1803
|
break;
|
|
1644
1804
|
this.turns.set(k, { ...this.lift(true, now), rigid: this.rigidOf(k) });
|
|
1645
1805
|
}
|
|
1646
|
-
for (let k = n - 1; k >= this.target; k--) {
|
|
1806
|
+
for (let k = n - 1; k >= this.target && !this.block; k--) {
|
|
1647
1807
|
if (!this.turned[k] || this.turns.has(k))
|
|
1648
1808
|
continue;
|
|
1649
1809
|
if (k < n - 1 && !follows(k + 1, false))
|
|
@@ -1652,7 +1812,7 @@ export class PageFlipper {
|
|
|
1652
1812
|
}
|
|
1653
1813
|
this.draw();
|
|
1654
1814
|
this.spread.classList.add("is-turning");
|
|
1655
|
-
if (this.turns.size > 0 || this.settledAt() !== this.target)
|
|
1815
|
+
if (this.turns.size > 0 || this.block || this.settledAt() !== this.target)
|
|
1656
1816
|
this.raf = requestAnimationFrame(this.frame);
|
|
1657
1817
|
else
|
|
1658
1818
|
this.settle();
|
|
@@ -1687,6 +1847,70 @@ export class PageFlipper {
|
|
|
1687
1847
|
return { x: 0, y: 1e9 };
|
|
1688
1848
|
return { x: this.sign * p.x, y: p.y };
|
|
1689
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
|
+
}
|
|
1690
1914
|
/** A board turns on the joint at the top of the spine; other leaves on
|
|
1691
1915
|
* the gutter. */
|
|
1692
1916
|
rigidOf(k) {
|
|
@@ -1885,14 +2109,42 @@ export class PageFlipper {
|
|
|
1885
2109
|
if (!mesh.parent)
|
|
1886
2110
|
this.stage.add(mesh);
|
|
1887
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
|
+
}
|
|
1888
2140
|
// The pages lying open: the top leaf of each stack.
|
|
1889
2141
|
let l = -1;
|
|
1890
2142
|
for (let j = 0; j < n; j++)
|
|
1891
|
-
if (this.turned[j] && !this.turns.has(j))
|
|
2143
|
+
if (this.turned[j] && !this.turns.has(j) && !this.inBlock(j))
|
|
1892
2144
|
l = j;
|
|
1893
2145
|
let r = n;
|
|
1894
2146
|
for (let j = n - 1; j >= 0; j--)
|
|
1895
|
-
if (!this.turned[j] && !this.turns.has(j))
|
|
2147
|
+
if (!this.turned[j] && !this.turns.has(j) && !this.inBlock(j))
|
|
1896
2148
|
r = j;
|
|
1897
2149
|
const leftSrc = this.book[l + 1]?.[0] ?? null;
|
|
1898
2150
|
const rightSrc = this.book[r]?.[1] ?? null;
|
|
@@ -1957,6 +2209,8 @@ export class PageFlipper {
|
|
|
1957
2209
|
this.pmrem?.dispose();
|
|
1958
2210
|
for (const mesh of this.leaves.values())
|
|
1959
2211
|
disposeLeaf(mesh);
|
|
2212
|
+
if (this.blockMesh)
|
|
2213
|
+
disposeLeaf(this.blockMesh);
|
|
1960
2214
|
for (const mesh of [this.left, this.right, this.desk, ...this.stacks]) {
|
|
1961
2215
|
mesh.geometry.dispose();
|
|
1962
2216
|
mesh.material.dispose();
|