leafthrough 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,79 @@
1
+ import { LeafThrough, type GoToOptions } from './LeafThrough.js';
2
+ import type { LeafThroughOptions, Layout, PageChangeDetail, PageSource } from './options.js';
3
+ export type * from './options.js';
4
+ declare const BaseElement: typeof HTMLElement;
5
+ /**
6
+ * `<leaf-through>`: works anywhere HTML does, including Angular, Solid and Lit.
7
+ *
8
+ * Pages come from the `pages` property, a `pages` attribute (JSON array or comma-separated
9
+ * URLs), or the element's `<img>` children: their `alt` describes the page, and a
10
+ * `<figcaption>` next to one in a `<figure>` gives the page's text. Everything else can be
11
+ * set through the `options` property or the attributes `start-page`, `layout`,
12
+ * `spread-query`, `page-aspect`, `loop`, `bleed`, `label`, `keyboard`, `preload` and `plain`.
13
+ *
14
+ * Events (all bubble): `pagechange` (detail: PageChangeDetail), `ready`, `pageload`
15
+ * (detail: `{ page }`), and `pageerror`
16
+ * (detail: `{ error, page }`). It isn't called `error`, which on the DOM means a script
17
+ * error and reaches `window`, where error trackers listen.
18
+ *
19
+ * ```html
20
+ * <leaf-through style="height: 80vh" layout="auto">
21
+ * <img src="/pages/01.jpg" alt="Cover: a lighthouse at dusk" />
22
+ * <figure>
23
+ * <img src="/pages/02.jpg" alt="" />
24
+ * <figcaption>Contents. Page 3: The keeper's year…</figcaption>
25
+ * </figure>
26
+ * </leaf-through>
27
+ * ```
28
+ */
29
+ export interface LeafThroughElementEventMap extends HTMLElementEventMap {
30
+ pagechange: CustomEvent<PageChangeDetail>;
31
+ ready: CustomEvent<void>;
32
+ pageload: CustomEvent<{
33
+ page: number;
34
+ }>;
35
+ pageerror: CustomEvent<{
36
+ error: unknown;
37
+ page?: number;
38
+ }>;
39
+ }
40
+ export declare class LeafThroughElement extends BaseElement {
41
+ static observedAttributes: string[];
42
+ private magazine;
43
+ private extra;
44
+ private explicitPages;
45
+ private readonly mount;
46
+ private childObserver;
47
+ constructor();
48
+ /** The pages, in reading order. */
49
+ get pages(): PageSource[];
50
+ set pages(pages: PageSource[]);
51
+ /** Any other options, merged over the attributes. */
52
+ get options(): Partial<LeafThroughOptions>;
53
+ set options(options: Partial<LeafThroughOptions>);
54
+ get page(): number;
55
+ get pageCount(): number;
56
+ get layout(): Layout | undefined;
57
+ /** The underlying instance, once connected. */
58
+ get instance(): LeafThrough | null;
59
+ next(): boolean;
60
+ prev(): boolean;
61
+ goTo(page: number, options?: GoToOptions): void;
62
+ addEventListener<K extends keyof LeafThroughElementEventMap>(type: K, listener: (this: LeafThroughElement, event: LeafThroughElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
63
+ addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
64
+ removeEventListener<K extends keyof LeafThroughElementEventMap>(type: K, listener: (this: LeafThroughElement, event: LeafThroughElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
65
+ removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
66
+ connectedCallback(): void;
67
+ disconnectedCallback(): void;
68
+ attributeChangedCallback(): void;
69
+ private pagesFromMarkup;
70
+ private optionsFromAttributes;
71
+ private sync;
72
+ }
73
+ /** Registers the element (as `leaf-through` unless you pick another tag). */
74
+ export declare function defineLeafThrough(tagName?: string): void;
75
+ declare global {
76
+ interface HTMLElementTagNameMap {
77
+ 'leaf-through': LeafThroughElement;
78
+ }
79
+ }
@@ -0,0 +1,182 @@
1
+ import {
2
+ LeafThrough
3
+ } from "./chunk-DQL4R32K.js";
4
+
5
+ // src/element.ts
6
+ var ATTRIBUTES = {
7
+ "start-page": "startPage",
8
+ layout: "layout",
9
+ "spread-query": "spreadQuery",
10
+ "page-aspect": "pageAspect",
11
+ loop: "loop",
12
+ bleed: "bleed",
13
+ label: "label",
14
+ keyboard: "keyboard",
15
+ preload: "preload",
16
+ plain: "plain"
17
+ };
18
+ var NUMBERS = /* @__PURE__ */ new Set(["startPage", "pageAspect", "preload"]);
19
+ var BOOLEANS = /* @__PURE__ */ new Set(["loop", "bleed", "plain"]);
20
+ var BaseElement = typeof HTMLElement !== "undefined" ? HTMLElement : class {
21
+ };
22
+ var LeafThroughElement = class extends BaseElement {
23
+ static observedAttributes = ["pages", ...Object.keys(ATTRIBUTES)];
24
+ magazine = null;
25
+ extra = {};
26
+ explicitPages = null;
27
+ mount;
28
+ childObserver = null;
29
+ constructor() {
30
+ super();
31
+ const shadow = this.attachShadow({ mode: "open" });
32
+ const style = document.createElement("style");
33
+ style.textContent = ":host{display:block;position:relative}:host([hidden]){display:none}div{width:100%;height:100%}";
34
+ this.mount = document.createElement("div");
35
+ shadow.append(style, this.mount);
36
+ const self = this;
37
+ for (const key of ["pages", "options"]) {
38
+ if (!Object.hasOwn(this, key)) continue;
39
+ const value = self[key];
40
+ delete self[key];
41
+ self[key] = value;
42
+ }
43
+ }
44
+ /** The pages, in reading order. */
45
+ get pages() {
46
+ return this.explicitPages ?? this.pagesFromMarkup();
47
+ }
48
+ set pages(pages) {
49
+ this.explicitPages = pages;
50
+ this.sync();
51
+ }
52
+ /** Any other options, merged over the attributes. */
53
+ get options() {
54
+ return this.extra;
55
+ }
56
+ set options(options) {
57
+ this.extra = { ...options };
58
+ if (options.pages) this.explicitPages = options.pages;
59
+ this.sync();
60
+ }
61
+ get page() {
62
+ return this.magazine?.page ?? 0;
63
+ }
64
+ get pageCount() {
65
+ return this.pages.length;
66
+ }
67
+ get layout() {
68
+ return this.magazine?.layout;
69
+ }
70
+ /** The underlying instance, once connected. */
71
+ get instance() {
72
+ return this.magazine;
73
+ }
74
+ next() {
75
+ return this.magazine?.next() ?? false;
76
+ }
77
+ prev() {
78
+ return this.magazine?.prev() ?? false;
79
+ }
80
+ goTo(page, options) {
81
+ this.magazine?.goTo(page, options);
82
+ }
83
+ addEventListener(type, listener, options) {
84
+ super.addEventListener(type, listener, options);
85
+ }
86
+ removeEventListener(type, listener, options) {
87
+ super.removeEventListener(type, listener, options);
88
+ }
89
+ connectedCallback() {
90
+ this.childObserver = new MutationObserver(() => {
91
+ if (!this.explicitPages) this.sync();
92
+ });
93
+ this.childObserver.observe(this, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["src", "alt"] });
94
+ this.sync();
95
+ }
96
+ disconnectedCallback() {
97
+ this.childObserver?.disconnect();
98
+ this.magazine?.dispose();
99
+ this.magazine = null;
100
+ }
101
+ attributeChangedCallback() {
102
+ this.sync();
103
+ }
104
+ pagesFromMarkup() {
105
+ const attribute = this.getAttribute("pages");
106
+ if (attribute) {
107
+ const trimmed = attribute.trim();
108
+ if (trimmed.startsWith("[")) {
109
+ try {
110
+ return JSON.parse(trimmed);
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+ return trimmed.split(",").map((s) => s.trim()).filter(Boolean);
116
+ }
117
+ return Array.from(this.querySelectorAll("img")).flatMap((img) => {
118
+ const src = img.currentSrc || img.src;
119
+ if (!src) return [];
120
+ const text = img.closest("figure")?.querySelector("figcaption")?.textContent?.trim();
121
+ return img.alt || text ? [{ src, alt: img.alt || void 0, text: text || void 0 }] : [src];
122
+ });
123
+ }
124
+ optionsFromAttributes() {
125
+ const out = {};
126
+ for (const [attribute, key] of Object.entries(ATTRIBUTES)) {
127
+ const value = this.getAttribute(attribute);
128
+ if (value === null) continue;
129
+ if (NUMBERS.has(key)) out[key] = Number(value);
130
+ else if (BOOLEANS.has(key)) out[key] = value !== "false";
131
+ else if (key === "keyboard") {
132
+ out.interaction = { keyboard: value === "global" ? "global" : value !== "false" };
133
+ } else out[key] = value;
134
+ }
135
+ return out;
136
+ }
137
+ sync() {
138
+ if (!this.isConnected) return;
139
+ const pages = this.pages;
140
+ const attributes = this.optionsFromAttributes();
141
+ const options = {
142
+ ...attributes,
143
+ ...this.extra,
144
+ interaction: { ...attributes.interaction, ...this.extra.interaction },
145
+ pages,
146
+ onPageChange: (detail) => {
147
+ this.extra.onPageChange?.(detail);
148
+ this.dispatchEvent(new CustomEvent("pagechange", { detail, bubbles: true, composed: true }));
149
+ },
150
+ onReady: () => {
151
+ this.extra.onReady?.();
152
+ this.dispatchEvent(new CustomEvent("ready", { bubbles: true, composed: true }));
153
+ },
154
+ onPageLoad: (page) => {
155
+ this.extra.onPageLoad?.(page);
156
+ this.dispatchEvent(new CustomEvent("pageload", { detail: { page }, bubbles: true, composed: true }));
157
+ },
158
+ onError: (error, page) => {
159
+ this.extra.onError?.(error, page);
160
+ this.dispatchEvent(new CustomEvent("pageerror", { detail: { error, page }, bubbles: true, composed: true }));
161
+ }
162
+ };
163
+ if (!pages.length) {
164
+ this.magazine?.dispose();
165
+ this.magazine = null;
166
+ return;
167
+ }
168
+ if (this.magazine) this.magazine.setOptions(options);
169
+ else this.magazine = new LeafThrough(this.mount, options);
170
+ }
171
+ };
172
+ function defineLeafThrough(tagName = "leaf-through") {
173
+ if (typeof customElements === "undefined" || customElements.get(tagName)) return;
174
+ customElements.define(tagName, class extends LeafThroughElement {
175
+ });
176
+ }
177
+ defineLeafThrough();
178
+ export {
179
+ LeafThroughElement,
180
+ defineLeafThrough
181
+ };
182
+ //# sourceMappingURL=element.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/element.ts"],"sourcesContent":["import { LeafThrough, type GoToOptions } from './LeafThrough.js'\nimport type { LeafThroughOptions, Layout, PageChangeDetail, PageSource } from './options.js'\n\nexport type * from './options.js'\n\n// attribute → option, for the simple ones\nconst ATTRIBUTES = {\n 'start-page': 'startPage',\n layout: 'layout',\n 'spread-query': 'spreadQuery',\n 'page-aspect': 'pageAspect',\n loop: 'loop',\n bleed: 'bleed',\n label: 'label',\n keyboard: 'keyboard',\n preload: 'preload',\n plain: 'plain',\n} as const\n\nconst NUMBERS = new Set(['startPage', 'pageAspect', 'preload'])\nconst BOOLEANS = new Set(['loop', 'bleed', 'plain'])\n\nconst BaseElement = (typeof HTMLElement !== 'undefined' ? HTMLElement : class {}) as typeof HTMLElement\n\n/**\n * `<leaf-through>`: works anywhere HTML does, including Angular, Solid and Lit.\n *\n * Pages come from the `pages` property, a `pages` attribute (JSON array or comma-separated\n * URLs), or the element's `<img>` children: their `alt` describes the page, and a\n * `<figcaption>` next to one in a `<figure>` gives the page's text. Everything else can be\n * set through the `options` property or the attributes `start-page`, `layout`,\n * `spread-query`, `page-aspect`, `loop`, `bleed`, `label`, `keyboard`, `preload` and `plain`.\n *\n * Events (all bubble): `pagechange` (detail: PageChangeDetail), `ready`, `pageload`\n * (detail: `{ page }`), and `pageerror`\n * (detail: `{ error, page }`). It isn't called `error`, which on the DOM means a script\n * error and reaches `window`, where error trackers listen.\n *\n * ```html\n * <leaf-through style=\"height: 80vh\" layout=\"auto\">\n * <img src=\"/pages/01.jpg\" alt=\"Cover: a lighthouse at dusk\" />\n * <figure>\n * <img src=\"/pages/02.jpg\" alt=\"\" />\n * <figcaption>Contents. Page 3: The keeper's year…</figcaption>\n * </figure>\n * </leaf-through>\n * ```\n */\nexport interface LeafThroughElementEventMap extends HTMLElementEventMap {\n pagechange: CustomEvent<PageChangeDetail>\n ready: CustomEvent<void>\n pageload: CustomEvent<{ page: number }>\n pageerror: CustomEvent<{ error: unknown; page?: number }>\n}\n\nexport class LeafThroughElement extends BaseElement {\n static observedAttributes = ['pages', ...Object.keys(ATTRIBUTES)]\n\n private magazine: LeafThrough | null = null\n private extra: Partial<LeafThroughOptions> = {}\n private explicitPages: PageSource[] | null = null\n private readonly mount: HTMLDivElement\n private childObserver: MutationObserver | null = null\n\n constructor() {\n super()\n const shadow = this.attachShadow({ mode: 'open' })\n const style = document.createElement('style')\n style.textContent = ':host{display:block;position:relative}:host([hidden]){display:none}div{width:100%;height:100%}'\n this.mount = document.createElement('div')\n // children stay in the light DOM as the page list (and as fallback content); hide them\n shadow.append(style, this.mount)\n\n // A property set before the element was defined is an own property that hides the\n // setter; move it through the setter, so later assignments reach the magazine.\n const self = this as unknown as Record<string, unknown>\n for (const key of ['pages', 'options']) {\n if (!Object.hasOwn(this, key)) continue\n const value = self[key]\n delete self[key]\n self[key] = value\n }\n }\n\n /** The pages, in reading order. */\n get pages(): PageSource[] {\n return this.explicitPages ?? this.pagesFromMarkup()\n }\n\n set pages(pages: PageSource[]) {\n this.explicitPages = pages\n this.sync()\n }\n\n /** Any other options, merged over the attributes. */\n get options(): Partial<LeafThroughOptions> {\n return this.extra\n }\n\n set options(options: Partial<LeafThroughOptions>) {\n this.extra = { ...options }\n if (options.pages) this.explicitPages = options.pages\n this.sync()\n }\n\n get page(): number {\n return this.magazine?.page ?? 0\n }\n\n get pageCount(): number {\n return this.pages.length\n }\n\n get layout(): Layout | undefined {\n return this.magazine?.layout\n }\n\n /** The underlying instance, once connected. */\n get instance(): LeafThrough | null {\n return this.magazine\n }\n\n next(): boolean {\n return this.magazine?.next() ?? false\n }\n\n prev(): boolean {\n return this.magazine?.prev() ?? false\n }\n\n goTo(page: number, options?: GoToOptions) {\n this.magazine?.goTo(page, options)\n }\n\n addEventListener<K extends keyof LeafThroughElementEventMap>(\n type: K,\n listener: (this: LeafThroughElement, event: LeafThroughElementEventMap[K]) => unknown,\n options?: boolean | AddEventListenerOptions,\n ): void\n addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void\n addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) {\n super.addEventListener(type, listener, options)\n }\n\n removeEventListener<K extends keyof LeafThroughElementEventMap>(\n type: K,\n listener: (this: LeafThroughElement, event: LeafThroughElementEventMap[K]) => unknown,\n options?: boolean | EventListenerOptions,\n ): void\n removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void\n removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) {\n super.removeEventListener(type, listener, options)\n }\n\n connectedCallback() {\n this.childObserver = new MutationObserver(() => {\n if (!this.explicitPages) this.sync()\n })\n this.childObserver.observe(this, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['src', 'alt'] })\n this.sync()\n }\n\n disconnectedCallback() {\n this.childObserver?.disconnect()\n this.magazine?.dispose()\n this.magazine = null\n }\n\n attributeChangedCallback() {\n this.sync()\n }\n\n private pagesFromMarkup(): PageSource[] {\n const attribute = this.getAttribute('pages')\n if (attribute) {\n const trimmed = attribute.trim()\n if (trimmed.startsWith('[')) {\n try {\n return JSON.parse(trimmed) as PageSource[]\n } catch {\n return []\n }\n }\n return trimmed.split(',').map((s) => s.trim()).filter(Boolean)\n }\n return Array.from(this.querySelectorAll('img')).flatMap((img): PageSource[] => {\n const src = img.currentSrc || img.src\n if (!src) return []\n const text = img.closest('figure')?.querySelector('figcaption')?.textContent?.trim()\n return img.alt || text ? [{ src, alt: img.alt || undefined, text: text || undefined }] : [src]\n })\n }\n\n private optionsFromAttributes(): Partial<LeafThroughOptions> {\n const out: Record<string, unknown> = {}\n for (const [attribute, key] of Object.entries(ATTRIBUTES)) {\n const value = this.getAttribute(attribute)\n if (value === null) continue\n if (NUMBERS.has(key)) out[key] = Number(value)\n else if (BOOLEANS.has(key)) out[key] = value !== 'false'\n else if (key === 'keyboard') {\n out.interaction = { keyboard: value === 'global' ? 'global' : value !== 'false' }\n } else out[key] = value\n }\n return out as Partial<LeafThroughOptions>\n }\n\n private sync() {\n if (!this.isConnected) return\n const pages = this.pages\n const attributes = this.optionsFromAttributes()\n const options: LeafThroughOptions = {\n ...attributes,\n ...this.extra,\n interaction: { ...attributes.interaction, ...this.extra.interaction },\n pages,\n onPageChange: (detail: PageChangeDetail) => {\n this.extra.onPageChange?.(detail)\n this.dispatchEvent(new CustomEvent('pagechange', { detail, bubbles: true, composed: true }))\n },\n onReady: () => {\n this.extra.onReady?.()\n this.dispatchEvent(new CustomEvent('ready', { bubbles: true, composed: true }))\n },\n onPageLoad: (page: number) => {\n this.extra.onPageLoad?.(page)\n this.dispatchEvent(new CustomEvent('pageload', { detail: { page }, bubbles: true, composed: true }))\n },\n onError: (error: unknown, page?: number) => {\n this.extra.onError?.(error, page)\n this.dispatchEvent(new CustomEvent('pageerror', { detail: { error, page }, bubbles: true, composed: true }))\n },\n }\n if (!pages.length) {\n this.magazine?.dispose()\n this.magazine = null\n return\n }\n if (this.magazine) this.magazine.setOptions(options)\n else this.magazine = new LeafThrough(this.mount, options)\n }\n}\n\n/** Registers the element (as `leaf-through` unless you pick another tag). */\nexport function defineLeafThrough(tagName = 'leaf-through') {\n if (typeof customElements === 'undefined' || customElements.get(tagName)) return\n customElements.define(tagName, class extends LeafThroughElement {})\n}\n\ndefineLeafThrough()\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'leaf-through': LeafThroughElement\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,51 @@
1
+ import type * as THREE from 'three';
2
+ import type { ResolvedSettings } from './options.js';
3
+ import type { SheetShading } from './paper/paperMaterial.js';
4
+ export interface EngineContext {
5
+ camera: THREE.PerspectiveCamera;
6
+ canvas: HTMLCanvasElement;
7
+ /** Receives pointer input; covers the pages. */
8
+ hitArea: HTMLElement;
9
+ settings: ResolvedSettings;
10
+ /** One texture per page, in reading order. */
11
+ textures: THREE.Texture[];
12
+ /** Plain paper, for missing or unprinted sides. */
13
+ blank: THREE.Texture;
14
+ pattern: THREE.Texture;
15
+ /** Where the magazine opens, in the engine's own position units. */
16
+ position: number;
17
+ /** Kept current by the controller as the magazine resizes. */
18
+ metrics: EngineMetrics;
19
+ /** Shared by every sheet; the engine keeps spineContact current. */
20
+ shading: SheetShading;
21
+ }
22
+ export interface EngineMetrics {
23
+ /** Pixels a drag spans per unit of `dragDistance`. */
24
+ dragWidth: number;
25
+ }
26
+ export interface Engine {
27
+ readonly group: THREE.Group;
28
+ /** Spread: sheets turned to the left. Single: pages turned. */
29
+ readonly position: number;
30
+ readonly maxPosition: number;
31
+ /** Whether a page is being dragged or is still completing a turn. */
32
+ readonly turning: boolean;
33
+ /** Which part of the pages takes input: a closed spread only turns from its cover side. */
34
+ readonly activeSide: 'both' | 'right' | 'left';
35
+ /**
36
+ * The pages that can be seen right now, as a range: the ones at rest and any mid-turn. A
37
+ * quick riffle leaves pages turning well behind the position, and they mustn't unload.
38
+ */
39
+ pagesInView(): {
40
+ from: number;
41
+ to: number;
42
+ };
43
+ /** Advances the simulation; returns true when the frame needs drawing. */
44
+ update(now: number): boolean;
45
+ /** Turns the top page one way; false when there's nothing to turn. */
46
+ flip(forward: boolean): boolean;
47
+ /** Moves to a position at once, without animating. */
48
+ jumpTo(position: number): void;
49
+ setPageTexture(page: number, texture: THREE.Texture): void;
50
+ dispose(): void;
51
+ }
@@ -0,0 +1,48 @@
1
+ import type { Layout } from './options.js';
2
+ export interface Rect {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /** What the camera looks at, in world units (a page is 1 wide, the spine is at x = 0). */
9
+ export interface Framing {
10
+ /** Fits the element when bleeding: the pages, plus any room the layout always needs. Takes input. */
11
+ box: Rect;
12
+ /** Everything a turning page can reach. The canvas covers this (clipped to the document). */
13
+ view: Rect;
14
+ /** Camera position over the floor; the view needn't be centred on it. */
15
+ eye: {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ distance: number;
20
+ /** World width that a drag of `dragDistance` × this turns a page all the way over. */
21
+ dragWidth: number;
22
+ /** Directional shadow frustum, already scaled to the page aspect. */
23
+ shadow: {
24
+ left: number;
25
+ right: number;
26
+ top: number;
27
+ bottom: number;
28
+ };
29
+ }
30
+ export declare function framingFor(layout: Layout, aspect: number): Framing;
31
+ /** The plain view: just the pages, centred, with nothing around them. */
32
+ export declare function plainFramingFor(layout: Layout, aspect: number): Framing;
33
+ export interface Fit {
34
+ /** Pixels per world unit. */
35
+ scale: number;
36
+ /** Where the whole view lands, in px relative to the root's top-left corner. */
37
+ view: Rect;
38
+ /** Where the box lands, in px relative to the root. */
39
+ box: Rect;
40
+ /** Height the root needs when its container has no height of its own. */
41
+ height: number;
42
+ }
43
+ /**
44
+ * Fits the framing into an element. With `bleed`, the box fills the element and the rest
45
+ * of the view spills past it; without, the whole view stays inside. An element with no
46
+ * height (`height` 0) takes its height from its width.
47
+ */
48
+ export declare function fit(framing: Framing, width: number, height: number, bleed: boolean): Fit;
@@ -0,0 +1,2 @@
1
+ export { LeafThrough, type LeafThroughEventMap, type GoToOptions } from './LeafThrough.js';
2
+ 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 ADDED
@@ -0,0 +1,13 @@
1
+ import {
2
+ DEFAULT_PAGE_ASPECT,
3
+ DEFAULT_SPREAD_QUERY,
4
+ LeafThrough,
5
+ defaultPageLabel
6
+ } from "./chunk-DQL4R32K.js";
7
+ export {
8
+ DEFAULT_PAGE_ASPECT,
9
+ DEFAULT_SPREAD_QUERY,
10
+ LeafThrough,
11
+ defaultPageLabel
12
+ };
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,194 @@
1
+ import type { ColorRepresentation, Texture } from 'three';
2
+ /** Anything a page can be printed from. Strings are image URLs. */
3
+ export type PageImage = string | HTMLImageElement | HTMLCanvasElement | ImageBitmap | Texture;
4
+ /** A page image with what's on it, for screen readers and the plain view. */
5
+ export interface PageDescriptor {
6
+ src: PageImage;
7
+ /** A short description of the page, like an image's alt text. */
8
+ alt?: string;
9
+ /** The page's text, or a transcript of it. Blank lines separate paragraphs. */
10
+ text?: string;
11
+ }
12
+ /** A page: its image, or a descriptor with the image and what's on it. */
13
+ export type PageSource = PageImage | PageDescriptor;
14
+ /**
15
+ * `spread`: an open magazine, two pages side by side, sheets turning over the spine.
16
+ * `single`: one page at a time, read pages rolling up behind the magazine.
17
+ */
18
+ export type Layout = 'spread' | 'single';
19
+ export interface PaperOptions {
20
+ /**
21
+ * Paper fibre texture. `true` uses the built-in one, `false` turns it off, or pass a
22
+ * tiling greyscale image (or its URL). Only the brightest specks show. Default `true`.
23
+ */
24
+ grain?: boolean | string | HTMLImageElement | HTMLCanvasElement;
25
+ /** How many times the grain tiles across a page. Default `1.46`. */
26
+ grainScale?: number;
27
+ /** How much the grain darkens the paper, 0–1. Default `0.17`. */
28
+ grainStrength?: number;
29
+ /** How much of the other side's print shows through, 0–1. Default `0.04` (spread), `0.3` (single). */
30
+ showThrough?: number;
31
+ /** How much glossier ink is than bare paper, 0–1. Default `0.33`. */
32
+ inkGloss?: number;
33
+ /** Default `0.5`. */
34
+ roughness?: number;
35
+ /** A touch of metalness gives the paper its sheen. Default `0.17`. */
36
+ metalness?: number;
37
+ /**
38
+ * Colour of unprinted paper: the blank back of single-layout pages, and what shows through
39
+ * transparent parts of page images. Default `#fcfcf9`.
40
+ */
41
+ backColor?: ColorRepresentation;
42
+ /** Scales the waviness of resting sheets (spread). `0` lays them flat. Default `1`. */
43
+ wrinkles?: number;
44
+ /** Scales how much resting pages bulge up off the spine. Default `1`. */
45
+ bulge?: number;
46
+ }
47
+ export interface LightingOptions {
48
+ /** Hemisphere light. Default sky `#ffffff`, ground `#a1aeaf`, intensity `1.5` (spread) or `1.7` (single). */
49
+ ambient?: {
50
+ sky?: ColorRepresentation;
51
+ ground?: ColorRepresentation;
52
+ intensity?: number;
53
+ };
54
+ /** The one directional light. `position` is in page widths, +z towards the viewer. */
55
+ sun?: {
56
+ color?: ColorRepresentation;
57
+ intensity?: number;
58
+ position?: [number, number, number];
59
+ };
60
+ /** Default `true`. */
61
+ shadows?: boolean;
62
+ /** Shadow map resolution. Default `2048`. */
63
+ shadowMapSize?: number;
64
+ /** Opacity of the shadow the magazine casts on the surface under it (spread). `0` hides it. Default `0.15`. */
65
+ floorShadow?: number;
66
+ }
67
+ export interface MotionOptions {
68
+ /** Multiplies the speed of every page turn. Default `1`. */
69
+ speed?: number;
70
+ /** Share of the canvas width a drag must travel to turn a page all the way. Default `0.8` (spread), `1.2` (single). */
71
+ dragDistance?: number;
72
+ /** Share of a turn a released drag must have made to complete it. Default `0.2` (spread), `0.1` (single). */
73
+ commitThreshold?: number;
74
+ /** Scales how much a turning sheet bends. Default `1`. */
75
+ curl?: number;
76
+ /** Largest tilt of the fold, in degrees, from where the page is held. Default `45` (spread), `35` (single). */
77
+ maxTilt?: number;
78
+ /** Small random differences between turns, so no two look the same. Default `true`. */
79
+ jitter?: boolean;
80
+ /** How far a hovered sheet lifts, as a share of a turn (spread). `0` disables. Default `0.03`. */
81
+ hoverLift?: number;
82
+ /**
83
+ * `'auto'` follows the user's `prefers-reduced-motion`. When reduced, turns are quicker and
84
+ * hovering doesn't lift pages. Default `'auto'`.
85
+ */
86
+ reducedMotion?: 'auto' | boolean;
87
+ }
88
+ export interface InteractionOptions {
89
+ /** Drag pages to turn them. Default `true`. */
90
+ drag?: boolean;
91
+ /** Click or tap a page to turn it. Default `true`. */
92
+ click?: boolean;
93
+ /** Press and hold to keep turning pages. Default `true`. */
94
+ hold?: boolean;
95
+ /** Hovering lifts the top sheet (spread). Default `true`. */
96
+ hover?: boolean;
97
+ /**
98
+ * Arrow keys, PageUp/PageDown, Home/End. `true` listens while the magazine has focus,
99
+ * `'global'` listens on the whole window. Default `true`.
100
+ */
101
+ keyboard?: boolean | 'global';
102
+ }
103
+ export interface PageChangeDetail {
104
+ /** First visible page, 0-based. */
105
+ page: number;
106
+ /** Every page currently showing, 0-based. */
107
+ visiblePages: number[];
108
+ pageCount: number;
109
+ layout: Layout;
110
+ }
111
+ export interface LeafThroughOptions {
112
+ /** The pages in reading order, front cover first. */
113
+ pages: PageSource[];
114
+ /** Page to open at, 0-based. Default `0`. Later changes are ignored; use `goTo()`. */
115
+ startPage?: number;
116
+ /** `'auto'` picks `spread` when `spreadQuery` matches, `single` otherwise. Default `'auto'`. */
117
+ layout?: 'auto' | Layout;
118
+ /** Media query for the spread layout. Default `(min-width: 768px) and (orientation: landscape)`. */
119
+ spreadQuery?: string;
120
+ /** Page height divided by width. Default `1.377`. */
121
+ pageAspect?: number;
122
+ /** Single layout: after the last page, close and start again from the cover. Default `true`. */
123
+ loop?: boolean;
124
+ /**
125
+ * `true` fits the magazine to the container and lets turning pages and shadows spill
126
+ * outside it, up to the edges of the document (the container must not clip).
127
+ * `false` keeps everything inside the container, which makes the pages smaller.
128
+ * Default `true`.
129
+ */
130
+ bleed?: boolean;
131
+ paper?: PaperOptions;
132
+ lighting?: LightingOptions;
133
+ motion?: MotionOptions;
134
+ interaction?: InteractionOptions;
135
+ /** Upper limit for the canvas pixel ratio. Default `2`. */
136
+ maxPixelRatio?: number;
137
+ /**
138
+ * How many pages either side of the visible ones to load ahead. Visible means on screen:
139
+ * pages mid-turn count, and in `single` so do read pages still rolled up behind the magazine.
140
+ * Pages more than twice as far away are unloaded, which frees their memory. `Infinity` loads
141
+ * and keeps every page. Default `6`.
142
+ */
143
+ preload?: number;
144
+ /**
145
+ * Shows the pages as flat images, without 3D. The magazine switches to this by itself when
146
+ * the browser can't run WebGL. Default `false`.
147
+ */
148
+ plain?: boolean;
149
+ /** Accessible name. Default `'Magazine'`. */
150
+ label?: string;
151
+ /** Text announced to screen readers when pages change. */
152
+ formatPageLabel?: (detail: PageChangeDetail) => string;
153
+ onPageChange?: (detail: PageChangeDetail) => void;
154
+ onReady?: () => void;
155
+ /** A page's image has loaded. Fires again if the page is unloaded and loads later. */
156
+ onPageLoad?: (page: number) => void;
157
+ onError?: (error: unknown, page?: number) => void;
158
+ }
159
+ export declare const DEFAULT_SPREAD_QUERY = "(min-width: 768px) and (orientation: landscape)";
160
+ export declare const DEFAULT_PAGE_ASPECT = 1.377;
161
+ export declare function defaultPageLabel({ visiblePages, pageCount }: PageChangeDetail): string;
162
+ export declare function isPageDescriptor(page: PageSource): page is PageDescriptor;
163
+ export declare const pageImage: (page: PageSource) => PageImage;
164
+ /** Everything an engine needs, with layout defaults filled in. */
165
+ export interface ResolvedSettings {
166
+ layout: Layout;
167
+ pageAspect: number;
168
+ loop: boolean;
169
+ bleed: boolean;
170
+ paper: Required<Omit<PaperOptions, 'grain'>> & {
171
+ grain: PaperOptions['grain'];
172
+ };
173
+ lighting: {
174
+ ambient: {
175
+ sky: ColorRepresentation;
176
+ ground: ColorRepresentation;
177
+ intensity: number;
178
+ };
179
+ sun: {
180
+ color: ColorRepresentation;
181
+ intensity: number;
182
+ position: [number, number, number];
183
+ };
184
+ shadows: boolean;
185
+ shadowMapSize: number;
186
+ floorShadow: number;
187
+ };
188
+ motion: Required<Omit<MotionOptions, 'reducedMotion'>> & {
189
+ reduced: boolean;
190
+ };
191
+ interaction: Required<InteractionOptions>;
192
+ maxPixelRatio: number;
193
+ }
194
+ export declare function resolveSettings(options: LeafThroughOptions, layout: Layout, prefersReducedMotion: boolean): ResolvedSettings;
@@ -0,0 +1,34 @@
1
+ import * as THREE from 'three';
2
+ import type { ResolvedSettings } from '../options.js';
3
+ export type SheetUniforms = Record<string, THREE.IUniform>;
4
+ /** Uniforms every sheet in a scene shares. */
5
+ export interface SheetShading {
6
+ /** Direction across of the turning sheet's root (x, z) and whether one is turning. */
7
+ spineContact: THREE.IUniform<THREE.Vector3>;
8
+ /** Towards the sun, in the magazine's frame. */
9
+ lightDirection: THREE.IUniform<THREE.Vector3>;
10
+ }
11
+ export interface SheetShader {
12
+ /** GLSL defining `vec3 sheetPosition(vec2 uv, float noise)` and its uniforms. */
13
+ vertexChunk: string;
14
+ /** GLSL defining `bool sheetHidden(vec3 p)` and its uniforms: true where not to draw. */
15
+ fragmentChunk?: string;
16
+ /** Finite difference step for normals, in page widths. */
17
+ epsilon: number;
18
+ /** Read per-vertex noise from the `aNoise` attribute (declared by the chunk). */
19
+ noise?: boolean;
20
+ /** Nudges the shadow caster sideways. */
21
+ shadowOffsetX: number;
22
+ }
23
+ export type SheetMesh = THREE.Mesh<THREE.BufferGeometry, THREE.MeshStandardMaterial>;
24
+ export declare function createSheetMesh(geometry: THREE.BufferGeometry, uniforms: SheetUniforms, shader: SheetShader, settings: ResolvedSettings): SheetMesh;
25
+ /**
26
+ * Every sheet rises from the spine at height 0, so along the spine they all but touch and
27
+ * the depth buffer can't separate them: sheets further down flicker through as a light seam.
28
+ * Bias each sheet's depth by its place in the stack instead: the top sheet wins, and a sheet
29
+ * that's turning wins over everything.
30
+ */
31
+ export declare function layerSheet(mesh: SheetMesh, depth: number, count: number, moving: boolean): void;
32
+ export declare function disposeSheetMesh(mesh: SheetMesh): void;
33
+ /** A 1×1 texture of one colour, for blank pages and placeholders. */
34
+ export declare function solidTexture(color: THREE.ColorRepresentation): THREE.DataTexture;
@@ -0,0 +1,2 @@
1
+ import * as THREE from 'three';
2
+ export declare function createPaperPattern(renderer: THREE.WebGLRenderer): THREE.CanvasTexture;