leafthrough 0.1.5 → 0.1.7

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 CHANGED
@@ -145,7 +145,9 @@ draws with your renderer and takes pointer input through your camera, so it turn
145
145
  it lies, however it's moved, turned or scaled. Every option, method, property and event of
146
146
  `LeafThrough` works the same, apart from those about its own canvas and DOM: the options
147
147
  `bleed`, `maxPixelRatio`, `plain`, `label` and `formatPageLabel`, and the properties
148
- `plain` and `element`.
148
+ `plain` and `element`. It has one option of its own, `deform`
149
+ ([Reshaping the paper](#reshaping-the-paper)), and its sheets stand apart by default
150
+ ([Depth](#depth)).
149
151
 
150
152
  ```ts
151
153
  import { LeafThroughObject } from 'leafthrough'
@@ -183,7 +185,15 @@ renderer.setAnimationLoop((now) => {
183
185
  sheets cast and receive shadows when your renderer draws them, and `sun.position`
184
186
  should point towards your shadow-casting directional light, in the magazine's own
185
187
  frame (only the direction counts). It places the shadow a turning sheet casts right by
186
- the spine, where the shadow map can't.
188
+ the spine, where the shadow map can't. `update({ lighting: { shadows } })` switches the
189
+ sheets' shadows without a rebuild. Use it rather than setting `castShadow` or
190
+ `receiveShadow` on the sheets, which the magazine sets itself.
191
+ - **Depth.** See [Depth](#depth): the sheets stand a little apart, which needs enough depth
192
+ precision from your camera.
193
+ - **Several magazines.** Magazines drawn by the same renderer share the built-in grain's
194
+ texture and the sheet geometry of each layout and page aspect: made for the first, freed
195
+ with the last. The grain itself is drawn once per page, and the wrinkles are sampled once
196
+ per aspect.
187
197
  - **Rendering on demand.** If you only draw when something changes, listen for `change`:
188
198
  it fires when input, loading or a method call needs a frame. Then keep drawing while
189
199
  `tick()` returns true.
@@ -197,11 +207,72 @@ magazine.covers(e.clientX, e.clientY) // whether the magazine takes a press t
197
207
  magazine.bounds // { pages, reach }, in the magazine's own units
198
208
  magazine.camera = otherCamera // cast input from another camera
199
209
  magazine.enabled = false // cancel active gestures and stop taking pointer/key input
210
+ magazine.update({ lighting: { shadows: false } }) // sheets stop casting and receiving, no rebuild
200
211
  magazine.addEventListener('change', () => requestRender())
201
212
  magazine.addEventListener('pagechange', (e) => e.detail) // also ready, pageload, error
202
213
  magazine.dispose() // free everything and leave the scene
203
214
  ```
204
215
 
216
+ ### Depth
217
+
218
+ In a `LeafThroughObject` the sheets stand apart, `paper.sheetGap` `0.0005` page widths
219
+ (0.1 mm paper on a 210 mm page), and sort by their real depth, so they stay in order from the front, from
220
+ behind and at any angle. Your camera has to resolve that gap. A 24-bit depth buffer tells
221
+ apart points roughly `distance² / (near × 16,777,216)` apart, in your scene's units, and that
222
+ must be well under the gap in the same units. Take a booklet 120 mm wide in a scene in
223
+ metres, seen from 4.4 m: its gap is 0.06 mm. With the camera's `near` at 0.1 the buffer
224
+ resolves about 0.012 mm, which is enough; at 0.01 it's about 0.12 mm, and hidden pages
225
+ flicker through. So keep `near` as large as your scene allows; `far` matters much less.
226
+ The magazine never changes your camera. With a coarse depth buffer you can widen the gap.
227
+
228
+ The standalone magazine instead draws sheets that touch at the spine and lets
229
+ `paper.depthBias` sort them: a polygon offset brings each sheet nearer the camera than
230
+ those under it. You can turn that on here too (`paper: { sheetGap: 0, depthBias: true }`),
231
+ but it has two limits in a scene of your own. It assumes the magazine is seen from the
232
+ front, so from behind it brings the wrong sheets forward. And it moves sheets by steps of
233
+ the depth buffer, so how far depends on `near`, `far` and distance: with a small `near`
234
+ it can pull pages through whatever lies just in front of the magazine.
235
+
236
+ ### Reshaping the paper
237
+
238
+ Experimental: this may change in a patch release. `deform` reshapes the paper with your
239
+ own GLSL, after the magazine has shaped it: bend a booklet coming out of its case, squeeze
240
+ it, roll it. It applies wherever the paper is drawn, in colour and in the shadow passes,
241
+ and lighting follows, because normals come from the deformed shape.
242
+
243
+ ```ts
244
+ const bend = { value: 0 }
245
+ const magazine = new LeafThroughObject({ renderer, camera }, {
246
+ pages,
247
+ deform: {
248
+ uniforms: { uBend: bend },
249
+ glsl: /* glsl */ `
250
+ uniform float uBend;
251
+ vec3 deformSheet(vec3 p, vec2 uv) {
252
+ // curl the pages up along x, around a cylinder of radius 1 / uBend
253
+ if (uBend < 1e-4) return p;
254
+ float a = p.x * uBend;
255
+ float r = 1.0 / uBend - p.z;
256
+ return vec3(r * sin(a), p.y, 1.0 / uBend - r * cos(a));
257
+ }`,
258
+ },
259
+ })
260
+
261
+ bend.value = 1.2 // read on the next frame: no rebuild
262
+ ```
263
+
264
+ - `deformSheet` gets `p`, a point of a sheet in the magazine's own units (see Units
265
+ above), and `uv`, where it is on that sheet: x from the spine (0) to the outer edge (1),
266
+ y from bottom to top. It returns where to draw that point, in the same units.
267
+ - three.js's common GLSL (`PI`, `saturate` and so on) is available. Name your uniforms
268
+ with a prefix of your own, as the magazine's start with `u`.
269
+ - Uniforms are read every frame: set `value` on your own objects, or pass new objects
270
+ under the same names through `update()`. Changing the GLSL, or which uniforms there are,
271
+ rebuilds the pages.
272
+ - It changes only what's drawn. Pointer input, `covers()`, `bounds` and the shadow a
273
+ turning sheet casts by the spine still work from the undeformed pages, so turn input off
274
+ (`enabled = false`) while the paper is far from its own shape.
275
+
205
276
  The [three.js demo](https://dmtrbrl.github.io/leafthrough/three.html) takes a magazine
206
277
  out of a portrait paperboard mailer on a table; its source is
207
278
  [`demo/main-three.ts`](https://github.com/dmtrbrl/leafthrough/blob/main/demo/main-three.ts),
@@ -244,14 +315,14 @@ The magazine fills the element you give it and keeps its proportions, centred.
244
315
  | `formatPageLabel` | `(detail) => string` | `"Pages 14–15 of 28"` | What screen readers hear when pages change. |
245
316
  | `onPageChange` | `(detail) => void` | | Same as the `pagechange` event. |
246
317
  | `onReady` | `() => void` | | The first pages are loaded and on screen. |
247
- | `onPageLoad` | `(page) => void` | | A page's image loaded. Same as the `pageload` event. |
318
+ | `onPageLoad` | `(page) => void` | | A page's image is loaded and on the GPU. Same as the `pageload` event. |
248
319
  | `onError` | `(error, page?) => void` | | A page failed to load. It stays blank paper until `retry()`. |
249
320
 
250
321
  **`paper`**
251
322
 
252
323
  | | Default | |
253
324
  | ------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------ |
254
- | `grain` | `true` | `false` turns it off. You can also pass a tiling greyscale image or its URL; only its brightest specks show. |
325
+ | `grain` | `true` | `false` turns it off. You can also pass a tiling greyscale image or its URL; only its brightest specks show. The built-in one is drawn once per page, however many magazines there are. |
255
326
  | `grainScale` | `1.46` | Tiles across a page. |
256
327
  | `grainStrength` | `0.17` | How much the grain darkens. |
257
328
  | `showThrough` | spread `0.04`, single `0.3` | How much of the other side shows through the paper. |
@@ -261,6 +332,8 @@ The magazine fills the element you give it and keeps its proportions, centred.
261
332
  | `wrinkles` | `1` | Waviness of resting sheets; `0` lays them flat. |
262
333
  | `bulge` | `1` | How much pages rise off the spine. |
263
334
  | `cutout` | `false` | Cuts each sheet to the shape of its front page: transparent areas are no paper at all, shadow included. |
335
+ | `sheetGap` | `0` (`0.0005` in a scene) | Space between stacked sheets, in page widths. 0.1 mm paper on a 120 mm page is `0.1 / 120`. |
336
+ | `depthBias` | `true` (`false` in a scene) | Draws each sheet nearer the camera than those under it, so sheets touching at the spine don't flicker. See [Depth](#depth). |
264
337
 
265
338
  **`lighting`**: `ambient: { sky, ground, intensity }`, `sun: { color, intensity, position }`,
266
339
  `shadows` (default `true`), `shadowMapSize` (default `2048`), and `floorShadow`. That last
@@ -311,7 +384,8 @@ magazine.addEventListener('error', (e) => e.detail) // { error, page }
311
384
 
312
385
  Both compare options by value. Changing callbacks or the label is free. Changing `pages`,
313
386
  `layout`, paper, lighting, motion or interaction rebuilds the scene (a few milliseconds)
314
- and keeps the reader on the same page. `startPage` only applies at creation; use `goTo()`
387
+ and keeps the reader on the same page. In a `LeafThroughObject`, `lighting.shadows` and
388
+ new values for `deform`'s uniforms apply without a rebuild. `startPage` only applies at creation; use `goTo()`
315
389
  after that. The React, Vue and Svelte bindings and the element use `setOptions()`, so
316
390
  removing a prop or attribute restores its default. Options are copied on every call, so an
317
391
  object or array changed in place and passed again (as Vue's reactive props are) counts as
@@ -341,11 +415,17 @@ pages: [
341
415
  front page's outline (draw the back page as its mirror image).
342
416
  - **Cross-origin URLs.** URLs on another origin need CORS headers, because the images are
343
417
  uploaded to WebGL.
344
- - **Textures.** A three.js `Texture` you pass in is never disposed, but the magazine sets its
345
- `colorSpace`, `premultiplyAlpha` and `anisotropy`. Give it a texture of its own if you
346
- use the same one elsewhere.
418
+ - **Textures.** A three.js `Texture` you pass in is taken as it is: never disposed, and its
419
+ `colorSpace`, `premultiplyAlpha` and `anisotropy` are left alone, so one that's on the
420
+ GPU already, or that your own material uses too, isn't uploaded again. Pages the magazine
421
+ loads itself are sRGB and premultiplied; for yours to look the same, set `colorSpace` to
422
+ `THREE.SRGBColorSpace`, and `premultiplyAlpha` to `true` if it has transparent parts (the
423
+ shader lays it over the paper as premultiplied colour).
347
424
  - **Load order.** Pages load nearest the reader first, four at a time, and the order follows
348
- the reader as they turn or jump. Pages that haven't arrived show blank paper.
425
+ the reader as they turn or jump. Pages that haven't arrived show blank paper. Once decoded,
426
+ the pages in view and next to them go to the GPU at once; the rest go one per frame, or
427
+ more in idle time, so a dozen arriving together don't drop a frame. `pageload` fires for
428
+ each as it's on the GPU.
349
429
  - **Memory.** Pages within `preload` of the visible ones load; pages more than twice that
350
430
  far away unload, freeing their GPU memory, and load again when the reader comes back.
351
431
  Visible means anything on screen: pages still turning in a quick riffle, and in `single`
@@ -402,7 +482,10 @@ How it works:
402
482
  - `content.ts` keeps the visible pages' content as DOM: hidden for screen readers in 3D,
403
483
  the pages themselves in the plain view.
404
484
  - `textures.ts` loads, prioritises and unloads page textures.
405
- - `src/paper` holds the shared paper material and the procedural grain.
485
+ - `src/paper` holds the shared paper material and the procedural grain. Sheets keep their
486
+ order by real depth, each lifted by `paper.sheetGap` for the sheets under it, or by a
487
+ polygon offset (`layerSheet`) where they touch. `deform` wraps the layout's sheet shape
488
+ in every pass.
406
489
  - `LeafThrough.ts` handles the DOM, sizing, loading, layout switching, keyboard and
407
490
  events. `LeafThroughObject.ts` does the same inside someone else's scene, without the DOM.
408
491
  - Engines take input through `EngineInput` (`engine.ts`): an element to listen on, a ray
@@ -410,6 +493,8 @@ How it works:
410
493
  their own floor plane, so the same code serves the magazine's own camera and any scene's.
411
494
  - `navigation.ts` maps pages to engine positions, riffles an animated `goTo()` and binds
412
495
  the keyboard, for both.
496
+ - `test/depth.test.ts` prints the pages that should be hidden pure red and checks that
497
+ none shows, across layouts, views, paper shapes, turns and camera depth ranges.
413
498
 
414
499
  ## Credits
415
500
 
@@ -1,14 +1,21 @@
1
1
  import * as THREE from 'three';
2
2
  import { type Rect } from './framing.js';
3
3
  import type { GoToOptions } from './LeafThrough.js';
4
- import { type LeafThroughOptions, type Layout, type PageChangeDetail } from './options.js';
4
+ import { type LeafThroughOptions, type Layout, type PageChangeDetail, type PaperDeform } from './options.js';
5
5
  /**
6
6
  * The magazine's options, less those about its own canvas and DOM. Of `lighting`, only
7
7
  * `shadows` (whether sheets cast and receive them) and `sun.position` apply: the lights are
8
8
  * your scene's. Point `sun.position` towards your shadow-casting directional light, in the
9
9
  * magazine's own frame; it places the shadow a turning sheet casts right by the spine.
10
+ * `lighting.shadows` changes without a rebuild.
11
+ *
12
+ * Its sheets stand apart by default (`paper.sheetGap` `0.0005`, about 0.1 mm paper on a
13
+ * 210 mm page) and `paper.depthBias` is off, so they sort by real depth from any side.
10
14
  */
11
- export type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'>;
15
+ export type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'> & {
16
+ /** Reshapes the paper in your own GLSL. Experimental. */
17
+ deform?: PaperDeform;
18
+ };
12
19
  /** What the magazine draws with, from your scene. */
13
20
  export interface LeafThroughObjectContext {
14
21
  /** The renderer drawing your scene. Pages load into it. */
@@ -138,6 +145,7 @@ export declare class LeafThroughObject extends THREE.Group<LeafThroughObjectEven
138
145
  private pickLayout;
139
146
  private watchSpreadQuery;
140
147
  private remount;
148
+ private resolve;
141
149
  private readonly axis;
142
150
  private pageAxis;
143
151
  private dragWidth;