leafthrough 0.1.3 → 0.1.5
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 +87 -7
- package/dist/LeafThrough.d.ts +2 -9
- package/dist/LeafThroughObject.d.ts +152 -0
- package/dist/{chunk-XPIDTVB5.js → chunk-GYSQO7QZ.js} +758 -619
- package/dist/chunk-GYSQO7QZ.js.map +1 -0
- package/dist/element.js +1 -1
- package/dist/engine.d.ts +25 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +367 -2
- package/dist/index.js.map +1 -1
- package/dist/navigation.d.ts +26 -0
- package/dist/options.d.ts +14 -0
- package/dist/paper/paperTextures.d.ts +25 -0
- package/dist/react.js +1 -1
- package/dist/single/SingleEngine.d.ts +7 -9
- package/dist/spread/SpreadEngine.d.ts +7 -8
- package/dist/svelte.js +1 -1
- package/dist/vue.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-XPIDTVB5.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A 3D magazine you can flip through. Pages are printed paper: they bend, roll, catch the
|
|
4
4
|
light and cast shadows. Built on three.js, written in TypeScript, with bindings for React,
|
|
5
|
-
Vue, Svelte, and a Web Component for everything else (Angular, Solid, Lit, plain HTML).
|
|
5
|
+
Vue, Svelte, and a Web Component for everything else (Angular, Solid, Lit, plain HTML). It
|
|
6
|
+
can also be [one object in your own three.js scene](#in-your-own-threejs-scene).
|
|
6
7
|
|
|
7
8
|
**[Live demo](https://dmtrbrl.github.io/leafthrough/)**, with one page per binding and a dev panel
|
|
8
|
-
for every option, method and event
|
|
9
|
+
for every option, method and event, and
|
|
10
|
+
[a three.js scene](https://dmtrbrl.github.io/leafthrough/three.html) with the magazine in it.
|
|
9
11
|
|
|
10
12
|
- **Two layouts.** `spread` is an open magazine with sheets turning over the spine.
|
|
11
13
|
`single` shows one page at a time, and read pages roll up behind the magazine. `auto`
|
|
@@ -136,6 +138,76 @@ error trackers listen. To register the element under another tag, call
|
|
|
136
138
|
|
|
137
139
|
**Solid**: `<leaf-through prop:pages={pages()} on:pagechange={(e) => setPage(e.detail.page)} />`.
|
|
138
140
|
|
|
141
|
+
## In your own three.js scene
|
|
142
|
+
|
|
143
|
+
`LeafThroughObject` is the magazine as a `THREE.Group`, for a scene you already have. It
|
|
144
|
+
draws with your renderer and takes pointer input through your camera, so it turns wherever
|
|
145
|
+
it lies, however it's moved, turned or scaled. Every option, method, property and event of
|
|
146
|
+
`LeafThrough` works the same, apart from those about its own canvas and DOM: the options
|
|
147
|
+
`bleed`, `maxPixelRatio`, `plain`, `label` and `formatPageLabel`, and the properties
|
|
148
|
+
`plain` and `element`.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { LeafThroughObject } from 'leafthrough'
|
|
152
|
+
|
|
153
|
+
const magazine = new LeafThroughObject({ renderer, camera }, { pages, layout: 'spread' })
|
|
154
|
+
magazine.rotation.x = -Math.PI / 2 // lay it on the table
|
|
155
|
+
magazine.scale.setScalar(0.21) // a page 21 cm wide, in a scene measured in metres
|
|
156
|
+
scene.add(magazine)
|
|
157
|
+
|
|
158
|
+
renderer.setAnimationLoop((now) => {
|
|
159
|
+
magazine.tick(now) // advance the pages, then draw as usual
|
|
160
|
+
renderer.render(scene, camera)
|
|
161
|
+
})
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- **Units.** A page is 1 wide and `pageAspect` tall. The spine runs up the y axis at
|
|
165
|
+
x = 0 and the pages lie on the z = 0 plane, facing +z. A spread closed on its front
|
|
166
|
+
cover shows it at x 0 to 1, and closed on its back cover at x -1 to 0.
|
|
167
|
+
`magazine.bounds.pages` is the pages at rest, the rectangle to frame with your camera;
|
|
168
|
+
`magazine.bounds.reach` is everything a turning page sweeps through.
|
|
169
|
+
- **Input.** The magazine listens for pointer events on `renderer.domElement` (or the
|
|
170
|
+
`domElement` you pass, which must show exactly the camera's view). It takes presses in
|
|
171
|
+
its hit area: the pages in `spread` (only the cover's side while closed), and in
|
|
172
|
+
`single` a box round the page with a strip left of the spine for turning back. It
|
|
173
|
+
doesn't look at your scene, so objects in front of the magazine don't block it.
|
|
174
|
+
`magazine.covers(clientX, clientY)` tells you whether it takes a press there; check that
|
|
175
|
+
in your own handlers to leave such presses alone. Hide the magazine (`visible = false`,
|
|
176
|
+
on it or a parent) and it takes none. With `interaction.keyboard: true` the keys work
|
|
177
|
+
while that element has focus; a canvas only takes focus with a `tabIndex`, so give it
|
|
178
|
+
one or use `'global'`. Drags scale with the magazine's size on screen, so they feel the
|
|
179
|
+
same near or far. For touch drags, give the element `touch-action: pan-y pinch-zoom`
|
|
180
|
+
(or `none`), so the browser doesn't take them for scrolling.
|
|
181
|
+
- **Light.** The paper is a `MeshStandardMaterial`, lit by your scene's lights,
|
|
182
|
+
environment, fog and tone mapping. Of the `lighting` options, `shadows` makes the
|
|
183
|
+
sheets cast and receive shadows when your renderer draws them, and `sun.position`
|
|
184
|
+
should point towards your shadow-casting directional light, in the magazine's own
|
|
185
|
+
frame (only the direction counts). It places the shadow a turning sheet casts right by
|
|
186
|
+
the spine, where the shadow map can't.
|
|
187
|
+
- **Rendering on demand.** If you only draw when something changes, listen for `change`:
|
|
188
|
+
it fires when input, loading or a method call needs a frame. Then keep drawing while
|
|
189
|
+
`tick()` returns true.
|
|
190
|
+
- **Accessibility is yours.** With no DOM of its own, it has no live region, no page
|
|
191
|
+
descriptions and no plain view: pages' `alt` and `text` aren't read. Describe the pages
|
|
192
|
+
in your own page, and announce `pagechange` if readers should hear it.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
magazine.tick(now) // advance; true while the pages still move
|
|
196
|
+
magazine.covers(e.clientX, e.clientY) // whether the magazine takes a press there
|
|
197
|
+
magazine.bounds // { pages, reach }, in the magazine's own units
|
|
198
|
+
magazine.camera = otherCamera // cast input from another camera
|
|
199
|
+
magazine.enabled = false // cancel active gestures and stop taking pointer/key input
|
|
200
|
+
magazine.addEventListener('change', () => requestRender())
|
|
201
|
+
magazine.addEventListener('pagechange', (e) => e.detail) // also ready, pageload, error
|
|
202
|
+
magazine.dispose() // free everything and leave the scene
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The [three.js demo](https://dmtrbrl.github.io/leafthrough/three.html) takes a magazine
|
|
206
|
+
out of a portrait paperboard mailer on a table; its source is
|
|
207
|
+
[`demo/main-three.ts`](https://github.com/dmtrbrl/leafthrough/blob/main/demo/main-three.ts),
|
|
208
|
+
with the native three.js mailer geometry in
|
|
209
|
+
[`demo/envelope.ts`](https://github.com/dmtrbrl/leafthrough/blob/main/demo/envelope.ts).
|
|
210
|
+
|
|
139
211
|
## Sizing
|
|
140
212
|
|
|
141
213
|
The magazine fills the element you give it and keeps its proportions, centred.
|
|
@@ -188,6 +260,7 @@ The magazine fills the element you give it and keeps its proportions, centred.
|
|
|
188
260
|
| `backColor` | `#fcfcf9` | Unprinted paper: the blank back of single-layout pages, and transparent parts of page images. |
|
|
189
261
|
| `wrinkles` | `1` | Waviness of resting sheets; `0` lays them flat. |
|
|
190
262
|
| `bulge` | `1` | How much pages rise off the spine. |
|
|
263
|
+
| `cutout` | `false` | Cuts each sheet to the shape of its front page: transparent areas are no paper at all, shadow included. |
|
|
191
264
|
|
|
192
265
|
**`lighting`**: `ambient: { sky, ground, intensity }`, `sun: { color, intensity, position }`,
|
|
193
266
|
`shadows` (default `true`), `shadowMapSize` (default `2048`), and `floorShadow`. That last
|
|
@@ -264,7 +337,8 @@ pages: [
|
|
|
264
337
|
readers get both, and changing them doesn't reload anything.
|
|
265
338
|
- **Transparent artwork.** PNG and WebP alpha is composited onto the paper: transparent
|
|
266
339
|
areas are unprinted paper in `paper.backColor`, including when the reverse side shows
|
|
267
|
-
through.
|
|
340
|
+
through. With `paper.cutout`, they're cut away instead, so a sheet can be any shape: its
|
|
341
|
+
front page's outline (draw the back page as its mirror image).
|
|
268
342
|
- **Cross-origin URLs.** URLs on another origin need CORS headers, because the images are
|
|
269
343
|
uploaded to WebGL.
|
|
270
344
|
- **Textures.** A three.js `Texture` you pass in is never disposed, but the magazine sets its
|
|
@@ -292,7 +366,8 @@ off screen or the tab is hidden.
|
|
|
292
366
|
The 3D canvas is hidden from screen readers, so give pages an `alt` and their `text` (see
|
|
293
367
|
[Pages](#pages)). The magazine keeps the visible pages' descriptions and text in the page,
|
|
294
368
|
visually hidden, where screen readers can read them; each page is a `figure` named by
|
|
295
|
-
`formatPageLabel`.
|
|
369
|
+
`formatPageLabel`. `LeafThroughObject` has no DOM, so none of this comes with it (see
|
|
370
|
+
[In your own three.js scene](#in-your-own-threejs-scene)).
|
|
296
371
|
|
|
297
372
|
**Plain view.** `plain: true` shows the pages as flat images, with the same layouts,
|
|
298
373
|
navigation, keyboard and events, and no 3D. Clicking the right half turns forward, the left
|
|
@@ -303,7 +378,7 @@ magazine also switches to it by itself when the browser can't run WebGL.
|
|
|
303
378
|
|
|
304
379
|
```bash
|
|
305
380
|
npm install
|
|
306
|
-
npm run dev # demo playground: a page per binding, each with the dev panel
|
|
381
|
+
npm run dev # demo playground: a page per binding, each with the dev panel, and a three.js scene
|
|
307
382
|
npm run typecheck
|
|
308
383
|
npm test # browser tests in headless Chromium (first run: npx playwright install chromium)
|
|
309
384
|
npm run build # dist/: ESM bundles + type declarations
|
|
@@ -329,7 +404,12 @@ How it works:
|
|
|
329
404
|
- `textures.ts` loads, prioritises and unloads page textures.
|
|
330
405
|
- `src/paper` holds the shared paper material and the procedural grain.
|
|
331
406
|
- `LeafThrough.ts` handles the DOM, sizing, loading, layout switching, keyboard and
|
|
332
|
-
events.
|
|
407
|
+
events. `LeafThroughObject.ts` does the same inside someone else's scene, without the DOM.
|
|
408
|
+
- Engines take input through `EngineInput` (`engine.ts`): an element to listen on, a ray
|
|
409
|
+
for a point on screen, and whether a press lands on the pages. They meet the ray with
|
|
410
|
+
their own floor plane, so the same code serves the magazine's own camera and any scene's.
|
|
411
|
+
- `navigation.ts` maps pages to engine positions, riffles an animated `goTo()` and binds
|
|
412
|
+
the keyboard, for both.
|
|
333
413
|
|
|
334
414
|
## Credits
|
|
335
415
|
|
|
@@ -338,4 +418,4 @@ The page deformation and interaction model are reverse-engineered from the magaz
|
|
|
338
418
|
|
|
339
419
|
## License
|
|
340
420
|
|
|
341
|
-
[MIT](LICENSE)
|
|
421
|
+
[MIT](https://github.com/dmtrbrl/leafthrough/blob/main/LICENSE)
|
package/dist/LeafThrough.d.ts
CHANGED
|
@@ -36,9 +36,7 @@ export declare class LeafThrough extends EventTarget {
|
|
|
36
36
|
private readonly content;
|
|
37
37
|
private readonly renderer;
|
|
38
38
|
private frameRequest;
|
|
39
|
-
private readonly
|
|
40
|
-
private readonly backBlank;
|
|
41
|
-
private pattern;
|
|
39
|
+
private readonly paper;
|
|
42
40
|
private loader;
|
|
43
41
|
private view;
|
|
44
42
|
private spreadMedia;
|
|
@@ -52,7 +50,7 @@ export declare class LeafThrough extends EventTarget {
|
|
|
52
50
|
private visible;
|
|
53
51
|
private needsRender;
|
|
54
52
|
private running;
|
|
55
|
-
private riffle;
|
|
53
|
+
private readonly riffle;
|
|
56
54
|
private lastPagesKey;
|
|
57
55
|
private ready;
|
|
58
56
|
private disposed;
|
|
@@ -93,10 +91,8 @@ export declare class LeafThrough extends EventTarget {
|
|
|
93
91
|
private remount;
|
|
94
92
|
private createPlain;
|
|
95
93
|
private createScene;
|
|
96
|
-
private patternFor;
|
|
97
94
|
private layoutBox;
|
|
98
95
|
private pagesKey;
|
|
99
|
-
private preloadRadius;
|
|
100
96
|
private loadPages;
|
|
101
97
|
private inView;
|
|
102
98
|
private followReader;
|
|
@@ -104,7 +100,6 @@ export declare class LeafThrough extends EventTarget {
|
|
|
104
100
|
private pageLoaded;
|
|
105
101
|
private fail;
|
|
106
102
|
private pageToPosition;
|
|
107
|
-
private positionToPage;
|
|
108
103
|
private pagesAt;
|
|
109
104
|
private detail;
|
|
110
105
|
private lastDetailKey;
|
|
@@ -112,8 +107,6 @@ export declare class LeafThrough extends EventTarget {
|
|
|
112
107
|
private syncContent;
|
|
113
108
|
private applyLabel;
|
|
114
109
|
private bindKeyboard;
|
|
115
|
-
private stopRiffle;
|
|
116
|
-
private stepRiffle;
|
|
117
110
|
private syncLoop;
|
|
118
111
|
private setLoop;
|
|
119
112
|
private placeHitArea;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { type Rect } from './framing.js';
|
|
3
|
+
import type { GoToOptions } from './LeafThrough.js';
|
|
4
|
+
import { type LeafThroughOptions, type Layout, type PageChangeDetail } from './options.js';
|
|
5
|
+
/**
|
|
6
|
+
* The magazine's options, less those about its own canvas and DOM. Of `lighting`, only
|
|
7
|
+
* `shadows` (whether sheets cast and receive them) and `sun.position` apply: the lights are
|
|
8
|
+
* your scene's. Point `sun.position` towards your shadow-casting directional light, in the
|
|
9
|
+
* magazine's own frame; it places the shadow a turning sheet casts right by the spine.
|
|
10
|
+
*/
|
|
11
|
+
export type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'>;
|
|
12
|
+
/** What the magazine draws with, from your scene. */
|
|
13
|
+
export interface LeafThroughObjectContext {
|
|
14
|
+
/** The renderer drawing your scene. Pages load into it. */
|
|
15
|
+
renderer: THREE.WebGLRenderer;
|
|
16
|
+
/** The camera your scene is drawn with. Pointer input is cast from it; change it later through `camera`. */
|
|
17
|
+
camera: THREE.Camera;
|
|
18
|
+
/**
|
|
19
|
+
* Takes pointer input, and keys with `interaction.keyboard: true` while it has focus. It must
|
|
20
|
+
* show the camera's whole view. Default `renderer.domElement`.
|
|
21
|
+
*/
|
|
22
|
+
domElement?: HTMLElement;
|
|
23
|
+
}
|
|
24
|
+
export interface LeafThroughObjectEventMap extends THREE.Object3DEventMap {
|
|
25
|
+
pagechange: {
|
|
26
|
+
detail: PageChangeDetail;
|
|
27
|
+
};
|
|
28
|
+
ready: {};
|
|
29
|
+
pageload: {
|
|
30
|
+
detail: {
|
|
31
|
+
page: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
error: {
|
|
35
|
+
detail: {
|
|
36
|
+
error: unknown;
|
|
37
|
+
page?: number;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** Something changed between frames: draw one, and keep drawing while `tick()` returns true. */
|
|
41
|
+
change: {};
|
|
42
|
+
}
|
|
43
|
+
/** Where the magazine lies in its own units. */
|
|
44
|
+
export interface LeafThroughBounds {
|
|
45
|
+
/** The pages at rest: what to frame. */
|
|
46
|
+
pages: Rect;
|
|
47
|
+
/** Everything a turning page can sweep through. */
|
|
48
|
+
reach: Rect;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A 3D magazine to add to your own three.js scene. It draws nothing itself: call `tick()`
|
|
52
|
+
* every frame you render, and render again whenever it fires `change`.
|
|
53
|
+
*
|
|
54
|
+
* In its own units a page is 1 wide and `pageAspect` tall, the spine runs up the y axis at
|
|
55
|
+
* x = 0, and the pages lie on the z = 0 plane facing +z. Move, turn and scale it like any
|
|
56
|
+
* object. Pointer input is cast through `camera` onto the pages, wherever they are.
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* const magazine = new LeafThroughObject({ renderer, camera }, { pages, layout: 'spread' })
|
|
60
|
+
* scene.add(magazine)
|
|
61
|
+
* renderer.setAnimationLoop((now) => {
|
|
62
|
+
* magazine.tick(now)
|
|
63
|
+
* renderer.render(scene, camera)
|
|
64
|
+
* })
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export declare class LeafThroughObject extends THREE.Group<LeafThroughObjectEventMap> {
|
|
68
|
+
/** The camera your scene is drawn with; pointer input is cast from it. */
|
|
69
|
+
camera: THREE.Camera;
|
|
70
|
+
readonly domElement: HTMLElement;
|
|
71
|
+
private inputEnabled;
|
|
72
|
+
/**
|
|
73
|
+
* Whether it takes pointer and key input. Turn it off while it's tucked away or moving,
|
|
74
|
+
* and on again; the pages stay as they are. Turning it off lets go of a page being held.
|
|
75
|
+
* Default `true`.
|
|
76
|
+
*/
|
|
77
|
+
get enabled(): boolean;
|
|
78
|
+
set enabled(value: boolean);
|
|
79
|
+
private readonly renderer;
|
|
80
|
+
private options;
|
|
81
|
+
private readonly paper;
|
|
82
|
+
private loader;
|
|
83
|
+
private view;
|
|
84
|
+
private spreadMedia;
|
|
85
|
+
private readonly motionMedia;
|
|
86
|
+
private readonly abort;
|
|
87
|
+
private readonly riffle;
|
|
88
|
+
private readonly metrics;
|
|
89
|
+
private readonly ray;
|
|
90
|
+
private readonly point;
|
|
91
|
+
private readonly ends;
|
|
92
|
+
private needsRender;
|
|
93
|
+
private lastPagesKey;
|
|
94
|
+
private lastDetailKey;
|
|
95
|
+
private inView;
|
|
96
|
+
private ready;
|
|
97
|
+
private readyFired;
|
|
98
|
+
private disposed;
|
|
99
|
+
constructor({ renderer, camera, domElement }: LeafThroughObjectContext, options: LeafThroughObjectOptions);
|
|
100
|
+
/** First visible page, 0-based. */
|
|
101
|
+
get page(): number;
|
|
102
|
+
get pageCount(): number;
|
|
103
|
+
get layout(): Layout;
|
|
104
|
+
get visiblePages(): number[];
|
|
105
|
+
/** True while a page is being dragged or is completing its turn. */
|
|
106
|
+
get turning(): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Where the magazine lies in its own units: frame `pages` with your camera. Changes with
|
|
109
|
+
* the layout, which `'auto'` can switch as the window resizes.
|
|
110
|
+
*/
|
|
111
|
+
get bounds(): LeafThroughBounds;
|
|
112
|
+
/** Turns to the next page. Returns false at the end. */
|
|
113
|
+
next(): boolean;
|
|
114
|
+
/** Turns to the previous page. Returns false at the start. */
|
|
115
|
+
prev(): boolean;
|
|
116
|
+
/** Shows `page` (0-based), jumping there or turning the pages one by one. */
|
|
117
|
+
goTo(page: number, { animate }?: GoToOptions): void;
|
|
118
|
+
/** Loads the pages that failed again. Also happens by itself when the browser comes back online. */
|
|
119
|
+
retry(): void;
|
|
120
|
+
/** Changes some options and keeps the rest. Structural changes rebuild the pages and keep the current page. */
|
|
121
|
+
update(options: Partial<LeafThroughObjectOptions>): void;
|
|
122
|
+
/** Replaces every option at once: anything left out goes back to its default. */
|
|
123
|
+
setOptions(options: LeafThroughObjectOptions): void;
|
|
124
|
+
/**
|
|
125
|
+
* Whether a press at this point on screen lands on the magazine, which then takes it. Check
|
|
126
|
+
* it in your own pointer handlers to leave such presses alone. False while it's hidden or
|
|
127
|
+
* not `enabled`.
|
|
128
|
+
*/
|
|
129
|
+
covers(clientX: number, clientY: number): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Advances the pages to `now` (a `requestAnimationFrame` timestamp). Call it every frame
|
|
132
|
+
* before rendering. Returns true while the pages are moving and need another frame.
|
|
133
|
+
*/
|
|
134
|
+
tick(now?: number): boolean;
|
|
135
|
+
/** Frees the pages, textures and listeners, and takes the magazine out of its parent. */
|
|
136
|
+
dispose(): void;
|
|
137
|
+
private applyOptions;
|
|
138
|
+
private pickLayout;
|
|
139
|
+
private watchSpreadQuery;
|
|
140
|
+
private remount;
|
|
141
|
+
private readonly axis;
|
|
142
|
+
private pageAxis;
|
|
143
|
+
private dragWidth;
|
|
144
|
+
private shown;
|
|
145
|
+
private requestFrame;
|
|
146
|
+
private pagesKey;
|
|
147
|
+
private loadPages;
|
|
148
|
+
private followReader;
|
|
149
|
+
private fail;
|
|
150
|
+
private detail;
|
|
151
|
+
private emitPageChange;
|
|
152
|
+
}
|