@zinejs/core 0.1.0

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 ADDED
@@ -0,0 +1,70 @@
1
+ # @zinejs/core
2
+
3
+ Framework-agnostic flipbook engine: turn images or PDFs into a page-flipping book. Zero dependencies. Ships a **CSS renderer** (works everywhere, no GPU) and a **WebGL2 renderer** (page-curl, auto-selected where a GPU is available), behind one API — you never choose a renderer.
4
+
5
+ > Pre-1.0 and under active development. Licensed **PolyForm Noncommercial 1.0.0** (free for non-commercial use).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @zinejs/core
11
+ ```
12
+
13
+ ## Image book
14
+
15
+ ```js
16
+ import { Zine, ImageSource } from '@zinejs/core';
17
+
18
+ const book = new Zine(document.getElementById('book'), {
19
+ source: new ImageSource([
20
+ 'page-01.png',
21
+ 'page-02.png',
22
+ 'page-03.png',
23
+ 'page-04.png',
24
+ ]),
25
+ });
26
+ ```
27
+
28
+ Give the container a size (e.g. `#book { width: 900px; height: 560px; }`). Flip by dragging a corner or clicking near an edge; zoom with double-click, Ctrl/⌘+wheel, or pinch.
29
+
30
+ ## PDF book
31
+
32
+ Install the plugin (see [`@zinejs/pdf`](https://www.npmjs.com/package/@zinejs/pdf)):
33
+
34
+ ```js
35
+ import { Zine } from '@zinejs/core';
36
+ import { PdfSource } from '@zinejs/pdf';
37
+
38
+ new Zine(document.getElementById('book'), {
39
+ source: new PdfSource('document.pdf'),
40
+ });
41
+ ```
42
+
43
+ ## Options
44
+
45
+ ```ts
46
+ new Zine(container, {
47
+ source, // ImageSource | PdfSource (required)
48
+ direction: 'ltr', // 'ltr' | 'rtl'
49
+ cover: false, // treat the first page as a standalone cover
50
+ startPage: 0,
51
+ flipDuration: 800, // ms
52
+ singlePageThreshold: 640, // px: below this, show one page at a time
53
+ clickToFlip: 'edge', // 'edge' | 'half' | 'off'
54
+ zoom: { enabled: true, max: 4, wheel: true, doubleClick: [1, 2, 4] },
55
+ renderer: 'auto', // 'auto' | 'css' | 'webgl2' | a custom Renderer
56
+ });
57
+ ```
58
+
59
+ ## Methods & events
60
+
61
+ ```js
62
+ book.flipNext(); book.flipPrev(); book.flipTo(12);
63
+ book.getPage(); book.getPageCount();
64
+ book.setZoom(2); book.resetZoom();
65
+ book.update(); book.destroy();
66
+
67
+ book.on('ready' | 'pageChanged' | 'flipStart' | 'flipEnd' | 'zoomChanged' | 'sourceError' | 'rendererFallback', handler);
68
+ ```
69
+
70
+ A JSON Schema for the options is published at `@zinejs/core/options.schema.json`.
@@ -0,0 +1,68 @@
1
+ import { t as e } from "./flipProgressToPose-DZWSuKoE.js";
2
+ //#region src/renderer/cssRenderer.ts
3
+ var t = 180 / Math.PI;
4
+ function n(e) {
5
+ let t = e === "forward" ? "to right" : "to left";
6
+ return [
7
+ `linear-gradient(${t}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,
8
+ `linear-gradient(${t}, transparent 40%, rgba(255,255,255,0.32) 66%, transparent 90%)`,
9
+ `linear-gradient(${t}, transparent 66%, rgba(0,0,0,0.35))`
10
+ ].join(",");
11
+ }
12
+ var r = class {
13
+ #e = null;
14
+ #t;
15
+ #n;
16
+ #r;
17
+ #i;
18
+ #a;
19
+ #o;
20
+ #s;
21
+ #c;
22
+ #l;
23
+ mount(e) {
24
+ let t = e.ownerDocument;
25
+ return this.#e = e, this.#t = t.createElement("div"), this.#t.className = "zine-clip", this.#t.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden;", this.#n = t.createElement("div"), this.#n.className = "zine-viewport", this.#n.style.cssText = "position:absolute;inset:0;transform-origin:0 0;", this.#r = t.createElement("div"), this.#r.className = "zine-book", this.#r.style.cssText = "position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;", this.#i = this.#u(t, "zine-page-left", "left:0;"), this.#a = this.#u(t, "zine-page-right", "right:0;"), this.#o = t.createElement("div"), this.#o.className = "zine-leaf", this.#o.style.cssText = "position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;", this.#s = t.createElement("canvas"), this.#s.className = "zine-leaf-front", this.#s.style.cssText = "position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;", this.#c = t.createElement("canvas"), this.#c.className = "zine-leaf-back", this.#c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);", this.#l = t.createElement("div"), this.#l.className = "zine-leaf-shadow", this.#l.style.cssText = "position:absolute;inset:0;opacity:0;pointer-events:none;", this.#l.style.background = n("forward"), this.#o.append(this.#s, this.#c, this.#l), this.#r.append(this.#i, this.#a, this.#o), this.#n.append(this.#r), this.#t.append(this.#n), e.append(this.#t), Promise.resolve();
26
+ }
27
+ destroy() {
28
+ this.#t?.remove(), this.#e = null;
29
+ }
30
+ renderSpread(e, t, n) {
31
+ n?.fill ? (this.#i.style.width = "100%", this.#a.style.display = "none", this.#d(this.#i, t.right ?? t.left)) : (this.#i.style.width = "50%", this.#a.style.display = "", this.#d(this.#i, t.left), this.#d(this.#a, t.right)), this.#o.style.display = "none", this.#o.style.transform = "", this.#l.style.opacity = "0";
32
+ }
33
+ beginFlip(e, t, r, i) {
34
+ i?.fill ? (this.#i.style.width = "100%", this.#a.style.display = "none", this.#o.style.width = "100%", this.#o.style.left = "0", this.#o.style.transformOrigin = r === "forward" ? "left center" : "right center", this.#d(this.#i, t.right ?? t.left), this.#d(this.#s, e.right ?? e.left), this.#d(this.#c, t.right ?? t.left)) : (this.#i.style.width = "50%", this.#a.style.display = "", this.#o.style.width = "50%", r === "forward" ? (this.#d(this.#i, e.left), this.#d(this.#a, t.right), this.#d(this.#s, e.right), this.#d(this.#c, t.left), this.#o.style.left = "50%", this.#o.style.transformOrigin = "left center") : (this.#d(this.#a, e.right), this.#d(this.#i, t.left), this.#d(this.#s, e.left), this.#d(this.#c, t.right), this.#o.style.left = "0", this.#o.style.transformOrigin = "right center")), this.#o.style.display = "block", this.#o.style.transform = "rotateY(0deg)", this.#l.style.background = n(r), this.#l.style.opacity = "0";
35
+ }
36
+ setFlipProgress(n, r) {
37
+ let i = e(n), a = i.angle * t;
38
+ this.#o.style.display = "block", this.#o.style.transform = `rotateY(${r === "forward" ? -a : a}deg)`, this.#l.style.opacity = String(i.shadowAlpha);
39
+ }
40
+ setViewTransform(e, t, n) {
41
+ this.#n.style.transform = `translate(${t}px, ${n}px) scale(${e})`;
42
+ }
43
+ measure() {
44
+ let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0;
45
+ return {
46
+ containerWidth: t,
47
+ containerHeight: n,
48
+ pageWidth: t / 2,
49
+ pageHeight: n
50
+ };
51
+ }
52
+ #u(e, t, n) {
53
+ let r = e.createElement("canvas");
54
+ return r.className = t, r.style.cssText = `position:absolute;top:0;width:50%;height:100%;${n}`, r;
55
+ }
56
+ #d(e, t) {
57
+ let n = e.getContext("2d");
58
+ if (t === null) {
59
+ n?.clearRect(0, 0, e.width, e.height);
60
+ return;
61
+ }
62
+ e.width = t.width, e.height = t.height, n?.drawImage(t, 0, 0);
63
+ }
64
+ };
65
+ //#endregion
66
+ export { r as CssRenderer };
67
+
68
+ //# sourceMappingURL=cssRenderer-BACNXO8M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cssRenderer-BACNXO8M.js","names":["#container","#clip","#viewport","#book","#pageLeft","#makeCanvas","#pageRight","#leaf","#leafFront","#leafBack","#leafShadow","#paint"],"sources":["../src/renderer/cssRenderer.ts"],"sourcesContent":["import { flipProgressToPose } from '../geometry/flipProgressToPose';\nimport type { Spread } from '../engine/spread';\nimport type {\n FlipDirection,\n LayoutMetrics,\n PageContent,\n Renderer,\n RenderOptions,\n SpreadContent,\n} from './types';\n\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/**\n * Layered gradients painted over the turning leaf to fake paper depth on the flat\n * fold: a crease shadow at the spine, a soft curve sheen, and a free-edge shade.\n * The spine is the leaf's pivot edge — left for a forward flip, right for backward.\n */\nfunction foldOverlay(direction: FlipDirection): string {\n const dir = direction === 'forward' ? 'to right' : 'to left';\n return [\n `linear-gradient(${dir}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,\n `linear-gradient(${dir}, transparent 40%, rgba(255,255,255,0.32) 66%, transparent 90%)`,\n `linear-gradient(${dir}, transparent 66%, rgba(0,0,0,0.35))`,\n ].join(',');\n}\n\n/**\n * Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages\n * are <canvas> elements; the turning leaf (front + back faces, backface-hidden)\n * rotates about the spine driven by flipProgressToPose(), with a gradient shadow\n * overlay for depth. `beginFlip` stages the surfaces; `setFlipProgress` animates.\n */\nexport class CssRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #clip!: HTMLDivElement;\n #viewport!: HTMLDivElement;\n #book!: HTMLDivElement;\n #pageLeft!: HTMLCanvasElement;\n #pageRight!: HTMLCanvasElement;\n #leaf!: HTMLDivElement;\n #leafFront!: HTMLCanvasElement;\n #leafBack!: HTMLCanvasElement;\n #leafShadow!: HTMLDivElement;\n\n mount(container: HTMLElement): Promise<void> {\n const doc = container.ownerDocument;\n this.#container = container;\n\n // Fixed-size clip box; NOT transformed, so zoom can't scale the crop region.\n this.#clip = doc.createElement('div');\n this.#clip.className = 'zine-clip';\n this.#clip.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;';\n\n // Transform target for zoom/pan; lives inside the clip.\n this.#viewport = doc.createElement('div');\n this.#viewport.className = 'zine-viewport';\n this.#viewport.style.cssText = 'position:absolute;inset:0;transform-origin:0 0;';\n\n this.#book = doc.createElement('div');\n this.#book.className = 'zine-book';\n this.#book.style.cssText =\n 'position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;';\n\n this.#pageLeft = this.#makeCanvas(doc, 'zine-page-left', 'left:0;');\n this.#pageRight = this.#makeCanvas(doc, 'zine-page-right', 'right:0;');\n\n this.#leaf = doc.createElement('div');\n this.#leaf.className = 'zine-leaf';\n this.#leaf.style.cssText =\n 'position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;';\n\n this.#leafFront = doc.createElement('canvas');\n this.#leafFront.className = 'zine-leaf-front';\n this.#leafFront.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;';\n\n this.#leafBack = doc.createElement('canvas');\n this.#leafBack.className = 'zine-leaf-back';\n this.#leafBack.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);';\n\n this.#leafShadow = doc.createElement('div');\n this.#leafShadow.className = 'zine-leaf-shadow';\n this.#leafShadow.style.cssText = 'position:absolute;inset:0;opacity:0;pointer-events:none;';\n this.#leafShadow.style.background = foldOverlay('forward'); // default; beginFlip sets per direction\n\n this.#leaf.append(this.#leafFront, this.#leafBack, this.#leafShadow);\n this.#book.append(this.#pageLeft, this.#pageRight, this.#leaf);\n this.#viewport.append(this.#book);\n this.#clip.append(this.#viewport);\n container.append(this.#clip);\n\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clip?.remove();\n this.#container = null;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n if (options?.fill) {\n // Single-page mode: the lone page fills the container; hide the other panel.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#paint(this.#pageLeft, content.right ?? content.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#paint(this.#pageLeft, content.left);\n this.#paint(this.#pageRight, content.right);\n }\n // A fresh spread cancels any in-progress flip.\n this.#leaf.style.display = 'none';\n this.#leaf.style.transform = '';\n this.#leafShadow.style.opacity = '0';\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n if (options?.fill) {\n // Single-page: a full-width leaf turns about one edge, revealing `to` underneath.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#leaf.style.width = '100%';\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = direction === 'forward' ? 'left center' : 'right center';\n this.#paint(this.#pageLeft, to.right ?? to.left);\n this.#paint(this.#leafFront, from.right ?? from.left);\n this.#paint(this.#leafBack, to.right ?? to.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#leaf.style.width = '50%';\n if (direction === 'forward') {\n // Right page lifts and swings left about the spine. Left stays; right reveals `to`.\n this.#paint(this.#pageLeft, from.left);\n this.#paint(this.#pageRight, to.right);\n this.#paint(this.#leafFront, from.right);\n this.#paint(this.#leafBack, to.left);\n this.#leaf.style.left = '50%';\n this.#leaf.style.transformOrigin = 'left center';\n } else {\n // Left page swings right about its right edge. Right stays; left reveals `to`.\n this.#paint(this.#pageRight, from.right);\n this.#paint(this.#pageLeft, to.left);\n this.#paint(this.#leafFront, from.left);\n this.#paint(this.#leafBack, to.right);\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = 'right center';\n }\n }\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = 'rotateY(0deg)';\n this.#leafShadow.style.background = foldOverlay(direction);\n this.#leafShadow.style.opacity = '0';\n }\n\n setFlipProgress(t: number, direction: FlipDirection): void {\n const pose = flipProgressToPose(t);\n const deg = pose.angle * RAD_TO_DEG;\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = `rotateY(${direction === 'forward' ? -deg : deg}deg)`;\n this.#leafShadow.style.opacity = String(pose.shadowAlpha);\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#viewport.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n return { containerWidth: w, containerHeight: h, pageWidth: w / 2, pageHeight: h };\n }\n\n #makeCanvas(doc: Document, className: string, extra: string): HTMLCanvasElement {\n const c = doc.createElement('canvas');\n c.className = className;\n c.style.cssText = `position:absolute;top:0;width:50%;height:100%;${extra}`;\n return c;\n }\n\n #paint(canvas: HTMLCanvasElement, content: PageContent | null): void {\n const ctx = canvas.getContext('2d');\n if (content === null) {\n ctx?.clearRect(0, 0, canvas.width, canvas.height);\n return;\n }\n canvas.width = content.width;\n canvas.height = content.height;\n ctx?.drawImage(content, 0, 0);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export type Listener<T> = (payload: T) => void;
2
+ /** Events typed as `void` are emitted with no payload argument. */
3
+ type PayloadArgs<T> = [T] extends [void] ? [] : [payload: T];
4
+ /** Minimal typed event emitter keyed by an event map. */
5
+ export declare class Emitter<Events> {
6
+ #private;
7
+ /** Subscribe; returns an unsubscribe function. */
8
+ on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void;
9
+ off<K extends keyof Events>(type: K, listener: Listener<Events[K]>): void;
10
+ emit<K extends keyof Events>(type: K, ...args: PayloadArgs<Events[K]>): void;
11
+ clear(): void;
12
+ }
13
+ /**
14
+ * Public event surface of a Zine instance. Payloads are provisional and will
15
+ * firm up as the engine wires the emitters that fire them.
16
+ */
17
+ export interface ZineEventMap {
18
+ ready: void;
19
+ flipStart: {
20
+ from: number;
21
+ to: number;
22
+ };
23
+ flipEnd: {
24
+ page: number;
25
+ };
26
+ pageChanged: {
27
+ page: number;
28
+ };
29
+ zoomChanged: {
30
+ scale: number;
31
+ };
32
+ sourceError: {
33
+ index: number;
34
+ error: unknown;
35
+ };
36
+ rendererFallback: {
37
+ from: string;
38
+ to: string;
39
+ };
40
+ }
41
+ export type ZineEmitter = Emitter<ZineEventMap>;
42
+ export {};
@@ -0,0 +1,94 @@
1
+ export interface GestureStart {
2
+ x: number;
3
+ y: number;
4
+ t: number;
5
+ }
6
+ export interface GestureMove {
7
+ x: number;
8
+ y: number;
9
+ /** Displacement from the start point. */
10
+ dx: number;
11
+ dy: number;
12
+ }
13
+ export interface GestureEnd {
14
+ x: number;
15
+ y: number;
16
+ dx: number;
17
+ dy: number;
18
+ /** Elapsed time from start to end (ms). */
19
+ dt: number;
20
+ /** Velocity of the final move segment (px/ms). */
21
+ vx: number;
22
+ vy: number;
23
+ /** A fast flick: final speed exceeded the swipe threshold and the gesture was not canceled. */
24
+ swipe: boolean;
25
+ /** The pointer was canceled (e.g. pointercancel) rather than lifted. */
26
+ canceled: boolean;
27
+ }
28
+ export interface PointerHandlers {
29
+ onStart?: (g: GestureStart) => void;
30
+ onMove?: (g: GestureMove) => void;
31
+ onEnd?: (g: GestureEnd) => void;
32
+ }
33
+ /**
34
+ * Recognizes a single-pointer drag from raw pointer samples and reports normalized
35
+ * gestures. DOM-agnostic on purpose: `bindPointerInput` maps real PointerEvents onto
36
+ * it, but the kinematics stay pure and unit-testable. Extra pointers are ignored
37
+ * (multi-touch pinch is a separate recognizer).
38
+ */
39
+ export declare class PointerRecognizer {
40
+ #private;
41
+ constructor(handlers?: PointerHandlers);
42
+ get active(): boolean;
43
+ down(id: number, x: number, y: number, t: number): void;
44
+ move(id: number, x: number, y: number, t: number): void;
45
+ up(id: number, x: number, y: number, t: number): void;
46
+ cancel(id: number, x: number, y: number, t: number): void;
47
+ }
48
+ /**
49
+ * Wire real PointerEvents from `target` into a recognizer. Returns an unbind
50
+ * function that removes every listener. Coordinates are passed through as
51
+ * client-space; mapping to page-local space is the engine's job.
52
+ */
53
+ export declare function bindPointerInput(target: EventTarget, recognizer: PointerRecognizer): () => void;
54
+ /**
55
+ * Wire PointerEvents from `target` into a drag recognizer and/or a pinch
56
+ * recognizer at once, so a single pointer stream feeds both. Returns an unbind.
57
+ */
58
+ export declare function bindGestures(target: EventTarget, recognizers: {
59
+ pointer?: PointerRecognizer;
60
+ pinch?: PinchRecognizer;
61
+ }): () => void;
62
+ export interface PinchStart {
63
+ centerX: number;
64
+ centerY: number;
65
+ /** Distance between the two pointers at pinch start. */
66
+ distance: number;
67
+ }
68
+ export interface PinchMove {
69
+ centerX: number;
70
+ centerY: number;
71
+ /** Current distance / start distance. */
72
+ scale: number;
73
+ }
74
+ export interface PinchHandlers {
75
+ onPinchStart?: (g: PinchStart) => void;
76
+ onPinchMove?: (g: PinchMove) => void;
77
+ onPinchEnd?: () => void;
78
+ }
79
+ /**
80
+ * Recognizes a two-pointer pinch and reports zoom scale (relative to the start
81
+ * distance) and the live center point. No timestamps: pinch drives zoom, not a
82
+ * flick. A third pointer is ignored; lifting either tracked pointer ends the pinch.
83
+ * Coordinating this with the drag recognizer (a second finger should suspend a
84
+ * single-finger drag) is the engine's job.
85
+ */
86
+ export declare class PinchRecognizer {
87
+ #private;
88
+ constructor(handlers?: PinchHandlers);
89
+ get pinching(): boolean;
90
+ down(id: number, x: number, y: number): void;
91
+ move(id: number, x: number, y: number): void;
92
+ up(id: number): void;
93
+ cancel(id: number): void;
94
+ }
@@ -0,0 +1,24 @@
1
+ export type Direction = 'ltr' | 'rtl';
2
+ export interface Spread {
3
+ /** Page index shown on the left, or null for a blank side. */
4
+ left: number | null;
5
+ /** Page index shown on the right, or null for a blank side. */
6
+ right: number | null;
7
+ }
8
+ export interface SpreadOptions {
9
+ direction?: Direction;
10
+ /** First page is a lone cover on the leading side; interior pages pair up after it. */
11
+ cover?: boolean;
12
+ /** One page per spread (narrow/portrait viewports). Takes precedence over `cover`. */
13
+ singlePage?: boolean;
14
+ }
15
+ /**
16
+ * Map a flat page list (indices 0..pageCount-1) into display spreads.
17
+ *
18
+ * Reading order is always preserved; `direction` only mirrors each spread's
19
+ * left/right positions, so RTL is just an LTR layout with the sides swapped.
20
+ * Out-of-range slots are `null` (a blank half on odd counts / the back cover).
21
+ */
22
+ export declare function buildSpreads(pageCount: number, options?: SpreadOptions): Spread[];
23
+ /** Whether a container this wide should fall back to one page per spread. */
24
+ export declare function shouldSinglePage(containerWidth: number, threshold: number): boolean;
@@ -0,0 +1,12 @@
1
+ export type FlipState = 'idle' | 'dragging' | 'animating' | 'zoomed-panning';
2
+ export type FlipEvent = 'grab' | 'release' | 'settle' | 'panStart' | 'panEnd' | 'flip';
3
+ /** Pure transition: the next state, or null if `event` is illegal from `state`. */
4
+ export declare function nextState(state: FlipState, event: FlipEvent): FlipState | null;
5
+ /** Stateful holder around `nextState`; illegal events are ignored. */
6
+ export declare class FlipMachine {
7
+ #private;
8
+ constructor(initial?: FlipState);
9
+ get state(): FlipState;
10
+ /** Apply `event`; returns the new state, or null (no-op) if the event was illegal. */
11
+ send(event: FlipEvent): FlipState | null;
12
+ }
@@ -0,0 +1,18 @@
1
+ /** Spread indices that should be mounted around `current`, clamped to the book. */
2
+ export declare function mountWindow(current: number, spreadCount: number, radius: number): number[];
3
+ export interface VirtualizerDelta {
4
+ toMount: number[];
5
+ toEvict: number[];
6
+ }
7
+ /**
8
+ * Tracks which spreads are mounted and, on each move, reports the difference:
9
+ * spreads entering the ±radius window (`toMount`) and those leaving it
10
+ * (`toEvict`). Default radius 2 = current spread ±2.
11
+ */
12
+ export declare class Virtualizer {
13
+ #private;
14
+ constructor(radius?: number);
15
+ get mounted(): number[];
16
+ /** Recenter on `current`; mounts/evicts to match the window and returns the delta. */
17
+ update(current: number, spreadCount: number): VirtualizerDelta;
18
+ }
@@ -0,0 +1,13 @@
1
+ //#region src/geometry/flipProgressToPose.ts
2
+ function e(e) {
3
+ let t = e < 0 ? 0 : e > 1 ? 1 : e, n = Math.sin(t * Math.PI);
4
+ return {
5
+ angle: t * Math.PI,
6
+ curl: n,
7
+ shadowAlpha: n
8
+ };
9
+ }
10
+ //#endregion
11
+ export { e as t };
12
+
13
+ //# sourceMappingURL=flipProgressToPose-DZWSuKoE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flipProgressToPose-DZWSuKoE.js","names":[],"sources":["../src/geometry/flipProgressToPose.ts"],"sourcesContent":["export interface FlipPose {\n /** Turning-page rotation about the spine, radians: 0 (flat/open) → π (fully turned). */\n angle: number;\n /** Mesh cylinder-wrap curl, 0..1 — flat at the ends, peaks edge-on at t=0.5. CSS renderer ignores it. */\n curl: number;\n /** Fold-shadow intensity, 0..1 — peaks edge-on at t=0.5. */\n shadowAlpha: number;\n}\n\n/**\n * Map a 0→1 flip progress to a renderer-neutral fold pose.\n *\n * Linear in `t` on purpose — easing belongs to the animator that produces `t`,\n * so seeking to a fixed progress (e.g. the 0/.25/.5/.75/1 visual-regression stops)\n * is deterministic. The CSS renderer consumes `angle` as a spine `rotateY`; the\n * WebGL2 renderer additionally bends its mesh by `curl`.\n */\nexport function flipProgressToPose(t: number): FlipPose {\n const clamped = t < 0 ? 0 : t > 1 ? 1 : t;\n const arc = Math.sin(clamped * Math.PI);\n return {\n angle: clamped * Math.PI,\n curl: arc,\n shadowAlpha: arc,\n };\n}\n"],"mappings":";AAiBA,SAAgB,EAAmB,GAAqB;CACtD,IAAM,IAAU,IAAI,IAAI,IAAI,IAAI,IAAI,IAAI,GAClC,IAAM,KAAK,IAAI,IAAU,KAAK,EAAE;CACtC,OAAO;EACL,OAAO,IAAU,KAAK;EACtB,MAAM;EACN,aAAa;CACf;AACF"}
@@ -0,0 +1,17 @@
1
+ export interface FlipPose {
2
+ /** Turning-page rotation about the spine, radians: 0 (flat/open) → π (fully turned). */
3
+ angle: number;
4
+ /** Mesh cylinder-wrap curl, 0..1 — flat at the ends, peaks edge-on at t=0.5. CSS renderer ignores it. */
5
+ curl: number;
6
+ /** Fold-shadow intensity, 0..1 — peaks edge-on at t=0.5. */
7
+ shadowAlpha: number;
8
+ }
9
+ /**
10
+ * Map a 0→1 flip progress to a renderer-neutral fold pose.
11
+ *
12
+ * Linear in `t` on purpose — easing belongs to the animator that produces `t`,
13
+ * so seeking to a fixed progress (e.g. the 0/.25/.5/.75/1 visual-regression stops)
14
+ * is deterministic. The CSS renderer consumes `angle` as a spine `rotateY`; the
15
+ * WebGL2 renderer additionally bends its mesh by `curl`.
16
+ */
17
+ export declare function flipProgressToPose(t: number): FlipPose;
@@ -0,0 +1,30 @@
1
+ export interface Vec2 {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export interface PageRect {
6
+ width: number;
7
+ height: number;
8
+ }
9
+ export interface FoldLine {
10
+ x1: number;
11
+ y1: number;
12
+ x2: number;
13
+ y2: number;
14
+ }
15
+ export interface Fold {
16
+ /** Crease line, clipped to the page rect (page-local coords). */
17
+ foldLine: FoldLine;
18
+ /** Crease orientation in radians (atan2 of the crease direction). */
19
+ angle: number;
20
+ /** Fold-shadow intensity, 0..1. */
21
+ shadowAlpha: number;
22
+ }
23
+ /**
24
+ * Book-fold geometry from a dragged corner.
25
+ *
26
+ * The folded flap reflects the grabbed `corner` onto the pointer `point`, so the
27
+ * crease is the perpendicular bisector of the segment corner→point. Coordinates
28
+ * are page-local: (0,0) top-left, x right, y down, page spanning (0,0)..(width,height).
29
+ */
30
+ export declare function foldFromPointer(point: Vec2, corner: Vec2, pageRect: PageRect): Fold;
@@ -0,0 +1,11 @@
1
+ import type { PageRect, Vec2 } from './foldFromPointer';
2
+ export type HitRegion = 'corner' | 'page' | 'outside';
3
+ /**
4
+ * Classify a page-local point into a hit region.
5
+ *
6
+ * Coordinates are page-local (see `foldFromPointer`): (0,0) top-left,
7
+ * page spanning (0,0)..(width,height). `cornerSize` is the width/height of each
8
+ * grabbable corner box. All four corners report `corner`; the engine decides
9
+ * which are actually grabbable for the current spread layout (LTR/RTL, cover).
10
+ */
11
+ export declare function hitTest(point: Vec2, pageRect: PageRect, cornerSize: number): HitRegion;
@@ -0,0 +1,9 @@
1
+ export { Zine } from './zine';
2
+ export type { ZineOptions, ZoomOptions } from './zine';
3
+ export { ImageSource } from './source/imageSource';
4
+ export type { ImageSourceOptions, ImageFit } from './source/imageSource';
5
+ export type { Source } from './source/types';
6
+ export type { RendererOption, RendererKind } from './renderer/select';
7
+ export type { Renderer, SpreadContent, PageContent, FlipDirection, LayoutMetrics, RenderOptions } from './renderer/types';
8
+ export type { ZineEventMap } from './engine/emitter';
9
+ export type { Direction, Spread } from './engine/spread';