leafthrough 0.1.3 → 0.1.4
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 +69 -4
- package/dist/LeafThrough.d.ts +2 -9
- package/dist/LeafThroughObject.d.ts +151 -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,7 +2,8 @@
|
|
|
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
9
|
for every option, method and event.
|
|
@@ -136,6 +137,63 @@ error trackers listen. To register the element under another tag, call
|
|
|
136
137
|
|
|
137
138
|
**Solid**: `<leaf-through prop:pages={pages()} on:pagechange={(e) => setPage(e.detail.page)} />`.
|
|
138
139
|
|
|
140
|
+
## In your own three.js scene
|
|
141
|
+
|
|
142
|
+
`LeafThroughObject` is the magazine as a `THREE.Group`, for a scene you already have: a
|
|
143
|
+
booklet in a CD case, a magazine on a desk. It draws with your renderer and takes pointer
|
|
144
|
+
input through your camera, so it turns wherever it lies, however it's moved, turned or
|
|
145
|
+
scaled. Every option, method and event of `LeafThrough` works the same, apart from those
|
|
146
|
+
about its own canvas and DOM (`bleed`, `maxPixelRatio`, `plain`, `label`,
|
|
147
|
+
`formatPageLabel`).
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { LeafThroughObject } from 'leafthrough'
|
|
151
|
+
|
|
152
|
+
const magazine = new LeafThroughObject({ renderer, camera }, { pages, layout: 'spread', pageAspect: 1 })
|
|
153
|
+
magazine.rotation.x = -Math.PI / 2 // lay it on the table
|
|
154
|
+
magazine.scale.setScalar(0.12) // a page 12 cm wide, in a scene measured in metres
|
|
155
|
+
scene.add(magazine)
|
|
156
|
+
|
|
157
|
+
renderer.setAnimationLoop((now) => {
|
|
158
|
+
magazine.tick(now) // advance the pages, then draw as usual
|
|
159
|
+
renderer.render(scene, camera)
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Units.** A page is 1 wide and `pageAspect` tall. The spine runs up the y axis at
|
|
164
|
+
x = 0 and the pages lie on the z = 0 plane, facing +z. A closed spread shows its cover
|
|
165
|
+
at x 0 to 1. `magazine.bounds.pages` is the pages at rest, the rectangle to frame with
|
|
166
|
+
your camera; `magazine.bounds.reach` is everything a turning page sweeps through.
|
|
167
|
+
- **Input.** The magazine listens for pointer events on `renderer.domElement` (or the
|
|
168
|
+
`domElement` you pass). A press it takes is one on its pages, which you can test with
|
|
169
|
+
`magazine.covers(clientX, clientY)`; check that in your own handlers to leave such
|
|
170
|
+
presses alone. Hide the magazine (`visible = false`, on it or a parent) and it takes
|
|
171
|
+
none. With `interaction.keyboard: true` the keys work while that element has focus; a
|
|
172
|
+
canvas usually doesn't take focus, so use `'global'`.
|
|
173
|
+
- **Light.** The paper is a `MeshStandardMaterial`, lit by your scene's lights,
|
|
174
|
+
environment, fog and tone mapping. Of the `lighting` options, `sun.position` (in the
|
|
175
|
+
magazine's own units) steers the paper's shading and `shadows` makes the sheets cast
|
|
176
|
+
and receive shadows when your renderer draws them.
|
|
177
|
+
- **Rendering on demand.** If you only draw when something changes, listen for `change`:
|
|
178
|
+
it fires when input, loading or a method call needs a frame. Then keep drawing while
|
|
179
|
+
`tick()` returns true.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
magazine.tick(now) // advance; true while the pages still move
|
|
183
|
+
magazine.covers(e.clientX, e.clientY) // whether a press there lands on the pages
|
|
184
|
+
magazine.bounds // { pages, reach }, in the magazine's own units
|
|
185
|
+
magazine.camera = otherCamera // cast input from another camera
|
|
186
|
+
magazine.enabled = false // cancel active gestures and stop taking pointer/key input
|
|
187
|
+
magazine.addEventListener('change', () => requestRender())
|
|
188
|
+
magazine.addEventListener('pagechange', (e) => e.detail) // also ready, pageload, error
|
|
189
|
+
magazine.dispose() // free everything and leave the scene
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The [three.js demo](https://dmtrbrl.github.io/leafthrough/three.html) takes a magazine
|
|
193
|
+
out of a portrait paperboard mailer on a table; its source is
|
|
194
|
+
[`demo/main-three.ts`](demo/main-three.ts), with the native three.js mailer geometry in
|
|
195
|
+
[`demo/envelope.ts`](demo/envelope.ts).
|
|
196
|
+
|
|
139
197
|
## Sizing
|
|
140
198
|
|
|
141
199
|
The magazine fills the element you give it and keeps its proportions, centred.
|
|
@@ -188,6 +246,7 @@ The magazine fills the element you give it and keeps its proportions, centred.
|
|
|
188
246
|
| `backColor` | `#fcfcf9` | Unprinted paper: the blank back of single-layout pages, and transparent parts of page images. |
|
|
189
247
|
| `wrinkles` | `1` | Waviness of resting sheets; `0` lays them flat. |
|
|
190
248
|
| `bulge` | `1` | How much pages rise off the spine. |
|
|
249
|
+
| `cutout` | `false` | Cuts each sheet to the shape of its front page: transparent areas are no paper at all, shadow included. |
|
|
191
250
|
|
|
192
251
|
**`lighting`**: `ambient: { sky, ground, intensity }`, `sun: { color, intensity, position }`,
|
|
193
252
|
`shadows` (default `true`), `shadowMapSize` (default `2048`), and `floorShadow`. That last
|
|
@@ -264,7 +323,8 @@ pages: [
|
|
|
264
323
|
readers get both, and changing them doesn't reload anything.
|
|
265
324
|
- **Transparent artwork.** PNG and WebP alpha is composited onto the paper: transparent
|
|
266
325
|
areas are unprinted paper in `paper.backColor`, including when the reverse side shows
|
|
267
|
-
through.
|
|
326
|
+
through. With `paper.cutout`, they're cut away instead, so a sheet can be any shape: its
|
|
327
|
+
front page's outline (draw the back page as its mirror image).
|
|
268
328
|
- **Cross-origin URLs.** URLs on another origin need CORS headers, because the images are
|
|
269
329
|
uploaded to WebGL.
|
|
270
330
|
- **Textures.** A three.js `Texture` you pass in is never disposed, but the magazine sets its
|
|
@@ -303,7 +363,7 @@ magazine also switches to it by itself when the browser can't run WebGL.
|
|
|
303
363
|
|
|
304
364
|
```bash
|
|
305
365
|
npm install
|
|
306
|
-
npm run dev # demo playground: a page per binding, each with the dev panel
|
|
366
|
+
npm run dev # demo playground: a page per binding, each with the dev panel, and a three.js scene
|
|
307
367
|
npm run typecheck
|
|
308
368
|
npm test # browser tests in headless Chromium (first run: npx playwright install chromium)
|
|
309
369
|
npm run build # dist/: ESM bundles + type declarations
|
|
@@ -329,7 +389,12 @@ How it works:
|
|
|
329
389
|
- `textures.ts` loads, prioritises and unloads page textures.
|
|
330
390
|
- `src/paper` holds the shared paper material and the procedural grain.
|
|
331
391
|
- `LeafThrough.ts` handles the DOM, sizing, loading, layout switching, keyboard and
|
|
332
|
-
events.
|
|
392
|
+
events. `LeafThroughObject.ts` does the same inside someone else's scene, without the DOM.
|
|
393
|
+
- Engines take input through `EngineInput` (`engine.ts`): an element to listen on, a ray
|
|
394
|
+
for a point on screen, and whether a press lands on the pages. They meet the ray with
|
|
395
|
+
their own floor plane, so the same code serves the magazine's own camera and any scene's.
|
|
396
|
+
- `navigation.ts` maps pages to engine positions, riffles an animated `goTo()` and binds
|
|
397
|
+
the keyboard, for both.
|
|
333
398
|
|
|
334
399
|
## Credits
|
|
335
400
|
|
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,151 @@
|
|
|
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
|
+
* `sun.position` (which way the paper is shaded) and `shadows` (whether sheets cast and
|
|
8
|
+
* receive them) apply: the lights are your scene's.
|
|
9
|
+
*/
|
|
10
|
+
export type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'>;
|
|
11
|
+
/** What the magazine draws with, from your scene. */
|
|
12
|
+
export interface LeafThroughObjectContext {
|
|
13
|
+
/** The renderer drawing your scene. Pages load into it. */
|
|
14
|
+
renderer: THREE.WebGLRenderer;
|
|
15
|
+
/** The camera your scene is drawn with. Pointer input is cast from it; change it later through `camera`. */
|
|
16
|
+
camera: THREE.Camera;
|
|
17
|
+
/**
|
|
18
|
+
* Takes pointer input, and keys with `interaction.keyboard: true` while it has focus. It must
|
|
19
|
+
* show the camera's whole view. Default `renderer.domElement`.
|
|
20
|
+
*/
|
|
21
|
+
domElement?: HTMLElement;
|
|
22
|
+
}
|
|
23
|
+
export interface LeafThroughObjectEventMap extends THREE.Object3DEventMap {
|
|
24
|
+
pagechange: {
|
|
25
|
+
detail: PageChangeDetail;
|
|
26
|
+
};
|
|
27
|
+
ready: {};
|
|
28
|
+
pageload: {
|
|
29
|
+
detail: {
|
|
30
|
+
page: number;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
error: {
|
|
34
|
+
detail: {
|
|
35
|
+
error: unknown;
|
|
36
|
+
page?: number;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** Something changed between frames: draw one, and keep drawing while `tick()` returns true. */
|
|
40
|
+
change: {};
|
|
41
|
+
}
|
|
42
|
+
/** Where the magazine lies in its own units. */
|
|
43
|
+
export interface LeafThroughBounds {
|
|
44
|
+
/** The pages at rest: what to frame. */
|
|
45
|
+
pages: Rect;
|
|
46
|
+
/** Everything a turning page can sweep through. */
|
|
47
|
+
reach: Rect;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A 3D magazine to add to your own three.js scene. It draws nothing itself: call `tick()`
|
|
51
|
+
* every frame you render, and render again whenever it fires `change`.
|
|
52
|
+
*
|
|
53
|
+
* In its own units a page is 1 wide and `pageAspect` tall, the spine runs up the y axis at
|
|
54
|
+
* x = 0, and the pages lie on the z = 0 plane facing +z. Move, turn and scale it like any
|
|
55
|
+
* object. Pointer input is cast through `camera` onto the pages, wherever they are.
|
|
56
|
+
*
|
|
57
|
+
* ```ts
|
|
58
|
+
* const magazine = new LeafThroughObject({ renderer, camera }, { pages, layout: 'spread' })
|
|
59
|
+
* scene.add(magazine)
|
|
60
|
+
* renderer.setAnimationLoop((now) => {
|
|
61
|
+
* magazine.tick(now)
|
|
62
|
+
* renderer.render(scene, camera)
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare class LeafThroughObject extends THREE.Group<LeafThroughObjectEventMap> {
|
|
67
|
+
/** The camera your scene is drawn with; pointer input is cast from it. */
|
|
68
|
+
camera: THREE.Camera;
|
|
69
|
+
readonly domElement: HTMLElement;
|
|
70
|
+
private inputEnabled;
|
|
71
|
+
/**
|
|
72
|
+
* Whether it takes pointer and key input. Turn it off while it's tucked away or moving,
|
|
73
|
+
* and on again; the pages stay as they are. Turning it off lets go of a page being held.
|
|
74
|
+
* Default `true`.
|
|
75
|
+
*/
|
|
76
|
+
get enabled(): boolean;
|
|
77
|
+
set enabled(value: boolean);
|
|
78
|
+
private readonly renderer;
|
|
79
|
+
private options;
|
|
80
|
+
private readonly paper;
|
|
81
|
+
private loader;
|
|
82
|
+
private view;
|
|
83
|
+
private spreadMedia;
|
|
84
|
+
private readonly motionMedia;
|
|
85
|
+
private readonly abort;
|
|
86
|
+
private readonly riffle;
|
|
87
|
+
private readonly metrics;
|
|
88
|
+
private readonly ray;
|
|
89
|
+
private readonly point;
|
|
90
|
+
private readonly ends;
|
|
91
|
+
private needsRender;
|
|
92
|
+
private lastPagesKey;
|
|
93
|
+
private lastDetailKey;
|
|
94
|
+
private inView;
|
|
95
|
+
private ready;
|
|
96
|
+
private readyFired;
|
|
97
|
+
private disposed;
|
|
98
|
+
constructor({ renderer, camera, domElement }: LeafThroughObjectContext, options: LeafThroughObjectOptions);
|
|
99
|
+
/** First visible page, 0-based. */
|
|
100
|
+
get page(): number;
|
|
101
|
+
get pageCount(): number;
|
|
102
|
+
get layout(): Layout;
|
|
103
|
+
get visiblePages(): number[];
|
|
104
|
+
/** True while a page is being dragged or is completing its turn. */
|
|
105
|
+
get turning(): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Where the magazine lies in its own units: frame `pages` with your camera. Changes with
|
|
108
|
+
* the layout, which `'auto'` can switch as the window resizes.
|
|
109
|
+
*/
|
|
110
|
+
get bounds(): LeafThroughBounds;
|
|
111
|
+
/** Turns to the next page. Returns false at the end. */
|
|
112
|
+
next(): boolean;
|
|
113
|
+
/** Turns to the previous page. Returns false at the start. */
|
|
114
|
+
prev(): boolean;
|
|
115
|
+
/** Shows `page` (0-based), jumping there or turning the pages one by one. */
|
|
116
|
+
goTo(page: number, { animate }?: GoToOptions): void;
|
|
117
|
+
/** Loads the pages that failed again. Also happens by itself when the browser comes back online. */
|
|
118
|
+
retry(): void;
|
|
119
|
+
/** Changes some options and keeps the rest. Structural changes rebuild the pages and keep the current page. */
|
|
120
|
+
update(options: Partial<LeafThroughObjectOptions>): void;
|
|
121
|
+
/** Replaces every option at once: anything left out goes back to its default. */
|
|
122
|
+
setOptions(options: LeafThroughObjectOptions): void;
|
|
123
|
+
/**
|
|
124
|
+
* Whether a press at this point on screen lands on the magazine, which then takes it. Check
|
|
125
|
+
* it in your own pointer handlers to leave such presses alone. False while it's hidden or
|
|
126
|
+
* not `enabled`.
|
|
127
|
+
*/
|
|
128
|
+
covers(clientX: number, clientY: number): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Advances the pages to `now` (a `requestAnimationFrame` timestamp). Call it every frame
|
|
131
|
+
* before rendering. Returns true while the pages are moving and need another frame.
|
|
132
|
+
*/
|
|
133
|
+
tick(now?: number): boolean;
|
|
134
|
+
/** Frees the pages, textures and listeners, and takes the magazine out of its parent. */
|
|
135
|
+
dispose(): void;
|
|
136
|
+
private applyOptions;
|
|
137
|
+
private pickLayout;
|
|
138
|
+
private watchSpreadQuery;
|
|
139
|
+
private remount;
|
|
140
|
+
private readonly axis;
|
|
141
|
+
private pageAxis;
|
|
142
|
+
private dragWidth;
|
|
143
|
+
private shown;
|
|
144
|
+
private requestFrame;
|
|
145
|
+
private pagesKey;
|
|
146
|
+
private loadPages;
|
|
147
|
+
private followReader;
|
|
148
|
+
private fail;
|
|
149
|
+
private detail;
|
|
150
|
+
private emitPageChange;
|
|
151
|
+
}
|