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/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 +119 -3
- package/dist/pageFlip.d.ts.map +1 -1
- package/dist/pageFlip.js +632 -50
- 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 +25 -1
- 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 +132 -5
- package/dist/viewer.js.map +1 -1
- package/package.json +11 -4
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 =
|
|
76
|
-
float
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
584
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
1956
|
-
* recto), −1 the other, 0
|
|
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();
|