leafthrough 0.1.2 → 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 +70 -5
- package/dist/LeafThrough.d.ts +2 -9
- package/dist/LeafThroughObject.d.ts +151 -0
- package/dist/{chunk-GJRP6UAH.js → chunk-GYSQO7QZ.js} +761 -622
- 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 +17 -2
- 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-GJRP6UAH.js.map +0 -1
package/dist/engine.d.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as THREE from 'three';
|
|
2
2
|
import type { ResolvedSettings } from './options.js';
|
|
3
3
|
import type { SheetShading } from './paper/paperMaterial.js';
|
|
4
4
|
export interface EngineContext {
|
|
5
|
-
|
|
6
|
-
canvas: HTMLCanvasElement;
|
|
7
|
-
/** Receives pointer input; covers the pages. */
|
|
8
|
-
hitArea: HTMLElement;
|
|
5
|
+
input: EngineInput;
|
|
9
6
|
settings: ResolvedSettings;
|
|
10
7
|
/** One texture per page, in reading order. */
|
|
11
8
|
textures: THREE.Texture[];
|
|
@@ -18,10 +15,23 @@ export interface EngineContext {
|
|
|
18
15
|
metrics: EngineMetrics;
|
|
19
16
|
/** Shared by every sheet; the engine keeps spineContact current. */
|
|
20
17
|
shading: SheetShading;
|
|
18
|
+
/** Asks for a frame: input and timers change the pages between frames. */
|
|
19
|
+
requestFrame: () => void;
|
|
20
|
+
}
|
|
21
|
+
/** Where pointer input comes from, and how a point on screen reaches the magazine. */
|
|
22
|
+
export interface EngineInput {
|
|
23
|
+
/** Receives pointer input. */
|
|
24
|
+
element: HTMLElement;
|
|
25
|
+
/** Sets `ray` to the world-space ray through a point on screen; false when there's none. */
|
|
26
|
+
ray(clientX: number, clientY: number, ray: THREE.Ray): boolean;
|
|
27
|
+
/** Whether a press at this point lands on the magazine. */
|
|
28
|
+
covers(clientX: number, clientY: number): boolean;
|
|
21
29
|
}
|
|
22
30
|
export interface EngineMetrics {
|
|
23
31
|
/** Pixels a drag spans per unit of `dragDistance`. */
|
|
24
|
-
dragWidth: number;
|
|
32
|
+
readonly dragWidth: number;
|
|
33
|
+
/** Screen displacement along the magazine’s projected x axis; defaults to horizontal. */
|
|
34
|
+
dragDelta?(dx: number, dy: number): number;
|
|
25
35
|
}
|
|
26
36
|
export interface Engine {
|
|
27
37
|
readonly group: THREE.Group;
|
|
@@ -47,5 +57,14 @@ export interface Engine {
|
|
|
47
57
|
/** Moves to a position at once, without animating. */
|
|
48
58
|
jumpTo(position: number): void;
|
|
49
59
|
setPageTexture(page: number, texture: THREE.Texture): void;
|
|
60
|
+
/** Releases pointer capture and cancels active gestures and hold timers. */
|
|
61
|
+
cancelInput?(): void;
|
|
50
62
|
dispose(): void;
|
|
51
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Where a world-space ray meets the floor of `object` (its z = 0 plane), in the object's own
|
|
66
|
+
* units; null when it misses. However the object is moved, turned or scaled.
|
|
67
|
+
*/
|
|
68
|
+
export declare function floorPoint(object: THREE.Object3D, ray: THREE.Ray, target: THREE.Vector3): THREE.Vector3 | null;
|
|
69
|
+
/** Sets `ray` to the ray from `camera` through a point on screen over `element`, which shows its view. */
|
|
70
|
+
export declare function rayThrough(camera: THREE.Camera, element: Element, clientX: number, clientY: number, ray: THREE.Ray): boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { LeafThrough, type LeafThroughEventMap, type GoToOptions } from './LeafThrough.js';
|
|
2
|
+
export { LeafThroughObject, type LeafThroughBounds, type LeafThroughObjectContext, type LeafThroughObjectEventMap, type LeafThroughObjectOptions, } from './LeafThroughObject.js';
|
|
3
|
+
export type { Rect } from './framing.js';
|
|
2
4
|
export { DEFAULT_PAGE_ASPECT, DEFAULT_SPREAD_QUERY, defaultPageLabel, type LeafThroughOptions, type InteractionOptions, type Layout, type LightingOptions, type MotionOptions, type PageChangeDetail, type PageDescriptor, type PageImage, type PageSource, type PaperOptions, } from './options.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,378 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_PAGE_ASPECT,
|
|
3
3
|
DEFAULT_SPREAD_QUERY,
|
|
4
|
+
LIVE_OPTIONS,
|
|
4
5
|
LeafThrough,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
PageLoader,
|
|
7
|
+
PaperTextures,
|
|
8
|
+
Riffle,
|
|
9
|
+
SingleEngine,
|
|
10
|
+
SpreadEngine,
|
|
11
|
+
bindKeyboard,
|
|
12
|
+
changedOptions,
|
|
13
|
+
defaultPageLabel,
|
|
14
|
+
floorPoint,
|
|
15
|
+
framingFor,
|
|
16
|
+
pageDetail,
|
|
17
|
+
pageImage,
|
|
18
|
+
pageToPosition,
|
|
19
|
+
pagesAt,
|
|
20
|
+
plainFramingFor,
|
|
21
|
+
preloadRadius,
|
|
22
|
+
rayThrough,
|
|
23
|
+
resolveSettings,
|
|
24
|
+
snapshot,
|
|
25
|
+
sourceKey
|
|
26
|
+
} from "./chunk-GYSQO7QZ.js";
|
|
27
|
+
|
|
28
|
+
// src/LeafThroughObject.ts
|
|
29
|
+
import * as THREE from "three";
|
|
30
|
+
var isClient = typeof window !== "undefined";
|
|
31
|
+
var LeafThroughObject = class extends THREE.Group {
|
|
32
|
+
/** The camera your scene is drawn with; pointer input is cast from it. */
|
|
33
|
+
camera;
|
|
34
|
+
domElement;
|
|
35
|
+
inputEnabled = true;
|
|
36
|
+
/**
|
|
37
|
+
* Whether it takes pointer and key input. Turn it off while it's tucked away or moving,
|
|
38
|
+
* and on again; the pages stay as they are. Turning it off lets go of a page being held.
|
|
39
|
+
* Default `true`.
|
|
40
|
+
*/
|
|
41
|
+
get enabled() {
|
|
42
|
+
return this.inputEnabled;
|
|
43
|
+
}
|
|
44
|
+
set enabled(value) {
|
|
45
|
+
if (this.inputEnabled && !value) this.view?.engine.cancelInput?.();
|
|
46
|
+
this.inputEnabled = value;
|
|
47
|
+
}
|
|
48
|
+
renderer;
|
|
49
|
+
options;
|
|
50
|
+
paper;
|
|
51
|
+
loader = null;
|
|
52
|
+
view = null;
|
|
53
|
+
spreadMedia = null;
|
|
54
|
+
motionMedia;
|
|
55
|
+
abort = new AbortController();
|
|
56
|
+
riffle = new Riffle();
|
|
57
|
+
metrics;
|
|
58
|
+
ray = new THREE.Ray();
|
|
59
|
+
point = new THREE.Vector3();
|
|
60
|
+
ends = [new THREE.Vector3(), new THREE.Vector3()];
|
|
61
|
+
needsRender = true;
|
|
62
|
+
lastPagesKey = "";
|
|
63
|
+
lastDetailKey = "";
|
|
64
|
+
inView = "";
|
|
65
|
+
ready = false;
|
|
66
|
+
readyFired = false;
|
|
67
|
+
disposed = false;
|
|
68
|
+
constructor({ renderer, camera, domElement = renderer.domElement }, options) {
|
|
69
|
+
super();
|
|
70
|
+
if (!isClient) throw new Error("LeafThrough needs a browser; create it on the client.");
|
|
71
|
+
this.renderer = renderer;
|
|
72
|
+
this.camera = camera;
|
|
73
|
+
this.domElement = domElement;
|
|
74
|
+
this.options = snapshot(options);
|
|
75
|
+
const object = this;
|
|
76
|
+
this.metrics = {
|
|
77
|
+
get dragWidth() {
|
|
78
|
+
return object.dragWidth();
|
|
79
|
+
},
|
|
80
|
+
dragDelta(dx, dy) {
|
|
81
|
+
const axis = object.pageAxis();
|
|
82
|
+
return (dx * axis.x + dy * axis.y) / (axis.length() || 1);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
this.paper = new PaperTextures(options.paper?.backColor ?? "#fcfcf9", {
|
|
86
|
+
onGrain: () => this.remount(),
|
|
87
|
+
onError: (error) => this.fail(error)
|
|
88
|
+
});
|
|
89
|
+
const signal = this.abort.signal;
|
|
90
|
+
this.watchSpreadQuery();
|
|
91
|
+
this.motionMedia = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
92
|
+
this.motionMedia.addEventListener("change", () => this.remount(), { signal });
|
|
93
|
+
window.addEventListener("online", () => this.retry(), { signal });
|
|
94
|
+
bindKeyboard(
|
|
95
|
+
domElement,
|
|
96
|
+
() => this.enabled && this.shown() ? this.view?.settings.interaction.keyboard : false,
|
|
97
|
+
{ next: () => this.next(), prev: () => this.prev(), first: () => this.goTo(0), last: () => this.goTo(this.pageCount - 1) },
|
|
98
|
+
signal
|
|
99
|
+
);
|
|
100
|
+
this.remount(options.startPage ?? 0);
|
|
101
|
+
}
|
|
102
|
+
// --- public API -----------------------------------------------------------------
|
|
103
|
+
/** First visible page, 0-based. */
|
|
104
|
+
get page() {
|
|
105
|
+
return this.detail().page;
|
|
106
|
+
}
|
|
107
|
+
get pageCount() {
|
|
108
|
+
return this.options.pages.length;
|
|
109
|
+
}
|
|
110
|
+
get layout() {
|
|
111
|
+
return this.view?.settings.layout ?? this.pickLayout();
|
|
112
|
+
}
|
|
113
|
+
get visiblePages() {
|
|
114
|
+
return this.detail().visiblePages;
|
|
115
|
+
}
|
|
116
|
+
/** True while a page is being dragged or is completing its turn. */
|
|
117
|
+
get turning() {
|
|
118
|
+
return this.view?.engine.turning ?? false;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Where the magazine lies in its own units: frame `pages` with your camera. Changes with
|
|
122
|
+
* the layout, which `'auto'` can switch as the window resizes.
|
|
123
|
+
*/
|
|
124
|
+
get bounds() {
|
|
125
|
+
const layout = this.layout;
|
|
126
|
+
const aspect = this.view?.settings.pageAspect ?? resolveSettings(this.options, layout, false).pageAspect;
|
|
127
|
+
return { pages: plainFramingFor(layout, aspect).box, reach: framingFor(layout, aspect).view };
|
|
128
|
+
}
|
|
129
|
+
/** Turns to the next page. Returns false at the end. */
|
|
130
|
+
next() {
|
|
131
|
+
this.riffle.stop();
|
|
132
|
+
return this.view?.engine.flip(true) ?? false;
|
|
133
|
+
}
|
|
134
|
+
/** Turns to the previous page. Returns false at the start. */
|
|
135
|
+
prev() {
|
|
136
|
+
this.riffle.stop();
|
|
137
|
+
return this.view?.engine.flip(false) ?? false;
|
|
138
|
+
}
|
|
139
|
+
/** Shows `page` (0-based), jumping there or turning the pages one by one. */
|
|
140
|
+
goTo(page, { animate = false } = {}) {
|
|
141
|
+
if (!this.view || Number.isNaN(page)) return;
|
|
142
|
+
this.riffle.stop();
|
|
143
|
+
const engine = this.view.engine;
|
|
144
|
+
const target = pageToPosition(page, this.view.settings.layout, this.pageCount);
|
|
145
|
+
if (!animate) {
|
|
146
|
+
engine.jumpTo(target);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (engine.position === target) return;
|
|
150
|
+
this.riffle.start(target, this.view.settings.motion.speed);
|
|
151
|
+
this.requestFrame();
|
|
152
|
+
}
|
|
153
|
+
/** Loads the pages that failed again. Also happens by itself when the browser comes back online. */
|
|
154
|
+
retry() {
|
|
155
|
+
this.loader?.retry();
|
|
156
|
+
}
|
|
157
|
+
/** Changes some options and keeps the rest. Structural changes rebuild the pages and keep the current page. */
|
|
158
|
+
update(options) {
|
|
159
|
+
this.applyOptions({ ...this.options, ...options });
|
|
160
|
+
}
|
|
161
|
+
/** Replaces every option at once: anything left out goes back to its default. */
|
|
162
|
+
setOptions(options) {
|
|
163
|
+
this.applyOptions(options);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Whether a press at this point on screen lands on the magazine, which then takes it. Check
|
|
167
|
+
* it in your own pointer handlers to leave such presses alone. False while it's hidden or
|
|
168
|
+
* not `enabled`.
|
|
169
|
+
*/
|
|
170
|
+
covers(clientX, clientY) {
|
|
171
|
+
const view = this.view;
|
|
172
|
+
if (!view || !this.enabled || !this.shown()) return false;
|
|
173
|
+
if (!rayThrough(this.camera, this.domElement, clientX, clientY, this.ray)) return false;
|
|
174
|
+
const point = floorPoint(view.engine.group, this.ray, this.point);
|
|
175
|
+
if (!point) return false;
|
|
176
|
+
const box = view.framing.box;
|
|
177
|
+
const side = view.engine.activeSide;
|
|
178
|
+
const left = side === "right" ? box.x + box.width / 2 : box.x;
|
|
179
|
+
const right = side === "left" ? box.x + box.width / 2 : box.x + box.width;
|
|
180
|
+
return point.x >= left && point.x <= right && point.y >= box.y && point.y <= box.y + box.height;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Advances the pages to `now` (a `requestAnimationFrame` timestamp). Call it every frame
|
|
184
|
+
* before rendering. Returns true while the pages are moving and need another frame.
|
|
185
|
+
*/
|
|
186
|
+
tick(now = performance.now()) {
|
|
187
|
+
const view = this.view;
|
|
188
|
+
if (!view) return false;
|
|
189
|
+
this.riffle.step(now, view.engine);
|
|
190
|
+
const moved = view.engine.update(now);
|
|
191
|
+
this.followReader(view.engine, view.settings.layout);
|
|
192
|
+
const draw = moved || this.needsRender;
|
|
193
|
+
this.needsRender = false;
|
|
194
|
+
if (this.ready && !this.readyFired) {
|
|
195
|
+
this.readyFired = true;
|
|
196
|
+
this.options.onReady?.();
|
|
197
|
+
this.dispatchEvent({ type: "ready" });
|
|
198
|
+
}
|
|
199
|
+
this.emitPageChange();
|
|
200
|
+
return draw || this.riffle.active;
|
|
201
|
+
}
|
|
202
|
+
/** Frees the pages, textures and listeners, and takes the magazine out of its parent. */
|
|
203
|
+
dispose() {
|
|
204
|
+
if (this.disposed) return;
|
|
205
|
+
this.disposed = true;
|
|
206
|
+
this.riffle.stop();
|
|
207
|
+
this.abort.abort();
|
|
208
|
+
this.spreadMedia?.abort.abort();
|
|
209
|
+
this.view?.dispose();
|
|
210
|
+
this.view = null;
|
|
211
|
+
this.loader?.dispose();
|
|
212
|
+
this.paper.dispose();
|
|
213
|
+
this.removeFromParent();
|
|
214
|
+
}
|
|
215
|
+
// --- options and layout -----------------------------------------------------------
|
|
216
|
+
applyOptions(options) {
|
|
217
|
+
if (this.disposed) return;
|
|
218
|
+
const previous = this.options;
|
|
219
|
+
const next = this.options = snapshot(options);
|
|
220
|
+
const changed = changedOptions(previous, next);
|
|
221
|
+
if (!changed.length) return;
|
|
222
|
+
if (changed.includes("spreadQuery")) this.watchSpreadQuery();
|
|
223
|
+
const page = this.page;
|
|
224
|
+
let rebuild = changed.some((key) => !LIVE_OPTIONS.has(key));
|
|
225
|
+
if (changed.includes("pages") && this.pagesKey(next) !== this.lastPagesKey) {
|
|
226
|
+
this.loader?.dispose();
|
|
227
|
+
this.loader = null;
|
|
228
|
+
rebuild = true;
|
|
229
|
+
}
|
|
230
|
+
if (changed.includes("preload")) this.loader?.setRadius(preloadRadius(this.options.preload));
|
|
231
|
+
if (rebuild) this.remount(page);
|
|
232
|
+
}
|
|
233
|
+
pickLayout() {
|
|
234
|
+
const layout = this.options.layout ?? "auto";
|
|
235
|
+
if (layout !== "auto") return layout;
|
|
236
|
+
return this.spreadMedia?.list.matches ? "spread" : "single";
|
|
237
|
+
}
|
|
238
|
+
watchSpreadQuery() {
|
|
239
|
+
this.spreadMedia?.abort.abort();
|
|
240
|
+
const list = window.matchMedia(this.options.spreadQuery ?? DEFAULT_SPREAD_QUERY);
|
|
241
|
+
const abort = new AbortController();
|
|
242
|
+
list.addEventListener("change", () => this.remount(), { signal: abort.signal });
|
|
243
|
+
this.spreadMedia = { list, abort };
|
|
244
|
+
}
|
|
245
|
+
remount(page = this.page) {
|
|
246
|
+
if (this.disposed) return;
|
|
247
|
+
this.riffle.stop();
|
|
248
|
+
this.view?.dispose();
|
|
249
|
+
const layout = this.pickLayout();
|
|
250
|
+
const settings = resolveSettings(this.options, layout, this.motionMedia.matches);
|
|
251
|
+
this.lastPagesKey = this.pagesKey(this.options);
|
|
252
|
+
if (!this.loader) this.loadPages(page, layout);
|
|
253
|
+
this.loader?.setWrap(layout === "single" && settings.loop);
|
|
254
|
+
const domElement = this.domElement;
|
|
255
|
+
const blank = this.paper.blank;
|
|
256
|
+
const ctx = {
|
|
257
|
+
input: {
|
|
258
|
+
element: domElement,
|
|
259
|
+
ray: (x, y, ray) => rayThrough(this.camera, domElement, x, y, ray),
|
|
260
|
+
covers: (x, y) => this.covers(x, y)
|
|
261
|
+
},
|
|
262
|
+
settings,
|
|
263
|
+
textures: this.options.pages.map((_, i) => this.loader?.textures[i] ?? blank),
|
|
264
|
+
blank: layout === "single" ? this.paper.back(settings.paper.backColor) : blank,
|
|
265
|
+
pattern: this.paper.grain(settings, this.renderer),
|
|
266
|
+
position: pageToPosition(page, layout, this.pageCount),
|
|
267
|
+
metrics: this.metrics,
|
|
268
|
+
shading: {
|
|
269
|
+
spineContact: { value: new THREE.Vector3(1, 0, 0) },
|
|
270
|
+
lightDirection: { value: new THREE.Vector3(...settings.lighting.sun.position).normalize() }
|
|
271
|
+
},
|
|
272
|
+
requestFrame: () => this.requestFrame()
|
|
273
|
+
};
|
|
274
|
+
const engine = layout === "spread" ? new SpreadEngine(ctx) : new SingleEngine(ctx);
|
|
275
|
+
this.add(engine.group);
|
|
276
|
+
this.view = {
|
|
277
|
+
engine,
|
|
278
|
+
settings,
|
|
279
|
+
framing: framingFor(layout, settings.pageAspect),
|
|
280
|
+
dispose: () => {
|
|
281
|
+
this.remove(engine.group);
|
|
282
|
+
engine.dispose();
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
this.requestFrame();
|
|
286
|
+
}
|
|
287
|
+
// A drag across this many pixels turns a page all the way: the magazine's drag width, at the
|
|
288
|
+
// size it shows on screen now.
|
|
289
|
+
axis = new THREE.Vector2();
|
|
290
|
+
pageAxis() {
|
|
291
|
+
const group = this.view?.engine.group ?? this;
|
|
292
|
+
group.updateWorldMatrix(true, false);
|
|
293
|
+
this.camera.updateWorldMatrix(true, false);
|
|
294
|
+
const rect = this.domElement.getBoundingClientRect();
|
|
295
|
+
const [spine, edge] = this.ends.map((end, x) => end.set(x, 0, 0).applyMatrix4(group.matrixWorld).project(this.camera));
|
|
296
|
+
return this.axis.set((edge.x - spine.x) * rect.width / 2, -((edge.y - spine.y) * rect.height) / 2);
|
|
297
|
+
}
|
|
298
|
+
dragWidth() {
|
|
299
|
+
return Math.max(1, (this.view?.framing.dragWidth ?? 1) * this.pageAxis().length());
|
|
300
|
+
}
|
|
301
|
+
// whether this and every parent are visible
|
|
302
|
+
shown() {
|
|
303
|
+
for (let object = this; object; object = object.parent) if (!object.visible) return false;
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
requestFrame() {
|
|
307
|
+
this.needsRender = true;
|
|
308
|
+
this.dispatchEvent({ type: "change" });
|
|
309
|
+
}
|
|
310
|
+
// --- pages ---------------------------------------------------------------------
|
|
311
|
+
// what the pages show; their alt and text don't count
|
|
312
|
+
pagesKey(options) {
|
|
313
|
+
return options.pages.map((p) => sourceKey(pageImage(p))).join("\n");
|
|
314
|
+
}
|
|
315
|
+
loadPages(around, layout) {
|
|
316
|
+
this.loader?.dispose();
|
|
317
|
+
const loader = new PageLoader(this.options.pages.map(pageImage), this.renderer, preloadRadius(this.options.preload), {
|
|
318
|
+
onLoad: (page, texture) => {
|
|
319
|
+
if (this.loader !== loader) return;
|
|
320
|
+
this.view?.engine.setPageTexture(page, texture);
|
|
321
|
+
this.options.onPageLoad?.(page);
|
|
322
|
+
this.dispatchEvent({ type: "pageload", detail: { page } });
|
|
323
|
+
},
|
|
324
|
+
onUnload: (page) => {
|
|
325
|
+
if (this.loader !== loader) return;
|
|
326
|
+
this.view?.engine.setPageTexture(page, this.paper.blank);
|
|
327
|
+
},
|
|
328
|
+
onError: (page, error) => this.fail(error, page)
|
|
329
|
+
});
|
|
330
|
+
this.loader = loader;
|
|
331
|
+
this.inView = "";
|
|
332
|
+
this.ready = this.readyFired = false;
|
|
333
|
+
const count = this.pageCount;
|
|
334
|
+
const first = [around - 1, around, around + 1, around + 2];
|
|
335
|
+
const visible = pagesAt(pageToPosition(around, layout, count), layout, count);
|
|
336
|
+
loader.start(visible[0] ?? around, visible[visible.length - 1] ?? around, first).then(() => {
|
|
337
|
+
if (this.loader !== loader || this.disposed) return;
|
|
338
|
+
this.ready = true;
|
|
339
|
+
this.requestFrame();
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// loads around what's on screen, pages mid-turn included, the page being read first
|
|
343
|
+
followReader(engine, layout) {
|
|
344
|
+
if (!this.loader) return;
|
|
345
|
+
const { from, to } = engine.pagesInView();
|
|
346
|
+
const around = pagesAt(engine.position, layout, this.pageCount)[0] ?? to;
|
|
347
|
+
const key = `${from}:${to}:${around}`;
|
|
348
|
+
if (key === this.inView) return;
|
|
349
|
+
this.inView = key;
|
|
350
|
+
this.loader.focus(from, to, around);
|
|
351
|
+
}
|
|
352
|
+
fail(error, page) {
|
|
353
|
+
if (this.options.onError) this.options.onError(error, page);
|
|
354
|
+
else console.warn("[leafthrough]", error);
|
|
355
|
+
this.dispatchEvent({ type: "error", detail: { error, page } });
|
|
356
|
+
}
|
|
357
|
+
detail() {
|
|
358
|
+
return pageDetail(this.view?.engine.position ?? 0, this.layout, this.pageCount);
|
|
359
|
+
}
|
|
360
|
+
emitPageChange() {
|
|
361
|
+
const detail = this.detail();
|
|
362
|
+
const key = `${detail.layout}:${detail.visiblePages.join(",")}/${detail.pageCount}`;
|
|
363
|
+
if (key === this.lastDetailKey) return;
|
|
364
|
+
const first = !this.lastDetailKey;
|
|
365
|
+
this.lastDetailKey = key;
|
|
366
|
+
if (first) return;
|
|
367
|
+
this.options.onPageChange?.(detail);
|
|
368
|
+
this.dispatchEvent({ type: "pagechange", detail });
|
|
369
|
+
}
|
|
370
|
+
};
|
|
7
371
|
export {
|
|
8
372
|
DEFAULT_PAGE_ASPECT,
|
|
9
373
|
DEFAULT_SPREAD_QUERY,
|
|
10
374
|
LeafThrough,
|
|
375
|
+
LeafThroughObject,
|
|
11
376
|
defaultPageLabel
|
|
12
377
|
};
|
|
13
378
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/LeafThroughObject.ts"],"sourcesContent":["import * as THREE from 'three'\nimport { sourceKey } from './content.js'\nimport { floorPoint, rayThrough, type Engine } from './engine.js'\nimport { framingFor, plainFramingFor, type Framing, type Rect } from './framing.js'\nimport type { GoToOptions } from './LeafThrough.js'\nimport { bindKeyboard, pageDetail, pagesAt, pageToPosition, Riffle } from './navigation.js'\nimport {\n changedOptions,\n DEFAULT_SPREAD_QUERY,\n LIVE_OPTIONS,\n pageImage,\n preloadRadius,\n resolveSettings,\n snapshot,\n type LeafThroughOptions,\n type Layout,\n type PageChangeDetail,\n type ResolvedSettings,\n} from './options.js'\nimport { PaperTextures } from './paper/paperTextures.js'\nimport { SingleEngine } from './single/SingleEngine.js'\nimport { SpreadEngine } from './spread/SpreadEngine.js'\nimport { PageLoader } from './textures.js'\n\n/**\n * The magazine's options, less those about its own canvas and DOM. Of `lighting`, only\n * `sun.position` (which way the paper is shaded) and `shadows` (whether sheets cast and\n * receive them) apply: the lights are your scene's.\n */\nexport type LeafThroughObjectOptions = Omit<LeafThroughOptions, 'bleed' | 'maxPixelRatio' | 'plain' | 'label' | 'formatPageLabel'>\n\n/** What the magazine draws with, from your scene. */\nexport interface LeafThroughObjectContext {\n /** The renderer drawing your scene. Pages load into it. */\n renderer: THREE.WebGLRenderer\n /** The camera your scene is drawn with. Pointer input is cast from it; change it later through `camera`. */\n camera: THREE.Camera\n /**\n * Takes pointer input, and keys with `interaction.keyboard: true` while it has focus. It must\n * show the camera's whole view. Default `renderer.domElement`.\n */\n domElement?: HTMLElement\n}\n\nexport interface LeafThroughObjectEventMap extends THREE.Object3DEventMap {\n pagechange: { detail: PageChangeDetail }\n ready: {}\n pageload: { detail: { page: number } }\n error: { detail: { error: unknown; page?: number } }\n /** Something changed between frames: draw one, and keep drawing while `tick()` returns true. */\n change: {}\n}\n\n/** Where the magazine lies in its own units. */\nexport interface LeafThroughBounds {\n /** The pages at rest: what to frame. */\n pages: Rect\n /** Everything a turning page can sweep through. */\n reach: Rect\n}\n\nconst isClient = typeof window !== 'undefined'\n\n/**\n * A 3D magazine to add to your own three.js scene. It draws nothing itself: call `tick()`\n * every frame you render, and render again whenever it fires `change`.\n *\n * In its own units a page is 1 wide and `pageAspect` tall, the spine runs up the y axis at\n * x = 0, and the pages lie on the z = 0 plane facing +z. Move, turn and scale it like any\n * object. Pointer input is cast through `camera` onto the pages, wherever they are.\n *\n * ```ts\n * const magazine = new LeafThroughObject({ renderer, camera }, { pages, layout: 'spread' })\n * scene.add(magazine)\n * renderer.setAnimationLoop((now) => {\n * magazine.tick(now)\n * renderer.render(scene, camera)\n * })\n * ```\n */\nexport class LeafThroughObject extends THREE.Group<LeafThroughObjectEventMap> {\n /** The camera your scene is drawn with; pointer input is cast from it. */\n camera: THREE.Camera\n readonly domElement: HTMLElement\n private inputEnabled = true\n\n /**\n * Whether it takes pointer and key input. Turn it off while it's tucked away or moving,\n * and on again; the pages stay as they are. Turning it off lets go of a page being held.\n * Default `true`.\n */\n get enabled() {\n return this.inputEnabled\n }\n set enabled(value: boolean) {\n if (this.inputEnabled && !value) this.view?.engine.cancelInput?.()\n this.inputEnabled = value\n }\n\n private readonly renderer: THREE.WebGLRenderer\n private options: LeafThroughObjectOptions\n private readonly paper: PaperTextures\n private loader: PageLoader | null = null\n private view: { engine: Engine; settings: ResolvedSettings; framing: Framing; dispose: () => void } | null = null\n\n private spreadMedia: { list: MediaQueryList; abort: AbortController } | null = null\n private readonly motionMedia: MediaQueryList\n private readonly abort = new AbortController()\n private readonly riffle = new Riffle()\n private readonly metrics: { readonly dragWidth: number; dragDelta(dx: number, dy: number): number }\n private readonly ray = new THREE.Ray()\n private readonly point = new THREE.Vector3()\n private readonly ends = [new THREE.Vector3(), new THREE.Vector3()]\n private needsRender = true\n private lastPagesKey = ''\n private lastDetailKey = ''\n private inView = ''\n private ready = false\n private readyFired = false\n private disposed = false\n\n constructor({ renderer, camera, domElement = renderer.domElement }: LeafThroughObjectContext, options: LeafThroughObjectOptions) {\n super()\n if (!isClient) throw new Error('LeafThrough needs a browser; create it on the client.')\n this.renderer = renderer\n this.camera = camera\n this.domElement = domElement\n this.options = snapshot(options)\n const object = this\n this.metrics = {\n get dragWidth() {\n return object.dragWidth()\n },\n dragDelta(dx, dy) {\n const axis = object.pageAxis()\n return (dx * axis.x + dy * axis.y) / (axis.length() || 1)\n },\n }\n\n this.paper = new PaperTextures(options.paper?.backColor ?? '#fcfcf9', {\n onGrain: () => this.remount(),\n onError: (error) => this.fail(error),\n })\n\n const signal = this.abort.signal\n this.watchSpreadQuery()\n this.motionMedia = window.matchMedia('(prefers-reduced-motion: reduce)')\n this.motionMedia.addEventListener('change', () => this.remount(), { signal })\n window.addEventListener('online', () => this.retry(), { signal })\n bindKeyboard(\n domElement,\n () => (this.enabled && this.shown() ? this.view?.settings.interaction.keyboard : false),\n { next: () => this.next(), prev: () => this.prev(), first: () => this.goTo(0), last: () => this.goTo(this.pageCount - 1) },\n signal,\n )\n\n this.remount(options.startPage ?? 0)\n }\n\n // --- public API -----------------------------------------------------------------\n\n /** First visible page, 0-based. */\n get page(): number {\n return this.detail().page\n }\n\n get pageCount(): number {\n return this.options.pages.length\n }\n\n get layout(): Layout {\n return this.view?.settings.layout ?? this.pickLayout()\n }\n\n get visiblePages(): number[] {\n return this.detail().visiblePages\n }\n\n /** True while a page is being dragged or is completing its turn. */\n get turning(): boolean {\n return this.view?.engine.turning ?? false\n }\n\n /**\n * Where the magazine lies in its own units: frame `pages` with your camera. Changes with\n * the layout, which `'auto'` can switch as the window resizes.\n */\n get bounds(): LeafThroughBounds {\n const layout = this.layout\n const aspect = this.view?.settings.pageAspect ?? resolveSettings(this.options, layout, false).pageAspect\n return { pages: plainFramingFor(layout, aspect).box, reach: framingFor(layout, aspect).view }\n }\n\n /** Turns to the next page. Returns false at the end. */\n next(): boolean {\n this.riffle.stop()\n return this.view?.engine.flip(true) ?? false\n }\n\n /** Turns to the previous page. Returns false at the start. */\n prev(): boolean {\n this.riffle.stop()\n return this.view?.engine.flip(false) ?? false\n }\n\n /** Shows `page` (0-based), jumping there or turning the pages one by one. */\n goTo(page: number, { animate = false }: GoToOptions = {}) {\n if (!this.view || Number.isNaN(page)) return\n this.riffle.stop()\n const engine = this.view.engine\n const target = pageToPosition(page, this.view.settings.layout, this.pageCount)\n if (!animate) {\n engine.jumpTo(target)\n return\n }\n if (engine.position === target) return\n this.riffle.start(target, this.view.settings.motion.speed)\n this.requestFrame()\n }\n\n /** Loads the pages that failed again. Also happens by itself when the browser comes back online. */\n retry() {\n this.loader?.retry()\n }\n\n /** Changes some options and keeps the rest. Structural changes rebuild the pages and keep the current page. */\n update(options: Partial<LeafThroughObjectOptions>) {\n this.applyOptions({ ...this.options, ...options })\n }\n\n /** Replaces every option at once: anything left out goes back to its default. */\n setOptions(options: LeafThroughObjectOptions) {\n this.applyOptions(options)\n }\n\n /**\n * Whether a press at this point on screen lands on the magazine, which then takes it. Check\n * it in your own pointer handlers to leave such presses alone. False while it's hidden or\n * not `enabled`.\n */\n covers(clientX: number, clientY: number): boolean {\n const view = this.view\n if (!view || !this.enabled || !this.shown()) return false\n if (!rayThrough(this.camera, this.domElement, clientX, clientY, this.ray)) return false\n const point = floorPoint(view.engine.group, this.ray, this.point)\n if (!point) return false\n // a closed spread only turns from its cover side\n const box = view.framing.box\n const side = view.engine.activeSide\n const left = side === 'right' ? box.x + box.width / 2 : box.x\n const right = side === 'left' ? box.x + box.width / 2 : box.x + box.width\n return point.x >= left && point.x <= right && point.y >= box.y && point.y <= box.y + box.height\n }\n\n /**\n * Advances the pages to `now` (a `requestAnimationFrame` timestamp). Call it every frame\n * before rendering. Returns true while the pages are moving and need another frame.\n */\n tick(now = performance.now()): boolean {\n const view = this.view\n if (!view) return false\n this.riffle.step(now, view.engine)\n const moved = view.engine.update(now)\n this.followReader(view.engine, view.settings.layout)\n const draw = moved || this.needsRender\n this.needsRender = false\n if (this.ready && !this.readyFired) {\n this.readyFired = true\n this.options.onReady?.()\n this.dispatchEvent({ type: 'ready' })\n }\n this.emitPageChange()\n return draw || this.riffle.active\n }\n\n /** Frees the pages, textures and listeners, and takes the magazine out of its parent. */\n dispose() {\n if (this.disposed) return\n this.disposed = true\n this.riffle.stop()\n this.abort.abort()\n this.spreadMedia?.abort.abort()\n this.view?.dispose()\n this.view = null\n this.loader?.dispose()\n this.paper.dispose()\n this.removeFromParent()\n }\n\n // --- options and layout -----------------------------------------------------------\n\n private applyOptions(options: LeafThroughObjectOptions) {\n if (this.disposed) return\n const previous = this.options\n const next = (this.options = snapshot(options))\n const changed = changedOptions(previous, next)\n if (!changed.length) return\n if (changed.includes('spreadQuery')) this.watchSpreadQuery()\n\n const page = this.page\n let rebuild = changed.some((key) => !LIVE_OPTIONS.has(key))\n if (changed.includes('pages') && this.pagesKey(next) !== this.lastPagesKey) {\n this.loader?.dispose()\n this.loader = null\n rebuild = true\n }\n if (changed.includes('preload')) this.loader?.setRadius(preloadRadius(this.options.preload))\n if (rebuild) this.remount(page)\n }\n\n private pickLayout(): Layout {\n const layout = this.options.layout ?? 'auto'\n if (layout !== 'auto') return layout\n return this.spreadMedia?.list.matches ? 'spread' : 'single'\n }\n\n private watchSpreadQuery() {\n this.spreadMedia?.abort.abort()\n const list = window.matchMedia(this.options.spreadQuery ?? DEFAULT_SPREAD_QUERY)\n const abort = new AbortController()\n list.addEventListener('change', () => this.remount(), { signal: abort.signal })\n this.spreadMedia = { list, abort }\n }\n\n private remount(page = this.page) {\n if (this.disposed) return\n this.riffle.stop()\n this.view?.dispose()\n\n const layout = this.pickLayout()\n const settings = resolveSettings(this.options, layout, this.motionMedia.matches)\n this.lastPagesKey = this.pagesKey(this.options)\n if (!this.loader) this.loadPages(page, layout)\n this.loader?.setWrap(layout === 'single' && settings.loop)\n\n const domElement = this.domElement\n const blank = this.paper.blank\n const ctx = {\n input: {\n element: domElement,\n ray: (x: number, y: number, ray: THREE.Ray) => rayThrough(this.camera, domElement, x, y, ray),\n covers: (x: number, y: number) => this.covers(x, y),\n },\n settings,\n textures: this.options.pages.map((_, i) => this.loader?.textures[i] ?? blank),\n blank: layout === 'single' ? this.paper.back(settings.paper.backColor) : blank,\n pattern: this.paper.grain(settings, this.renderer),\n position: pageToPosition(page, layout, this.pageCount),\n metrics: this.metrics,\n shading: {\n spineContact: { value: new THREE.Vector3(1, 0, 0) },\n lightDirection: { value: new THREE.Vector3(...settings.lighting.sun.position).normalize() },\n },\n requestFrame: () => this.requestFrame(),\n }\n const engine: Engine = layout === 'spread' ? new SpreadEngine(ctx) : new SingleEngine(ctx)\n this.add(engine.group)\n this.view = {\n engine,\n settings,\n framing: framingFor(layout, settings.pageAspect),\n dispose: () => {\n this.remove(engine.group)\n engine.dispose()\n },\n }\n this.requestFrame()\n }\n\n // A drag across this many pixels turns a page all the way: the magazine's drag width, at the\n // size it shows on screen now.\n private readonly axis = new THREE.Vector2()\n\n private pageAxis() {\n const group = this.view?.engine.group ?? this\n group.updateWorldMatrix(true, false)\n this.camera.updateWorldMatrix(true, false)\n const rect = this.domElement.getBoundingClientRect()\n const [spine, edge] = this.ends.map((end, x) => end.set(x, 0, 0).applyMatrix4(group.matrixWorld).project(this.camera))\n return this.axis.set(((edge.x - spine.x) * rect.width) / 2, -((edge.y - spine.y) * rect.height) / 2)\n }\n\n private dragWidth() {\n return Math.max(1, (this.view?.framing.dragWidth ?? 1) * this.pageAxis().length())\n }\n\n // whether this and every parent are visible\n private shown() {\n for (let object: THREE.Object3D | null = this; object; object = object.parent) if (!object.visible) return false\n return true\n }\n\n private requestFrame() {\n this.needsRender = true\n this.dispatchEvent({ type: 'change' })\n }\n\n // --- pages ---------------------------------------------------------------------\n\n // what the pages show; their alt and text don't count\n private pagesKey(options: LeafThroughObjectOptions) {\n return options.pages.map((p) => sourceKey(pageImage(p))).join('\\n')\n }\n\n private loadPages(around: number, layout: Layout) {\n this.loader?.dispose()\n const loader = new PageLoader(this.options.pages.map(pageImage), this.renderer, preloadRadius(this.options.preload), {\n onLoad: (page, texture) => {\n if (this.loader !== loader) return\n this.view?.engine.setPageTexture(page, texture)\n this.options.onPageLoad?.(page)\n this.dispatchEvent({ type: 'pageload', detail: { page } })\n },\n onUnload: (page) => {\n if (this.loader !== loader) return\n this.view?.engine.setPageTexture(page, this.paper.blank)\n },\n onError: (page, error) => this.fail(error, page),\n })\n this.loader = loader\n this.inView = ''\n this.ready = this.readyFired = false\n // the pages around the start, so the first turn either way has its next page ready\n const count = this.pageCount\n const first = [around - 1, around, around + 1, around + 2]\n const visible = pagesAt(pageToPosition(around, layout, count), layout, count)\n loader.start(visible[0] ?? around, visible[visible.length - 1] ?? around, first).then(() => {\n if (this.loader !== loader || this.disposed) return\n this.ready = true\n this.requestFrame()\n })\n }\n\n // loads around what's on screen, pages mid-turn included, the page being read first\n private followReader(engine: Engine, layout: Layout) {\n if (!this.loader) return\n const { from, to } = engine.pagesInView()\n const around = pagesAt(engine.position, layout, this.pageCount)[0] ?? to\n const key = `${from}:${to}:${around}`\n if (key === this.inView) return\n this.inView = key\n this.loader.focus(from, to, around)\n }\n\n private fail(error: unknown, page?: number) {\n if (this.options.onError) this.options.onError(error, page)\n else console.warn('[leafthrough]', error)\n this.dispatchEvent({ type: 'error', detail: { error, page } })\n }\n\n private detail(): PageChangeDetail {\n return pageDetail(this.view?.engine.position ?? 0, this.layout, this.pageCount)\n }\n\n private emitPageChange() {\n const detail = this.detail()\n const key = `${detail.layout}:${detail.visiblePages.join(',')}/${detail.pageCount}`\n if (key === this.lastDetailKey) return\n const first = !this.lastDetailKey\n this.lastDetailKey = key\n if (first) return\n this.options.onPageChange?.(detail)\n this.dispatchEvent({ type: 'pagechange', detail })\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Engine } from './engine.js';
|
|
2
|
+
import type { InteractionOptions, Layout, PageChangeDetail } from './options.js';
|
|
3
|
+
export declare function pageToPosition(page: number, layout: Layout, count: number): number;
|
|
4
|
+
export declare function positionToPage(position: number, layout: Layout): number;
|
|
5
|
+
/** The pages showing at an engine position. */
|
|
6
|
+
export declare function pagesAt(position: number, layout: Layout, count: number): number[];
|
|
7
|
+
export declare function pageDetail(position: number, layout: Layout, count: number): PageChangeDetail;
|
|
8
|
+
/** Turns one page every `interval` ms until the engine reaches `target`. */
|
|
9
|
+
export declare class Riffle {
|
|
10
|
+
private state;
|
|
11
|
+
get active(): boolean;
|
|
12
|
+
start(target: number, speed: number): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
step(now: number, engine: Engine): void;
|
|
15
|
+
}
|
|
16
|
+
export interface KeyboardActions {
|
|
17
|
+
next(): unknown;
|
|
18
|
+
prev(): unknown;
|
|
19
|
+
first(): unknown;
|
|
20
|
+
last(): unknown;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Arrow keys, PageUp/PageDown and Home/End. With `true` they work while `element` has focus;
|
|
24
|
+
* with `'global'` anywhere on the page, except in text fields.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bindKeyboard(element: HTMLElement, mode: () => InteractionOptions['keyboard'] | undefined, actions: KeyboardActions, signal: AbortSignal): void;
|
package/dist/options.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface PaperOptions {
|
|
|
43
43
|
wrinkles?: number;
|
|
44
44
|
/** Scales how much resting pages bulge up off the spine. Default `1`. */
|
|
45
45
|
bulge?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Cuts each sheet to the shape of its front page: where that image is transparent there's no
|
|
48
|
+
* paper, so the sheet and its shadow take its outline (a die-cut cover, a flap). Default
|
|
49
|
+
* `false`: transparent areas are unprinted paper.
|
|
50
|
+
*/
|
|
51
|
+
cutout?: boolean;
|
|
46
52
|
}
|
|
47
53
|
export interface LightingOptions {
|
|
48
54
|
/** Hemisphere light. Default sky `#ffffff`, ground `#a1aeaf`, intensity `1.5` (spread) or `1.7` (single). */
|
|
@@ -80,8 +86,9 @@ export interface MotionOptions {
|
|
|
80
86
|
/** How far a hovered sheet lifts, as a share of a turn (spread). `0` disables. Default `0.03`. */
|
|
81
87
|
hoverLift?: number;
|
|
82
88
|
/**
|
|
83
|
-
* `'auto'` follows the user's `prefers-reduced-motion`. When reduced,
|
|
84
|
-
*
|
|
89
|
+
* `'auto'` follows the user's `prefers-reduced-motion`. When reduced, the decorative motion
|
|
90
|
+
* goes: hovered pages don't lift and turns don't wobble at random. Turns themselves keep their
|
|
91
|
+
* speed, and press and hold still riffles. Default `'auto'`.
|
|
85
92
|
*/
|
|
86
93
|
reducedMotion?: 'auto' | boolean;
|
|
87
94
|
}
|
|
@@ -192,3 +199,11 @@ export interface ResolvedSettings {
|
|
|
192
199
|
maxPixelRatio: number;
|
|
193
200
|
}
|
|
194
201
|
export declare function resolveSettings(options: LeafThroughOptions, layout: Layout, prefersReducedMotion: boolean): ResolvedSettings;
|
|
202
|
+
/** Options that don't need a rebuild (pages only need one when their images change). */
|
|
203
|
+
export declare const LIVE_OPTIONS: Set<keyof LeafThroughOptions>;
|
|
204
|
+
/** How many pages either side of the visible ones load, from the `preload` option. */
|
|
205
|
+
export declare function preloadRadius(preload: number | undefined): number;
|
|
206
|
+
export declare function snapshot<T>(value: T): T;
|
|
207
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
208
|
+
/** The options that changed between two snapshots. */
|
|
209
|
+
export declare function changedOptions<T extends object>(previous: T, next: T): (keyof T)[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { ResolvedSettings } from '../options.js';
|
|
3
|
+
export interface PaperTextureEvents {
|
|
4
|
+
/** A custom grain image arrived; sheets made before it show none. */
|
|
5
|
+
onGrain(): void;
|
|
6
|
+
onError(error: unknown): void;
|
|
7
|
+
}
|
|
8
|
+
/** Plain paper and the grain, kept across rebuilds and made again only when their options change. */
|
|
9
|
+
export declare class PaperTextures {
|
|
10
|
+
private readonly events;
|
|
11
|
+
/** Plain paper, for missing pages and the spread's unprinted sides. */
|
|
12
|
+
readonly blank: THREE.DataTexture;
|
|
13
|
+
backBlank: {
|
|
14
|
+
texture: THREE.DataTexture;
|
|
15
|
+
color: string;
|
|
16
|
+
};
|
|
17
|
+
private pattern;
|
|
18
|
+
private disposed;
|
|
19
|
+
constructor(backColor: THREE.ColorRepresentation, events: PaperTextureEvents);
|
|
20
|
+
/** The blank backs of single-layout pages, in `paper.backColor`. */
|
|
21
|
+
back(color: THREE.ColorRepresentation): THREE.Texture;
|
|
22
|
+
grain(settings: ResolvedSettings, renderer: THREE.WebGLRenderer): THREE.Texture;
|
|
23
|
+
dispose(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function colorKey(color: THREE.ColorRepresentation): string;
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import type
|
|
2
|
+
import { type Engine, type EngineContext } from '../engine.js';
|
|
3
3
|
/**
|
|
4
4
|
* One page at a time: turned pages swing round the spine and roll up behind the magazine.
|
|
5
5
|
*/
|
|
@@ -9,10 +9,9 @@ export declare class SingleEngine implements Engine {
|
|
|
9
9
|
private readonly settings;
|
|
10
10
|
private readonly halfHeight;
|
|
11
11
|
private readonly liftHeight;
|
|
12
|
-
private readonly
|
|
13
|
-
private readonly canvas;
|
|
14
|
-
private readonly hitArea;
|
|
12
|
+
private readonly input;
|
|
15
13
|
private readonly metrics;
|
|
14
|
+
private readonly requestFrame;
|
|
16
15
|
private readonly geometry;
|
|
17
16
|
private readonly sheets;
|
|
18
17
|
private readonly count;
|
|
@@ -30,11 +29,8 @@ export declare class SingleEngine implements Engine {
|
|
|
30
29
|
private touch;
|
|
31
30
|
private dirty;
|
|
32
31
|
private last;
|
|
33
|
-
private readonly
|
|
34
|
-
private readonly ndc;
|
|
35
|
-
private readonly floor;
|
|
32
|
+
private readonly ray;
|
|
36
33
|
private readonly hit;
|
|
37
|
-
private readonly spine;
|
|
38
34
|
constructor(ctx: EngineContext);
|
|
39
35
|
get position(): number;
|
|
40
36
|
get turning(): boolean;
|
|
@@ -45,9 +41,11 @@ export declare class SingleEngine implements Engine {
|
|
|
45
41
|
};
|
|
46
42
|
private pileTop;
|
|
47
43
|
setPageTexture(page: number, texture: THREE.Texture): void;
|
|
44
|
+
cancelInput(): void;
|
|
48
45
|
dispose(): void;
|
|
49
46
|
private randomWobble;
|
|
50
47
|
private seconds;
|
|
48
|
+
private wake;
|
|
51
49
|
private pileHeight;
|
|
52
50
|
private pileReach;
|
|
53
51
|
private paced;
|
|
@@ -55,7 +53,7 @@ export declare class SingleEngine implements Engine {
|
|
|
55
53
|
private shutting;
|
|
56
54
|
private inFlight;
|
|
57
55
|
private busy;
|
|
58
|
-
private
|
|
56
|
+
private pointAt;
|
|
59
57
|
private grab;
|
|
60
58
|
private canTurn;
|
|
61
59
|
private sheetAt;
|