postext-folio 0.5.3 → 0.6.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 +16 -2
- package/dist/bookGeometry.d.ts +3 -0
- package/dist/bookGeometry.d.ts.map +1 -1
- package/dist/bookGeometry.js +19 -0
- package/dist/bookGeometry.js.map +1 -1
- package/dist/index.d.ts +3 -2
- 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 +115 -1
- package/dist/pageFlip.d.ts.map +1 -1
- package/dist/pageFlip.js +511 -17
- package/dist/pageFlip.js.map +1 -1
- package/dist/paper.d.ts +7 -1
- package/dist/paper.d.ts.map +1 -1
- package/dist/paper.js +5 -1
- package/dist/paper.js.map +1 -1
- package/dist/postext.d.ts +29 -1
- package/dist/postext.d.ts.map +1 -1
- package/dist/postext.js +232 -0
- 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/videos.d.ts +56 -0
- package/dist/videos.d.ts.map +1 -0
- package/dist/videos.js +173 -0
- package/dist/videos.js.map +1 -0
- package/dist/viewer.d.ts +18 -1
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +127 -5
- package/dist/viewer.js.map +1 -1
- package/package.json +11 -4
package/dist/pageFlip.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
2
|
import { resolveFolioConfig } from "postext";
|
|
3
|
-
import { along, BINDINGS, gutterOcclusion, profiles, stackGeometry } from "./bookGeometry";
|
|
3
|
+
import { along, BINDINGS, gutterOcclusion, profiles, stackGeometry, topAt } from "./bookGeometry";
|
|
4
4
|
import { environment } from "./environments";
|
|
5
5
|
import { pagePaper, paperSpec } from "./paper";
|
|
6
6
|
import { loadDeskMaps } from "./deskTextures";
|
|
@@ -68,12 +68,17 @@ const OCCLUSION = /* glsl */ `
|
|
|
68
68
|
// How far the reflection seen at p runs under paper: its ray marched
|
|
69
69
|
// out through the height map (a lifted leaf hides what it would mirror,
|
|
70
70
|
// 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.
|
|
71
|
+
// march starts a few px out, past the paper round p itself. Its steps
|
|
72
|
+
// are shifted a little from pixel to pixel: at fixed steps the leaf's
|
|
73
|
+
// outline was cut in at each of them, a row of teeth (#483).
|
|
72
74
|
float reflectionOcclusion(vec3 p, vec3 r, float channel) {
|
|
73
75
|
if (uShadowOn < 0.5) return 0.0;
|
|
76
|
+
// Interleaved gradient noise: neighbours take evenly spread shifts.
|
|
77
|
+
float spin = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
|
|
74
78
|
float hidden = 0.0;
|
|
75
|
-
for (int i =
|
|
76
|
-
float
|
|
79
|
+
for (int i = 0; i < 8; i++) {
|
|
80
|
+
float s = (float(i) + spin) / 8.0;
|
|
81
|
+
float t = 6.0 + s * s * uOccRadius * 2.0;
|
|
77
82
|
vec3 q = p + r * t;
|
|
78
83
|
hidden = max(hidden, smoothstep(0.0, 4.0, casterAt(q.xy, channel) - q.z - uOccBias));
|
|
79
84
|
}
|
|
@@ -169,6 +174,25 @@ function keySpecular(occlusion = "1.0") {
|
|
|
169
174
|
function keyShadowed(fragmentShader) {
|
|
170
175
|
return fragmentShader.replace("#include <shadowmap_pars_fragment>", SHADOW_PARS);
|
|
171
176
|
}
|
|
177
|
+
/** A caster's own copy of a mesh's shape. The height map is drawn before
|
|
178
|
+
* the main view, and three.js uploads a geometry once per frame number,
|
|
179
|
+
* which the key's shadow pass at the end of the last main view had
|
|
180
|
+
* already taken: a caster sharing a leaf's geometry drew the leaf where
|
|
181
|
+
* it lay a frame before, and a leaf moving down was shaded by its own
|
|
182
|
+
* old self, moving up it was not (#488). */
|
|
183
|
+
function casterGeometry(source) {
|
|
184
|
+
const g = new BufferGeometry();
|
|
185
|
+
g.setAttribute("position", source.attributes.position.clone());
|
|
186
|
+
if (source.index)
|
|
187
|
+
g.setIndex(source.index.clone());
|
|
188
|
+
return g;
|
|
189
|
+
}
|
|
190
|
+
/** Brings a caster's copy to its mesh's shape as laid out this frame. */
|
|
191
|
+
function syncCaster(caster, source) {
|
|
192
|
+
const to = caster.geometry.attributes.position;
|
|
193
|
+
to.array.set(source.attributes.position.array);
|
|
194
|
+
to.needsUpdate = true;
|
|
195
|
+
}
|
|
172
196
|
function casterMaterial(leaf) {
|
|
173
197
|
return new ShaderMaterial({
|
|
174
198
|
vertexShader: CASTER_VERTEX,
|
|
@@ -198,6 +222,21 @@ function occluded(m, sign) {
|
|
|
198
222
|
};
|
|
199
223
|
return m;
|
|
200
224
|
}
|
|
225
|
+
/** What lies behind the leaves in the air (#484): the scene drawn without
|
|
226
|
+
* the translucent leaves (colour, mipmapped, and depth), shared by every
|
|
227
|
+
* page. `uBehindSize`: the screen in device px; `uBehindClip`: the
|
|
228
|
+
* camera's near and far planes; `uBehindScale`: texels of that picture
|
|
229
|
+
* per book unit at a view depth of 1; `uBehindFloor`: how far (book
|
|
230
|
+
* units) the sheet's fibres spread even what touches it. */
|
|
231
|
+
const behindUniforms = {
|
|
232
|
+
uBehind: { value: null },
|
|
233
|
+
uBehindDepth: { value: null },
|
|
234
|
+
uBehindOn: { value: 0 },
|
|
235
|
+
uBehindSize: { value: new Vector2(1, 1) },
|
|
236
|
+
uBehindClip: { value: new Vector2(1, 2) },
|
|
237
|
+
uBehindScale: { value: 1 },
|
|
238
|
+
uBehindFloor: { value: 1 },
|
|
239
|
+
};
|
|
201
240
|
/**
|
|
202
241
|
* A page's material: three.js's physical (PBR) material lit by the
|
|
203
242
|
* environment and the key light, the printed page on each face, the
|
|
@@ -216,13 +255,27 @@ function pageMaterial(mirror, sign) {
|
|
|
216
255
|
uHasBack: { value: 0 },
|
|
217
256
|
uMirror: { value: mirror ? 1 : 0 },
|
|
218
257
|
uShowThrough: { value: 0 },
|
|
258
|
+
uUnder: { value: null },
|
|
259
|
+
uHasUnder: { value: 0 },
|
|
260
|
+
uUnderK: { value: 0 },
|
|
261
|
+
uTransmit: { value: 0 },
|
|
219
262
|
uPaper: { value: new Color(1, 1, 1) },
|
|
220
263
|
uLeafOcc: { value: 0.45 },
|
|
221
264
|
uOccBias: { value: 6 },
|
|
265
|
+
uVideo: { value: null },
|
|
266
|
+
uVidFace: { value: 0 },
|
|
267
|
+
uVidO: { value: new Vector2() },
|
|
268
|
+
uVidA: { value: new Vector2(1, 0) },
|
|
269
|
+
uVidB: { value: new Vector2(0, 1) },
|
|
270
|
+
uVidCrop: { value: new Vector4(0, 0, 1, 1) },
|
|
271
|
+
uReflection: { value: null },
|
|
272
|
+
uReflectionOn: { value: 0 },
|
|
273
|
+
uReflectionSize: { value: new Vector2(1, 1) },
|
|
274
|
+
uReflectionNormal: { value: new Vector3(0, 0, 1) },
|
|
222
275
|
};
|
|
223
276
|
m.userData.uniforms = uniforms;
|
|
224
277
|
m.onBeforeCompile = (shader) => {
|
|
225
|
-
Object.assign(shader.uniforms, occlusionUniforms, uniforms, { uSign: { value: sign } });
|
|
278
|
+
Object.assign(shader.uniforms, occlusionUniforms, behindUniforms, uniforms, { uSign: { value: sign } });
|
|
226
279
|
shader.vertexShader = shader.vertexShader
|
|
227
280
|
.replace("#include <common>", `#include <common>
|
|
228
281
|
attribute float ao;
|
|
@@ -241,9 +294,33 @@ function pageMaterial(mirror, sign) {
|
|
|
241
294
|
uniform float uHasBack;
|
|
242
295
|
uniform float uMirror;
|
|
243
296
|
uniform float uShowThrough;
|
|
297
|
+
uniform sampler2D uUnder;
|
|
298
|
+
uniform float uHasUnder;
|
|
299
|
+
uniform float uUnderK;
|
|
300
|
+
uniform float uTransmit;
|
|
301
|
+
uniform sampler2D uBehind;
|
|
302
|
+
uniform sampler2D uBehindDepth;
|
|
303
|
+
uniform float uBehindOn;
|
|
304
|
+
uniform vec2 uBehindSize;
|
|
305
|
+
uniform vec2 uBehindClip;
|
|
306
|
+
uniform float uBehindScale;
|
|
307
|
+
uniform float uBehindFloor;
|
|
308
|
+
// The print of both faces at this point: what light coming
|
|
309
|
+
// through the sheet passes on its way.
|
|
310
|
+
vec3 transInk = vec3(1.0);
|
|
244
311
|
uniform vec3 uPaper;
|
|
245
312
|
uniform float uLeafOcc;
|
|
246
313
|
uniform float uSign;
|
|
314
|
+
uniform sampler2D uVideo;
|
|
315
|
+
uniform float uVidFace;
|
|
316
|
+
uniform vec2 uVidO;
|
|
317
|
+
uniform vec2 uVidA;
|
|
318
|
+
uniform vec2 uVidB;
|
|
319
|
+
uniform vec4 uVidCrop;
|
|
320
|
+
uniform sampler2D uReflection;
|
|
321
|
+
uniform float uReflectionOn;
|
|
322
|
+
uniform vec2 uReflectionSize;
|
|
323
|
+
uniform vec3 uReflectionNormal;
|
|
247
324
|
varying float vAo;
|
|
248
325
|
varying vec2 vPageUv;
|
|
249
326
|
varying vec3 vBookPos;
|
|
@@ -257,12 +334,34 @@ function pageMaterial(mirror, sign) {
|
|
|
257
334
|
vec2 uvB = vec2(1.0 - u, vPageUv.y);
|
|
258
335
|
vec3 ink = uPaper;
|
|
259
336
|
if (has > 0.5 && has < 1.5) ink = front ? texture2D(uFront, uvF).rgb : texture2D(uBack, uvB).rgb;
|
|
337
|
+
// A video playing on this face, printed over its poster: the
|
|
338
|
+
// point of the page (from its top left corner) in the picture's
|
|
339
|
+
// own frame, which may lie turned on the page.
|
|
340
|
+
if (uVidFace > 0.5 && (uVidFace < 1.5) == front) {
|
|
341
|
+
vec2 tuv = front ? uvF : uvB;
|
|
342
|
+
vec2 p = vec2(tuv.x, 1.0 - tuv.y) - uVidO;
|
|
343
|
+
vec2 st = vec2(dot(p, uVidA) / dot(uVidA, uVidA), dot(p, uVidB) / dot(uVidB, uVidB));
|
|
344
|
+
if (st.x >= 0.0 && st.x <= 1.0 && st.y >= 0.0 && st.y <= 1.0) {
|
|
345
|
+
vec2 v = uVidCrop.xy + st * uVidCrop.zw;
|
|
346
|
+
ink = texture2D(uVideo, vec2(v.x, 1.0 - v.y)).rgb;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
260
349
|
// The other side's print, seen through the sheet (the same point
|
|
261
350
|
// of paper, so mirrored as it should be).
|
|
262
351
|
float other = front ? uHasBack : uHasFront;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
352
|
+
vec3 through = vec3(1.0);
|
|
353
|
+
if ((uShowThrough > 0.0 || uTransmit > 0.0) && other > 0.5 && other < 1.5) {
|
|
354
|
+
through = front ? texture2D(uBack, uvB).rgb : texture2D(uFront, uvF).rgb;
|
|
355
|
+
}
|
|
356
|
+
transInk = ink * through;
|
|
357
|
+
if (uShowThrough > 0.0) ink *= mix(vec3(1.0), through, uShowThrough);
|
|
358
|
+
// The page lying under an open page: its print comes through the
|
|
359
|
+
// whole sheet (the fibres spread it by a fraction of a millimetre,
|
|
360
|
+
// finer than the page's own texels: a softer sample would blur
|
|
361
|
+
// a page seen small to grey).
|
|
362
|
+
if (uHasUnder > 0.5 && uUnderK > 0.0) {
|
|
363
|
+
vec3 under = texture2D(uUnder, front ? uvF : uvB).rgb;
|
|
364
|
+
ink *= mix(vec3(1.0), under, uUnderK);
|
|
266
365
|
}
|
|
267
366
|
diffuseColor.rgb *= ink;
|
|
268
367
|
}`)
|
|
@@ -277,7 +376,49 @@ function pageMaterial(mirror, sign) {
|
|
|
277
376
|
// The reflection's ray, in book coordinates.
|
|
278
377
|
vec3 reflected = (vec4(reflect(-geometryViewDir, normal), 0.0) * viewMatrix).xyz;
|
|
279
378
|
reflected.x *= uSign;
|
|
280
|
-
|
|
379
|
+
// A mirrored leaf stands for what it hides itself.
|
|
380
|
+
${keySpecular("1.0 - 0.85 * (uReflectionOn > 0.5 ? 0.0 : reflectionOcclusion(vBookPos, reflected, 1.0))")}
|
|
381
|
+
#ifdef USE_CLEARCOAT
|
|
382
|
+
if (uReflectionOn > 0.5) {
|
|
383
|
+
// The leaves in the air, mirrored in a gloss page: where one is
|
|
384
|
+
// seen, it takes the place of the environment it hides, through
|
|
385
|
+
// the coating's Fresnel and the paper's own. A plane holds only
|
|
386
|
+
// where the page lies in it: the mirror fades up the gutter.
|
|
387
|
+
vec2 st = gl_FragCoord.xy / uReflectionSize;
|
|
388
|
+
vec4 seen = texture2D(uReflection, vec2(1.0 - st.x, st.y));
|
|
389
|
+
float lies = smoothstep(0.97, 0.995, abs(dot((vec4(normalize(vNormal), 0.0) * viewMatrix).xyz, uReflectionNormal)));
|
|
390
|
+
float a = seen.a * lies;
|
|
391
|
+
vec3 leaf = seen.rgb * lies;
|
|
392
|
+
clearcoatSpecularIndirect = clearcoatSpecularIndirect * (1.0 - a)
|
|
393
|
+
+ leaf * EnvironmentBRDF(geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness);
|
|
394
|
+
reflectedLight.indirectSpecular = reflectedLight.indirectSpecular * (1.0 - a)
|
|
395
|
+
+ leaf * EnvironmentBRDF(geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness);
|
|
396
|
+
}
|
|
397
|
+
#endif
|
|
398
|
+
if (uBehindOn > 0.5 && uTransmit > 0.0) {
|
|
399
|
+
// Thin paper in the air lets through the light behind it (#484):
|
|
400
|
+
// what lies there on screen, seen through the print of both
|
|
401
|
+
// faces. The sheet scatters what it lets through about evenly
|
|
402
|
+
// (near Lambertian), so a thing a gap behind it is spread over
|
|
403
|
+
// about twice that gap: sharp where the leaf touches the page
|
|
404
|
+
// under it, a soft glow a few millimetres off. A page close
|
|
405
|
+
// under it is lit only through the leaf, so its print comes
|
|
406
|
+
// back at about half the leaf's transmittance (1 − opacity, as
|
|
407
|
+
// the open page shows the one under it); what lies further, out
|
|
408
|
+
// of the leaf's shade, at the whole of it. Stronger and sharper,
|
|
409
|
+
// the page under a leaf about to land read as lying over it
|
|
410
|
+
// (#492). What it lets through it does not reflect.
|
|
411
|
+
vec2 st = gl_FragCoord.xy / uBehindSize;
|
|
412
|
+
float depth = texture2D(uBehindDepth, st).x;
|
|
413
|
+
float zBehind = uBehindClip.x * uBehindClip.y / (uBehindClip.y - depth * (uBehindClip.y - uBehindClip.x));
|
|
414
|
+
float gap = max(0.0, zBehind - vViewPosition.z);
|
|
415
|
+
float spread = (uBehindFloor + 2.0 * gap) * uBehindScale / vViewPosition.z;
|
|
416
|
+
vec4 seen = textureLod(uBehind, st, log2(max(1.0, 2.0 * spread)));
|
|
417
|
+
float carried = uTransmit * mix(0.5, 1.0, smoothstep(0.0, 8.0 * uBehindFloor, gap));
|
|
418
|
+
float lost = carried * seen.a;
|
|
419
|
+
reflectedLight.directDiffuse *= 1.0 - lost;
|
|
420
|
+
reflectedLight.indirectDiffuse = reflectedLight.indirectDiffuse * (1.0 - lost) + carried * seen.rgb * transInk;
|
|
421
|
+
}`);
|
|
281
422
|
};
|
|
282
423
|
return m;
|
|
283
424
|
}
|
|
@@ -645,6 +786,13 @@ export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q,
|
|
|
645
786
|
if (amp > 1e-6)
|
|
646
787
|
pz += amp * smooth(0, 2 * amp, clear) * Math.sin(2 * Math.PI * (time / 520) - (3 * u) / W + (2 * v) / H);
|
|
647
788
|
}
|
|
789
|
+
// Never into the book: over a block (and past its head or foot) a
|
|
790
|
+
// leaf stands at least just over its top. A fold's flap is carried
|
|
791
|
+
// level from the foot of its fold, and on a thin side hanging from
|
|
792
|
+
// the spine that foot lies low: the flap went through the tall block
|
|
793
|
+
// on the other side (#489).
|
|
794
|
+
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));
|
|
648
796
|
pos.setXYZ(iy * (NX + 1) + ix, px, y, pz);
|
|
649
797
|
}
|
|
650
798
|
}
|
|
@@ -660,9 +808,25 @@ function disposeLeaf(mesh) {
|
|
|
660
808
|
parts.slab.geometry.dispose();
|
|
661
809
|
parts.slab.material.dispose();
|
|
662
810
|
}
|
|
811
|
+
mesh.userData.caster?.geometry.dispose();
|
|
663
812
|
mesh.geometry.dispose();
|
|
664
813
|
mesh.material.dispose();
|
|
665
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* The plane an open page lies in past the gutter's shoulder (world,
|
|
817
|
+
* normal up), its side running out from the spine along world x as
|
|
818
|
+
* `out` (±1): level on a block lying on the desk, sloping down to it on
|
|
819
|
+
* the thin side of a thick book, which hangs from the top of the spine.
|
|
820
|
+
* A gloss page mirrors the leaves in the air in it (#483).
|
|
821
|
+
*/
|
|
822
|
+
export function pagePlane(p, W, out) {
|
|
823
|
+
const [x0, z0] = along(p, W * 0.55);
|
|
824
|
+
const [x1, z1] = along(p, W);
|
|
825
|
+
const dx = out * (x1 - x0);
|
|
826
|
+
const dz = z1 - z0;
|
|
827
|
+
const normal = new Vector3(-dz * Math.sign(dx || 1), 0, Math.abs(dx)).normalize();
|
|
828
|
+
return new Plane().setFromNormalAndCoplanarPoint(normal, new Vector3(out * x0, 0, z0));
|
|
829
|
+
}
|
|
666
830
|
/**
|
|
667
831
|
* Turns the pages of the viewer's DOM spread in a 3D scene: the book lies
|
|
668
832
|
* open on a desk, seen from in front and above (tilted by `folio.tilt`),
|
|
@@ -703,6 +867,12 @@ export class PageFlipper {
|
|
|
703
867
|
spineSrc = null;
|
|
704
868
|
spineTex = null;
|
|
705
869
|
spineFaces = [];
|
|
870
|
+
/** The book's own cover folded round the spine while it is in the air:
|
|
871
|
+
* a strip on the spine's outer side, up to where the cover leaves it.
|
|
872
|
+
* `top`: the back of the block's height; `back`: its thickness; `side`:
|
|
873
|
+
* the spine's outer side (the empty stack's, −1 left). */
|
|
874
|
+
hinge;
|
|
875
|
+
fold = { top: 0, back: 0, side: -1 };
|
|
706
876
|
edges;
|
|
707
877
|
key = new DirectionalLight(0xffffff, 1);
|
|
708
878
|
pmrem = null;
|
|
@@ -716,6 +886,16 @@ export class PageFlipper {
|
|
|
716
886
|
shadowScene = new Scene();
|
|
717
887
|
shadowCamera = new OrthographicCamera(-1, 1, 1, -1, 1, 4000);
|
|
718
888
|
shadowTarget = null;
|
|
889
|
+
/** The leaves in the air mirrored in each gloss open page (#483), and
|
|
890
|
+
* the camera that sees them so (the view mirrored in the page). */
|
|
891
|
+
mirrors = { left: null, right: null };
|
|
892
|
+
mirrorCamera = new PerspectiveCamera();
|
|
893
|
+
mirrorOk = false;
|
|
894
|
+
/** What lies behind the translucent leaves in the air (#484). */
|
|
895
|
+
behindTarget = null;
|
|
896
|
+
/** The drawing buffer (device px) and the device pixel ratio. */
|
|
897
|
+
buffer = new Vector2(1, 1);
|
|
898
|
+
dpr = 1;
|
|
719
899
|
/** Draw the leaves in the air, and the rest of the book, into the
|
|
720
900
|
* height map. */
|
|
721
901
|
leafCaster;
|
|
@@ -829,11 +1009,16 @@ export class PageFlipper {
|
|
|
829
1009
|
depthBuffer: false,
|
|
830
1010
|
generateMipmaps: false,
|
|
831
1011
|
});
|
|
1012
|
+
this.mirrorOk = true;
|
|
832
1013
|
}
|
|
1014
|
+
this.mirrorCamera.matrixAutoUpdate = false;
|
|
833
1015
|
// A mirror turns three.js's face culling round with it (the front of a
|
|
834
1016
|
// page stays its front).
|
|
835
1017
|
this.stage.scale.x = this.sign;
|
|
836
|
-
this.
|
|
1018
|
+
this.hinge = new Mesh(new BoxGeometry(1, 1, 1), this.coverMaterial);
|
|
1019
|
+
this.hinge.receiveShadow = true;
|
|
1020
|
+
this.hinge.visible = false;
|
|
1021
|
+
this.stage.add(this.desk, this.covers, this.left, this.right, this.hinge, this.key, this.key.target);
|
|
837
1022
|
this.scene.add(this.stage);
|
|
838
1023
|
this.target = at;
|
|
839
1024
|
this.reported = at;
|
|
@@ -1065,7 +1250,17 @@ export class PageFlipper {
|
|
|
1065
1250
|
const coated = p.finish !== "uncoated";
|
|
1066
1251
|
const s = p.textureStrength * (coated ? 0.35 : 1) * 0.6;
|
|
1067
1252
|
m.normalScale.set(s, s);
|
|
1068
|
-
m.userData.uniforms
|
|
1253
|
+
const u = m.userData.uniforms;
|
|
1254
|
+
u.uShowThrough.value = spec.showThrough;
|
|
1255
|
+
// An open page shows the page under it; a leaf in the air, what is
|
|
1256
|
+
// behind it (a block of leaves turning together is opaque).
|
|
1257
|
+
const open = mesh === this.left || mesh === this.right;
|
|
1258
|
+
// Print under a sheet shows through at 1 − opacity (what opacity
|
|
1259
|
+
// measures), half the light the sheet lets through: as much as a
|
|
1260
|
+
// leaf lifting off it lets through close over the page, so nothing
|
|
1261
|
+
// changes when the reader takes hold of it (#492).
|
|
1262
|
+
u.uUnderK.value = open ? spec.transmission / 2 : 0;
|
|
1263
|
+
u.uTransmit.value = open || mesh === this.blockMesh ? 0 : spec.transmission;
|
|
1069
1264
|
m.needsUpdate = true;
|
|
1070
1265
|
}
|
|
1071
1266
|
// ── Layout ──
|
|
@@ -1264,6 +1459,7 @@ export class PageFlipper {
|
|
|
1264
1459
|
// book's own cover), no more.
|
|
1265
1460
|
const back = Math.min(0.04 * W, Math.max(1.5, (this.coverSpec?.caliperMm ?? BINDINGS[binding].boardMm) * k));
|
|
1266
1461
|
this.clearSpine();
|
|
1462
|
+
this.fold = { top: meet > 0.4 ? meet - 0.3 : 0, back, side: tL < tR ? -1 : 1 };
|
|
1267
1463
|
if (meet > 0.4) {
|
|
1268
1464
|
// In a case the back is wrapped in the case's spine (the wall beside
|
|
1269
1465
|
// an empty side, the headcap at head and tail); without one, the
|
|
@@ -1314,10 +1510,12 @@ export class PageFlipper {
|
|
|
1314
1510
|
tex.repeat.set(W / k / tile, H / k / tile);
|
|
1315
1511
|
}
|
|
1316
1512
|
// The book at rest in the height map.
|
|
1317
|
-
for (const c of this.staticCasters)
|
|
1513
|
+
for (const c of this.staticCasters) {
|
|
1318
1514
|
c.removeFromParent();
|
|
1515
|
+
c.geometry.dispose();
|
|
1516
|
+
}
|
|
1319
1517
|
this.staticCasters = [this.left, this.right, ...this.stacks, ...this.covers.children].map((m) => {
|
|
1320
|
-
const c = new Mesh(m.geometry, this.staticCaster);
|
|
1518
|
+
const c = new Mesh(casterGeometry(m.geometry), this.staticCaster);
|
|
1321
1519
|
c.position.copy(m.position);
|
|
1322
1520
|
c.frustumCulled = false;
|
|
1323
1521
|
this.shadowScene.add(c);
|
|
@@ -1357,6 +1555,8 @@ export class PageFlipper {
|
|
|
1357
1555
|
const dpr = Math.min(window.devicePixelRatio || 1, 3);
|
|
1358
1556
|
this.renderer.setPixelRatio(1);
|
|
1359
1557
|
this.renderer.setSize(Math.round(cw * dpr), Math.round(ch * dpr), false);
|
|
1558
|
+
this.buffer.set(Math.round(cw * dpr), Math.round(ch * dpr));
|
|
1559
|
+
this.dpr = dpr;
|
|
1360
1560
|
const spread = this.spread.getBoundingClientRect();
|
|
1361
1561
|
const W = spread.width / 2 || 1;
|
|
1362
1562
|
const H = spread.height || 1;
|
|
@@ -1532,6 +1732,76 @@ export class PageFlipper {
|
|
|
1532
1732
|
mesh.material.userData.uniforms.uPaper.value.setRGB(r, g, b, SRGBColorSpace);
|
|
1533
1733
|
this.redraw();
|
|
1534
1734
|
}
|
|
1735
|
+
/** The video playing on a page (#477), or none. */
|
|
1736
|
+
video = null;
|
|
1737
|
+
/**
|
|
1738
|
+
* Draws a video on the page whose source is `src` (see
|
|
1739
|
+
* {@link PageVideoFrame}), over its painting, on whichever face of
|
|
1740
|
+
* whichever leaf carries that page: lying open or turning, the picture
|
|
1741
|
+
* bends with the paper. `null` takes it off. The book is drawn again at
|
|
1742
|
+
* every new frame of the video.
|
|
1743
|
+
*/
|
|
1744
|
+
setVideo(video) {
|
|
1745
|
+
const old = this.video;
|
|
1746
|
+
if (old && (!video || video.element !== old.element)) {
|
|
1747
|
+
old.element.cancelVideoFrameCallback?.(old.callback);
|
|
1748
|
+
old.texture.dispose();
|
|
1749
|
+
this.video = null;
|
|
1750
|
+
}
|
|
1751
|
+
if (video) {
|
|
1752
|
+
let texture = this.video?.texture;
|
|
1753
|
+
if (!texture) {
|
|
1754
|
+
texture = new VideoTexture(video.element);
|
|
1755
|
+
texture.colorSpace = SRGBColorSpace;
|
|
1756
|
+
texture.minFilter = LinearFilter;
|
|
1757
|
+
texture.generateMipmaps = false;
|
|
1758
|
+
}
|
|
1759
|
+
this.video = { ...video, texture, callback: this.video?.callback ?? 0 };
|
|
1760
|
+
if (!this.video.callback)
|
|
1761
|
+
this.watchVideo();
|
|
1762
|
+
}
|
|
1763
|
+
for (const mesh of this.pageMeshes())
|
|
1764
|
+
this.dressVideo(mesh);
|
|
1765
|
+
this.redraw();
|
|
1766
|
+
}
|
|
1767
|
+
/** Draws the book again at each new frame of the video (a turning book
|
|
1768
|
+
* is drawn by its own frames). */
|
|
1769
|
+
watchVideo() {
|
|
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);
|
|
1783
|
+
}
|
|
1784
|
+
/** The meshes that show pages: the open ones, the leaves in the air and
|
|
1785
|
+
* the block. */
|
|
1786
|
+
pageMeshes() {
|
|
1787
|
+
return [this.left, this.right, ...this.leaves.values(), ...(this.blockMesh ? [this.blockMesh] : [])];
|
|
1788
|
+
}
|
|
1789
|
+
/** Sets a mesh's video uniforms: on the face that shows the video's
|
|
1790
|
+
* page, off elsewhere. */
|
|
1791
|
+
dressVideo(mesh) {
|
|
1792
|
+
const u = mesh.material.userData.uniforms;
|
|
1793
|
+
const v = this.video;
|
|
1794
|
+
const face = v && v.src ? (mesh.userData.Front === v.src ? 1 : mesh.userData.Back === v.src ? 2 : 0) : 0;
|
|
1795
|
+
u.uVidFace.value = face;
|
|
1796
|
+
u.uVideo.value = face && v ? v.texture : null;
|
|
1797
|
+
if (!face || !v)
|
|
1798
|
+
return;
|
|
1799
|
+
const { origin, across, down, crop } = v.frame;
|
|
1800
|
+
u.uVidO.value.set(origin.x, origin.y);
|
|
1801
|
+
u.uVidA.value.set(across.x, across.y);
|
|
1802
|
+
u.uVidB.value.set(down.x, down.y);
|
|
1803
|
+
u.uVidCrop.value.set(crop?.x ?? 0, crop?.y ?? 0, crop?.width ?? 1, crop?.height ?? 1);
|
|
1804
|
+
}
|
|
1535
1805
|
/** A page source drawn again in place (a canvas repainted): its
|
|
1536
1806
|
* texture is uploaded again and the book redrawn. */
|
|
1537
1807
|
touch(src) {
|
|
@@ -1614,7 +1884,7 @@ export class PageFlipper {
|
|
|
1614
1884
|
}
|
|
1615
1885
|
const used = new Set(book.flat());
|
|
1616
1886
|
for (const mesh of [this.left, this.right])
|
|
1617
|
-
used.add(mesh.userData.Front).add(mesh.userData.Back);
|
|
1887
|
+
used.add(mesh.userData.Front).add(mesh.userData.Back).add(mesh.userData.Under);
|
|
1618
1888
|
for (const [src, tex] of this.ready) {
|
|
1619
1889
|
if (used.has(src))
|
|
1620
1890
|
continue;
|
|
@@ -1629,6 +1899,7 @@ export class PageFlipper {
|
|
|
1629
1899
|
if (mesh.userData[face] === src)
|
|
1630
1900
|
return;
|
|
1631
1901
|
mesh.userData[face] = src;
|
|
1902
|
+
this.dressVideo(mesh);
|
|
1632
1903
|
u[`uHas${face}`].value = 0;
|
|
1633
1904
|
u[`u${face}`].value = null;
|
|
1634
1905
|
if (src === "")
|
|
@@ -1668,7 +1939,7 @@ export class PageFlipper {
|
|
|
1668
1939
|
mesh.material.userData.uniforms.uPaper.value.setRGB(...this.paper, SRGBColorSpace);
|
|
1669
1940
|
mesh.castShadow = mesh.receiveShadow = true;
|
|
1670
1941
|
mesh.frustumCulled = false;
|
|
1671
|
-
const caster = new Mesh(mesh.geometry, this.leafCaster);
|
|
1942
|
+
const caster = new Mesh(casterGeometry(mesh.geometry), this.leafCaster);
|
|
1672
1943
|
caster.frustumCulled = false;
|
|
1673
1944
|
caster.visible = false;
|
|
1674
1945
|
this.shadowScene.add(caster);
|
|
@@ -1692,6 +1963,49 @@ export class PageFlipper {
|
|
|
1692
1963
|
mm += this.specOf(k).caliperMm;
|
|
1693
1964
|
return Math.max(0.5, mm * (this.appearance.singlePage ? 0.5 : 1) * this.pxPerMm());
|
|
1694
1965
|
}
|
|
1966
|
+
/**
|
|
1967
|
+
* The book's own cover in the air is still folded round the spine: on
|
|
1968
|
+
* the spine's outer side its fold rises from the back of the block to
|
|
1969
|
+
* where the cover leaves the spine. Without it the edge of the page
|
|
1970
|
+
* under the cover showed between the two (a white line under a dark
|
|
1971
|
+
* cover), the cover being lifted off the pages at the spine too.
|
|
1972
|
+
*/
|
|
1973
|
+
foldCover(air) {
|
|
1974
|
+
const covers = this.appearance.coverLeaves;
|
|
1975
|
+
const isCover = (k) => k === covers?.front || k === covers?.back;
|
|
1976
|
+
// The lowest the spine edge of a leaf's faces stands.
|
|
1977
|
+
const spineZ = (mesh) => {
|
|
1978
|
+
const parts = mesh.userData.board;
|
|
1979
|
+
let z = Infinity;
|
|
1980
|
+
for (const m of parts?.back.visible ? [mesh, parts.back] : [mesh]) {
|
|
1981
|
+
const pos = m.geometry.attributes.position;
|
|
1982
|
+
for (let iy = 0; iy <= NY; iy++)
|
|
1983
|
+
z = Math.min(z, pos.getZ(iy * (NX + 1)));
|
|
1984
|
+
}
|
|
1985
|
+
return z;
|
|
1986
|
+
};
|
|
1987
|
+
let at = Infinity;
|
|
1988
|
+
if (this.coverSpec) {
|
|
1989
|
+
for (const { k, mesh } of air)
|
|
1990
|
+
if (isCover(k))
|
|
1991
|
+
at = Math.min(at, spineZ(mesh));
|
|
1992
|
+
const block = this.block;
|
|
1993
|
+
if (block && this.blockMesh && [covers?.front, covers?.back].some((c) => c !== undefined && c >= block.lo && c < block.hi))
|
|
1994
|
+
at = Math.min(at, spineZ(this.blockMesh));
|
|
1995
|
+
}
|
|
1996
|
+
const { top, back, side } = this.fold;
|
|
1997
|
+
// Right under the cover, and a little in under it past the spine: a
|
|
1998
|
+
// slot left between the two showed the page under the cover, seen
|
|
1999
|
+
// low over the spine.
|
|
2000
|
+
const reach = at - 0.03;
|
|
2001
|
+
const inner = 0.3;
|
|
2002
|
+
this.hinge.visible = top > 0 && reach > top;
|
|
2003
|
+
if (!this.hinge.visible)
|
|
2004
|
+
return;
|
|
2005
|
+
const depth = reach - top + 0.05;
|
|
2006
|
+
this.hinge.scale.set(back / 2 + inner, this.H, depth);
|
|
2007
|
+
this.hinge.position.set((side * (back / 2 - inner)) / 2, 0, top - 0.05 + depth / 2);
|
|
2008
|
+
}
|
|
1695
2009
|
/** A board leaf's other face and edges, carried with its mesh. */
|
|
1696
2010
|
boardParts(mesh) {
|
|
1697
2011
|
let parts = mesh.userData.board;
|
|
@@ -2173,6 +2487,7 @@ export class PageFlipper {
|
|
|
2173
2487
|
if (!mesh.parent)
|
|
2174
2488
|
this.stage.add(mesh);
|
|
2175
2489
|
}
|
|
2490
|
+
this.foldCover(air);
|
|
2176
2491
|
// The pages lying open: the top leaf of each stack.
|
|
2177
2492
|
let l = -1;
|
|
2178
2493
|
for (let j = 0; j < n; j++)
|
|
@@ -2189,12 +2504,21 @@ export class PageFlipper {
|
|
|
2189
2504
|
// What shows through an open page: the other side of its leaf.
|
|
2190
2505
|
this.assign(this.left, "Front", l >= 0 ? (this.book[l]?.[1] ?? null) : null);
|
|
2191
2506
|
this.assign(this.right, "Back", this.book[r + 1]?.[0] ?? null);
|
|
2507
|
+
// And through the whole sheet, the page lying under it: the next
|
|
2508
|
+
// leaf's face on the same side.
|
|
2509
|
+
const lying = (j, left) => j >= 0 && j < n && this.turned[j] === left && !this.turns.has(j) && !this.inBlock(j);
|
|
2510
|
+
this.assign(this.left, "Under", lying(l - 1, true) ? (this.book[l]?.[0] ?? null) : null);
|
|
2511
|
+
this.assign(this.right, "Under", lying(r + 1, false) ? (this.book[r + 1]?.[1] ?? null) : null);
|
|
2192
2512
|
const extra = this.appearance.extraLeaves;
|
|
2193
2513
|
this.left.visible = leftSrc !== null || (extra?.before ?? 0) > 0;
|
|
2194
2514
|
this.right.visible = rightSrc !== null || (extra?.after ?? 0) > 0;
|
|
2195
2515
|
this.dressPage(this.left, l >= 0 ? this.specOf(l) : this.bookSpec);
|
|
2196
2516
|
this.dressPage(this.right, r < n ? this.specOf(r) : this.bookSpec);
|
|
2197
|
-
// The height map lifted leaves occlude the sky from
|
|
2517
|
+
// The height map lifted leaves occlude the sky from, with the leaves
|
|
2518
|
+
// where they lie this frame.
|
|
2519
|
+
for (const mesh of [...this.leaves.values(), this.blockMesh])
|
|
2520
|
+
if (mesh?.visible)
|
|
2521
|
+
syncCaster(mesh.userData.caster, mesh.geometry);
|
|
2198
2522
|
for (const c of this.staticCasters)
|
|
2199
2523
|
c.visible = true;
|
|
2200
2524
|
this.staticCasters[0].visible = this.left.visible;
|
|
@@ -2205,8 +2529,170 @@ export class PageFlipper {
|
|
|
2205
2529
|
this.renderer.render(this.shadowScene, this.shadowCamera);
|
|
2206
2530
|
this.renderer.setRenderTarget(null);
|
|
2207
2531
|
}
|
|
2532
|
+
this.mirror(air.length > 0 || this.block !== null);
|
|
2533
|
+
this.behind(air.map((a) => a.mesh));
|
|
2208
2534
|
this.renderer.render(this.scene, this.camera);
|
|
2209
2535
|
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Thin paper lets light through (#484). While translucent leaves are in
|
|
2538
|
+
* the air, the scene is drawn once more without them (at half the
|
|
2539
|
+
* screen's resolution: the sheet blurs what it lets through anyway),
|
|
2540
|
+
* colour and depth: each such leaf reads, at its own place on screen,
|
|
2541
|
+
* what lies behind it there and how far, and lets that light through.
|
|
2542
|
+
* They cast no shadow and take no sky from the pages in that picture:
|
|
2543
|
+
* a page under a thin leaf is lit through it, and a leaf just taken
|
|
2544
|
+
* hold of shows the page under it as it did lying on it. Nothing is
|
|
2545
|
+
* drawn at rest or on opaque stock.
|
|
2546
|
+
*/
|
|
2547
|
+
behind(air) {
|
|
2548
|
+
const u = behindUniforms;
|
|
2549
|
+
u.uBehindOn.value = 0;
|
|
2550
|
+
u.uBehind.value = u.uBehindDepth.value = null;
|
|
2551
|
+
const lit = air.filter((m) => m.visible && m.material.userData.uniforms.uTransmit.value > 0);
|
|
2552
|
+
if (!this.mirrorOk || !lit.length)
|
|
2553
|
+
return;
|
|
2554
|
+
const w = Math.max(1, Math.round(this.buffer.x / 2));
|
|
2555
|
+
const h = Math.max(1, Math.round(this.buffer.y / 2));
|
|
2556
|
+
let target = this.behindTarget;
|
|
2557
|
+
if (!target) {
|
|
2558
|
+
target = new WebGLRenderTarget(w, h, { type: HalfFloatType, minFilter: LinearMipmapLinearFilter, magFilter: LinearFilter, generateMipmaps: true });
|
|
2559
|
+
target.depthTexture = new DepthTexture(w, h);
|
|
2560
|
+
this.behindTarget = target;
|
|
2561
|
+
}
|
|
2562
|
+
else if (target.width !== w || target.height !== h)
|
|
2563
|
+
target.setSize(w, h);
|
|
2564
|
+
for (const m of lit)
|
|
2565
|
+
m.visible = false;
|
|
2566
|
+
// The key's shadow map drawn again without them (the main view draws
|
|
2567
|
+
// it with them after), and the open pages' sky not cut by them.
|
|
2568
|
+
const shadows = this.renderer.shadowMap.autoUpdate;
|
|
2569
|
+
this.renderer.shadowMap.autoUpdate = false;
|
|
2570
|
+
this.renderer.shadowMap.needsUpdate = true;
|
|
2571
|
+
const occ = [this.left, this.right].map((m) => m.material.userData.uniforms.uLeafOcc);
|
|
2572
|
+
const was = occ.map((o) => o.value);
|
|
2573
|
+
for (const o of occ)
|
|
2574
|
+
o.value = 0;
|
|
2575
|
+
this.renderer.setRenderTarget(target);
|
|
2576
|
+
this.renderer.clear();
|
|
2577
|
+
this.renderer.render(this.scene, this.camera);
|
|
2578
|
+
this.renderer.setRenderTarget(null);
|
|
2579
|
+
this.renderer.shadowMap.autoUpdate = shadows;
|
|
2580
|
+
occ.forEach((o, i) => (o.value = was[i]));
|
|
2581
|
+
for (const m of lit)
|
|
2582
|
+
m.visible = true;
|
|
2583
|
+
u.uBehind.value = target.texture;
|
|
2584
|
+
u.uBehindDepth.value = target.depthTexture;
|
|
2585
|
+
u.uBehindSize.value.copy(this.buffer);
|
|
2586
|
+
u.uBehindClip.value.set(this.camera.near, this.camera.far);
|
|
2587
|
+
u.uBehindScale.value = (h / 2) * this.camera.projectionMatrix.elements[5];
|
|
2588
|
+
// Light spreads about a quarter of a millimetre in the sheet.
|
|
2589
|
+
u.uBehindFloor.value = 0.25 * this.pxPerMm();
|
|
2590
|
+
u.uBehindOn.value = 1;
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Gloss paper mirrors the leaves turning over it (#483). For each open
|
|
2594
|
+
* page on gloss stock, while anything is in the air, the leaves in the
|
|
2595
|
+
* air are drawn from the camera mirrored in that page's plane: a
|
|
2596
|
+
* camera under the page looking up at the leaves as they are, so each
|
|
2597
|
+
* is lit, shadowed and occluded as it is and shows the face the page
|
|
2598
|
+
* sees. Turned left for right, that picture lies on the page's own
|
|
2599
|
+
* place on screen; the page reads it as its reflection. Two pages at
|
|
2600
|
+
* the same height share one picture. Nothing is drawn at rest or on
|
|
2601
|
+
* any other finish.
|
|
2602
|
+
*/
|
|
2603
|
+
mirror(airborne) {
|
|
2604
|
+
const sides = [
|
|
2605
|
+
{ side: "left", page: this.left },
|
|
2606
|
+
{ side: "right", page: this.right },
|
|
2607
|
+
];
|
|
2608
|
+
let last = null;
|
|
2609
|
+
for (const { side, page } of sides) {
|
|
2610
|
+
const u = page.material.userData.uniforms;
|
|
2611
|
+
const on = this.mirrorOk && airborne && page.visible && page.material.userData.spec?.paper.finish === "gloss";
|
|
2612
|
+
u.uReflectionOn.value = on ? 1 : 0;
|
|
2613
|
+
if (!on)
|
|
2614
|
+
continue;
|
|
2615
|
+
// Stage x runs out from the spine, to the left on the left page,
|
|
2616
|
+
// and the stage is mirrored for a right-bound book.
|
|
2617
|
+
const plane = pagePlane(this.surfaces[side], this.W, (side === "left" ? -1 : 1) * this.sign);
|
|
2618
|
+
if (!last || last.plane.normal.dot(plane.normal) < 0.99999 || Math.abs(last.plane.constant - plane.constant) > 0.25) {
|
|
2619
|
+
const target = this.mirrorTarget(side);
|
|
2620
|
+
this.drawMirror(target, plane);
|
|
2621
|
+
last = { plane, target };
|
|
2622
|
+
}
|
|
2623
|
+
u.uReflection.value = last.target.texture;
|
|
2624
|
+
u.uReflectionSize.value.copy(this.buffer);
|
|
2625
|
+
u.uReflectionNormal.value.copy(plane.normal);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
/** A side's mirror picture, about as sharp as the screen (in CSS px
|
|
2629
|
+
* up to 1.5 device px each: a gloss coat is never a perfect mirror). */
|
|
2630
|
+
mirrorTarget(side) {
|
|
2631
|
+
const f = Math.min(1, 1.5 / this.dpr);
|
|
2632
|
+
const w = Math.max(1, Math.round(this.buffer.x * f));
|
|
2633
|
+
const h = Math.max(1, Math.round(this.buffer.y * f));
|
|
2634
|
+
let target = this.mirrors[side];
|
|
2635
|
+
if (!target) {
|
|
2636
|
+
target = new WebGLRenderTarget(w, h, { type: HalfFloatType, minFilter: LinearFilter, magFilter: LinearFilter, generateMipmaps: false, samples: 4 });
|
|
2637
|
+
this.mirrors[side] = target;
|
|
2638
|
+
}
|
|
2639
|
+
else if (target.width !== w || target.height !== h)
|
|
2640
|
+
target.setSize(w, h);
|
|
2641
|
+
return target;
|
|
2642
|
+
}
|
|
2643
|
+
/** Draws the leaves in the air as `plane` (an open page's) mirrors them. */
|
|
2644
|
+
drawMirror(target, plane) {
|
|
2645
|
+
const cam = this.mirrorCamera;
|
|
2646
|
+
// The eye mirrored in the plane, turned round its up axis so it is
|
|
2647
|
+
// still a camera (no mirror in it): its picture is the reflection
|
|
2648
|
+
// turned left for right, and its frustum the main one turned so too.
|
|
2649
|
+
const { x, y, z } = plane.normal;
|
|
2650
|
+
const d = -plane.constant;
|
|
2651
|
+
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);
|
|
2652
|
+
cam.matrix.copy(reflect).multiply(this.camera.matrixWorld).multiply(new Matrix4().makeScale(-1, 1, 1));
|
|
2653
|
+
cam.updateMatrixWorld(true);
|
|
2654
|
+
const p = cam.projectionMatrix.copy(this.camera.projectionMatrix);
|
|
2655
|
+
p.elements[8] *= -1;
|
|
2656
|
+
// Its near plane laid along the page (an oblique frustum): nothing
|
|
2657
|
+
// under the page shows in its mirror.
|
|
2658
|
+
const near = new Plane(plane.normal.clone(), plane.constant + 0.2).applyMatrix4(cam.matrixWorldInverse);
|
|
2659
|
+
const clip = new Vector4(near.normal.x, near.normal.y, near.normal.z, near.constant);
|
|
2660
|
+
const e = p.elements;
|
|
2661
|
+
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]);
|
|
2662
|
+
clip.multiplyScalar(2 / clip.dot(q));
|
|
2663
|
+
e[2] = clip.x;
|
|
2664
|
+
e[6] = clip.y;
|
|
2665
|
+
e[10] = clip.z + 1;
|
|
2666
|
+
e[14] = clip.w;
|
|
2667
|
+
cam.projectionMatrixInverse.copy(p).invert();
|
|
2668
|
+
// Only the leaves in the air (and the light on them).
|
|
2669
|
+
const keep = new Set([this.key, this.key.target, ...this.leaves.values()]);
|
|
2670
|
+
if (this.blockMesh)
|
|
2671
|
+
keep.add(this.blockMesh);
|
|
2672
|
+
const hidden = [];
|
|
2673
|
+
for (const c of this.stage.children) {
|
|
2674
|
+
if (keep.has(c) || !c.visible)
|
|
2675
|
+
continue;
|
|
2676
|
+
c.visible = false;
|
|
2677
|
+
hidden.push(c);
|
|
2678
|
+
}
|
|
2679
|
+
// The key's shadow map as the main view drew it last: the leaves'
|
|
2680
|
+
// own faces under them are not lit by the key anyway.
|
|
2681
|
+
const shadows = this.renderer.shadowMap.autoUpdate;
|
|
2682
|
+
this.renderer.shadowMap.autoUpdate = false;
|
|
2683
|
+
// The picture behind the leaves lies on the main view's screen, not
|
|
2684
|
+
// this one's.
|
|
2685
|
+
const behind = behindUniforms.uBehindOn.value;
|
|
2686
|
+
behindUniforms.uBehindOn.value = 0;
|
|
2687
|
+
this.renderer.setRenderTarget(target);
|
|
2688
|
+
this.renderer.clear();
|
|
2689
|
+
this.renderer.render(this.scene, cam);
|
|
2690
|
+
this.renderer.setRenderTarget(null);
|
|
2691
|
+
this.renderer.shadowMap.autoUpdate = shadows;
|
|
2692
|
+
behindUniforms.uBehindOn.value = behind;
|
|
2693
|
+
for (const c of hidden)
|
|
2694
|
+
c.visible = true;
|
|
2695
|
+
}
|
|
2210
2696
|
/** Back to a transparent canvas (the DOM spread shows), unless leaves
|
|
2211
2697
|
* are moving again. A persistent book is drawn at rest instead. */
|
|
2212
2698
|
clear() {
|
|
@@ -2233,14 +2719,21 @@ export class PageFlipper {
|
|
|
2233
2719
|
}
|
|
2234
2720
|
dispose() {
|
|
2235
2721
|
this.disposed = true;
|
|
2722
|
+
this.setVideo(null);
|
|
2236
2723
|
cancelAnimationFrame(this.raf);
|
|
2237
2724
|
cancelAnimationFrame(this.still);
|
|
2238
2725
|
cancelAnimationFrame(this.orbitFrame);
|
|
2239
2726
|
for (const tex of this.ready.values())
|
|
2240
2727
|
tex.dispose();
|
|
2241
2728
|
this.shadowTarget?.dispose();
|
|
2729
|
+
this.mirrors.left?.dispose();
|
|
2730
|
+
this.mirrors.right?.dispose();
|
|
2731
|
+
this.behindTarget?.depthTexture?.dispose();
|
|
2732
|
+
this.behindTarget?.dispose();
|
|
2242
2733
|
this.leafCaster.dispose();
|
|
2243
2734
|
this.staticCaster.dispose();
|
|
2735
|
+
for (const c of this.staticCasters)
|
|
2736
|
+
c.geometry.dispose();
|
|
2244
2737
|
this.scene.environment?.dispose();
|
|
2245
2738
|
this.pmrem?.dispose();
|
|
2246
2739
|
for (const mesh of this.leaves.values())
|
|
@@ -2253,6 +2746,7 @@ export class PageFlipper {
|
|
|
2253
2746
|
}
|
|
2254
2747
|
for (const c of this.covers.children)
|
|
2255
2748
|
c.geometry.dispose();
|
|
2749
|
+
this.hinge.geometry.dispose();
|
|
2256
2750
|
this.clearSpine();
|
|
2257
2751
|
this.spineTex?.dispose();
|
|
2258
2752
|
this.coverMaterial.normalMap?.dispose();
|