postext-folio 0.5.3 → 0.6.1

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/dist/pageFlip.js CHANGED
@@ -1,12 +1,20 @@
1
- import { BoxGeometry, BufferAttribute, Color, CustomBlending, DirectionalLight, DoubleSide, Group, HalfFloatType, LinearMipmapLinearFilter, MaxEquation, Mesh, MeshPhysicalMaterial, MeshStandardMaterial, LinearFilter, NeutralToneMapping, OneFactor, OrthographicCamera, PCFShadowMap, PerspectiveCamera, Plane, PlaneGeometry, PMREMGenerator, Raycaster, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, SRGBColorSpace, Texture, TextureLoader, Vector2, Vector3, Vector4, WebGLRenderer, WebGLRenderTarget, } from "three";
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, VideoTexture, 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
- import { along, BINDINGS, gutterOcclusion, profiles, stackGeometry } from "./bookGeometry";
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";
7
11
  import { cachedDesk, cachedRelief } from "./procedural";
8
12
  /** Columns and rows of a leaf's mesh: fine enough for a tight roll. */
9
13
  const NX = 96;
14
+ /** How far a leaf's folded paper stays over its own paper beneath it. */
15
+ const OWN_GAP = 1.2;
16
+ /** How much higher than at rest a leaf in the air stays over a block. */
17
+ const BLOCK_GAP = 1.5;
10
18
  const NY = 120;
11
19
  /** Columns of a page lying open (its curve runs across only). */
12
20
  const NX_OPEN = 72;
@@ -23,6 +31,9 @@ export const BLOCK_PAGES = 10;
23
31
  /** How long a block of leaves takes to go over (ms), a little longer for
24
32
  * a thicker one. */
25
33
  const BLOCK_DURATION = 1150;
34
+ /** A page is taken by its outer part only: from this fraction of its
35
+ * width from the spine out to the fore-edge (#494). */
36
+ const GRIP = 0.5;
26
37
  /** A leaf follows the one before it once that one is this far through its turn. */
27
38
  const GAP = 0.14;
28
39
  // Ambient occlusion from the book's own shape: each frame the book is
@@ -68,12 +79,17 @@ const OCCLUSION = /* glsl */ `
68
79
  // How far the reflection seen at p runs under paper: its ray marched
69
80
  // out through the height map (a lifted leaf hides what it would mirror,
70
81
  // and so does the leaf's own roll over the inside of its curl). The
71
- // march starts a few px out, past the paper round p itself.
82
+ // march starts a few px out, past the paper round p itself. Its steps
83
+ // are shifted a little from pixel to pixel: at fixed steps the leaf's
84
+ // outline was cut in at each of them, a row of teeth (#483).
72
85
  float reflectionOcclusion(vec3 p, vec3 r, float channel) {
73
86
  if (uShadowOn < 0.5) return 0.0;
87
+ // Interleaved gradient noise: neighbours take evenly spread shifts.
88
+ float spin = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
74
89
  float hidden = 0.0;
75
- for (int i = 1; i <= 6; i++) {
76
- float t = 6.0 + float(i * i) / 36.0 * uOccRadius * 2.0;
90
+ for (int i = 0; i < 8; i++) {
91
+ float s = (float(i) + spin) / 8.0;
92
+ float t = 6.0 + s * s * uOccRadius * 2.0;
77
93
  vec3 q = p + r * t;
78
94
  hidden = max(hidden, smoothstep(0.0, 4.0, casterAt(q.xy, channel) - q.z - uOccBias));
79
95
  }
@@ -169,6 +185,25 @@ function keySpecular(occlusion = "1.0") {
169
185
  function keyShadowed(fragmentShader) {
170
186
  return fragmentShader.replace("#include <shadowmap_pars_fragment>", SHADOW_PARS);
171
187
  }
188
+ /** A caster's own copy of a mesh's shape. The height map is drawn before
189
+ * the main view, and three.js uploads a geometry once per frame number,
190
+ * which the key's shadow pass at the end of the last main view had
191
+ * already taken: a caster sharing a leaf's geometry drew the leaf where
192
+ * it lay a frame before, and a leaf moving down was shaded by its own
193
+ * old self, moving up it was not (#488). */
194
+ function casterGeometry(source) {
195
+ const g = new BufferGeometry();
196
+ g.setAttribute("position", source.attributes.position.clone());
197
+ if (source.index)
198
+ g.setIndex(source.index.clone());
199
+ return g;
200
+ }
201
+ /** Brings a caster's copy to its mesh's shape as laid out this frame. */
202
+ function syncCaster(caster, source) {
203
+ const to = caster.geometry.attributes.position;
204
+ to.array.set(source.attributes.position.array);
205
+ to.needsUpdate = true;
206
+ }
172
207
  function casterMaterial(leaf) {
173
208
  return new ShaderMaterial({
174
209
  vertexShader: CASTER_VERTEX,
@@ -198,6 +233,21 @@ function occluded(m, sign) {
198
233
  };
199
234
  return m;
200
235
  }
236
+ /** What lies behind the leaves in the air (#484): the scene drawn without
237
+ * the translucent leaves (colour, mipmapped, and depth), shared by every
238
+ * page. `uBehindSize`: the screen in device px; `uBehindClip`: the
239
+ * camera's near and far planes; `uBehindScale`: texels of that picture
240
+ * per book unit at a view depth of 1; `uBehindFloor`: how far (book
241
+ * units) the sheet's fibres spread even what touches it. */
242
+ const behindUniforms = {
243
+ uBehind: { value: null },
244
+ uBehindDepth: { value: null },
245
+ uBehindOn: { value: 0 },
246
+ uBehindSize: { value: new Vector2(1, 1) },
247
+ uBehindClip: { value: new Vector2(1, 2) },
248
+ uBehindScale: { value: 1 },
249
+ uBehindFloor: { value: 1 },
250
+ };
201
251
  /**
202
252
  * A page's material: three.js's physical (PBR) material lit by the
203
253
  * environment and the key light, the printed page on each face, the
@@ -216,13 +266,27 @@ function pageMaterial(mirror, sign) {
216
266
  uHasBack: { value: 0 },
217
267
  uMirror: { value: mirror ? 1 : 0 },
218
268
  uShowThrough: { value: 0 },
269
+ uUnder: { value: null },
270
+ uHasUnder: { value: 0 },
271
+ uUnderK: { value: 0 },
272
+ uTransmit: { value: 0 },
219
273
  uPaper: { value: new Color(1, 1, 1) },
220
274
  uLeafOcc: { value: 0.45 },
221
275
  uOccBias: { value: 6 },
276
+ uVideo: { value: null },
277
+ uVidFace: { value: 0 },
278
+ uVidO: { value: new Vector2() },
279
+ uVidA: { value: new Vector2(1, 0) },
280
+ uVidB: { value: new Vector2(0, 1) },
281
+ uVidCrop: { value: new Vector4(0, 0, 1, 1) },
282
+ uReflection: { value: null },
283
+ uReflectionOn: { value: 0 },
284
+ uReflectionSize: { value: new Vector2(1, 1) },
285
+ uReflectionNormal: { value: new Vector3(0, 0, 1) },
222
286
  };
223
287
  m.userData.uniforms = uniforms;
224
288
  m.onBeforeCompile = (shader) => {
225
- Object.assign(shader.uniforms, occlusionUniforms, uniforms, { uSign: { value: sign } });
289
+ Object.assign(shader.uniforms, occlusionUniforms, behindUniforms, uniforms, { uSign: { value: sign } });
226
290
  shader.vertexShader = shader.vertexShader
227
291
  .replace("#include <common>", `#include <common>
228
292
  attribute float ao;
@@ -241,9 +305,33 @@ function pageMaterial(mirror, sign) {
241
305
  uniform float uHasBack;
242
306
  uniform float uMirror;
243
307
  uniform float uShowThrough;
308
+ uniform sampler2D uUnder;
309
+ uniform float uHasUnder;
310
+ uniform float uUnderK;
311
+ uniform float uTransmit;
312
+ uniform sampler2D uBehind;
313
+ uniform sampler2D uBehindDepth;
314
+ uniform float uBehindOn;
315
+ uniform vec2 uBehindSize;
316
+ uniform vec2 uBehindClip;
317
+ uniform float uBehindScale;
318
+ uniform float uBehindFloor;
319
+ // The print of both faces at this point: what light coming
320
+ // through the sheet passes on its way.
321
+ vec3 transInk = vec3(1.0);
244
322
  uniform vec3 uPaper;
245
323
  uniform float uLeafOcc;
246
324
  uniform float uSign;
325
+ uniform sampler2D uVideo;
326
+ uniform float uVidFace;
327
+ uniform vec2 uVidO;
328
+ uniform vec2 uVidA;
329
+ uniform vec2 uVidB;
330
+ uniform vec4 uVidCrop;
331
+ uniform sampler2D uReflection;
332
+ uniform float uReflectionOn;
333
+ uniform vec2 uReflectionSize;
334
+ uniform vec3 uReflectionNormal;
247
335
  varying float vAo;
248
336
  varying vec2 vPageUv;
249
337
  varying vec3 vBookPos;
@@ -257,12 +345,34 @@ function pageMaterial(mirror, sign) {
257
345
  vec2 uvB = vec2(1.0 - u, vPageUv.y);
258
346
  vec3 ink = uPaper;
259
347
  if (has > 0.5 && has < 1.5) ink = front ? texture2D(uFront, uvF).rgb : texture2D(uBack, uvB).rgb;
348
+ // A video playing on this face, printed over its poster: the
349
+ // point of the page (from its top left corner) in the picture's
350
+ // own frame, which may lie turned on the page.
351
+ if (uVidFace > 0.5 && (uVidFace < 1.5) == front) {
352
+ vec2 tuv = front ? uvF : uvB;
353
+ vec2 p = vec2(tuv.x, 1.0 - tuv.y) - uVidO;
354
+ vec2 st = vec2(dot(p, uVidA) / dot(uVidA, uVidA), dot(p, uVidB) / dot(uVidB, uVidB));
355
+ if (st.x >= 0.0 && st.x <= 1.0 && st.y >= 0.0 && st.y <= 1.0) {
356
+ vec2 v = uVidCrop.xy + st * uVidCrop.zw;
357
+ ink = texture2D(uVideo, vec2(v.x, 1.0 - v.y)).rgb;
358
+ }
359
+ }
260
360
  // The other side's print, seen through the sheet (the same point
261
361
  // of paper, so mirrored as it should be).
262
362
  float other = front ? uHasBack : uHasFront;
263
- if (uShowThrough > 0.0 && other > 0.5 && other < 1.5) {
264
- vec3 through = front ? texture2D(uBack, uvB).rgb : texture2D(uFront, uvF).rgb;
265
- ink *= mix(vec3(1.0), through, uShowThrough);
363
+ vec3 through = vec3(1.0);
364
+ if ((uShowThrough > 0.0 || uTransmit > 0.0) && other > 0.5 && other < 1.5) {
365
+ through = front ? texture2D(uBack, uvB).rgb : texture2D(uFront, uvF).rgb;
366
+ }
367
+ transInk = ink * through;
368
+ if (uShowThrough > 0.0) ink *= mix(vec3(1.0), through, uShowThrough);
369
+ // The page lying under an open page: its print comes through the
370
+ // whole sheet (the fibres spread it by a fraction of a millimetre,
371
+ // finer than the page's own texels: a softer sample would blur
372
+ // a page seen small to grey).
373
+ if (uHasUnder > 0.5 && uUnderK > 0.0) {
374
+ vec3 under = texture2D(uUnder, front ? uvF : uvB).rgb;
375
+ ink *= mix(vec3(1.0), under, uUnderK);
266
376
  }
267
377
  diffuseColor.rgb *= ink;
268
378
  }`)
@@ -277,7 +387,49 @@ function pageMaterial(mirror, sign) {
277
387
  // The reflection's ray, in book coordinates.
278
388
  vec3 reflected = (vec4(reflect(-geometryViewDir, normal), 0.0) * viewMatrix).xyz;
279
389
  reflected.x *= uSign;
280
- ${keySpecular("1.0 - 0.85 * reflectionOcclusion(vBookPos, reflected, 1.0)")}`);
390
+ // A mirrored leaf stands for what it hides itself.
391
+ ${keySpecular("1.0 - 0.85 * (uReflectionOn > 0.5 ? 0.0 : reflectionOcclusion(vBookPos, reflected, 1.0))")}
392
+ #ifdef USE_CLEARCOAT
393
+ if (uReflectionOn > 0.5) {
394
+ // The leaves in the air, mirrored in a gloss page: where one is
395
+ // seen, it takes the place of the environment it hides, through
396
+ // the coating's Fresnel and the paper's own. A plane holds only
397
+ // where the page lies in it: the mirror fades up the gutter.
398
+ vec2 st = gl_FragCoord.xy / uReflectionSize;
399
+ vec4 seen = texture2D(uReflection, vec2(1.0 - st.x, st.y));
400
+ float lies = smoothstep(0.97, 0.995, abs(dot((vec4(normalize(vNormal), 0.0) * viewMatrix).xyz, uReflectionNormal)));
401
+ float a = seen.a * lies;
402
+ vec3 leaf = seen.rgb * lies;
403
+ clearcoatSpecularIndirect = clearcoatSpecularIndirect * (1.0 - a)
404
+ + leaf * EnvironmentBRDF(geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness);
405
+ reflectedLight.indirectSpecular = reflectedLight.indirectSpecular * (1.0 - a)
406
+ + leaf * EnvironmentBRDF(geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness);
407
+ }
408
+ #endif
409
+ if (uBehindOn > 0.5 && uTransmit > 0.0) {
410
+ // Thin paper in the air lets through the light behind it (#484):
411
+ // what lies there on screen, seen through the print of both
412
+ // faces. The sheet scatters what it lets through about evenly
413
+ // (near Lambertian), so a thing a gap behind it is spread over
414
+ // about twice that gap: sharp where the leaf touches the page
415
+ // under it, a soft glow a few millimetres off. A page close
416
+ // under it is lit only through the leaf, so its print comes
417
+ // back at about half the leaf's transmittance (1 − opacity, as
418
+ // the open page shows the one under it); what lies further, out
419
+ // of the leaf's shade, at the whole of it. Stronger and sharper,
420
+ // the page under a leaf about to land read as lying over it
421
+ // (#492). What it lets through it does not reflect.
422
+ vec2 st = gl_FragCoord.xy / uBehindSize;
423
+ float depth = texture2D(uBehindDepth, st).x;
424
+ float zBehind = uBehindClip.x * uBehindClip.y / (uBehindClip.y - depth * (uBehindClip.y - uBehindClip.x));
425
+ float gap = max(0.0, zBehind - vViewPosition.z);
426
+ float spread = (uBehindFloor + 2.0 * gap) * uBehindScale / vViewPosition.z;
427
+ vec4 seen = textureLod(uBehind, st, log2(max(1.0, 2.0 * spread)));
428
+ float carried = uTransmit * mix(0.5, 1.0, smoothstep(0.0, 8.0 * uBehindFloor, gap));
429
+ float lost = carried * seen.a;
430
+ reflectedLight.directDiffuse *= 1.0 - lost;
431
+ reflectedLight.indirectDiffuse = reflectedLight.indirectDiffuse * (1.0 - lost) + carried * seen.rgb * transInk;
432
+ }`);
281
433
  };
282
434
  return m;
283
435
  }
@@ -482,12 +634,33 @@ export function foldOf(G, P, W, H, roll = 1) {
482
634
  // straight, lands over the hand: R sinθ + (c − Rθ) cosθ = c − D.
483
635
  const arc = R * theta;
484
636
  const k = arc > 0 ? Math.min(1, D / arc) : 0;
485
- let c = (D + k * (R * Math.sin(theta) - arc * Math.cos(theta))) / (1 - Math.cos(theta));
637
+ const free = (D + k * (R * Math.sin(theta) - arc * Math.cos(theta))) / (1 - Math.cos(theta));
638
+ let c = free;
486
639
  for (const S of [-H / 2, H / 2])
487
640
  c = Math.min(c, G.u * n.u + (G.v - S) * n.v);
488
641
  if (c <= 0)
489
642
  return null;
490
- return { F: { u: G.u - n.u * c, v: G.v - n.v * c }, n, R, theta };
643
+ const F = { u: G.u - n.u * c, v: G.v - n.v * c };
644
+ // Never all at once (#502). Paper lying beyond the point taken, along
645
+ // the pull (a page taken by the middle of an edge and pulled across),
646
+ // turns over as the hand carries it away, not the moment it moves (a
647
+ // pixel's pull stood half the page up). A fold held off the spine short
648
+ // of the hand turns the less the nearer it comes to the point taken, so
649
+ // that it is gone, not dropped, when it reaches it.
650
+ let beyond = 0;
651
+ for (const u of [0, W])
652
+ for (const v of [-H / 2, H / 2])
653
+ beyond = Math.max(beyond, (u - G.u) * n.u + (v - G.v) * n.v);
654
+ const turns = (c / free) * (beyond > 0 ? smooth(0, 0.5 * beyond, D) : 1);
655
+ return { F, n, R, theta: theta * turns };
656
+ }
657
+ /** Paper `d` past the fold, round a roll of radius R and straight on at
658
+ * θ: how far it reaches back across the page, and how high it stands. */
659
+ function rollOf(d, R, theta) {
660
+ if (R > 1e-3 && d < R * theta)
661
+ return [R * Math.sin(d / R), R * (1 - Math.cos(d / R))];
662
+ const rest = d - R * theta;
663
+ return [R * Math.sin(theta) + rest * Math.cos(theta), R * (1 - Math.cos(theta)) + rest * Math.sin(theta)];
491
664
  }
492
665
  /** How far through its turn a leaf is: the point taken, from where it
493
666
  * was to its mirror image across the spine. */
@@ -563,13 +736,17 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
563
736
  // corner barely lifted, a few degrees through the middle of the turn.
564
737
  const inAir = smooth(0.02, 0.22, q) * smooth(0.02, 0.22, 1 - q);
565
738
  const lean = LEAN * inAir;
566
- const cosA = Math.cos(lean);
567
- const sinA = Math.sin(lean);
568
739
  // Each side's chord, from the spine to its fore-edge: a side hanging
569
740
  // from a standing spine slopes down to the desk, and a leaf leaning off
570
741
  // it leans off that slope, not off a level plane over it.
571
742
  const chordR = (zr - zr0) / (xr || 1);
572
743
  const chordL = (zl - zl0) / (xl || 1);
744
+ // The plane a leaf lifts off along, by side: the chord turned up by the
745
+ // lean. A leaf keeps its width along it (stepping the chord's rise per
746
+ // unit across, on a side sloping to the desk, stretched it by a tenth
747
+ // and more, #495).
748
+ const tiltR = Math.atan(chordR) + lean;
749
+ const tiltL = Math.atan(chordL) + lean;
573
750
  // Onto the book. At rest (and as it lands) the leaf lies on the open
574
751
  // book's surface, into the gutter and out. In the air it is lifted off
575
752
  // at the spine: its own plane rises from the gutter's floor at a small
@@ -580,8 +757,9 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
580
757
  let px = rx;
581
758
  let pz = rz + z + lift;
582
759
  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;
760
+ const tilt = s >= 0 ? tiltR : tiltL;
761
+ const fx = (s >= 0 ? 1 : -1) * Math.abs(s) * Math.cos(tilt);
762
+ const plane = (s >= 0 ? zr0 : zl0) + Math.abs(s) * Math.sin(tilt) + z + lift;
585
763
  const under = surfaceAt(book, fx)[1] + lift + 0.6;
586
764
  const flying = smoothMax(plane, under, 0.006 * W);
587
765
  px += (fx - px) * inAir;
@@ -589,6 +767,33 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
589
767
  }
590
768
  return [px, pz];
591
769
  };
770
+ // Thin paper ripples in the air, more towards its free edge. Never
771
+ // down into the pages under it: the ripple dies away where the leaf
772
+ // lies close over them (it stays at least `lift` above them), or a
773
+ // leaf held low over its stack dipped into the page under it, whose
774
+ // print then showed through (#447).
775
+ const ripple = (u, v, px, pz) => {
776
+ const amp = flutter * W * (u / W) * (u / W);
777
+ if (flutter <= 0 || amp <= 1e-6)
778
+ return 0;
779
+ const clear = pz - surfaceAt(book, px)[1] - lift;
780
+ return amp * smooth(0, 2 * amp, clear) * Math.sin(2 * Math.PI * (time / 520) - (3 * u) / W + (2 * v) / H);
781
+ };
782
+ // Where the leaf's paper lies unfolded, column by column (before it
783
+ // ripples): [out from the spine, u, height].
784
+ const clearing = BLOCK_GAP * smooth(0, 0.06, q) * smooth(0, 0.06, 1 - q);
785
+ const own = [];
786
+ if (fold) {
787
+ for (let ix = 0; ix <= NX; ix++) {
788
+ const u = column(ix / NX) * W;
789
+ let [ox, oz] = onBook(sx * u, 0);
790
+ if (r > 0) {
791
+ ox += (u * Math.cos(phi) - offset * Math.sin(phi) - ox) * r;
792
+ oz += (hinge + lift + u * Math.sin(phi) + offset * Math.cos(phi) - oz) * r;
793
+ }
794
+ own.push([sx * ox, u, oz]);
795
+ }
796
+ }
592
797
  for (let iy = 0; iy <= NY; iy++) {
593
798
  const v = (0.5 - iy / NY) * H;
594
799
  for (let ix = 0; ix <= NX; ix++) {
@@ -599,22 +804,13 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
599
804
  let d = 0;
600
805
  let dd = 0;
601
806
  if (fold) {
602
- const { F, n, R } = fold;
807
+ const { F, n } = fold;
603
808
  d = (u - F.u) * n.u + (v - F.v) * n.v;
604
809
  if (d > 0) {
605
810
  // Round the roll, then straight on at the fold's angle.
606
- const theta = fold.theta;
607
- if (R > 1e-3 && d < R * theta) {
608
- dd = R * Math.sin(d / R);
609
- z = R * (1 - Math.cos(d / R));
610
- }
611
- else {
612
- const rest = d - R * theta;
613
- dd = R * Math.sin(theta) + rest * Math.cos(theta);
614
- z = R * (1 - Math.cos(theta)) + rest * Math.sin(theta);
615
- }
616
- x = u - n.u * (d - dd);
811
+ [dd, z] = rollOf(d, fold.R, fold.theta);
617
812
  y = v - n.v * (d - dd);
813
+ x = u - n.u * (d - dd);
618
814
  }
619
815
  }
620
816
  let [px, pz] = onBook(sx * x, z);
@@ -634,17 +830,42 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
634
830
  y += (v - y) * r;
635
831
  pz += (hinge + lift + u * Math.sin(phi) + offset * Math.cos(phi) - pz) * r;
636
832
  }
637
- // Thin paper ripples in the air, more towards its free edge. Never
638
- // down into the pages under it: the ripple dies away where the leaf
639
- // lies close over them (it stays at least `lift` above them), or a
640
- // leaf held low over its stack dipped into the page under it, whose
641
- // print then showed through (#447).
642
- if (flutter > 0) {
643
- const amp = flutter * W * (u / W) * (u / W);
644
- const clear = pz - surfaceAt(book, px)[1] - lift;
645
- if (amp > 1e-6)
646
- pz += amp * smooth(0, 2 * amp, clear) * Math.sin(2 * Math.PI * (time / 520) - (3 * u) / W + (2 * v) / H);
833
+ pz += ripple(u, v, px, pz);
834
+ // Never through the leaf's own paper not yet folded, as it ripples
835
+ // too: carried level from a fold low on a side sloping up to the
836
+ // spine, the paper folded back over it ran under it, crossing it.
837
+ if (fold && d > 0) {
838
+ const at = sx * px;
839
+ // (A row past the head and foot too: a triangle reaching past them
840
+ // from paper kept over sagged through.)
841
+ if (at > own[0][0] && at < own[NX][0] && Math.abs(y) <= H / 2 + (1.5 * H) / NY) {
842
+ let lo = 0;
843
+ let hi = NX;
844
+ while (hi - lo > 1) {
845
+ const mid = (lo + hi) >> 1;
846
+ if (own[mid][0] <= at)
847
+ lo = mid;
848
+ else
849
+ hi = mid;
850
+ }
851
+ const f = (at - own[lo][0]) / (own[hi][0] - own[lo][0] || 1);
852
+ const ou = own[lo][1] + (own[hi][1] - own[lo][1]) * f;
853
+ if ((ou - fold.F.u) * fold.n.u + (y - fold.F.v) * fold.n.v < 0) {
854
+ const oz = own[lo][2] + (own[hi][2] - own[lo][2]) * f;
855
+ pz = Math.max(pz, oz + ripple(ou, y, px, oz) + OWN_GAP);
856
+ }
857
+ }
647
858
  }
859
+ // Never into the book: over a block (and past its head or foot) a
860
+ // leaf stands at least just over its top. A fold's flap is carried
861
+ // level from the foot of its fold, and on a thin side hanging from
862
+ // the spine that foot lies low: the flap went through the tall block
863
+ // on the other side (#489).
864
+ // In the air a little higher still (until it lands): half a pixel
865
+ // over the block is hardly any depth at the view's distance, and a
866
+ // leaf lying over a tall block's top drew through its page in teeth.
867
+ if (!board && Math.abs(px) < (px >= 0 ? xr : xl))
868
+ pz = Math.max(pz, topAt(px >= 0 ? book.right : book.left, Math.abs(px)) + Math.min(lift, 0.5) + clearing);
648
869
  pos.setXYZ(iy * (NX + 1) + ix, px, y, pz);
649
870
  }
650
871
  }
@@ -660,9 +881,25 @@ function disposeLeaf(mesh) {
660
881
  parts.slab.geometry.dispose();
661
882
  parts.slab.material.dispose();
662
883
  }
884
+ mesh.userData.caster?.geometry.dispose();
663
885
  mesh.geometry.dispose();
664
886
  mesh.material.dispose();
665
887
  }
888
+ /**
889
+ * The plane an open page lies in past the gutter's shoulder (world,
890
+ * normal up), its side running out from the spine along world x as
891
+ * `out` (±1): level on a block lying on the desk, sloping down to it on
892
+ * the thin side of a thick book, which hangs from the top of the spine.
893
+ * A gloss page mirrors the leaves in the air in it (#483).
894
+ */
895
+ export function pagePlane(p, W, out) {
896
+ const [x0, z0] = along(p, W * 0.55);
897
+ const [x1, z1] = along(p, W);
898
+ const dx = out * (x1 - x0);
899
+ const dz = z1 - z0;
900
+ const normal = new Vector3(-dz * Math.sign(dx || 1), 0, Math.abs(dx)).normalize();
901
+ return new Plane().setFromNormalAndCoplanarPoint(normal, new Vector3(out * x0, 0, z0));
902
+ }
666
903
  /**
667
904
  * Turns the pages of the viewer's DOM spread in a 3D scene: the book lies
668
905
  * open on a desk, seen from in front and above (tilted by `folio.tilt`),
@@ -693,6 +930,7 @@ export class PageFlipper {
693
930
  renderer;
694
931
  scene = new Scene();
695
932
  camera = new PerspectiveCamera(FOV, 1, 1, 10000);
933
+ composer;
696
934
  left;
697
935
  right;
698
936
  desk;
@@ -703,6 +941,12 @@ export class PageFlipper {
703
941
  spineSrc = null;
704
942
  spineTex = null;
705
943
  spineFaces = [];
944
+ /** The book's own cover folded round the spine while it is in the air:
945
+ * a strip on the spine's outer side, up to where the cover leaves it.
946
+ * `top`: the back of the block's height; `back`: its thickness; `side`:
947
+ * the spine's outer side (the empty stack's, −1 left). */
948
+ hinge;
949
+ fold = { top: 0, back: 0, side: -1 };
706
950
  edges;
707
951
  key = new DirectionalLight(0xffffff, 1);
708
952
  pmrem = null;
@@ -716,6 +960,16 @@ export class PageFlipper {
716
960
  shadowScene = new Scene();
717
961
  shadowCamera = new OrthographicCamera(-1, 1, 1, -1, 1, 4000);
718
962
  shadowTarget = null;
963
+ /** The leaves in the air mirrored in each gloss open page (#483), and
964
+ * the camera that sees them so (the view mirrored in the page). */
965
+ mirrors = { left: null, right: null };
966
+ mirrorCamera = new PerspectiveCamera();
967
+ mirrorOk = false;
968
+ /** What lies behind the translucent leaves in the air (#484). */
969
+ behindTarget = null;
970
+ /** The drawing buffer (device px) and the device pixel ratio. */
971
+ buffer = new Vector2(1, 1);
972
+ dpr = 1;
719
973
  /** Draw the leaves in the air, and the rest of the book, into the
720
974
  * height map. */
721
975
  leafCaster;
@@ -785,10 +1039,20 @@ export class PageFlipper {
785
1039
  this.staticCaster = casterMaterial(false);
786
1040
  // `__postextFolioPreserve`: keep the drawing buffer (to read the canvas back while debugging).
787
1041
  const preserveDrawingBuffer = !!globalThis.__postextFolioPreserve;
788
- this.renderer = new WebGLRenderer({ canvas, alpha: true, antialias: true, premultipliedAlpha: true, preserveDrawingBuffer });
1042
+ this.renderer = new WebGLRenderer({ canvas, alpha: true, antialias: false, premultipliedAlpha: true, preserveDrawingBuffer });
789
1043
  this.renderer.outputColorSpace = SRGBColorSpace;
790
1044
  this.renderer.toneMapping = NeutralToneMapping;
791
1045
  this.renderer.setClearColor(0x000000, 0);
1046
+ // Edges smoothed by SMAA over the finished picture (#503), not by the
1047
+ // canvas's own four samples a pixel: those left steps on long, nearly
1048
+ // level edges of strong contrast (a light page over the dark block)
1049
+ // and cost the most of a frame (resolved into a preserved drawing
1050
+ // buffer); SMAA smooths them better for a fraction of it. The scene
1051
+ // is drawn into a float picture, toned for the screen, then smoothed.
1052
+ this.composer = new EffectComposer(this.renderer, new WebGLRenderTarget(1, 1, { type: HalfFloatType }));
1053
+ this.composer.addPass(new RenderPass(this.scene, this.camera));
1054
+ this.composer.addPass(new OutputPass());
1055
+ this.composer.addPass(new SMAAPass());
792
1056
  if (this.renderer.shadowMap) {
793
1057
  this.renderer.shadowMap.enabled = true;
794
1058
  this.renderer.shadowMap.type = PCFShadowMap;
@@ -829,11 +1093,16 @@ export class PageFlipper {
829
1093
  depthBuffer: false,
830
1094
  generateMipmaps: false,
831
1095
  });
1096
+ this.mirrorOk = true;
832
1097
  }
1098
+ this.mirrorCamera.matrixAutoUpdate = false;
833
1099
  // A mirror turns three.js's face culling round with it (the front of a
834
1100
  // page stays its front).
835
1101
  this.stage.scale.x = this.sign;
836
- this.stage.add(this.desk, this.covers, this.left, this.right, this.key, this.key.target);
1102
+ this.hinge = new Mesh(new BoxGeometry(1, 1, 1), this.coverMaterial);
1103
+ this.hinge.receiveShadow = true;
1104
+ this.hinge.visible = false;
1105
+ this.stage.add(this.desk, this.covers, this.left, this.right, this.hinge, this.key, this.key.target);
837
1106
  this.scene.add(this.stage);
838
1107
  this.target = at;
839
1108
  this.reported = at;
@@ -1065,7 +1334,17 @@ export class PageFlipper {
1065
1334
  const coated = p.finish !== "uncoated";
1066
1335
  const s = p.textureStrength * (coated ? 0.35 : 1) * 0.6;
1067
1336
  m.normalScale.set(s, s);
1068
- m.userData.uniforms.uShowThrough.value = spec.showThrough;
1337
+ const u = m.userData.uniforms;
1338
+ u.uShowThrough.value = spec.showThrough;
1339
+ // An open page shows the page under it; a leaf in the air, what is
1340
+ // behind it (a block of leaves turning together is opaque).
1341
+ const open = mesh === this.left || mesh === this.right;
1342
+ // Print under a sheet shows through at 1 − opacity (what opacity
1343
+ // measures), half the light the sheet lets through: as much as a
1344
+ // leaf lifting off it lets through close over the page, so nothing
1345
+ // changes when the reader takes hold of it (#492).
1346
+ u.uUnderK.value = open ? spec.transmission / 2 : 0;
1347
+ u.uTransmit.value = open || mesh === this.blockMesh ? 0 : spec.transmission;
1069
1348
  m.needsUpdate = true;
1070
1349
  }
1071
1350
  // ── Layout ──
@@ -1264,6 +1543,7 @@ export class PageFlipper {
1264
1543
  // book's own cover), no more.
1265
1544
  const back = Math.min(0.04 * W, Math.max(1.5, (this.coverSpec?.caliperMm ?? BINDINGS[binding].boardMm) * k));
1266
1545
  this.clearSpine();
1546
+ this.fold = { top: meet > 0.4 ? meet - 0.3 : 0, back, side: tL < tR ? -1 : 1 };
1267
1547
  if (meet > 0.4) {
1268
1548
  // In a case the back is wrapped in the case's spine (the wall beside
1269
1549
  // an empty side, the headcap at head and tail); without one, the
@@ -1314,10 +1594,12 @@ export class PageFlipper {
1314
1594
  tex.repeat.set(W / k / tile, H / k / tile);
1315
1595
  }
1316
1596
  // The book at rest in the height map.
1317
- for (const c of this.staticCasters)
1597
+ for (const c of this.staticCasters) {
1318
1598
  c.removeFromParent();
1599
+ c.geometry.dispose();
1600
+ }
1319
1601
  this.staticCasters = [this.left, this.right, ...this.stacks, ...this.covers.children].map((m) => {
1320
- const c = new Mesh(m.geometry, this.staticCaster);
1602
+ const c = new Mesh(casterGeometry(m.geometry), this.staticCaster);
1321
1603
  c.position.copy(m.position);
1322
1604
  c.frustumCulled = false;
1323
1605
  this.shadowScene.add(c);
@@ -1357,6 +1639,10 @@ export class PageFlipper {
1357
1639
  const dpr = Math.min(window.devicePixelRatio || 1, 3);
1358
1640
  this.renderer.setPixelRatio(1);
1359
1641
  this.renderer.setSize(Math.round(cw * dpr), Math.round(ch * dpr), false);
1642
+ this.composer.setPixelRatio(1);
1643
+ this.composer.setSize(Math.round(cw * dpr), Math.round(ch * dpr));
1644
+ this.buffer.set(Math.round(cw * dpr), Math.round(ch * dpr));
1645
+ this.dpr = dpr;
1360
1646
  const spread = this.spread.getBoundingClientRect();
1361
1647
  const W = spread.width / 2 || 1;
1362
1648
  const H = spread.height || 1;
@@ -1532,6 +1818,76 @@ export class PageFlipper {
1532
1818
  mesh.material.userData.uniforms.uPaper.value.setRGB(r, g, b, SRGBColorSpace);
1533
1819
  this.redraw();
1534
1820
  }
1821
+ /** The video playing on a page (#477), or none. */
1822
+ video = null;
1823
+ /**
1824
+ * Draws a video on the page whose source is `src` (see
1825
+ * {@link PageVideoFrame}), over its painting, on whichever face of
1826
+ * whichever leaf carries that page: lying open or turning, the picture
1827
+ * bends with the paper. `null` takes it off. The book is drawn again at
1828
+ * every new frame of the video.
1829
+ */
1830
+ setVideo(video) {
1831
+ const old = this.video;
1832
+ if (old && (!video || video.element !== old.element)) {
1833
+ old.element.cancelVideoFrameCallback?.(old.callback);
1834
+ old.texture.dispose();
1835
+ this.video = null;
1836
+ }
1837
+ if (video) {
1838
+ let texture = this.video?.texture;
1839
+ if (!texture) {
1840
+ texture = new VideoTexture(video.element);
1841
+ texture.colorSpace = SRGBColorSpace;
1842
+ texture.minFilter = LinearFilter;
1843
+ texture.generateMipmaps = false;
1844
+ }
1845
+ this.video = { ...video, texture, callback: this.video?.callback ?? 0 };
1846
+ if (!this.video.callback)
1847
+ this.watchVideo();
1848
+ }
1849
+ for (const mesh of this.pageMeshes())
1850
+ this.dressVideo(mesh);
1851
+ this.redraw();
1852
+ }
1853
+ /** Draws the book again at each new frame of the video (a turning book
1854
+ * is drawn by its own frames). */
1855
+ watchVideo() {
1856
+ const v = this.video;
1857
+ if (!v)
1858
+ return;
1859
+ const el = v.element;
1860
+ const next = () => {
1861
+ if (this.video?.element !== el || this.disposed)
1862
+ return;
1863
+ this.redraw();
1864
+ v.callback = el.requestVideoFrameCallback ? el.requestVideoFrameCallback(next) : requestAnimationFrame(next);
1865
+ if (this.video)
1866
+ this.video.callback = v.callback;
1867
+ };
1868
+ v.callback = el.requestVideoFrameCallback ? el.requestVideoFrameCallback(next) : requestAnimationFrame(next);
1869
+ }
1870
+ /** The meshes that show pages: the open ones, the leaves in the air and
1871
+ * the block. */
1872
+ pageMeshes() {
1873
+ return [this.left, this.right, ...this.leaves.values(), ...(this.blockMesh ? [this.blockMesh] : [])];
1874
+ }
1875
+ /** Sets a mesh's video uniforms: on the face that shows the video's
1876
+ * page, off elsewhere. */
1877
+ dressVideo(mesh) {
1878
+ const u = mesh.material.userData.uniforms;
1879
+ const v = this.video;
1880
+ const face = v && v.src ? (mesh.userData.Front === v.src ? 1 : mesh.userData.Back === v.src ? 2 : 0) : 0;
1881
+ u.uVidFace.value = face;
1882
+ u.uVideo.value = face && v ? v.texture : null;
1883
+ if (!face || !v)
1884
+ return;
1885
+ const { origin, across, down, crop } = v.frame;
1886
+ u.uVidO.value.set(origin.x, origin.y);
1887
+ u.uVidA.value.set(across.x, across.y);
1888
+ u.uVidB.value.set(down.x, down.y);
1889
+ u.uVidCrop.value.set(crop?.x ?? 0, crop?.y ?? 0, crop?.width ?? 1, crop?.height ?? 1);
1890
+ }
1535
1891
  /** A page source drawn again in place (a canvas repainted): its
1536
1892
  * texture is uploaded again and the book redrawn. */
1537
1893
  touch(src) {
@@ -1614,7 +1970,7 @@ export class PageFlipper {
1614
1970
  }
1615
1971
  const used = new Set(book.flat());
1616
1972
  for (const mesh of [this.left, this.right])
1617
- used.add(mesh.userData.Front).add(mesh.userData.Back);
1973
+ used.add(mesh.userData.Front).add(mesh.userData.Back).add(mesh.userData.Under);
1618
1974
  for (const [src, tex] of this.ready) {
1619
1975
  if (used.has(src))
1620
1976
  continue;
@@ -1629,6 +1985,7 @@ export class PageFlipper {
1629
1985
  if (mesh.userData[face] === src)
1630
1986
  return;
1631
1987
  mesh.userData[face] = src;
1988
+ this.dressVideo(mesh);
1632
1989
  u[`uHas${face}`].value = 0;
1633
1990
  u[`u${face}`].value = null;
1634
1991
  if (src === "")
@@ -1668,7 +2025,7 @@ export class PageFlipper {
1668
2025
  mesh.material.userData.uniforms.uPaper.value.setRGB(...this.paper, SRGBColorSpace);
1669
2026
  mesh.castShadow = mesh.receiveShadow = true;
1670
2027
  mesh.frustumCulled = false;
1671
- const caster = new Mesh(mesh.geometry, this.leafCaster);
2028
+ const caster = new Mesh(casterGeometry(mesh.geometry), this.leafCaster);
1672
2029
  caster.frustumCulled = false;
1673
2030
  caster.visible = false;
1674
2031
  this.shadowScene.add(caster);
@@ -1692,6 +2049,49 @@ export class PageFlipper {
1692
2049
  mm += this.specOf(k).caliperMm;
1693
2050
  return Math.max(0.5, mm * (this.appearance.singlePage ? 0.5 : 1) * this.pxPerMm());
1694
2051
  }
2052
+ /**
2053
+ * The book's own cover in the air is still folded round the spine: on
2054
+ * the spine's outer side its fold rises from the back of the block to
2055
+ * where the cover leaves the spine. Without it the edge of the page
2056
+ * under the cover showed between the two (a white line under a dark
2057
+ * cover), the cover being lifted off the pages at the spine too.
2058
+ */
2059
+ foldCover(air) {
2060
+ const covers = this.appearance.coverLeaves;
2061
+ const isCover = (k) => k === covers?.front || k === covers?.back;
2062
+ // The lowest the spine edge of a leaf's faces stands.
2063
+ const spineZ = (mesh) => {
2064
+ const parts = mesh.userData.board;
2065
+ let z = Infinity;
2066
+ for (const m of parts?.back.visible ? [mesh, parts.back] : [mesh]) {
2067
+ const pos = m.geometry.attributes.position;
2068
+ for (let iy = 0; iy <= NY; iy++)
2069
+ z = Math.min(z, pos.getZ(iy * (NX + 1)));
2070
+ }
2071
+ return z;
2072
+ };
2073
+ let at = Infinity;
2074
+ if (this.coverSpec) {
2075
+ for (const { k, mesh } of air)
2076
+ if (isCover(k))
2077
+ at = Math.min(at, spineZ(mesh));
2078
+ const block = this.block;
2079
+ if (block && this.blockMesh && [covers?.front, covers?.back].some((c) => c !== undefined && c >= block.lo && c < block.hi))
2080
+ at = Math.min(at, spineZ(this.blockMesh));
2081
+ }
2082
+ const { top, back, side } = this.fold;
2083
+ // Right under the cover, and a little in under it past the spine: a
2084
+ // slot left between the two showed the page under the cover, seen
2085
+ // low over the spine.
2086
+ const reach = at - 0.03;
2087
+ const inner = 0.3;
2088
+ this.hinge.visible = top > 0 && reach > top;
2089
+ if (!this.hinge.visible)
2090
+ return;
2091
+ const depth = reach - top + 0.05;
2092
+ this.hinge.scale.set(back / 2 + inner, this.H, depth);
2093
+ this.hinge.position.set((side * (back / 2 - inner)) / 2, 0, top - 0.05 + depth / 2);
2094
+ }
1695
2095
  /** A board leaf's other face and edges, carried with its mesh. */
1696
2096
  boardParts(mesh) {
1697
2097
  let parts = mesh.userData.board;
@@ -1952,13 +2352,14 @@ export class PageFlipper {
1952
2352
  rigidOf(k) {
1953
2353
  return this.specOf(k).rigidity > 0.5;
1954
2354
  }
1955
- /** The page under the pointer: +1 the one a forward turn takes (the
1956
- * recto), −1 the other, 0 none. */
2355
+ /** The page under the pointer, by its outer part (where a hand takes
2356
+ * it): +1 the one a forward turn takes (the recto), −1 the other, 0
2357
+ * none (off the pages, or nearer the spine). */
1957
2358
  hit(event) {
1958
2359
  if (!this.surfaces)
1959
2360
  this.layout();
1960
2361
  const { x, y } = this.toWorld(event);
1961
- if (Math.abs(y) > this.H / 2 || Math.abs(x) > this.W * 1.05)
2362
+ if (Math.abs(y) > this.H / 2 || Math.abs(x) > this.W * 1.05 || Math.abs(x) < GRIP * this.W)
1962
2363
  return 0;
1963
2364
  return x > 0 ? 1 : -1;
1964
2365
  }
@@ -1972,7 +2373,7 @@ export class PageFlipper {
1972
2373
  const at = this.settledAt();
1973
2374
  const forward = x > 0;
1974
2375
  const k = forward ? at : at - 1;
1975
- if (k < 0 || k >= this.turned.length || Math.abs(x) > this.W * 1.05 || Math.abs(y) > this.H / 2)
2376
+ 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)
1976
2377
  return false;
1977
2378
  const G = { u: Math.max(0.15 * this.W, Math.min(this.W, Math.abs(x))), v: y };
1978
2379
  this.turns.set(k, { forward, G, P: { ...G }, held: { aim: { ...G }, samples: [] }, rigid: this.rigidOf(k) });
@@ -2173,6 +2574,7 @@ export class PageFlipper {
2173
2574
  if (!mesh.parent)
2174
2575
  this.stage.add(mesh);
2175
2576
  }
2577
+ this.foldCover(air);
2176
2578
  // The pages lying open: the top leaf of each stack.
2177
2579
  let l = -1;
2178
2580
  for (let j = 0; j < n; j++)
@@ -2189,12 +2591,21 @@ export class PageFlipper {
2189
2591
  // What shows through an open page: the other side of its leaf.
2190
2592
  this.assign(this.left, "Front", l >= 0 ? (this.book[l]?.[1] ?? null) : null);
2191
2593
  this.assign(this.right, "Back", this.book[r + 1]?.[0] ?? null);
2594
+ // And through the whole sheet, the page lying under it: the next
2595
+ // leaf's face on the same side.
2596
+ const lying = (j, left) => j >= 0 && j < n && this.turned[j] === left && !this.turns.has(j) && !this.inBlock(j);
2597
+ this.assign(this.left, "Under", lying(l - 1, true) ? (this.book[l]?.[0] ?? null) : null);
2598
+ this.assign(this.right, "Under", lying(r + 1, false) ? (this.book[r + 1]?.[1] ?? null) : null);
2192
2599
  const extra = this.appearance.extraLeaves;
2193
2600
  this.left.visible = leftSrc !== null || (extra?.before ?? 0) > 0;
2194
2601
  this.right.visible = rightSrc !== null || (extra?.after ?? 0) > 0;
2195
2602
  this.dressPage(this.left, l >= 0 ? this.specOf(l) : this.bookSpec);
2196
2603
  this.dressPage(this.right, r < n ? this.specOf(r) : this.bookSpec);
2197
- // The height map lifted leaves occlude the sky from.
2604
+ // The height map lifted leaves occlude the sky from, with the leaves
2605
+ // where they lie this frame.
2606
+ for (const mesh of [...this.leaves.values(), this.blockMesh])
2607
+ if (mesh?.visible)
2608
+ syncCaster(mesh.userData.caster, mesh.geometry);
2198
2609
  for (const c of this.staticCasters)
2199
2610
  c.visible = true;
2200
2611
  this.staticCasters[0].visible = this.left.visible;
@@ -2205,7 +2616,169 @@ export class PageFlipper {
2205
2616
  this.renderer.render(this.shadowScene, this.shadowCamera);
2206
2617
  this.renderer.setRenderTarget(null);
2207
2618
  }
2619
+ this.mirror(air.length > 0 || this.block !== null);
2620
+ this.behind(air.map((a) => a.mesh));
2621
+ this.composer.render();
2622
+ }
2623
+ /**
2624
+ * Thin paper lets light through (#484). While translucent leaves are in
2625
+ * the air, the scene is drawn once more without them (at half the
2626
+ * screen's resolution: the sheet blurs what it lets through anyway),
2627
+ * colour and depth: each such leaf reads, at its own place on screen,
2628
+ * what lies behind it there and how far, and lets that light through.
2629
+ * They cast no shadow and take no sky from the pages in that picture:
2630
+ * a page under a thin leaf is lit through it, and a leaf just taken
2631
+ * hold of shows the page under it as it did lying on it. Nothing is
2632
+ * drawn at rest or on opaque stock.
2633
+ */
2634
+ behind(air) {
2635
+ const u = behindUniforms;
2636
+ u.uBehindOn.value = 0;
2637
+ u.uBehind.value = u.uBehindDepth.value = null;
2638
+ const lit = air.filter((m) => m.visible && m.material.userData.uniforms.uTransmit.value > 0);
2639
+ if (!this.mirrorOk || !lit.length)
2640
+ return;
2641
+ const w = Math.max(1, Math.round(this.buffer.x / 2));
2642
+ const h = Math.max(1, Math.round(this.buffer.y / 2));
2643
+ let target = this.behindTarget;
2644
+ if (!target) {
2645
+ target = new WebGLRenderTarget(w, h, { type: HalfFloatType, minFilter: LinearMipmapLinearFilter, magFilter: LinearFilter, generateMipmaps: true });
2646
+ target.depthTexture = new DepthTexture(w, h);
2647
+ this.behindTarget = target;
2648
+ }
2649
+ else if (target.width !== w || target.height !== h)
2650
+ target.setSize(w, h);
2651
+ for (const m of lit)
2652
+ m.visible = false;
2653
+ // The key's shadow map drawn again without them (the main view draws
2654
+ // it with them after), and the open pages' sky not cut by them.
2655
+ const shadows = this.renderer.shadowMap.autoUpdate;
2656
+ this.renderer.shadowMap.autoUpdate = false;
2657
+ this.renderer.shadowMap.needsUpdate = true;
2658
+ const occ = [this.left, this.right].map((m) => m.material.userData.uniforms.uLeafOcc);
2659
+ const was = occ.map((o) => o.value);
2660
+ for (const o of occ)
2661
+ o.value = 0;
2662
+ this.renderer.setRenderTarget(target);
2663
+ this.renderer.clear();
2208
2664
  this.renderer.render(this.scene, this.camera);
2665
+ this.renderer.setRenderTarget(null);
2666
+ this.renderer.shadowMap.autoUpdate = shadows;
2667
+ occ.forEach((o, i) => (o.value = was[i]));
2668
+ for (const m of lit)
2669
+ m.visible = true;
2670
+ u.uBehind.value = target.texture;
2671
+ u.uBehindDepth.value = target.depthTexture;
2672
+ u.uBehindSize.value.copy(this.buffer);
2673
+ u.uBehindClip.value.set(this.camera.near, this.camera.far);
2674
+ u.uBehindScale.value = (h / 2) * this.camera.projectionMatrix.elements[5];
2675
+ // Light spreads about a quarter of a millimetre in the sheet.
2676
+ u.uBehindFloor.value = 0.25 * this.pxPerMm();
2677
+ u.uBehindOn.value = 1;
2678
+ }
2679
+ /**
2680
+ * Gloss paper mirrors the leaves turning over it (#483). For each open
2681
+ * page on gloss stock, while anything is in the air, the leaves in the
2682
+ * air are drawn from the camera mirrored in that page's plane: a
2683
+ * camera under the page looking up at the leaves as they are, so each
2684
+ * is lit, shadowed and occluded as it is and shows the face the page
2685
+ * sees. Turned left for right, that picture lies on the page's own
2686
+ * place on screen; the page reads it as its reflection. Two pages at
2687
+ * the same height share one picture. Nothing is drawn at rest or on
2688
+ * any other finish.
2689
+ */
2690
+ mirror(airborne) {
2691
+ const sides = [
2692
+ { side: "left", page: this.left },
2693
+ { side: "right", page: this.right },
2694
+ ];
2695
+ let last = null;
2696
+ for (const { side, page } of sides) {
2697
+ const u = page.material.userData.uniforms;
2698
+ const on = this.mirrorOk && airborne && page.visible && page.material.userData.spec?.paper.finish === "gloss";
2699
+ u.uReflectionOn.value = on ? 1 : 0;
2700
+ if (!on)
2701
+ continue;
2702
+ // Stage x runs out from the spine, to the left on the left page,
2703
+ // and the stage is mirrored for a right-bound book.
2704
+ const plane = pagePlane(this.surfaces[side], this.W, (side === "left" ? -1 : 1) * this.sign);
2705
+ if (!last || last.plane.normal.dot(plane.normal) < 0.99999 || Math.abs(last.plane.constant - plane.constant) > 0.25) {
2706
+ const target = this.mirrorTarget(side);
2707
+ this.drawMirror(target, plane);
2708
+ last = { plane, target };
2709
+ }
2710
+ u.uReflection.value = last.target.texture;
2711
+ u.uReflectionSize.value.copy(this.buffer);
2712
+ u.uReflectionNormal.value.copy(plane.normal);
2713
+ }
2714
+ }
2715
+ /** A side's mirror picture, about as sharp as the screen (in CSS px
2716
+ * up to 1.5 device px each: a gloss coat is never a perfect mirror). */
2717
+ mirrorTarget(side) {
2718
+ const f = Math.min(1, 1.5 / this.dpr);
2719
+ const w = Math.max(1, Math.round(this.buffer.x * f));
2720
+ const h = Math.max(1, Math.round(this.buffer.y * f));
2721
+ let target = this.mirrors[side];
2722
+ if (!target) {
2723
+ target = new WebGLRenderTarget(w, h, { type: HalfFloatType, minFilter: LinearFilter, magFilter: LinearFilter, generateMipmaps: false, samples: 4 });
2724
+ this.mirrors[side] = target;
2725
+ }
2726
+ else if (target.width !== w || target.height !== h)
2727
+ target.setSize(w, h);
2728
+ return target;
2729
+ }
2730
+ /** Draws the leaves in the air as `plane` (an open page's) mirrors them. */
2731
+ drawMirror(target, plane) {
2732
+ const cam = this.mirrorCamera;
2733
+ // The eye mirrored in the plane, turned round its up axis so it is
2734
+ // still a camera (no mirror in it): its picture is the reflection
2735
+ // turned left for right, and its frustum the main one turned so too.
2736
+ const { x, y, z } = plane.normal;
2737
+ const d = -plane.constant;
2738
+ const reflect = new Matrix4().set(1 - 2 * x * x, -2 * x * y, -2 * x * z, 2 * d * x, -2 * y * x, 1 - 2 * y * y, -2 * y * z, 2 * d * y, -2 * z * x, -2 * z * y, 1 - 2 * z * z, 2 * d * z, 0, 0, 0, 1);
2739
+ cam.matrix.copy(reflect).multiply(this.camera.matrixWorld).multiply(new Matrix4().makeScale(-1, 1, 1));
2740
+ cam.updateMatrixWorld(true);
2741
+ const p = cam.projectionMatrix.copy(this.camera.projectionMatrix);
2742
+ p.elements[8] *= -1;
2743
+ // Its near plane laid along the page (an oblique frustum): nothing
2744
+ // under the page shows in its mirror.
2745
+ const near = new Plane(plane.normal.clone(), plane.constant + 0.2).applyMatrix4(cam.matrixWorldInverse);
2746
+ const clip = new Vector4(near.normal.x, near.normal.y, near.normal.z, near.constant);
2747
+ const e = p.elements;
2748
+ const q = new Vector4((Math.sign(clip.x) + e[8]) / e[0], (Math.sign(clip.y) + e[9]) / e[5], -1, (1 + e[10]) / e[14]);
2749
+ clip.multiplyScalar(2 / clip.dot(q));
2750
+ e[2] = clip.x;
2751
+ e[6] = clip.y;
2752
+ e[10] = clip.z + 1;
2753
+ e[14] = clip.w;
2754
+ cam.projectionMatrixInverse.copy(p).invert();
2755
+ // Only the leaves in the air (and the light on them).
2756
+ const keep = new Set([this.key, this.key.target, ...this.leaves.values()]);
2757
+ if (this.blockMesh)
2758
+ keep.add(this.blockMesh);
2759
+ const hidden = [];
2760
+ for (const c of this.stage.children) {
2761
+ if (keep.has(c) || !c.visible)
2762
+ continue;
2763
+ c.visible = false;
2764
+ hidden.push(c);
2765
+ }
2766
+ // The key's shadow map as the main view drew it last: the leaves'
2767
+ // own faces under them are not lit by the key anyway.
2768
+ const shadows = this.renderer.shadowMap.autoUpdate;
2769
+ this.renderer.shadowMap.autoUpdate = false;
2770
+ // The picture behind the leaves lies on the main view's screen, not
2771
+ // this one's.
2772
+ const behind = behindUniforms.uBehindOn.value;
2773
+ behindUniforms.uBehindOn.value = 0;
2774
+ this.renderer.setRenderTarget(target);
2775
+ this.renderer.clear();
2776
+ this.renderer.render(this.scene, cam);
2777
+ this.renderer.setRenderTarget(null);
2778
+ this.renderer.shadowMap.autoUpdate = shadows;
2779
+ behindUniforms.uBehindOn.value = behind;
2780
+ for (const c of hidden)
2781
+ c.visible = true;
2209
2782
  }
2210
2783
  /** Back to a transparent canvas (the DOM spread shows), unless leaves
2211
2784
  * are moving again. A persistent book is drawn at rest instead. */
@@ -2233,14 +2806,22 @@ export class PageFlipper {
2233
2806
  }
2234
2807
  dispose() {
2235
2808
  this.disposed = true;
2809
+ this.setVideo(null);
2236
2810
  cancelAnimationFrame(this.raf);
2237
2811
  cancelAnimationFrame(this.still);
2238
2812
  cancelAnimationFrame(this.orbitFrame);
2239
2813
  for (const tex of this.ready.values())
2240
2814
  tex.dispose();
2241
2815
  this.shadowTarget?.dispose();
2816
+ this.composer.dispose();
2817
+ this.mirrors.left?.dispose();
2818
+ this.mirrors.right?.dispose();
2819
+ this.behindTarget?.depthTexture?.dispose();
2820
+ this.behindTarget?.dispose();
2242
2821
  this.leafCaster.dispose();
2243
2822
  this.staticCaster.dispose();
2823
+ for (const c of this.staticCasters)
2824
+ c.geometry.dispose();
2244
2825
  this.scene.environment?.dispose();
2245
2826
  this.pmrem?.dispose();
2246
2827
  for (const mesh of this.leaves.values())
@@ -2253,6 +2834,7 @@ export class PageFlipper {
2253
2834
  }
2254
2835
  for (const c of this.covers.children)
2255
2836
  c.geometry.dispose();
2837
+ this.hinge.geometry.dispose();
2256
2838
  this.clearSpine();
2257
2839
  this.spineTex?.dispose();
2258
2840
  this.coverMaterial.normalMap?.dispose();