postext-folio 0.6.0 → 0.7.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 +8 -4
- package/dist/bookGeometry.d.ts +1 -1
- package/dist/bookGeometry.d.ts.map +1 -1
- package/dist/bookGeometry.js +5 -0
- package/dist/bookGeometry.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pageFlip.d.ts +31 -19
- package/dist/pageFlip.d.ts.map +1 -1
- package/dist/pageFlip.js +254 -120
- package/dist/pageFlip.js.map +1 -1
- package/dist/paper.d.ts +8 -1
- package/dist/paper.d.ts.map +1 -1
- package/dist/paper.js +21 -4
- package/dist/paper.js.map +1 -1
- package/dist/postext.d.ts +16 -7
- package/dist/postext.d.ts.map +1 -1
- package/dist/postext.js +172 -84
- 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 +4 -1
- package/dist/styles.js.map +1 -1
- package/dist/videoAtlas.d.ts +90 -0
- package/dist/videoAtlas.d.ts.map +1 -0
- package/dist/videoAtlas.js +241 -0
- package/dist/videoAtlas.js.map +1 -0
- package/dist/videos.d.ts +46 -0
- package/dist/videos.d.ts.map +1 -1
- package/dist/videos.js +55 -1
- package/dist/videos.js.map +1 -1
- package/dist/viewer.d.ts +7 -1
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +53 -40
- package/dist/viewer.js.map +1 -1
- package/package.json +3 -3
package/dist/pageFlip.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { BoxGeometry, BufferAttribute, BufferGeometry, Color, CustomBlending, DepthTexture, DirectionalLight, DoubleSide, Group, HalfFloatType, LinearMipmapLinearFilter, MaxEquation, Mesh, MeshPhysicalMaterial, MeshStandardMaterial, LinearFilter, Matrix4, NeutralToneMapping, OneFactor, OrthographicCamera, PCFShadowMap, PerspectiveCamera, Plane, PlaneGeometry, PMREMGenerator, Raycaster, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, SRGBColorSpace, Texture, TextureLoader, Vector2, Vector3, Vector4,
|
|
1
|
+
import { BoxGeometry, BufferAttribute, BufferGeometry, Color, CustomBlending, DepthTexture, DirectionalLight, DoubleSide, Group, HalfFloatType, LinearMipmapLinearFilter, MaxEquation, Mesh, MeshPhysicalMaterial, MeshStandardMaterial, LinearFilter, Matrix4, NeutralToneMapping, OneFactor, OrthographicCamera, PCFShadowMap, PerspectiveCamera, Plane, PlaneGeometry, PMREMGenerator, Raycaster, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, SRGBColorSpace, Texture, TextureLoader, Vector2, Vector3, Vector4, WebGLRenderer, WebGLRenderTarget, } from "three";
|
|
2
|
+
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
|
|
3
|
+
import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js";
|
|
4
|
+
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
|
|
5
|
+
import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
|
|
2
6
|
import { resolveFolioConfig } from "postext";
|
|
3
7
|
import { along, BINDINGS, gutterOcclusion, profiles, stackGeometry, topAt } from "./bookGeometry";
|
|
4
8
|
import { environment } from "./environments";
|
|
5
9
|
import { pagePaper, paperSpec } from "./paper";
|
|
6
10
|
import { loadDeskMaps } from "./deskTextures";
|
|
11
|
+
import { ATLAS_MAX, MAX_PAGE_VIDEOS, VideoAtlas } from "./videoAtlas";
|
|
7
12
|
import { cachedDesk, cachedRelief } from "./procedural";
|
|
8
13
|
/** Columns and rows of a leaf's mesh: fine enough for a tight roll. */
|
|
9
14
|
const NX = 96;
|
|
15
|
+
/** How far a leaf's folded paper stays over its own paper beneath it. */
|
|
16
|
+
const OWN_GAP = 1.2;
|
|
17
|
+
/** How much higher than at rest a leaf in the air stays over a block. */
|
|
18
|
+
const BLOCK_GAP = 1.5;
|
|
10
19
|
const NY = 120;
|
|
11
20
|
/** Columns of a page lying open (its curve runs across only). */
|
|
12
21
|
const NX_OPEN = 72;
|
|
@@ -23,6 +32,9 @@ export const BLOCK_PAGES = 10;
|
|
|
23
32
|
/** How long a block of leaves takes to go over (ms), a little longer for
|
|
24
33
|
* a thicker one. */
|
|
25
34
|
const BLOCK_DURATION = 1150;
|
|
35
|
+
/** A page is taken by its outer part only: from this fraction of its
|
|
36
|
+
* width from the spine out to the fore-edge (#494). */
|
|
37
|
+
const GRIP = 0.5;
|
|
26
38
|
/** A leaf follows the one before it once that one is this far through its turn. */
|
|
27
39
|
const GAP = 0.14;
|
|
28
40
|
// Ambient occlusion from the book's own shape: each frame the book is
|
|
@@ -263,11 +275,11 @@ function pageMaterial(mirror, sign) {
|
|
|
263
275
|
uLeafOcc: { value: 0.45 },
|
|
264
276
|
uOccBias: { value: 6 },
|
|
265
277
|
uVideo: { value: null },
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
uVidTexel: { value: new Vector2(1, 1) },
|
|
279
|
+
uVidCount: { value: 0 },
|
|
280
|
+
uVidOA: { value: Array.from({ length: MAX_PAGE_VIDEOS }, () => new Vector4()) },
|
|
281
|
+
uVidDF: { value: Array.from({ length: MAX_PAGE_VIDEOS }, () => new Vector4()) },
|
|
282
|
+
uVidRect: { value: Array.from({ length: MAX_PAGE_VIDEOS }, () => new Vector4()) },
|
|
271
283
|
uReflection: { value: null },
|
|
272
284
|
uReflectionOn: { value: 0 },
|
|
273
285
|
uReflectionSize: { value: new Vector2(1, 1) },
|
|
@@ -312,11 +324,11 @@ function pageMaterial(mirror, sign) {
|
|
|
312
324
|
uniform float uLeafOcc;
|
|
313
325
|
uniform float uSign;
|
|
314
326
|
uniform sampler2D uVideo;
|
|
315
|
-
uniform
|
|
316
|
-
uniform
|
|
317
|
-
uniform
|
|
318
|
-
uniform
|
|
319
|
-
uniform vec4
|
|
327
|
+
uniform vec2 uVidTexel;
|
|
328
|
+
uniform float uVidCount;
|
|
329
|
+
uniform vec4 uVidOA[${MAX_PAGE_VIDEOS}];
|
|
330
|
+
uniform vec4 uVidDF[${MAX_PAGE_VIDEOS}];
|
|
331
|
+
uniform vec4 uVidRect[${MAX_PAGE_VIDEOS}];
|
|
320
332
|
uniform sampler2D uReflection;
|
|
321
333
|
uniform float uReflectionOn;
|
|
322
334
|
uniform vec2 uReflectionSize;
|
|
@@ -334,15 +346,22 @@ function pageMaterial(mirror, sign) {
|
|
|
334
346
|
vec2 uvB = vec2(1.0 - u, vPageUv.y);
|
|
335
347
|
vec3 ink = uPaper;
|
|
336
348
|
if (has > 0.5 && has < 1.5) ink = front ? texture2D(uFront, uvF).rgb : texture2D(uBack, uvB).rgb;
|
|
337
|
-
//
|
|
338
|
-
// point of the page (from its top left corner) in
|
|
339
|
-
// own frame, which may lie turned on the page
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
// The videos playing on this face, printed over their posters:
|
|
350
|
+
// the point of the page (from its top left corner) in each
|
|
351
|
+
// picture's own frame, which may lie turned on the page, then in
|
|
352
|
+
// its slot of the atlas (kept half a texel inside it, clear of
|
|
353
|
+
// the next slot).
|
|
354
|
+
vec2 tuv = front ? uvF : uvB;
|
|
355
|
+
for (int i = 0; i < ${MAX_PAGE_VIDEOS}; i++) {
|
|
356
|
+
if (float(i) >= uVidCount) break;
|
|
357
|
+
vec4 oa = uVidOA[i];
|
|
358
|
+
vec4 df = uVidDF[i];
|
|
359
|
+
if ((df.z < 1.5) != front) continue;
|
|
360
|
+
vec2 p = vec2(tuv.x, 1.0 - tuv.y) - oa.xy;
|
|
361
|
+
vec2 st = vec2(dot(p, oa.zw) / dot(oa.zw, oa.zw), dot(p, df.xy) / dot(df.xy, df.xy));
|
|
344
362
|
if (st.x >= 0.0 && st.x <= 1.0 && st.y >= 0.0 && st.y <= 1.0) {
|
|
345
|
-
|
|
363
|
+
vec4 r = uVidRect[i];
|
|
364
|
+
vec2 v = clamp(r.xy + st * r.zw, r.xy + 0.5 * uVidTexel, r.xy + r.zw - 0.5 * uVidTexel);
|
|
346
365
|
ink = texture2D(uVideo, vec2(v.x, 1.0 - v.y)).rgb;
|
|
347
366
|
}
|
|
348
367
|
}
|
|
@@ -623,12 +642,33 @@ export function foldOf(G, P, W, H, roll = 1) {
|
|
|
623
642
|
// straight, lands over the hand: R sinθ + (c − Rθ) cosθ = c − D.
|
|
624
643
|
const arc = R * theta;
|
|
625
644
|
const k = arc > 0 ? Math.min(1, D / arc) : 0;
|
|
626
|
-
|
|
645
|
+
const free = (D + k * (R * Math.sin(theta) - arc * Math.cos(theta))) / (1 - Math.cos(theta));
|
|
646
|
+
let c = free;
|
|
627
647
|
for (const S of [-H / 2, H / 2])
|
|
628
648
|
c = Math.min(c, G.u * n.u + (G.v - S) * n.v);
|
|
629
649
|
if (c <= 0)
|
|
630
650
|
return null;
|
|
631
|
-
|
|
651
|
+
const F = { u: G.u - n.u * c, v: G.v - n.v * c };
|
|
652
|
+
// Never all at once (#502). Paper lying beyond the point taken, along
|
|
653
|
+
// the pull (a page taken by the middle of an edge and pulled across),
|
|
654
|
+
// turns over as the hand carries it away, not the moment it moves (a
|
|
655
|
+
// pixel's pull stood half the page up). A fold held off the spine short
|
|
656
|
+
// of the hand turns the less the nearer it comes to the point taken, so
|
|
657
|
+
// that it is gone, not dropped, when it reaches it.
|
|
658
|
+
let beyond = 0;
|
|
659
|
+
for (const u of [0, W])
|
|
660
|
+
for (const v of [-H / 2, H / 2])
|
|
661
|
+
beyond = Math.max(beyond, (u - G.u) * n.u + (v - G.v) * n.v);
|
|
662
|
+
const turns = (c / free) * (beyond > 0 ? smooth(0, 0.5 * beyond, D) : 1);
|
|
663
|
+
return { F, n, R, theta: theta * turns };
|
|
664
|
+
}
|
|
665
|
+
/** Paper `d` past the fold, round a roll of radius R and straight on at
|
|
666
|
+
* θ: how far it reaches back across the page, and how high it stands. */
|
|
667
|
+
function rollOf(d, R, theta) {
|
|
668
|
+
if (R > 1e-3 && d < R * theta)
|
|
669
|
+
return [R * Math.sin(d / R), R * (1 - Math.cos(d / R))];
|
|
670
|
+
const rest = d - R * theta;
|
|
671
|
+
return [R * Math.sin(theta) + rest * Math.cos(theta), R * (1 - Math.cos(theta)) + rest * Math.sin(theta)];
|
|
632
672
|
}
|
|
633
673
|
/** How far through its turn a leaf is: the point taken, from where it
|
|
634
674
|
* was to its mirror image across the spine. */
|
|
@@ -704,13 +744,17 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
|
|
|
704
744
|
// corner barely lifted, a few degrees through the middle of the turn.
|
|
705
745
|
const inAir = smooth(0.02, 0.22, q) * smooth(0.02, 0.22, 1 - q);
|
|
706
746
|
const lean = LEAN * inAir;
|
|
707
|
-
const cosA = Math.cos(lean);
|
|
708
|
-
const sinA = Math.sin(lean);
|
|
709
747
|
// Each side's chord, from the spine to its fore-edge: a side hanging
|
|
710
748
|
// from a standing spine slopes down to the desk, and a leaf leaning off
|
|
711
749
|
// it leans off that slope, not off a level plane over it.
|
|
712
750
|
const chordR = (zr - zr0) / (xr || 1);
|
|
713
751
|
const chordL = (zl - zl0) / (xl || 1);
|
|
752
|
+
// The plane a leaf lifts off along, by side: the chord turned up by the
|
|
753
|
+
// lean. A leaf keeps its width along it (stepping the chord's rise per
|
|
754
|
+
// unit across, on a side sloping to the desk, stretched it by a tenth
|
|
755
|
+
// and more, #495).
|
|
756
|
+
const tiltR = Math.atan(chordR) + lean;
|
|
757
|
+
const tiltL = Math.atan(chordL) + lean;
|
|
714
758
|
// Onto the book. At rest (and as it lands) the leaf lies on the open
|
|
715
759
|
// book's surface, into the gutter and out. In the air it is lifted off
|
|
716
760
|
// at the spine: its own plane rises from the gutter's floor at a small
|
|
@@ -721,8 +765,9 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
|
|
|
721
765
|
let px = rx;
|
|
722
766
|
let pz = rz + z + lift;
|
|
723
767
|
if (inAir > 0) {
|
|
724
|
-
const
|
|
725
|
-
const
|
|
768
|
+
const tilt = s >= 0 ? tiltR : tiltL;
|
|
769
|
+
const fx = (s >= 0 ? 1 : -1) * Math.abs(s) * Math.cos(tilt);
|
|
770
|
+
const plane = (s >= 0 ? zr0 : zl0) + Math.abs(s) * Math.sin(tilt) + z + lift;
|
|
726
771
|
const under = surfaceAt(book, fx)[1] + lift + 0.6;
|
|
727
772
|
const flying = smoothMax(plane, under, 0.006 * W);
|
|
728
773
|
px += (fx - px) * inAir;
|
|
@@ -730,6 +775,33 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
|
|
|
730
775
|
}
|
|
731
776
|
return [px, pz];
|
|
732
777
|
};
|
|
778
|
+
// Thin paper ripples in the air, more towards its free edge. Never
|
|
779
|
+
// down into the pages under it: the ripple dies away where the leaf
|
|
780
|
+
// lies close over them (it stays at least `lift` above them), or a
|
|
781
|
+
// leaf held low over its stack dipped into the page under it, whose
|
|
782
|
+
// print then showed through (#447).
|
|
783
|
+
const ripple = (u, v, px, pz) => {
|
|
784
|
+
const amp = flutter * W * (u / W) * (u / W);
|
|
785
|
+
if (flutter <= 0 || amp <= 1e-6)
|
|
786
|
+
return 0;
|
|
787
|
+
const clear = pz - surfaceAt(book, px)[1] - lift;
|
|
788
|
+
return amp * smooth(0, 2 * amp, clear) * Math.sin(2 * Math.PI * (time / 520) - (3 * u) / W + (2 * v) / H);
|
|
789
|
+
};
|
|
790
|
+
// Where the leaf's paper lies unfolded, column by column (before it
|
|
791
|
+
// ripples): [out from the spine, u, height].
|
|
792
|
+
const clearing = BLOCK_GAP * smooth(0, 0.06, q) * smooth(0, 0.06, 1 - q);
|
|
793
|
+
const own = [];
|
|
794
|
+
if (fold) {
|
|
795
|
+
for (let ix = 0; ix <= NX; ix++) {
|
|
796
|
+
const u = column(ix / NX) * W;
|
|
797
|
+
let [ox, oz] = onBook(sx * u, 0);
|
|
798
|
+
if (r > 0) {
|
|
799
|
+
ox += (u * Math.cos(phi) - offset * Math.sin(phi) - ox) * r;
|
|
800
|
+
oz += (hinge + lift + u * Math.sin(phi) + offset * Math.cos(phi) - oz) * r;
|
|
801
|
+
}
|
|
802
|
+
own.push([sx * ox, u, oz]);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
733
805
|
for (let iy = 0; iy <= NY; iy++) {
|
|
734
806
|
const v = (0.5 - iy / NY) * H;
|
|
735
807
|
for (let ix = 0; ix <= NX; ix++) {
|
|
@@ -740,22 +812,13 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
|
|
|
740
812
|
let d = 0;
|
|
741
813
|
let dd = 0;
|
|
742
814
|
if (fold) {
|
|
743
|
-
const { F, n
|
|
815
|
+
const { F, n } = fold;
|
|
744
816
|
d = (u - F.u) * n.u + (v - F.v) * n.v;
|
|
745
817
|
if (d > 0) {
|
|
746
818
|
// Round the roll, then straight on at the fold's angle.
|
|
747
|
-
|
|
748
|
-
if (R > 1e-3 && d < R * theta) {
|
|
749
|
-
dd = R * Math.sin(d / R);
|
|
750
|
-
z = R * (1 - Math.cos(d / R));
|
|
751
|
-
}
|
|
752
|
-
else {
|
|
753
|
-
const rest = d - R * theta;
|
|
754
|
-
dd = R * Math.sin(theta) + rest * Math.cos(theta);
|
|
755
|
-
z = R * (1 - Math.cos(theta)) + rest * Math.sin(theta);
|
|
756
|
-
}
|
|
757
|
-
x = u - n.u * (d - dd);
|
|
819
|
+
[dd, z] = rollOf(d, fold.R, fold.theta);
|
|
758
820
|
y = v - n.v * (d - dd);
|
|
821
|
+
x = u - n.u * (d - dd);
|
|
759
822
|
}
|
|
760
823
|
}
|
|
761
824
|
let [px, pz] = onBook(sx * x, z);
|
|
@@ -775,24 +838,42 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
|
|
|
775
838
|
y += (v - y) * r;
|
|
776
839
|
pz += (hinge + lift + u * Math.sin(phi) + offset * Math.cos(phi) - pz) * r;
|
|
777
840
|
}
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (
|
|
787
|
-
|
|
841
|
+
pz += ripple(u, v, px, pz);
|
|
842
|
+
// Never through the leaf's own paper not yet folded, as it ripples
|
|
843
|
+
// too: carried level from a fold low on a side sloping up to the
|
|
844
|
+
// spine, the paper folded back over it ran under it, crossing it.
|
|
845
|
+
if (fold && d > 0) {
|
|
846
|
+
const at = sx * px;
|
|
847
|
+
// (A row past the head and foot too: a triangle reaching past them
|
|
848
|
+
// from paper kept over sagged through.)
|
|
849
|
+
if (at > own[0][0] && at < own[NX][0] && Math.abs(y) <= H / 2 + (1.5 * H) / NY) {
|
|
850
|
+
let lo = 0;
|
|
851
|
+
let hi = NX;
|
|
852
|
+
while (hi - lo > 1) {
|
|
853
|
+
const mid = (lo + hi) >> 1;
|
|
854
|
+
if (own[mid][0] <= at)
|
|
855
|
+
lo = mid;
|
|
856
|
+
else
|
|
857
|
+
hi = mid;
|
|
858
|
+
}
|
|
859
|
+
const f = (at - own[lo][0]) / (own[hi][0] - own[lo][0] || 1);
|
|
860
|
+
const ou = own[lo][1] + (own[hi][1] - own[lo][1]) * f;
|
|
861
|
+
if ((ou - fold.F.u) * fold.n.u + (y - fold.F.v) * fold.n.v < 0) {
|
|
862
|
+
const oz = own[lo][2] + (own[hi][2] - own[lo][2]) * f;
|
|
863
|
+
pz = Math.max(pz, oz + ripple(ou, y, px, oz) + OWN_GAP);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
788
866
|
}
|
|
789
867
|
// Never into the book: over a block (and past its head or foot) a
|
|
790
868
|
// leaf stands at least just over its top. A fold's flap is carried
|
|
791
869
|
// level from the foot of its fold, and on a thin side hanging from
|
|
792
870
|
// the spine that foot lies low: the flap went through the tall block
|
|
793
871
|
// on the other side (#489).
|
|
872
|
+
// In the air a little higher still (until it lands): half a pixel
|
|
873
|
+
// over the block is hardly any depth at the view's distance, and a
|
|
874
|
+
// leaf lying over a tall block's top drew through its page in teeth.
|
|
794
875
|
if (!board && Math.abs(px) < (px >= 0 ? xr : xl))
|
|
795
|
-
pz = Math.max(pz, topAt(px >= 0 ? book.right : book.left, Math.abs(px)) + Math.min(lift, 0.5));
|
|
876
|
+
pz = Math.max(pz, topAt(px >= 0 ? book.right : book.left, Math.abs(px)) + Math.min(lift, 0.5) + clearing);
|
|
796
877
|
pos.setXYZ(iy * (NX + 1) + ix, px, y, pz);
|
|
797
878
|
}
|
|
798
879
|
}
|
|
@@ -857,6 +938,7 @@ export class PageFlipper {
|
|
|
857
938
|
renderer;
|
|
858
939
|
scene = new Scene();
|
|
859
940
|
camera = new PerspectiveCamera(FOV, 1, 1, 10000);
|
|
941
|
+
composer;
|
|
860
942
|
left;
|
|
861
943
|
right;
|
|
862
944
|
desk;
|
|
@@ -965,10 +1047,20 @@ export class PageFlipper {
|
|
|
965
1047
|
this.staticCaster = casterMaterial(false);
|
|
966
1048
|
// `__postextFolioPreserve`: keep the drawing buffer (to read the canvas back while debugging).
|
|
967
1049
|
const preserveDrawingBuffer = !!globalThis.__postextFolioPreserve;
|
|
968
|
-
this.renderer = new WebGLRenderer({ canvas, alpha: true, antialias:
|
|
1050
|
+
this.renderer = new WebGLRenderer({ canvas, alpha: true, antialias: false, premultipliedAlpha: true, preserveDrawingBuffer });
|
|
969
1051
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
|
970
1052
|
this.renderer.toneMapping = NeutralToneMapping;
|
|
971
1053
|
this.renderer.setClearColor(0x000000, 0);
|
|
1054
|
+
// Edges smoothed by SMAA over the finished picture (#503), not by the
|
|
1055
|
+
// canvas's own four samples a pixel: those left steps on long, nearly
|
|
1056
|
+
// level edges of strong contrast (a light page over the dark block)
|
|
1057
|
+
// and cost the most of a frame (resolved into a preserved drawing
|
|
1058
|
+
// buffer); SMAA smooths them better for a fraction of it. The scene
|
|
1059
|
+
// is drawn into a float picture, toned for the screen, then smoothed.
|
|
1060
|
+
this.composer = new EffectComposer(this.renderer, new WebGLRenderTarget(1, 1, { type: HalfFloatType }));
|
|
1061
|
+
this.composer.addPass(new RenderPass(this.scene, this.camera));
|
|
1062
|
+
this.composer.addPass(new OutputPass());
|
|
1063
|
+
this.composer.addPass(new SMAAPass());
|
|
972
1064
|
if (this.renderer.shadowMap) {
|
|
973
1065
|
this.renderer.shadowMap.enabled = true;
|
|
974
1066
|
this.renderer.shadowMap.type = PCFShadowMap;
|
|
@@ -1039,7 +1131,8 @@ export class PageFlipper {
|
|
|
1039
1131
|
setAppearance(appearance) {
|
|
1040
1132
|
this.appearance = appearance;
|
|
1041
1133
|
this.resolved = resolveFolioConfig(withStockForExtent(appearance.folio, this.pageCount(appearance)));
|
|
1042
|
-
|
|
1134
|
+
const mm = appearance.pageWidthMm;
|
|
1135
|
+
this.bookSpec = paperSpec(this.resolved.paper, mm);
|
|
1043
1136
|
// The settings now give the view the reader orbited to (it was just
|
|
1044
1137
|
// saved as the book's): they drive it again, so editing them moves it.
|
|
1045
1138
|
const { yaw, pitch } = this.orbit;
|
|
@@ -1056,20 +1149,22 @@ export class PageFlipper {
|
|
|
1056
1149
|
const key = JSON.stringify(own);
|
|
1057
1150
|
let spec = specs.get(key);
|
|
1058
1151
|
if (!spec)
|
|
1059
|
-
specs.set(key, (spec = paperSpec(pagePaper(appearance.folio, own))));
|
|
1152
|
+
specs.set(key, (spec = paperSpec(pagePaper(appearance.folio, own), mm)));
|
|
1060
1153
|
return spec;
|
|
1061
1154
|
});
|
|
1062
1155
|
const r = this.resolved;
|
|
1063
1156
|
const covers = appearance.coverLeaves;
|
|
1064
1157
|
const ownCovers = !!covers && (covers.front !== undefined || covers.back !== undefined);
|
|
1065
|
-
|
|
1158
|
+
// A newspaper's front page is the outer sheet, no heavier than the
|
|
1159
|
+
// others: it turns as a page does.
|
|
1160
|
+
this.coverSpec = !ownCovers || r.binding.type === "folded"
|
|
1066
1161
|
? null
|
|
1067
1162
|
: r.binding.type === "saddleStitch"
|
|
1068
1163
|
? // A stapled booklet's cover is a sheet like the others, of the same
|
|
1069
1164
|
// finish and a little heavier: it bends and turns as a page does.
|
|
1070
|
-
paperSpec({ ...r.paper, grammage: Math.min(350, Math.round(r.paper.grammage * 1.5)), showThrough: false })
|
|
1165
|
+
paperSpec({ ...r.paper, grammage: Math.min(350, Math.round(r.paper.grammage * 1.5)), showThrough: false }, mm)
|
|
1071
1166
|
: {
|
|
1072
|
-
...paperSpec({ ...r.paper, type: "board", grammage: 1250, bulk: 1.6, finish: "silk", texture: "smooth", shade: { hex: "#ffffff", model: "hex" }, showThrough: false }),
|
|
1167
|
+
...paperSpec({ ...r.paper, type: "board", grammage: 1250, bulk: 1.6, finish: "silk", texture: "smooth", shade: { hex: "#ffffff", model: "hex" }, showThrough: false }, mm),
|
|
1073
1168
|
// The book's own printed covers: in a case binding a printed
|
|
1074
1169
|
// (litho-laminated) case, on the case's 2–3 mm board; on a
|
|
1075
1170
|
// paperback a laminated cover card, about 0.3 mm.
|
|
@@ -1117,9 +1212,17 @@ export class PageFlipper {
|
|
|
1117
1212
|
c.clearcoat = mat === "paper" ? 0.35 : 0;
|
|
1118
1213
|
c.normalMap?.dispose();
|
|
1119
1214
|
c.normalMap = mat === "cloth" ? cachedRelief("linen").normal.clone() : mat === "leather" ? cachedDesk("leather").normal.clone() : null;
|
|
1215
|
+
if (r.binding.type === "folded") {
|
|
1216
|
+
// No cover at all: the back of the fold is the outer sheet's paper.
|
|
1217
|
+
c.color.set(r.paper.shade.hex);
|
|
1218
|
+
c.roughness = this.bookSpec.roughness;
|
|
1219
|
+
c.sheen = this.bookSpec.sheen;
|
|
1220
|
+
c.sheenColor.set(1, 1, 1);
|
|
1221
|
+
c.clearcoat = this.bookSpec.clearcoat;
|
|
1222
|
+
}
|
|
1120
1223
|
c.needsUpdate = true;
|
|
1121
|
-
// The spine.
|
|
1122
|
-
const spine = r.binding.type === "saddleStitch" ? null : (appearance.spineImage ?? null);
|
|
1224
|
+
// The spine: none on a stapled or folded book.
|
|
1225
|
+
const spine = r.binding.type === "saddleStitch" || r.binding.type === "folded" ? null : (appearance.spineImage ?? null);
|
|
1123
1226
|
if (spine !== this.spineSrc) {
|
|
1124
1227
|
this.spineSrc = spine;
|
|
1125
1228
|
this.spineTex?.dispose();
|
|
@@ -1379,8 +1482,8 @@ export class PageFlipper {
|
|
|
1379
1482
|
const tR = thick(right, extra.after);
|
|
1380
1483
|
const binding = this.resolved.binding.type;
|
|
1381
1484
|
// A board on top of a stack lies flat; the document's own covers
|
|
1382
|
-
// replace the case.
|
|
1383
|
-
const noCase = !!this.coverSpec;
|
|
1485
|
+
// replace the case, and folded sheets have none.
|
|
1486
|
+
const noCase = !!this.coverSpec || binding === "folded";
|
|
1384
1487
|
const rigid = (leaf) => leaf !== undefined && this.specOf(leaf).rigidity > 0.5;
|
|
1385
1488
|
const { left: pl, right: pr, board } = profiles(binding, this.W, k, tL, tR, {
|
|
1386
1489
|
flatLeft: rigid(left[left.length - 1]),
|
|
@@ -1445,7 +1548,7 @@ export class PageFlipper {
|
|
|
1445
1548
|
this.stacks.push(mesh);
|
|
1446
1549
|
this.stage.add(mesh);
|
|
1447
1550
|
}
|
|
1448
|
-
if (noCase)
|
|
1551
|
+
if (noCase && binding !== "folded")
|
|
1449
1552
|
this.coverMaterial.color.copy(this.boardColor());
|
|
1450
1553
|
// The back of the block: the folds sewn at the spine, under the line
|
|
1451
1554
|
// where the two open pages meet (nothing shows through the gutter).
|
|
@@ -1555,6 +1658,8 @@ export class PageFlipper {
|
|
|
1555
1658
|
const dpr = Math.min(window.devicePixelRatio || 1, 3);
|
|
1556
1659
|
this.renderer.setPixelRatio(1);
|
|
1557
1660
|
this.renderer.setSize(Math.round(cw * dpr), Math.round(ch * dpr), false);
|
|
1661
|
+
this.composer.setPixelRatio(1);
|
|
1662
|
+
this.composer.setSize(Math.round(cw * dpr), Math.round(ch * dpr));
|
|
1558
1663
|
this.buffer.set(Math.round(cw * dpr), Math.round(ch * dpr));
|
|
1559
1664
|
this.dpr = dpr;
|
|
1560
1665
|
const spread = this.spread.getBoundingClientRect();
|
|
@@ -1567,6 +1672,9 @@ export class PageFlipper {
|
|
|
1567
1672
|
mesh.material.userData.spec = null;
|
|
1568
1673
|
}
|
|
1569
1674
|
this.buildBook();
|
|
1675
|
+
// The videos' slots follow the size the pages are seen at.
|
|
1676
|
+
if (this.videos.length)
|
|
1677
|
+
this.packVideos();
|
|
1570
1678
|
// The desk: far wider than the view, tiled at its physical scale.
|
|
1571
1679
|
const k = this.pxPerMm();
|
|
1572
1680
|
const size = 40 * Math.max(W, H);
|
|
@@ -1732,75 +1840,99 @@ export class PageFlipper {
|
|
|
1732
1840
|
mesh.material.userData.uniforms.uPaper.value.setRGB(r, g, b, SRGBColorSpace);
|
|
1733
1841
|
this.redraw();
|
|
1734
1842
|
}
|
|
1735
|
-
/** The
|
|
1736
|
-
|
|
1843
|
+
/** The videos playing on the pages (#477, #507). */
|
|
1844
|
+
videos = [];
|
|
1845
|
+
/** Their pictures, drawn into one texture. */
|
|
1846
|
+
atlas = null;
|
|
1847
|
+
/** The slot sizes the atlas was packed for. */
|
|
1848
|
+
atlasKey = "";
|
|
1737
1849
|
/**
|
|
1738
|
-
* Draws
|
|
1739
|
-
* {@link PageVideoFrame}), over
|
|
1740
|
-
* whichever leaf carries
|
|
1741
|
-
* bends with the paper.
|
|
1742
|
-
*
|
|
1850
|
+
* Draws videos on the pages whose sources are their `src` (see
|
|
1851
|
+
* {@link PageVideoFrame}), over their paintings, on whichever face of
|
|
1852
|
+
* whichever leaf carries each page: lying open or turning, a picture
|
|
1853
|
+
* bends with the paper. Any number play together, up to
|
|
1854
|
+
* `MAX_PAGE_VIDEOS` (8) drawn at once; an empty list takes them all off.
|
|
1855
|
+
* The book is drawn again at every new frame of any of them.
|
|
1743
1856
|
*/
|
|
1857
|
+
setVideos(videos) {
|
|
1858
|
+
const crop = (v) => JSON.stringify(v.frame.crop ?? null);
|
|
1859
|
+
// The same pictures (a page painted on a new canvas, a layout that
|
|
1860
|
+
// moved one): the atlas stays as it is unless their sizes changed.
|
|
1861
|
+
const same = videos.length === this.videos.length && videos.every((v, i) => v.element === this.videos[i].element && crop(v) === crop(this.videos[i]));
|
|
1862
|
+
this.videos = videos.slice();
|
|
1863
|
+
if (!same)
|
|
1864
|
+
this.atlasKey = "";
|
|
1865
|
+
this.packVideos();
|
|
1866
|
+
this.dressVideos();
|
|
1867
|
+
this.redraw();
|
|
1868
|
+
}
|
|
1869
|
+
/** One video on a page, or none (see {@link setVideos}). */
|
|
1744
1870
|
setVideo(video) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1871
|
+
this.setVideos(video ? [video] : []);
|
|
1872
|
+
}
|
|
1873
|
+
/** Packs the videos' pictures into the atlas, each at about the size it
|
|
1874
|
+
* is seen on screen at rest, again when the book's size changes. */
|
|
1875
|
+
packVideos() {
|
|
1876
|
+
const sizes = this.videos.map(({ frame }) => {
|
|
1877
|
+
const w = Math.hypot(frame.across.x * this.W, frame.across.y * this.H) * this.dpr;
|
|
1878
|
+
const h = Math.hypot(frame.down.x * this.W, frame.down.y * this.H) * this.dpr;
|
|
1879
|
+
return { width: Math.max(16, Math.round(w)), height: Math.max(16, Math.round(h)) };
|
|
1880
|
+
});
|
|
1881
|
+
const key = this.videos.map((v, i) => `${sizes[i].width}x${sizes[i].height}`).join(",") + `|${this.videos.length}`;
|
|
1882
|
+
if (key === this.atlasKey && (this.atlas || !this.videos.length))
|
|
1883
|
+
return;
|
|
1884
|
+
this.atlasKey = key;
|
|
1885
|
+
if (!this.videos.length) {
|
|
1886
|
+
this.atlas?.dispose();
|
|
1887
|
+
this.atlas = null;
|
|
1762
1888
|
}
|
|
1763
|
-
|
|
1764
|
-
this.
|
|
1889
|
+
else {
|
|
1890
|
+
const max = Math.min(ATLAS_MAX, this.renderer.capabilities.maxTextureSize ?? ATLAS_MAX);
|
|
1891
|
+
this.atlas ??= new VideoAtlas((first) => {
|
|
1892
|
+
if (first)
|
|
1893
|
+
this.dressVideos();
|
|
1894
|
+
this.redraw();
|
|
1895
|
+
}, max);
|
|
1896
|
+
this.atlas.set(this.videos.map((v, i) => ({ element: v.element, ...(v.frame.crop ? { crop: v.frame.crop } : {}), ...sizes[i] })));
|
|
1897
|
+
}
|
|
1898
|
+
this.dressVideos();
|
|
1765
1899
|
this.redraw();
|
|
1766
1900
|
}
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
const v = this.video;
|
|
1771
|
-
if (!v)
|
|
1772
|
-
return;
|
|
1773
|
-
const el = v.element;
|
|
1774
|
-
const next = () => {
|
|
1775
|
-
if (this.video?.element !== el || this.disposed)
|
|
1776
|
-
return;
|
|
1777
|
-
this.redraw();
|
|
1778
|
-
v.callback = el.requestVideoFrameCallback ? el.requestVideoFrameCallback(next) : requestAnimationFrame(next);
|
|
1779
|
-
if (this.video)
|
|
1780
|
-
this.video.callback = v.callback;
|
|
1781
|
-
};
|
|
1782
|
-
v.callback = el.requestVideoFrameCallback ? el.requestVideoFrameCallback(next) : requestAnimationFrame(next);
|
|
1901
|
+
dressVideos() {
|
|
1902
|
+
for (const mesh of this.pageMeshes())
|
|
1903
|
+
this.dressVideo(mesh);
|
|
1783
1904
|
}
|
|
1784
1905
|
/** The meshes that show pages: the open ones, the leaves in the air and
|
|
1785
1906
|
* the block. */
|
|
1786
1907
|
pageMeshes() {
|
|
1787
1908
|
return [this.left, this.right, ...this.leaves.values(), ...(this.blockMesh ? [this.blockMesh] : [])];
|
|
1788
1909
|
}
|
|
1789
|
-
/** Sets a mesh's video uniforms: on the
|
|
1790
|
-
*
|
|
1910
|
+
/** Sets a mesh's video uniforms: the videos on the faces that show their
|
|
1911
|
+
* pages (once their pictures are in the atlas), none elsewhere. */
|
|
1791
1912
|
dressVideo(mesh) {
|
|
1792
1913
|
const u = mesh.material.userData.uniforms;
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1914
|
+
const atlas = this.atlas;
|
|
1915
|
+
let n = 0;
|
|
1916
|
+
for (const v of atlas ? this.videos : []) {
|
|
1917
|
+
if (n >= MAX_PAGE_VIDEOS)
|
|
1918
|
+
break;
|
|
1919
|
+
// A blank page ("") is no page of its own: it shows no video.
|
|
1920
|
+
const face = v.src ? (mesh.userData.Front === v.src ? 1 : mesh.userData.Back === v.src ? 2 : 0) : 0;
|
|
1921
|
+
const rect = face ? atlas.rectOf(v.element) : null;
|
|
1922
|
+
if (!rect)
|
|
1923
|
+
continue;
|
|
1924
|
+
const { origin, across, down } = v.frame;
|
|
1925
|
+
u.uVidOA.value[n].set(origin.x, origin.y, across.x, across.y);
|
|
1926
|
+
u.uVidDF.value[n].set(down.x, down.y, face, 0);
|
|
1927
|
+
u.uVidRect.value[n].set(rect.x, rect.y, rect.width, rect.height);
|
|
1928
|
+
n++;
|
|
1929
|
+
}
|
|
1930
|
+
u.uVidCount.value = n;
|
|
1931
|
+
u.uVideo.value = n ? atlas.texture : null;
|
|
1932
|
+
if (n) {
|
|
1933
|
+
const size = atlas.size;
|
|
1934
|
+
u.uVidTexel.value.set(1 / size.width, 1 / size.height);
|
|
1935
|
+
}
|
|
1804
1936
|
}
|
|
1805
1937
|
/** A page source drawn again in place (a canvas repainted): its
|
|
1806
1938
|
* texture is uploaded again and the book redrawn. */
|
|
@@ -2266,13 +2398,14 @@ export class PageFlipper {
|
|
|
2266
2398
|
rigidOf(k) {
|
|
2267
2399
|
return this.specOf(k).rigidity > 0.5;
|
|
2268
2400
|
}
|
|
2269
|
-
/** The page under the pointer
|
|
2270
|
-
* recto), −1 the other, 0
|
|
2401
|
+
/** The page under the pointer, by its outer part (where a hand takes
|
|
2402
|
+
* it): +1 the one a forward turn takes (the recto), −1 the other, 0
|
|
2403
|
+
* none (off the pages, or nearer the spine). */
|
|
2271
2404
|
hit(event) {
|
|
2272
2405
|
if (!this.surfaces)
|
|
2273
2406
|
this.layout();
|
|
2274
2407
|
const { x, y } = this.toWorld(event);
|
|
2275
|
-
if (Math.abs(y) > this.H / 2 || Math.abs(x) > this.W * 1.05)
|
|
2408
|
+
if (Math.abs(y) > this.H / 2 || Math.abs(x) > this.W * 1.05 || Math.abs(x) < GRIP * this.W)
|
|
2276
2409
|
return 0;
|
|
2277
2410
|
return x > 0 ? 1 : -1;
|
|
2278
2411
|
}
|
|
@@ -2286,7 +2419,7 @@ export class PageFlipper {
|
|
|
2286
2419
|
const at = this.settledAt();
|
|
2287
2420
|
const forward = x > 0;
|
|
2288
2421
|
const k = forward ? at : at - 1;
|
|
2289
|
-
if (k < 0 || k >= this.turned.length || Math.abs(x) > this.W * 1.05 || Math.abs(y) > this.H / 2)
|
|
2422
|
+
if (k < 0 || k >= this.turned.length || Math.abs(x) > this.W * 1.05 || Math.abs(x) < GRIP * this.W || Math.abs(y) > this.H / 2)
|
|
2290
2423
|
return false;
|
|
2291
2424
|
const G = { u: Math.max(0.15 * this.W, Math.min(this.W, Math.abs(x))), v: y };
|
|
2292
2425
|
this.turns.set(k, { forward, G, P: { ...G }, held: { aim: { ...G }, samples: [] }, rigid: this.rigidOf(k) });
|
|
@@ -2531,7 +2664,7 @@ export class PageFlipper {
|
|
|
2531
2664
|
}
|
|
2532
2665
|
this.mirror(air.length > 0 || this.block !== null);
|
|
2533
2666
|
this.behind(air.map((a) => a.mesh));
|
|
2534
|
-
this.
|
|
2667
|
+
this.composer.render();
|
|
2535
2668
|
}
|
|
2536
2669
|
/**
|
|
2537
2670
|
* Thin paper lets light through (#484). While translucent leaves are in
|
|
@@ -2719,13 +2852,14 @@ export class PageFlipper {
|
|
|
2719
2852
|
}
|
|
2720
2853
|
dispose() {
|
|
2721
2854
|
this.disposed = true;
|
|
2722
|
-
this.
|
|
2855
|
+
this.setVideos([]);
|
|
2723
2856
|
cancelAnimationFrame(this.raf);
|
|
2724
2857
|
cancelAnimationFrame(this.still);
|
|
2725
2858
|
cancelAnimationFrame(this.orbitFrame);
|
|
2726
2859
|
for (const tex of this.ready.values())
|
|
2727
2860
|
tex.dispose();
|
|
2728
2861
|
this.shadowTarget?.dispose();
|
|
2862
|
+
this.composer.dispose();
|
|
2729
2863
|
this.mirrors.left?.dispose();
|
|
2730
2864
|
this.mirrors.right?.dispose();
|
|
2731
2865
|
this.behindTarget?.depthTexture?.dispose();
|