postext-folio 0.52.1 → 0.52.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pageFlip.js CHANGED
@@ -1,914 +1,23 @@
1
- import { BoxGeometry, BufferAttribute, BufferGeometry, Color, CustomBlending, DepthTexture, DirectionalLight, DoubleSide, Group, HalfFloatType, LinearMipmapLinearFilter, MaxEquation, Mesh, MeshPhysicalMaterial, MeshStandardMaterial, LinearFilter, Matrix4, NeutralToneMapping, OneFactor, OrthographicCamera, PCFShadowMap, PerspectiveCamera, Plane, PlaneGeometry, PMREMGenerator, Raycaster, Scene, ShaderChunk, ShaderMaterial, ShadowMaterial, SRGBColorSpace, Texture, TextureLoader, Vector2, Vector3, Vector4, WebGLRenderer, WebGLRenderTarget, } from "three";
2
- import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
3
- import { OutputPass } from "three/examples/jsm/postprocessing/OutputPass.js";
4
- import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
5
- import { SMAAPass } from "three/examples/jsm/postprocessing/SMAAPass.js";
6
- import { resolveFolioConfig } from "postext";
7
- import { along, BINDINGS, gutterOcclusion, profiles, stackGeometry, topAt } from "./bookGeometry.js";
8
- import { environment } from "./environments.js";
9
- import { pagePaper, paperSpec } from "./paper.js";
10
- import { loadDeskMaps } from "./deskTextures.js";
11
- import { ATLAS_MAX, MAX_PAGE_VIDEOS, VideoAtlas } from "./videoAtlas.js";
12
- import { cachedDesk, cachedRelief } from "./procedural.js";
13
- import { Loupe } from "./loupe.js";
14
- /** Columns and rows of a leaf's mesh: fine enough for a tight roll. */
15
- const NX = 96;
16
- /** How far a leaf's folded paper stays over its own paper beneath it. */
17
- const OWN_GAP = 1.2;
18
- /** How much higher than at rest a leaf in the air stays over a block. */
19
- const BLOCK_GAP = 1.5;
20
- const NY = 120;
21
- /** Columns of a page lying open (its curve runs across only). */
22
- const NX_OPEN = 72;
23
- /** Vertical field of view. */
24
- const FOV = 24;
25
- /** Radius of the roll at mid-turn for 90 g/m² offset, against the page's width. */
26
- const ROLL = 0.11;
27
- /** Milliseconds one leaf takes to turn. */
28
- const DURATION = 1000;
29
- /** A jump of more pages than this is made in one move: the block of
30
- * leaves in between is lifted together and laid over on the other side,
31
- * instead of every leaf turning on its own. */
32
- export const BLOCK_PAGES = 10;
33
- /** How long a block of leaves takes to go over (ms), a little longer for
34
- * a thicker one. */
35
- const BLOCK_DURATION = 1150;
36
- /** A page is taken by its outer part only: from this fraction of its
37
- * width from the spine out to the fore-edge (#494). */
38
- const GRIP = 0.5;
39
- /** A leaf follows the one before it once that one is this far through its turn. */
40
- const GAP = 0.14;
41
- // Ambient occlusion from the book's own shape: each frame the book is
42
- // drawn, seen from straight above, into a height map: channel r the
43
- // highest of everything (blocks, covers, open pages, leaves), channel g the
44
- // leaves in the air alone. A point is occluded by paper rising round it,
45
- // more the nearer and higher it is: the book's contact shadow on the desk
46
- // and the dark under a lifted leaf. The open pages read only the leaves'
47
- // channel (their gutter is worked out from the cross-section).
48
- const OCCLUSION = /* glsl */ `
49
- uniform sampler2D uShadowMap;
50
- uniform vec4 uShadowBox;
51
- uniform float uShadowOn;
52
- uniform float uOccRadius;
53
- uniform float uOccBias;
54
- float casterAt(vec2 s, float channel) {
55
- vec2 uv = (s - uShadowBox.xy) / uShadowBox.zw;
56
- if (uv.x < 0.0 || uv.y < 0.0 || uv.x > 1.0 || uv.y > 1.0) return -1e4;
57
- vec4 t = texture2D(uShadowMap, uv);
58
- return (channel > 0.5 ? t.g : t.r) - 10.0;
59
- }
60
- // Horizon-based: in each of 8 directions, the highest paper seen from p
61
- // (as an elevation angle) hides that much of the sky: sin(horizon) of a
62
- // cosine-weighted slice.
63
- float bookOcclusion(vec3 p, float channel) {
64
- if (uShadowOn < 0.5) return 0.0;
65
- float spin = fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453);
66
- float occ = 0.0;
67
- for (int d = 0; d < 8; d++) {
68
- float a = (float(d) + spin) * 0.785398;
69
- vec2 dir = vec2(cos(a), sin(a));
70
- float horizon = 0.0;
71
- for (int i = 0; i < 4; i++) {
72
- float t = (float(i) + 0.25 + 0.5 * spin) / 4.0;
73
- float r = t * t * uOccRadius + 1.0;
74
- float h = casterAt(p.xy + r * dir, channel) - p.z - uOccBias;
75
- horizon = max(horizon, h / sqrt(h * h + r * r));
76
- }
77
- occ += horizon;
78
- }
79
- return occ / 8.0;
80
- }
81
- // How far the reflection seen at p runs under paper: its ray marched
82
- // out through the height map (a lifted leaf hides what it would mirror,
83
- // and so does the leaf's own roll over the inside of its curl). The
84
- // march starts a few px out, past the paper round p itself. Its steps
85
- // are shifted a little from pixel to pixel: at fixed steps the leaf's
86
- // outline was cut in at each of them, a row of teeth (#483).
87
- float reflectionOcclusion(vec3 p, vec3 r, float channel) {
88
- if (uShadowOn < 0.5) return 0.0;
89
- // Interleaved gradient noise: neighbours take evenly spread shifts.
90
- float spin = fract(52.9829189 * fract(dot(gl_FragCoord.xy, vec2(0.06711056, 0.00583715))));
91
- float hidden = 0.0;
92
- for (int i = 0; i < 8; i++) {
93
- float s = (float(i) + spin) / 8.0;
94
- float t = 6.0 + s * s * uOccRadius * 2.0;
95
- vec3 q = p + r * t;
96
- hidden = max(hidden, smoothstep(0.0, 4.0, casterAt(q.xy, channel) - q.z - uOccBias));
97
- }
98
- return hidden;
99
- }
100
- `;
101
- /** Declares the book position varying in a vertex shader and sets it:
102
- * the world position with the mirror of a right-bound book undone. */
103
- const BOOK_POS_PARS = "uniform float uSign;\nvarying vec3 vBookPos;";
104
- const BOOK_POS = "vBookPos = (modelMatrix * vec4(transformed, 1.0)).xyz;\nvBookPos.x *= uSign;";
105
- // The casters lie outside the stage, in the book's own coordinates (as
106
- // `vBookPos` reads the map): no mirror here (#334).
107
- const CASTER_VERTEX = /* glsl */ `
108
- varying float vZ;
109
- void main() {
110
- vec4 w = modelMatrix * vec4(position, 1.0);
111
- vZ = w.z;
112
- gl_Position = projectionMatrix * viewMatrix * vec4(w.xy, 0.0, 1.0);
113
- }
114
- `;
115
- const CASTER_FRAGMENT = /* glsl */ `
116
- uniform float uLeaf;
117
- varying float vZ;
118
- void main() {
119
- gl_FragColor = vec4(vZ + 10.0, uLeaf * (vZ + 10.0), 0.0, 1.0);
120
- }
121
- `;
122
- /** The occlusion uniforms every receiving material shares (same objects). */
123
- const occlusionUniforms = {
124
- uShadowMap: { value: null },
125
- uShadowBox: { value: new Vector4(0, 0, 1, 1) },
126
- uShadowOn: { value: 0 },
127
- uOccRadius: { value: 60 },
128
- uOccBias: { value: 1 },
129
- };
130
- // The key light's soft shadow, each of its samples compared against the
131
- // receiver's own plane carried to that sample (receiver-plane depth bias):
132
- // three.js compares them all at the receiver's depth, so a sheet steep to
133
- // the light, a leaf in the air, found itself nearer the light a few texels
134
- // away and fell in its own shadow (#330). The shadow is kept (`keyShadow`)
135
- // so the reflections can be cut with it too.
136
- const SHADOW_PCF_HEAD = "float getShadow( sampler2DShadow shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) {";
137
- const SHADOW_PCF = /* glsl */ `
138
- ${SHADOW_PCF_HEAD}
139
- shadowCoord.xyz /= shadowCoord.w;
140
- shadowCoord.z += shadowBias;
141
- // How the receiver's depth in the map runs with the map's uv.
142
- vec3 sx = dFdx(shadowCoord.xyz);
143
- vec3 sy = dFdy(shadowCoord.xyz);
144
- float det = sx.x * sy.y - sx.y * sy.x;
145
- vec2 slope = abs(det) > 1e-12 ? vec2(sy.y * sx.z - sx.y * sy.z, sx.x * sy.z - sy.x * sx.z) / det : vec2(0.0);
146
- float shadow = 1.0;
147
- if (shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0 && shadowCoord.z <= 1.0) {
148
- float radius = shadowRadius / shadowMapSize.x;
149
- float phi = interleavedGradientNoise(gl_FragCoord.xy) * PI2;
150
- shadow = 0.0;
151
- for (int i = 0; i < 5; i++) {
152
- vec2 o = vogelDiskSample(i, 5, phi) * radius;
153
- // Held in where the plane runs nearly along the light.
154
- float dz = clamp(dot(slope, o), -0.05, 0.05);
155
- shadow += texture(shadowMap, vec3(shadowCoord.xy + o, shadowCoord.z + dz));
156
- }
157
- shadow *= 0.2;
158
- }
159
- keyShadow = shadow;
160
- return mix(1.0, shadow, shadowIntensity);
161
- }
162
- `;
163
- const SHADOW_PARS = (() => {
164
- const chunk = ShaderChunk.shadowmap_pars_fragment;
165
- const start = chunk.indexOf(SHADOW_PCF_HEAD);
166
- const end = chunk.indexOf("#elif defined( SHADOWMAP_TYPE_VSM )", start);
167
- // Another three.js: its own shadows (no receiver plane).
168
- const pars = start < 0 || end < 0 ? chunk : chunk.slice(0, start) + SHADOW_PCF + chunk.slice(end);
169
- return `float keyShadow = 1.0;\n${pars}`;
170
- })();
171
- /** How much of the environment's reflection the key's shadow takes away:
172
- * the key stands where the environment's brightest emitter is, so what
173
- * hides it from a point hides that emitter's reflection too. */
174
- function keySpecular(occlusion = "1.0") {
175
- return /* glsl */ `{
176
- float keySpec = mix(1.0, keyShadow, 0.85) * (${occlusion});
177
- reflectedLight.indirectSpecular *= keySpec;
178
- #ifdef USE_CLEARCOAT
179
- clearcoatSpecularIndirect *= keySpec;
180
- #endif
181
- #ifdef USE_SHEEN
182
- sheenSpecularIndirect *= keySpec;
183
- #endif
184
- }`;
185
- }
186
- /** Gives a material the key's shadow on its own plane. */
187
- function keyShadowed(fragmentShader) {
188
- return fragmentShader.replace("#include <shadowmap_pars_fragment>", SHADOW_PARS);
189
- }
190
- /** A caster's own copy of a mesh's shape. The height map is drawn before
191
- * the main view, and three.js uploads a geometry once per frame number,
192
- * which the key's shadow pass at the end of the last main view had
193
- * already taken: a caster sharing a leaf's geometry drew the leaf where
194
- * it lay a frame before, and a leaf moving down was shaded by its own
195
- * old self, moving up it was not (#488). */
196
- function casterGeometry(source) {
197
- const g = new BufferGeometry();
198
- g.setAttribute("position", source.attributes.position.clone());
199
- if (source.index)
200
- g.setIndex(source.index.clone());
201
- return g;
202
- }
203
- /** Brings a caster's copy to its mesh's shape as laid out this frame. */
204
- function syncCaster(caster, source) {
205
- const to = caster.geometry.attributes.position;
206
- to.array.set(source.attributes.position.array);
207
- to.needsUpdate = true;
208
- }
209
- function casterMaterial(leaf) {
210
- return new ShaderMaterial({
211
- vertexShader: CASTER_VERTEX,
212
- fragmentShader: CASTER_FRAGMENT,
213
- side: DoubleSide,
214
- blending: CustomBlending,
215
- blendEquation: MaxEquation,
216
- blendEquationAlpha: MaxEquation,
217
- blendSrc: OneFactor,
218
- blendDst: OneFactor,
219
- depthTest: false,
220
- depthWrite: false,
221
- uniforms: { uLeaf: { value: leaf ? 1 : 0 } },
222
- });
223
- }
224
- /** A standard material (the desk, the covers) with the book's occlusion
225
- * on its ambient light. */
226
- function occluded(m, sign) {
227
- m.onBeforeCompile = (shader) => {
228
- Object.assign(shader.uniforms, occlusionUniforms, { uSign: { value: sign } });
229
- shader.vertexShader = shader.vertexShader
230
- .replace("#include <common>", `#include <common>\n${BOOK_POS_PARS}`)
231
- .replace("#include <begin_vertex>", `#include <begin_vertex>\n${BOOK_POS}`);
232
- shader.fragmentShader = keyShadowed(shader.fragmentShader)
233
- .replace("#include <common>", `#include <common>\nvarying vec3 vBookPos;\n${OCCLUSION}`)
234
- .replace("#include <aomap_fragment>", "#include <aomap_fragment>\n{ float occ = 1.0 - bookOcclusion(vBookPos, 0.0); reflectedLight.indirectDiffuse *= occ; reflectedLight.indirectSpecular *= occ; }\n" + keySpecular());
235
- };
236
- return m;
237
- }
238
- /** What lies behind the leaves in the air (#484): the scene drawn without
239
- * the translucent leaves (colour, mipmapped, and depth), shared by every
240
- * page. `uBehindSize`: the screen in device px; `uBehindClip`: the
241
- * camera's near and far planes; `uBehindScale`: texels of that picture
242
- * per book unit at a view depth of 1; `uBehindFloor`: how far (book
243
- * units) the sheet's fibres spread even what touches it. */
244
- const behindUniforms = {
245
- uBehind: { value: null },
246
- uBehindDepth: { value: null },
247
- uBehindOn: { value: 0 },
248
- uBehindSize: { value: new Vector2(1, 1) },
249
- uBehindClip: { value: new Vector2(1, 2) },
250
- uBehindScale: { value: 1 },
251
- uBehindFloor: { value: 1 },
252
- };
253
- /**
254
- * A page's material: three.js's physical (PBR) material lit by the
255
- * environment and the key light, the printed page on each face, the
256
- * paper's shade, finish (roughness, a clear coat on coated stock, a fibre
257
- * sheen on uncoated) and relief (a normal map), and the print on the other
258
- * side showing faintly through thin paper. Ambient light is cut by the
259
- * gutter's occlusion (a vertex attribute, from the book's cross-section)
260
- * and by lifted leaves nearby.
261
- */
262
- function pageMaterial(mirror, sign) {
263
- const m = new MeshPhysicalMaterial({ side: DoubleSide, roughness: 0.85 });
264
- const uniforms = {
265
- uFront: { value: null },
266
- uBack: { value: null },
267
- uHasFront: { value: 0 },
268
- uHasBack: { value: 0 },
269
- uMirror: { value: mirror ? 1 : 0 },
270
- uShowThrough: { value: 0 },
271
- uUnder: { value: null },
272
- uHasUnder: { value: 0 },
273
- uUnderK: { value: 0 },
274
- uTransmit: { value: 0 },
275
- uPaper: { value: new Color(1, 1, 1) },
276
- uLeafOcc: { value: 0.45 },
277
- uOccBias: { value: 6 },
278
- uVideo: { value: null },
279
- uVidTexel: { value: new Vector2(1, 1) },
280
- uVidCount: { value: 0 },
281
- uVidOA: { value: Array.from({ length: MAX_PAGE_VIDEOS }, () => new Vector4()) },
282
- uVidDF: { value: Array.from({ length: MAX_PAGE_VIDEOS }, () => new Vector4()) },
283
- uVidRect: { value: Array.from({ length: MAX_PAGE_VIDEOS }, () => new Vector4()) },
284
- uReflection: { value: null },
285
- uReflectionOn: { value: 0 },
286
- uReflectionSize: { value: new Vector2(1, 1) },
287
- uReflectionNormal: { value: new Vector3(0, 0, 1) },
288
- };
289
- m.userData.uniforms = uniforms;
290
- m.onBeforeCompile = (shader) => {
291
- Object.assign(shader.uniforms, occlusionUniforms, behindUniforms, uniforms, { uSign: { value: sign } });
292
- shader.vertexShader = shader.vertexShader
293
- .replace("#include <common>", `#include <common>
294
- attribute float ao;
295
- varying float vAo;
296
- varying vec2 vPageUv;
297
- ${BOOK_POS_PARS}`)
298
- .replace("#include <begin_vertex>", `#include <begin_vertex>
299
- vAo = ao;
300
- vPageUv = uv;
301
- ${BOOK_POS}`);
302
- shader.fragmentShader = keyShadowed(shader.fragmentShader)
303
- .replace("#include <common>", `#include <common>
304
- uniform sampler2D uFront;
305
- uniform sampler2D uBack;
306
- uniform float uHasFront;
307
- uniform float uHasBack;
308
- uniform float uMirror;
309
- uniform float uShowThrough;
310
- uniform sampler2D uUnder;
311
- uniform float uHasUnder;
312
- uniform float uUnderK;
313
- uniform float uTransmit;
314
- uniform sampler2D uBehind;
315
- uniform sampler2D uBehindDepth;
316
- uniform float uBehindOn;
317
- uniform vec2 uBehindSize;
318
- uniform vec2 uBehindClip;
319
- uniform float uBehindScale;
320
- uniform float uBehindFloor;
321
- // The print of both faces at this point: what light coming
322
- // through the sheet passes on its way.
323
- vec3 transInk = vec3(1.0);
324
- uniform vec3 uPaper;
325
- uniform float uLeafOcc;
326
- uniform float uSign;
327
- uniform sampler2D uVideo;
328
- uniform vec2 uVidTexel;
329
- uniform float uVidCount;
330
- uniform vec4 uVidOA[${MAX_PAGE_VIDEOS}];
331
- uniform vec4 uVidDF[${MAX_PAGE_VIDEOS}];
332
- uniform vec4 uVidRect[${MAX_PAGE_VIDEOS}];
333
- uniform sampler2D uReflection;
334
- uniform float uReflectionOn;
335
- uniform vec2 uReflectionSize;
336
- uniform vec3 uReflectionNormal;
337
- varying float vAo;
338
- varying vec2 vPageUv;
339
- varying vec3 vBookPos;
340
- ${OCCLUSION}`)
341
- .replace("#include <map_fragment>", `{
342
- // 0 no page, 1 its image, 2 blank paper; uv.x runs from the spine.
343
- bool front = gl_FrontFacing;
344
- float has = front ? uHasFront : uHasBack;
345
- float u = uMirror > 0.5 ? 1.0 - vPageUv.x : vPageUv.x;
346
- vec2 uvF = vec2(u, vPageUv.y);
347
- vec2 uvB = vec2(1.0 - u, vPageUv.y);
348
- vec3 ink = uPaper;
349
- if (has > 0.5 && has < 1.5) ink = front ? texture2D(uFront, uvF).rgb : texture2D(uBack, uvB).rgb;
350
- // The videos playing on this face, printed over their posters:
351
- // the point of the page (from its top left corner) in each
352
- // picture's own frame, which may lie turned on the page, then in
353
- // its slot of the atlas (kept half a texel inside it, clear of
354
- // the next slot).
355
- vec2 tuv = front ? uvF : uvB;
356
- for (int i = 0; i < ${MAX_PAGE_VIDEOS}; i++) {
357
- if (float(i) >= uVidCount) break;
358
- vec4 oa = uVidOA[i];
359
- vec4 df = uVidDF[i];
360
- if ((df.z < 1.5) != front) continue;
361
- vec2 p = vec2(tuv.x, 1.0 - tuv.y) - oa.xy;
362
- vec2 st = vec2(dot(p, oa.zw) / dot(oa.zw, oa.zw), dot(p, df.xy) / dot(df.xy, df.xy));
363
- if (st.x >= 0.0 && st.x <= 1.0 && st.y >= 0.0 && st.y <= 1.0) {
364
- vec4 r = uVidRect[i];
365
- vec2 v = clamp(r.xy + st * r.zw, r.xy + 0.5 * uVidTexel, r.xy + r.zw - 0.5 * uVidTexel);
366
- ink = texture2D(uVideo, vec2(v.x, 1.0 - v.y)).rgb;
367
- }
368
- }
369
- // The other side's print, seen through the sheet (the same point
370
- // of paper, so mirrored as it should be).
371
- float other = front ? uHasBack : uHasFront;
372
- vec3 through = vec3(1.0);
373
- if ((uShowThrough > 0.0 || uTransmit > 0.0) && other > 0.5 && other < 1.5) {
374
- through = front ? texture2D(uBack, uvB).rgb : texture2D(uFront, uvF).rgb;
375
- }
376
- transInk = ink * through;
377
- if (uShowThrough > 0.0) ink *= mix(vec3(1.0), through, uShowThrough);
378
- // The page lying under an open page: its print comes through the
379
- // whole sheet (the fibres spread it by a fraction of a millimetre,
380
- // finer than the page's own texels: a softer sample would blur
381
- // a page seen small to grey).
382
- if (uHasUnder > 0.5 && uUnderK > 0.0) {
383
- vec3 under = texture2D(uUnder, front ? uvF : uvB).rgb;
384
- ink *= mix(vec3(1.0), under, uUnderK);
385
- }
386
- diffuseColor.rgb *= ink;
387
- }`)
388
- .replace("#include <aomap_fragment>", `#include <aomap_fragment>
389
- {
390
- // A leaf's own roll counts among the leaves it is occluded by:
391
- // held to what its open side would let in.
392
- float occ = vAo * (1.0 - uLeafOcc * bookOcclusion(vBookPos, 1.0));
393
- reflectedLight.indirectDiffuse *= occ;
394
- reflectedLight.indirectSpecular *= mix(1.0, occ, 0.85);
395
- }
396
- // The reflection's ray, in book coordinates.
397
- vec3 reflected = (vec4(reflect(-geometryViewDir, normal), 0.0) * viewMatrix).xyz;
398
- reflected.x *= uSign;
399
- // A mirrored leaf stands for what it hides itself.
400
- ${keySpecular("1.0 - 0.85 * (uReflectionOn > 0.5 ? 0.0 : reflectionOcclusion(vBookPos, reflected, 1.0))")}
401
- #ifdef USE_CLEARCOAT
402
- if (uReflectionOn > 0.5) {
403
- // The leaves in the air, mirrored in a gloss page: where one is
404
- // seen, it takes the place of the environment it hides, through
405
- // the coating's Fresnel and the paper's own. A plane holds only
406
- // where the page lies in it: the mirror fades up the gutter.
407
- vec2 st = gl_FragCoord.xy / uReflectionSize;
408
- vec4 seen = texture2D(uReflection, vec2(1.0 - st.x, st.y));
409
- float lies = smoothstep(0.97, 0.995, abs(dot((vec4(normalize(vNormal), 0.0) * viewMatrix).xyz, uReflectionNormal)));
410
- float a = seen.a * lies;
411
- vec3 leaf = seen.rgb * lies;
412
- clearcoatSpecularIndirect = clearcoatSpecularIndirect * (1.0 - a)
413
- + leaf * EnvironmentBRDF(geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness);
414
- reflectedLight.indirectSpecular = reflectedLight.indirectSpecular * (1.0 - a)
415
- + leaf * EnvironmentBRDF(geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness);
416
- }
417
- #endif
418
- if (uBehindOn > 0.5 && uTransmit > 0.0) {
419
- // Thin paper in the air lets through the light behind it (#484):
420
- // what lies there on screen, seen through the print of both
421
- // faces. The sheet scatters what it lets through about evenly
422
- // (near Lambertian), so a thing a gap behind it is spread over
423
- // about twice that gap: sharp where the leaf touches the page
424
- // under it, a soft glow a few millimetres off. A page close
425
- // under it is lit only through the leaf, so its print comes
426
- // back at about half the leaf's transmittance (1 − opacity, as
427
- // the open page shows the one under it); what lies further, out
428
- // of the leaf's shade, at the whole of it. Stronger and sharper,
429
- // the page under a leaf about to land read as lying over it
430
- // (#492). What it lets through it does not reflect.
431
- vec2 st = gl_FragCoord.xy / uBehindSize;
432
- float depth = texture2D(uBehindDepth, st).x;
433
- float zBehind = uBehindClip.x * uBehindClip.y / (uBehindClip.y - depth * (uBehindClip.y - uBehindClip.x));
434
- float gap = max(0.0, zBehind - vViewPosition.z);
435
- float spread = (uBehindFloor + 2.0 * gap) * uBehindScale / vViewPosition.z;
436
- vec4 seen = textureLod(uBehind, st, log2(max(1.0, 2.0 * spread)));
437
- float carried = uTransmit * mix(0.5, 1.0, smoothstep(0.0, 8.0 * uBehindFloor, gap));
438
- float lost = carried * seen.a;
439
- reflectedLight.directDiffuse *= 1.0 - lost;
440
- reflectedLight.indirectDiffuse = reflectedLight.indirectDiffuse * (1.0 - lost) + carried * seen.rgb * transInk;
441
- }`);
442
- };
443
- return m;
444
- }
445
- /** The uniforms every page block's edge material shares: book units per
446
- * mm, and how strongly the cut face is relieved (uncoated stock shows
447
- * its fibres; a coated sheet is cut cleaner). */
448
- const edgeUniforms = {
449
- uEdgeK: { value: 1 },
450
- uEdgeRelief: { value: 1 },
451
- };
452
- // The cut face of a page block, as a height field in mm over (leaf, mm
453
- // along the cut): every leaf a rounded ridge standing a hair proud of or
454
- // behind its neighbours (sheets are never quite in line), the sections
455
- // and the bundles the knife took together a little out of line with the
456
- // next, and the faint streaks the guillotine drags across the block. Each
457
- // scale fades out where it is finer than a pixel, so the face never
458
- // shimmers. The relief bends the normal (bump mapping from the screen-
459
- // space derivatives of the field), so the face catches a grazing light.
460
- const EDGE_RELIEF = /* glsl */ `
461
- uniform float uEdgeK;
462
- uniform float uEdgeRelief;
463
- varying float vRun;
464
- float eHash(float n) { return fract(sin(n * 127.1) * 43758.5453); }
465
- float eHash2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
466
- float eNoise(vec2 p) {
467
- vec2 i = floor(p);
468
- vec2 f = fract(p);
469
- vec2 u = f * f * (3.0 - 2.0 * f);
470
- return mix(mix(eHash2(i), eHash2(i + vec2(1.0, 0.0)), u.x), mix(eHash2(i + vec2(0.0, 1.0)), eHash2(i + vec2(1.0, 1.0)), u.x), u.y);
471
- }
472
- // fade: x = the leaves, y = the bundles, z = the fine knife marks.
473
- float edgeHeight(vec2 lv, vec3 fade) {
474
- float id = floor(lv.x);
475
- float ridge = sin(fract(lv.x) * PI);
476
- float proud = eHash(id) - 0.5 + 0.8 * (eNoise(vec2(id * 0.37, lv.y * 0.12)) - 0.5);
477
- float leaf = (0.45 * ridge + proud) * 0.04 * fade.x;
478
- float bundle = (eNoise(vec2(lv.x / 5.0, lv.y * 0.03)) - 0.5) * 0.12 * fade.y
479
- + (eNoise(vec2(lv.x / 24.0 + 7.0, lv.y * 0.02)) - 0.5) * 0.18;
480
- float knife = (eNoise(vec2(lv.y * 0.7, lv.x / 90.0)) - 0.5) * 0.035
481
- + (eNoise(vec2(lv.y * 3.0, lv.x / 40.0)) - 0.5) * 0.006 * fade.z;
482
- return leaf + bundle + knife;
483
- }
484
- vec3 edgeNormal(vec3 surfPos, vec3 n, vec2 dH, float faceDir) {
485
- vec3 sx = dFdx(surfPos);
486
- vec3 sy = dFdy(surfPos);
487
- vec3 r1 = cross(sy, n);
488
- vec3 r2 = cross(n, sx);
489
- float det = dot(sx, r1) * faceDir;
490
- vec3 grad = sign(det) * (dH.x * r1 + dH.y * r2);
491
- return normalize(abs(det) * n - grad);
492
- }
493
- `;
494
- /** The page block's edges: paper with a fine line between leaves (fading
495
- * to their average where the lines are finer than a pixel), its cut face
496
- * in relief. */
497
- function edgeMaterial(sign, bands, paper) {
498
- const m = new MeshStandardMaterial({ side: DoubleSide, roughness: 0.92 });
499
- if (paper)
500
- m.color.copy(paper);
501
- const band = { uBoardLo: { value: bands?.lo ?? -1 }, uBoardHi: { value: bands?.hi ?? 2 }, uBoardColor: { value: bands?.color ?? new Color(0, 0, 0) } };
502
- m.onBeforeCompile = (shader) => {
503
- Object.assign(shader.uniforms, occlusionUniforms, edgeUniforms, band, { uSign: { value: sign } });
504
- shader.vertexShader = shader.vertexShader
505
- .replace("#include <common>", `#include <common>\nattribute float layer;\nattribute float lambda;\nattribute float run;\nvarying float vLayer;\nvarying float vLambda;\nvarying float vRun;\n${BOOK_POS_PARS}`)
506
- .replace("#include <begin_vertex>", `#include <begin_vertex>\nvLayer = layer;\nvLambda = lambda;\nvRun = run;\n${BOOK_POS}`);
507
- shader.fragmentShader = keyShadowed(shader.fragmentShader)
508
- .replace("#include <common>", `#include <common>\nuniform float uBoardLo;\nuniform float uBoardHi;\nuniform vec3 uBoardColor;\nvarying float vLayer;\nvarying float vLambda;\nvarying vec3 vBookPos;\n${OCCLUSION}\n${EDGE_RELIEF}`)
509
- .replace("#include <normal_fragment_maps>", `{
510
- vec2 lv = vec2(vLayer, vRun / uEdgeK);
511
- vec2 dx = dFdx(lv);
512
- vec2 dy = dFdy(lv);
513
- float wl = fwidth(vLayer);
514
- vec3 fade = vec3(
515
- 1.0 - smoothstep(0.3, 0.8, wl),
516
- 1.0 - smoothstep(2.0, 5.0, wl),
517
- 1.0 - smoothstep(0.05, 0.15, fwidth(lv.y))
518
- );
519
- float h0 = edgeHeight(lv, fade);
520
- vec2 dH = vec2(edgeHeight(lv + dx, fade) - h0, edgeHeight(lv + dy, fade) - h0) * uEdgeK * uEdgeRelief;
521
- if (vLambda >= uBoardLo && vLambda <= uBoardHi) normal = edgeNormal(-vViewPosition, normal, dH, faceDirection);
522
- }`)
523
- .replace("#include <map_fragment>", `{
524
- float w = fwidth(vLayer);
525
- float d = abs(fract(vLayer) - 0.5) * 2.0;
526
- float line = smoothstep(0.55 - w, 0.95 + w, d);
527
- float fine = clamp(w * 1.5, 0.0, 1.0);
528
- diffuseColor.rgb *= 1.0 - mix(0.22 * line, 0.08, fine);
529
- // The bundles' sheets are never quite one shade.
530
- diffuseColor.rgb *= 1.0 + 0.06 * uEdgeRelief * (eNoise(vec2(vLayer / 9.0, 3.0)) - 0.5);
531
- // A board lying in the block (the book's own cover) shows its
532
- // own edge, not paper.
533
- if (vLambda < uBoardLo || vLambda > uBoardHi) diffuseColor.rgb = uBoardColor;
534
- }`)
535
- .replace("#include <aomap_fragment>", "#include <aomap_fragment>\nreflectedLight.indirectDiffuse *= 1.0 - bookOcclusion(vBookPos, 0.0);");
536
- };
537
- return m;
538
- }
539
- const ease = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);
540
- const easeOut = (t) => 1 - Math.pow(1 - t, 3);
541
- const smooth = (a, b, x) => {
542
- const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
543
- return t * t * (3 - 2 * t);
544
- };
545
- /**
546
- * The stock a publisher would print a book of `pages` pages on, when its
547
- * settings name none: a long book goes on lighter paper, a very long one
548
- * on bible paper (2,000 pages make a volume about 4.5 cm thick, not 11).
549
- * Settings that name a stock, a grammage or a bulk are kept as they are.
550
- */
551
- export function withStockForExtent(folio, pages) {
552
- const paper = folio?.paper;
553
- if (paper?.type !== undefined || paper?.grammage !== undefined || paper?.bulk !== undefined)
554
- return folio;
555
- if (pages > 1000)
556
- return { ...folio, paper: { ...paper, type: "bible" } };
557
- if (pages > 600)
558
- return { ...folio, paper: { ...paper, grammage: 70 } };
559
- return folio;
560
- }
561
- /** The colour round the edge of a printed cover (its ground), averaged: the
562
- * colour its boards and spine show when the book carries its own covers.
563
- * Null when the image cannot be read. */
564
- function edgeColorOf(image) {
565
- try {
566
- const c = document.createElement("canvas");
567
- c.width = c.height = 32;
568
- const g = c.getContext("2d");
569
- if (!g)
570
- return null;
571
- g.drawImage(image, 0, 0, 32, 32);
572
- const d = g.getImageData(0, 0, 32, 32).data;
573
- let r = 0;
574
- let gr = 0;
575
- let b = 0;
576
- let n = 0;
577
- for (let y = 0; y < 32; y++)
578
- for (let x = 0; x < 32; x++) {
579
- if (x > 1 && x < 30 && y > 1 && y < 30)
580
- continue;
581
- const i = (y * 32 + x) * 4;
582
- r += d[i];
583
- gr += d[i + 1];
584
- b += d[i + 2];
585
- n++;
586
- }
587
- return new Color().setRGB(r / n / 255, gr / n / 255, b / n / 255, SRGBColorSpace);
588
- }
589
- catch {
590
- return null;
591
- }
592
- }
593
- /** The lowest the reader may orbit the view: 70° from straight above. */
594
- const MAX_PITCH = (70 * Math.PI) / 180;
595
- /** How far (radians) a leaf in mid-turn stands off the pages at the spine:
596
- * barely; it is sewn there, and more reads as the leaf lifting off the
597
- * book parallel to it and dropping back. */
598
- const LEAN = 0.02;
599
- /** Where column `t` (0 … 1) of a page's mesh lies across it: closer
600
- * together near the spine, where the gutter bends tightest. */
601
- const column = (t) => Math.pow(t, 1.45);
602
- /** Sets a page mesh's u coordinates to its columns. */
603
- function spreadColumns(geometry, nx) {
604
- const uv = geometry.attributes.uv;
605
- for (let i = 0; i < uv.count; i++)
606
- uv.setX(i, column((i % (nx + 1)) / nx));
607
- uv.needsUpdate = true;
608
- }
609
- /** max(a, b), rounded over a band `k` wide (no crease where they meet). */
610
- function smoothMax(a, b, k) {
611
- return (a + b + Math.sqrt((a - b) * (a - b) + k * k)) / 2;
612
- }
613
- /** Where arc length `signed` from the spine lies on the open book's
614
- * surface (right page positive): [x, z]. */
615
- function surfaceAt(book, signed) {
616
- const [bx, bz] = along(signed >= 0 ? book.right : book.left, Math.abs(signed));
617
- return [(signed >= 0 ? 1 : -1) * bx, bz];
618
- }
619
- /**
620
- * How a leaf folds when the point `G` of it has been carried to `P`: the
621
- * paper stays flat up to a fold line, rolls round a cylinder of radius R
622
- * and lies over itself beyond it, back up (the page-curl cylinder). The
623
- * fold is square to the pull, placed so the point taken lands under the
624
- * hand, and never crosses the spine the leaf is sewn to. R grows as the
625
- * page lifts and shrinks to nothing as it lands on the other side; a
626
- * stiffer paper (`roll`) bends in a wider roll.
627
- */
628
- export function foldOf(G, P, W, H, roll = 1) {
629
- const du = G.u - P.u;
630
- const dv = G.v - P.v;
631
- const D = Math.hypot(du, dv);
632
- if (D < 0.5)
633
- return null;
634
- const n = { u: du / D, v: dv / D };
635
- const q = progressFrom(G, P);
636
- const R = Math.min(0.5, ROLL * roll) * W * (0.3 + 0.7 * smooth(0, 0.2, q)) * (1 - smooth(0.62, 1, q)) * Math.min(1, D / (0.1 * W));
637
- // How far the paper past the fold has turned: a leaf held up by the hand
638
- // stands in the air (about 110° early in the turn), and only lies back
639
- // flat (180°) as it lands. Folding it flat over itself all the way (the
640
- // table-paper curl) made a tent of a leaf dragged across the spine.
641
- const theta = Math.PI * (0.62 + 0.38 * smooth(0.05, 1, q));
642
- // The fold sits where the point taken, carried round the roll and on
643
- // straight, lands over the hand: R sinθ + (c − Rθ) cosθ = c − D.
644
- const arc = R * theta;
645
- const k = arc > 0 ? Math.min(1, D / arc) : 0;
646
- const free = (D + k * (R * Math.sin(theta) - arc * Math.cos(theta))) / (1 - Math.cos(theta));
647
- let c = free;
648
- for (const S of [-H / 2, H / 2])
649
- c = Math.min(c, G.u * n.u + (G.v - S) * n.v);
650
- if (c <= 0)
651
- return null;
652
- const F = { u: G.u - n.u * c, v: G.v - n.v * c };
653
- // Never all at once (#502). Paper lying beyond the point taken, along
654
- // the pull (a page taken by the middle of an edge and pulled across),
655
- // turns over as the hand carries it away, not the moment it moves (a
656
- // pixel's pull stood half the page up). A fold held off the spine short
657
- // of the hand turns the less the nearer it comes to the point taken, so
658
- // that it is gone, not dropped, when it reaches it.
659
- let beyond = 0;
660
- for (const u of [0, W])
661
- for (const v of [-H / 2, H / 2])
662
- beyond = Math.max(beyond, (u - G.u) * n.u + (v - G.v) * n.v);
663
- const turns = (c / free) * (beyond > 0 ? smooth(0, 0.5 * beyond, D) : 1);
664
- return { F, n, R, theta: theta * turns };
665
- }
666
- /** Paper `d` past the fold, round a roll of radius R and straight on at
667
- * θ: how far it reaches back across the page, and how high it stands. */
668
- function rollOf(d, R, theta) {
669
- if (R > 1e-3 && d < R * theta)
670
- return [R * Math.sin(d / R), R * (1 - Math.cos(d / R))];
671
- const rest = d - R * theta;
672
- return [R * Math.sin(theta) + rest * Math.cos(theta), R * (1 - Math.cos(theta)) + rest * Math.sin(theta)];
673
- }
674
- /** How far through its turn a leaf is: the point taken, from where it
675
- * was to its mirror image across the spine. */
676
- export function progressFrom(G, P) {
677
- return Math.min(1, Math.max(0, (G.u - P.u) / (2 * G.u)));
678
- }
679
- /** The hand can carry the point taken no farther from the spine's ends
680
- * than the paper reaches. */
681
- function reach(G, P, H) {
682
- let { u, v } = P;
683
- for (let i = 0; i < 4; i++) {
684
- for (const S of [-H / 2, H / 2]) {
685
- const r = Math.hypot(G.u, G.v - S);
686
- const d = Math.hypot(u, v - S);
687
- if (d > r) {
688
- u *= r / d;
689
- v = S + ((v - S) * r) / d;
690
- }
691
- }
692
- }
693
- return { u, v };
694
- }
695
- /**
696
- * Lays a leaf's mesh out. The curl is worked out on the flat page (u from
697
- * the spine), then wrapped onto the book: the part on the right follows
698
- * the right stack's top (into the gutter and out), the part carried over
699
- * the spine the left one's. A stiff leaf (`rigidity`) turns more as a
700
- * plate on its hinge: a board does not bend at all.
701
- */
702
- export function layLeaf(geometry, fold, forward, W, H, lift, book, rigidity, q, flutter = 0, time = 0, board) {
703
- const pos = geometry.attributes.position;
704
- const sx = forward ? 1 : -1;
705
- const [xr, zr] = along(book.right, W);
706
- const [xl, zl] = along(book.left, W);
707
- const zr0 = along(book.right, 0)[1];
708
- const zl0 = along(book.left, 0)[1];
709
- const qa = forward ? q : 1 - q;
710
- let hinge;
711
- let phi;
712
- let r;
713
- let offset = 0;
714
- if (board) {
715
- // A board (a cover): a rigid slab `thick` deep, turning on the joint at
716
- // the top of the spine. It leaves the block it lay on (its chord, from
717
- // the spine to the fore-edge) and lands on the other side's, however
718
- // that lies: flat on an open book, hanging down to the desk from a
719
- // spine still standing. As it comes down to the lower side the joint
720
- // comes down the spine with it (the cloth flexes), so it lands instead
721
- // of dropping. The pivot is its middle plane: it lies with its inner
722
- // face on the block under it and lands with its outer face on the
723
- // other, and `face` sets which of its faces this mesh draws.
724
- const descend = zl0 < zr0 ? smooth(0.4, 1, qa) : 1 - smooth(0.4, 1, 1 - qa);
725
- hinge = zr0 + (zl0 - zr0) * descend + board.thick / 2;
726
- const a0 = Math.atan2(zr - zr0, xr);
727
- const a1 = Math.PI - Math.atan2(zl - zl0, xl);
728
- // The hand holds the point it took over the desk: a rigid plate turned
729
- // by φ brings it to u·cos φ, so the board stands where the hand has
730
- // carried that point (q = (1 − cos)/2), not on an eased curve that laid
731
- // it level in the air while the hand was still over the slope.
732
- phi = a0 + ((a1 - a0) * Math.acos(Math.min(1, Math.max(-1, 1 - 2 * qa)))) / Math.PI;
733
- r = 1;
734
- offset = (board.face * board.thick) / 2;
735
- }
736
- else {
737
- // A stiff leaf rests on its stack's fore-edge and swings over the gutter.
738
- r = rigidity * smooth(0, 0.06, q) * smooth(0, 0.06, 1 - q);
739
- hinge = along(book.right, 0)[1];
740
- const a0 = Math.atan2(zr - hinge, xr);
741
- const a1 = Math.PI - Math.atan2(zl - hinge, xl);
742
- phi = a0 + (a1 - a0) * qa;
743
- }
744
- // How far a turning leaf stands off the pages: none at rest or for a
745
- // corner barely lifted, a few degrees through the middle of the turn.
746
- const inAir = smooth(0.02, 0.22, q) * smooth(0.02, 0.22, 1 - q);
747
- const lean = LEAN * inAir;
748
- // Each side's chord, from the spine to its fore-edge: a side hanging
749
- // from a standing spine slopes down to the desk, and a leaf leaning off
750
- // it leans off that slope, not off a level plane over it.
751
- const chordR = (zr - zr0) / (xr || 1);
752
- const chordL = (zl - zl0) / (xl || 1);
753
- // The plane a leaf lifts off along, by side: the chord turned up by the
754
- // lean. A leaf keeps its width along it (stepping the chord's rise per
755
- // unit across, on a side sloping to the desk, stretched it by a tenth
756
- // and more, #495).
757
- const tiltR = Math.atan(chordR) + lean;
758
- const tiltL = Math.atan(chordL) + lean;
759
- // Onto the book. At rest (and as it lands) the leaf lies on the open
760
- // book's surface, into the gutter and out. In the air it is lifted off
761
- // at the spine: its own plane rises from the gutter's floor at a small
762
- // angle, and it touches the pages only where they stand higher (the
763
- // gutter's walls), joined smoothly. `z`: how far over that it stands.
764
- const onBook = (s, z) => {
765
- const [rx, rz] = surfaceAt(book, s);
766
- let px = rx;
767
- let pz = rz + z + lift;
768
- if (inAir > 0) {
769
- const tilt = s >= 0 ? tiltR : tiltL;
770
- const fx = (s >= 0 ? 1 : -1) * Math.abs(s) * Math.cos(tilt);
771
- const plane = (s >= 0 ? zr0 : zl0) + Math.abs(s) * Math.sin(tilt) + z + lift;
772
- const under = surfaceAt(book, fx)[1] + lift + 0.6;
773
- const flying = smoothMax(plane, under, 0.006 * W);
774
- px += (fx - px) * inAir;
775
- pz += (flying - pz) * inAir;
776
- }
777
- return [px, pz];
778
- };
779
- // Thin paper ripples in the air, more towards its free edge. Never
780
- // down into the pages under it: the ripple dies away where the leaf
781
- // lies close over them (it stays at least `lift` above them), or a
782
- // leaf held low over its stack dipped into the page under it, whose
783
- // print then showed through (#447).
784
- const ripple = (u, v, px, pz) => {
785
- const amp = flutter * W * (u / W) * (u / W);
786
- if (flutter <= 0 || amp <= 1e-6)
787
- return 0;
788
- const clear = pz - surfaceAt(book, px)[1] - lift;
789
- return amp * smooth(0, 2 * amp, clear) * Math.sin(2 * Math.PI * (time / 520) - (3 * u) / W + (2 * v) / H);
790
- };
791
- // Where the leaf's paper lies unfolded, column by column (before it
792
- // ripples): [out from the spine, u, height].
793
- const clearing = BLOCK_GAP * smooth(0, 0.06, q) * smooth(0, 0.06, 1 - q);
794
- const own = [];
795
- if (fold) {
796
- for (let ix = 0; ix <= NX; ix++) {
797
- const u = column(ix / NX) * W;
798
- let [ox, oz] = onBook(sx * u, 0);
799
- if (r > 0) {
800
- ox += (u * Math.cos(phi) - offset * Math.sin(phi) - ox) * r;
801
- oz += (hinge + lift + u * Math.sin(phi) + offset * Math.cos(phi) - oz) * r;
802
- }
803
- own.push([sx * ox, u, oz]);
804
- }
805
- }
806
- for (let iy = 0; iy <= NY; iy++) {
807
- const v = (0.5 - iy / NY) * H;
808
- for (let ix = 0; ix <= NX; ix++) {
809
- const u = column(ix / NX) * W;
810
- let x = u;
811
- let y = v;
812
- let z = 0;
813
- let d = 0;
814
- let dd = 0;
815
- if (fold) {
816
- const { F, n } = fold;
817
- d = (u - F.u) * n.u + (v - F.v) * n.v;
818
- if (d > 0) {
819
- // Round the roll, then straight on at the fold's angle.
820
- [dd, z] = rollOf(d, fold.R, fold.theta);
821
- y = v - n.v * (d - dd);
822
- x = u - n.u * (d - dd);
823
- }
824
- }
825
- let [px, pz] = onBook(sx * x, z);
826
- if (fold && d > 0) {
827
- // Paper up in the air is not pressed onto the pages under it: wrapped
828
- // onto them it dipped into the gutter and was squeezed across it (a
829
- // crease where the curl passed over the spine). It goes on from the
830
- // foot of its fold, level, as the fold has it, and only comes down
831
- // onto the pages as it does (lying back over itself, landing).
832
- const [fx, fz] = onBook(sx * (u - fold.n.u * d), 0);
833
- const w = smooth(0, 0.05 * W, z);
834
- px += (fx + sx * fold.n.u * dd - px) * w;
835
- pz += (fz + z - pz) * w;
836
- }
837
- if (r > 0) {
838
- px += (u * Math.cos(phi) - offset * Math.sin(phi) - px) * r;
839
- y += (v - y) * r;
840
- pz += (hinge + lift + u * Math.sin(phi) + offset * Math.cos(phi) - pz) * r;
841
- }
842
- pz += ripple(u, v, px, pz);
843
- // Never through the leaf's own paper not yet folded, as it ripples
844
- // too: carried level from a fold low on a side sloping up to the
845
- // spine, the paper folded back over it ran under it, crossing it.
846
- if (fold && d > 0) {
847
- const at = sx * px;
848
- // (A row past the head and foot too: a triangle reaching past them
849
- // from paper kept over sagged through.)
850
- if (at > own[0][0] && at < own[NX][0] && Math.abs(y) <= H / 2 + (1.5 * H) / NY) {
851
- let lo = 0;
852
- let hi = NX;
853
- while (hi - lo > 1) {
854
- const mid = (lo + hi) >> 1;
855
- if (own[mid][0] <= at)
856
- lo = mid;
857
- else
858
- hi = mid;
859
- }
860
- const f = (at - own[lo][0]) / (own[hi][0] - own[lo][0] || 1);
861
- const ou = own[lo][1] + (own[hi][1] - own[lo][1]) * f;
862
- if ((ou - fold.F.u) * fold.n.u + (y - fold.F.v) * fold.n.v < 0) {
863
- const oz = own[lo][2] + (own[hi][2] - own[lo][2]) * f;
864
- pz = Math.max(pz, oz + ripple(ou, y, px, oz) + OWN_GAP);
865
- }
866
- }
867
- }
868
- // Never into the book: over a block (and past its head or foot) a
869
- // leaf stands at least just over its top. A fold's flap is carried
870
- // level from the foot of its fold, and on a thin side hanging from
871
- // the spine that foot lies low: the flap went through the tall block
872
- // on the other side (#489).
873
- // In the air a little higher still (until it lands): half a pixel
874
- // over the block is hardly any depth at the view's distance, and a
875
- // leaf lying over a tall block's top drew through its page in teeth.
876
- if (!board && Math.abs(px) < (px >= 0 ? xr : xl))
877
- pz = Math.max(pz, topAt(px >= 0 ? book.right : book.left, Math.abs(px)) + Math.min(lift, 0.5) + clearing);
878
- pos.setXYZ(iy * (NX + 1) + ix, px, y, pz);
879
- }
880
- }
881
- pos.needsUpdate = true;
882
- geometry.computeVertexNormals();
883
- return { phi, pivot: hinge + lift };
884
- }
885
- /** Frees a leaf's mesh and a board's parts. */
886
- function disposeLeaf(mesh) {
887
- const parts = mesh.userData.board;
888
- if (parts) {
889
- parts.back.geometry.dispose();
890
- parts.slab.geometry.dispose();
891
- parts.slab.material.dispose();
892
- }
893
- mesh.userData.caster?.geometry.dispose();
894
- mesh.geometry.dispose();
895
- mesh.material.dispose();
896
- }
897
- /**
898
- * The plane an open page lies in past the gutter's shoulder (world,
899
- * normal up), its side running out from the spine along world x as
900
- * `out` (±1): level on a block lying on the desk, sloping down to it on
901
- * the thin side of a thick book, which hangs from the top of the spine.
902
- * A gloss page mirrors the leaves in the air in it (#483).
903
- */
904
- export function pagePlane(p, W, out) {
905
- const [x0, z0] = along(p, W * 0.55);
906
- const [x1, z1] = along(p, W);
907
- const dx = out * (x1 - x0);
908
- const dz = z1 - z0;
909
- const normal = new Vector3(-dz * Math.sign(dx || 1), 0, Math.abs(dx)).normalize();
910
- return new Plane().setFromNormalAndCoplanarPoint(normal, new Vector3(out * x0, 0, z0));
911
- }
1
+ import { WebGLRenderer, Scene, PerspectiveCamera, Mesh, PlaneGeometry, MeshStandardMaterial, Group, MeshPhysicalMaterial, Texture, BoxGeometry, DirectionalLight, PMREMGenerator, OrthographicCamera, WebGLRenderTarget, Vector2, TextureLoader, Raycaster, SRGBColorSpace, NeutralToneMapping, HalfFloatType, PCFShadowMap, LinearFilter, BufferAttribute, ShadowMaterial, Color, Vector3, LinearMipmapLinearFilter, Plane, DepthTexture, Matrix4, Vector4 } 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';
6
+ import { resolveFolioConfig } from 'postext';
7
+ import { BINDINGS, profiles, gutterOcclusion, along, stackGeometry } from './bookGeometry.js';
8
+ import { environment } from './environments.js';
9
+ import { paperSpec, pagePaper } from './paper.js';
10
+ import { loadDeskMaps } from './deskTextures.js';
11
+ import { VideoAtlas, ATLAS_MAX, MAX_PAGE_VIDEOS } from './videoAtlas.js';
12
+ import { cachedDesk, cachedRelief } from './procedural.js';
13
+ import { Loupe } from './loupe.js';
14
+ import { FOV, NX_OPEN, NX, NY, BLOCK_PAGES, DURATION, BLOCK_DURATION, GAP, GRIP } from './pageFlip/constants.js';
15
+ import { spreadColumns, column, MAX_PITCH, disposeLeaf, progressFrom, reach, foldOf, layLeaf, pagePlane } from './pageFlip/leaf.js';
16
+ import { edgeMaterial, pageMaterial, withStockForExtent, edgeUniforms, edgeColorOf, ease, easeOut, smooth, behindUniforms } from './pageFlip/materials.js';
17
+ import { occluded, casterMaterial, casterGeometry, occlusionUniforms, syncCaster } from './pageFlip/occlusion.js';
18
+ export { BLOCK_PAGES } from './pageFlip/constants.js';
19
+ export { withStockForExtent } from './pageFlip/materials.js';
20
+ export { foldOf, progressFrom, layLeaf, pagePlane } from './pageFlip/leaf.js';
912
21
  /**
913
22
  * Turns the pages of the viewer's DOM spread in a 3D scene: the book lies
914
23
  * open on a desk, seen from in front and above (tilted by `folio.tilt`),