@zinejs/core 0.6.0 → 0.7.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.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#radius","#mounted","#listeners","#state","#handlers","#activeId","#start","#prev","#last","#end","#reset","#pinching","#points","#pair","#startDistance","#remove","#canvas","#ctx","#shown","#base","#front","#back","#rawPages","#resolvePages","#resolved","#image","#cache","#renderer","#container","#source","#direction","#deepLinkEnabled","#disableContextMenu","#spreadMode","#configuredMode","#curl","#clickToFlip","#clickZoneSize","#singlePageThreshold","#responsiveSpread","#controlsOption","#startPageOption","#buildSpreadModel","#flipDuration","#zoomEnabled","#wheelZoom","#doubleClickLevels","#maxZoom","#honorDoubleClickInFlipZone","#clickFlipDelayValue","#ready","#init","#currentPage","#spreads","#current","#singlePage","#narrow","#rebuildSpreads","#renderCurrent","#applySinglePage","#getPage","#outline","#emitter","#stepFlip","#requestFlip","#startFlip","#spreadIndexForPage","#machine","#activeAnim","#queuedFlip","#finishActiveAnim","#raf","#flushPendingUpgrades","#scale","#tx","#ty","#clampPan","#refreshZoomTiles","#destroyed","#clearPendingClickFlip","#resizeObserver","#deepLink","#unsubscribeDeepLink","#controlsCleanup","#a11yCleanup","#unbindWheel","#unbindDblClick","#unbindContextMenu","#unbindInput","#tileTimer","#tileGeneration","#zoomOverlay","#leadPage","#runFlip","#flipGeneration","#resolveContent","#currentContent","#anchorY","#animateProgress","#commit","#effectiveDuration","#paintSpread","#prefetchWindow","#announce","#drainQueuedFlip","#pinching","#pan","#contentRect","#toBookPoint","#press","#pendingGrab","#drag","#stageDrag","#promoteGrab","#maybeClickFlip","#cancelFlip","#clickFlipDirection","#pendingClickTimer","#pinchBaseScale","#mountWithFallback","#onPageUpdate","#onDragStart","#onDragMove","#onDragEnd","#onPinchStart","#onPinchMove","#onPinchEnd","#bindWheelZoom","#bindDoubleClickZoom","#bindContextMenu","#setupReducedMotion","#setupA11y","#observeResize","#bindDeepLink","#mountControls","#fallbackToCss","#fellBack","#reducedMotion","#onKeyDown","#liveRegion","#effectiveMode","#effectiveModeShown","#applyContainerAspect","#lastAspect","#pendingUpgrades","#renderZoomTiles","#createZoomOverlay","#scheduleUpdate","#updateScheduled","#virtualizer","#urls","#preload","#decode","#cache"],"sources":["../src/engine/spread.ts","../src/engine/virtualizer.ts","../src/engine/emitter.ts","../src/engine/stateMachine.ts","../src/engine/input.ts","../src/geometry/hitTest.ts","../src/engine/deeplink.ts","../src/renderer/select.ts","../src/engine/zoomTile.ts","../src/source/compose.ts","../src/zine.ts","../src/source/imageSource.ts"],"sourcesContent":["export type Direction = 'ltr' | 'rtl';\n\n/**\n * How pages group into spreads:\n * - `double`: paired from the start — `[0|1] [2|3] …`\n * - `single`: one page per spread — `[0] [1] …`\n * - `cover`: first page alone, then paired — `[·|0] [1|2] …`\n * - `book`: first AND last alone (front + back cover) — `[·|0] [1|2] … [N-1|·]`\n */\nexport type SpreadMode = 'double' | 'single' | 'cover' | 'book';\n\nexport interface Spread {\n /** Page index shown on the left, or null for a blank side. */\n left: number | null;\n /** Page index shown on the right, or null for a blank side. */\n right: number | null;\n}\n\nexport interface SpreadOptions {\n direction?: Direction;\n mode?: SpreadMode;\n}\n\n/**\n * Map a flat page list (indices 0..pageCount-1) into display spreads.\n *\n * Reading order is always preserved; `direction` only mirrors each spread's\n * left/right positions, so RTL is just an LTR layout with the sides swapped.\n * Out-of-range slots are `null` (a blank half on odd counts / a lone cover).\n */\nexport function buildSpreads(pageCount: number, options: SpreadOptions = {}): Spread[] {\n const { direction = 'ltr', mode = 'cover' } = options;\n if (pageCount <= 0) return [];\n\n const page = (i: number): number | null => (i >= 0 && i < pageCount ? i : null);\n const spreads: Spread[] = [];\n\n if (mode === 'single') {\n for (let i = 0; i < pageCount; i++) spreads.push({ left: null, right: i });\n } else if (mode === 'double') {\n for (let i = 0; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else if (mode === 'cover') {\n spreads.push({ left: null, right: 0 });\n for (let i = 1; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else {\n // book: front cover alone, interior paired, back cover alone\n spreads.push({ left: null, right: 0 });\n if (pageCount > 1) {\n for (let i = 1; i <= pageCount - 2; i += 2) {\n spreads.push({ left: page(i), right: i + 1 <= pageCount - 2 ? page(i + 1) : null });\n }\n spreads.push({ left: pageCount - 1, right: null });\n }\n }\n\n return direction === 'rtl'\n ? spreads.map((s) => ({ left: s.right, right: s.left }))\n : spreads;\n}\n\n/** Whether a container this wide should fall back to one page per spread. */\nexport function shouldSinglePage(containerWidth: number, threshold: number): boolean {\n return containerWidth < threshold;\n}\n","/** Spread indices that should be mounted around `current`, clamped to the book. */\nexport function mountWindow(current: number, spreadCount: number, radius: number): number[] {\n const window: number[] = [];\n const from = Math.max(0, current - radius);\n const to = Math.min(spreadCount - 1, current + radius);\n for (let i = from; i <= to; i++) {\n window.push(i);\n }\n return window;\n}\n\nexport interface VirtualizerDelta {\n toMount: number[];\n toEvict: number[];\n}\n\n/**\n * Tracks which spreads are mounted and, on each move, reports the difference:\n * spreads entering the ±radius window (`toMount`) and those leaving it\n * (`toEvict`). Default radius 2 = current spread ±2.\n */\nexport class Virtualizer {\n #radius: number;\n #mounted = new Set<number>();\n\n constructor(radius = 2) {\n this.#radius = radius;\n }\n\n get mounted(): number[] {\n return [...this.#mounted].sort((a, b) => a - b);\n }\n\n /** Recenter on `current`; mounts/evicts to match the window and returns the delta. */\n update(current: number, spreadCount: number): VirtualizerDelta {\n const window = new Set(mountWindow(current, spreadCount, this.#radius));\n const toMount = [...window].filter((i) => !this.#mounted.has(i)).sort((a, b) => a - b);\n const toEvict = [...this.#mounted].filter((i) => !window.has(i)).sort((a, b) => a - b);\n for (const i of toEvict) this.#mounted.delete(i);\n for (const i of toMount) this.#mounted.add(i);\n return { toMount, toEvict };\n }\n}\n","import type { SpreadMode } from './spread.js';\n\nexport type Listener<T> = (payload: T) => void;\n\n/** Events typed as `void` are emitted with no payload argument. */\ntype PayloadArgs<T> = [T] extends [void] ? [] : [payload: T];\n\n/** Minimal typed event emitter keyed by an event map. */\nexport class Emitter<Events> {\n #listeners = new Map<keyof Events, Set<Listener<unknown>>>();\n\n /** Subscribe; returns an unsubscribe function. */\n on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void {\n let set = this.#listeners.get(type);\n if (!set) {\n set = new Set();\n this.#listeners.set(type, set);\n }\n set.add(listener as Listener<unknown>);\n return () => this.off(type, listener);\n }\n\n off<K extends keyof Events>(type: K, listener: Listener<Events[K]>): void {\n this.#listeners.get(type)?.delete(listener as Listener<unknown>);\n }\n\n emit<K extends keyof Events>(type: K, ...args: PayloadArgs<Events[K]>): void {\n const payload = args[0] as Events[K];\n // Copy so a listener that unsubscribes mid-emit doesn't disturb iteration.\n const set = this.#listeners.get(type);\n if (!set) return;\n for (const listener of [...set]) {\n (listener as Listener<Events[K]>)(payload);\n }\n }\n\n clear(): void {\n this.#listeners.clear();\n }\n}\n\n/**\n * Public event surface of a Zine instance. Payloads are provisional and will\n * firm up as the engine wires the emitters that fire them.\n */\nexport interface ZineEventMap {\n ready: void;\n flipStart: { from: number; to: number };\n flipEnd: { page: number };\n pageChanged: { page: number };\n /** Pages regrouped: the spread mode changed, or a narrow container forced one page. */\n spreadChanged: { mode: SpreadMode; singlePage: boolean };\n zoomChanged: { scale: number };\n sourceError: { index: number; error: unknown };\n rendererFallback: { from: string; to: string };\n}\n\nexport type ZineEmitter = Emitter<ZineEventMap>;\n","export type FlipState = 'idle' | 'dragging' | 'animating' | 'zoomed-panning';\n\nexport type FlipEvent = 'grab' | 'release' | 'settle' | 'panStart' | 'panEnd' | 'flip';\n\n/**\n * Legal transitions. A flip drag runs idle → dragging → animating → idle; a\n * programmatic flip jumps idle → animating directly via `flip`; zoom panning\n * (only reachable when zoomed in) is a separate branch idle → zoomed-panning →\n * idle. Any (state, event) pair absent here is illegal.\n */\nconst TRANSITIONS: Record<FlipState, Partial<Record<FlipEvent, FlipState>>> = {\n idle: { grab: 'dragging', panStart: 'zoomed-panning', flip: 'animating' },\n dragging: { release: 'animating' },\n animating: { settle: 'idle' },\n 'zoomed-panning': { panEnd: 'idle' },\n};\n\n/** Pure transition: the next state, or null if `event` is illegal from `state`. */\nexport function nextState(state: FlipState, event: FlipEvent): FlipState | null {\n return TRANSITIONS[state][event] ?? null;\n}\n\n/** Stateful holder around `nextState`; illegal events are ignored. */\nexport class FlipMachine {\n #state: FlipState;\n\n constructor(initial: FlipState = 'idle') {\n this.#state = initial;\n }\n\n get state(): FlipState {\n return this.#state;\n }\n\n /** Apply `event`; returns the new state, or null (no-op) if the event was illegal. */\n send(event: FlipEvent): FlipState | null {\n const next = nextState(this.#state, event);\n if (next !== null) {\n this.#state = next;\n }\n return next;\n }\n}\n","export interface GestureStart {\n x: number;\n y: number;\n t: number;\n}\n\nexport interface GestureMove {\n x: number;\n y: number;\n /** Displacement from the start point. */\n dx: number;\n dy: number;\n}\n\nexport interface GestureEnd {\n x: number;\n y: number;\n dx: number;\n dy: number;\n /** Elapsed time from start to end (ms). */\n dt: number;\n /** Velocity of the final move segment (px/ms). */\n vx: number;\n vy: number;\n /** A fast flick: final speed exceeded the swipe threshold and the gesture was not canceled. */\n swipe: boolean;\n /** The pointer was canceled (e.g. pointercancel) rather than lifted. */\n canceled: boolean;\n}\n\nexport interface PointerHandlers {\n onStart?: (g: GestureStart) => void;\n onMove?: (g: GestureMove) => void;\n onEnd?: (g: GestureEnd) => void;\n}\n\n/** Flick threshold in px/ms; a release faster than this is a swipe. */\nconst SWIPE_VELOCITY = 0.3;\n\ninterface Sample {\n x: number;\n y: number;\n t: number;\n}\n\n/**\n * Recognizes a single-pointer drag from raw pointer samples and reports normalized\n * gestures. DOM-agnostic on purpose: `bindPointerInput` maps real PointerEvents onto\n * it, but the kinematics stay pure and unit-testable. Extra pointers are ignored\n * (multi-touch pinch is a separate recognizer).\n */\nexport class PointerRecognizer {\n #handlers: PointerHandlers;\n #activeId: number | null = null;\n #start: Sample | null = null;\n #prev: Sample | null = null;\n #last: Sample | null = null;\n\n constructor(handlers: PointerHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get active(): boolean {\n return this.#activeId !== null;\n }\n\n down(id: number, x: number, y: number, t: number): void {\n if (this.#activeId !== null) return; // single-pointer; ignore extra pointers\n this.#activeId = id;\n const s: Sample = { x, y, t };\n this.#start = s;\n this.#prev = s;\n this.#last = s;\n this.#handlers.onStart?.({ x, y, t });\n }\n\n move(id: number, x: number, y: number, t: number): void {\n if (id !== this.#activeId || this.#start === null) return;\n this.#prev = this.#last;\n this.#last = { x, y, t };\n this.#handlers.onMove?.({ x, y, dx: x - this.#start.x, dy: y - this.#start.y });\n }\n\n up(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, false);\n }\n\n cancel(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, true);\n }\n\n #end(id: number, x: number, y: number, t: number, canceled: boolean): void {\n if (id !== this.#activeId || this.#start === null) return;\n const start = this.#start;\n // Velocity comes from the last move segment, not the release point — on lift the\n // pointer usually stops, so release-relative velocity would kill every flick.\n const last = this.#last ?? start;\n const prev = this.#prev ?? start;\n const segDt = last.t - prev.t;\n const vx = segDt > 0 ? (last.x - prev.x) / segDt : 0;\n const vy = segDt > 0 ? (last.y - prev.y) / segDt : 0;\n const speed = Math.hypot(vx, vy);\n this.#reset();\n this.#handlers.onEnd?.({\n x,\n y,\n dx: x - start.x,\n dy: y - start.y,\n dt: t - start.t,\n vx,\n vy,\n swipe: !canceled && speed > SWIPE_VELOCITY,\n canceled,\n });\n }\n\n #reset(): void {\n this.#activeId = null;\n this.#start = null;\n this.#prev = null;\n this.#last = null;\n }\n}\n\n/**\n * Wire real PointerEvents from `target` into a recognizer. Returns an unbind\n * function that removes every listener. Coordinates are passed through as\n * client-space; mapping to page-local space is the engine's job.\n */\nexport function bindPointerInput(target: EventTarget, recognizer: PointerRecognizer): () => void {\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\n/**\n * Wire PointerEvents from `target` into a drag recognizer and/or a pinch\n * recognizer at once, so a single pointer stream feeds both. Returns an unbind.\n */\nexport function bindGestures(\n target: EventTarget,\n recognizers: { pointer?: PointerRecognizer; pinch?: PinchRecognizer },\n): () => void {\n const { pointer, pinch } = recognizers;\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.down(p.pointerId, p.clientX, p.clientY);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.move(p.pointerId, p.clientX, p.clientY);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.up(p.pointerId);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.cancel(p.pointerId);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\nexport interface PinchStart {\n centerX: number;\n centerY: number;\n /** Distance between the two pointers at pinch start. */\n distance: number;\n}\n\nexport interface PinchMove {\n centerX: number;\n centerY: number;\n /** Current distance / start distance. */\n scale: number;\n}\n\nexport interface PinchHandlers {\n onPinchStart?: (g: PinchStart) => void;\n onPinchMove?: (g: PinchMove) => void;\n onPinchEnd?: () => void;\n}\n\ninterface Pt {\n x: number;\n y: number;\n}\n\nfunction distance(a: Pt, b: Pt): number {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n\n/**\n * Recognizes a two-pointer pinch and reports zoom scale (relative to the start\n * distance) and the live center point. No timestamps: pinch drives zoom, not a\n * flick. A third pointer is ignored; lifting either tracked pointer ends the pinch.\n * Coordinating this with the drag recognizer (a second finger should suspend a\n * single-finger drag) is the engine's job.\n */\nexport class PinchRecognizer {\n #handlers: PinchHandlers;\n #points = new Map<number, Pt>();\n #startDistance = 0;\n #pinching = false;\n\n constructor(handlers: PinchHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get pinching(): boolean {\n return this.#pinching;\n }\n\n down(id: number, x: number, y: number): void {\n if (this.#points.has(id) || this.#points.size >= 2) return; // track only two\n this.#points.set(id, { x, y });\n if (this.#points.size === 2) {\n const { a, b } = this.#pair();\n this.#startDistance = distance(a, b);\n this.#pinching = true;\n this.#handlers.onPinchStart?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n distance: this.#startDistance,\n });\n }\n }\n\n move(id: number, x: number, y: number): void {\n const p = this.#points.get(id);\n if (!p) return;\n p.x = x;\n p.y = y;\n if (!this.#pinching) return;\n const { a, b } = this.#pair();\n this.#handlers.onPinchMove?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n scale: this.#startDistance > 0 ? distance(a, b) / this.#startDistance : 1,\n });\n }\n\n up(id: number): void {\n this.#remove(id);\n }\n\n cancel(id: number): void {\n this.#remove(id);\n }\n\n #remove(id: number): void {\n if (!this.#points.delete(id)) return;\n if (this.#pinching && this.#points.size < 2) {\n this.#pinching = false;\n this.#startDistance = 0;\n this.#handlers.onPinchEnd?.();\n }\n }\n\n #pair(): { a: Pt; b: Pt } {\n const values = [...this.#points.values()];\n const a = values[0];\n const b = values[1];\n if (!a || !b) throw new Error('pinch requires two active pointers');\n return { a, b };\n }\n}\n","import type { PageRect, Vec2 } from './foldFromPointer';\n\nexport type HitRegion = 'corner' | 'page' | 'outside';\n\n/**\n * Classify a page-local point into a hit region.\n *\n * Coordinates are page-local (see `foldFromPointer`): (0,0) top-left,\n * page spanning (0,0)..(width,height). `cornerSize` is the width/height of each\n * grabbable corner box. All four corners report `corner`; the engine decides\n * which are actually grabbable for the current spread layout (LTR/RTL, cover).\n */\nexport function hitTest(point: Vec2, pageRect: PageRect, cornerSize: number): HitRegion {\n const { width, height } = pageRect;\n if (point.x < 0 || point.x > width || point.y < 0 || point.y > height) {\n return 'outside';\n }\n const nearX = point.x <= cornerSize || point.x >= width - cornerSize;\n const nearY = point.y <= cornerSize || point.y >= height - cornerSize;\n return nearX && nearY ? 'corner' : 'page';\n}\n","/**\n * Keeping the page in the URL, so a link opens where the reader was.\n *\n * The hash is treated as shared ground rather than ours: only a `page` key is read or written,\n * anything else in it is preserved, and updates go through `replaceState` so turning pages does\n * not fill the reader's back button with history entries.\n */\nconst KEY = 'page';\n\n/**\n * Whether some book on this page already owns the hash.\n *\n * Two books sharing one URL would each overwrite the other's page, and the second to load would\n * open on the first one's page. Only the first claims it; the rest still work, they just do not\n * appear in the address bar.\n */\nlet claimed = false;\n\n/** Take the hash if it is going, and report whether this caller got it. */\nexport function claimHash(): boolean {\n if (claimed) return false;\n claimed = true;\n return true;\n}\n\n/** Give the hash back, so a book that replaces this one can own it. */\nexport function releaseHash(): void {\n claimed = false;\n}\n\n/** Read the 1-based page from a hash, as a 0-based index. Null when it carries no page. */\nexport function pageFromHash(hash: string): number | null {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n const raw = params.get(KEY);\n if (raw === null) return null;\n const page = Number(raw);\n return Number.isInteger(page) && page >= 1 ? page - 1 : null;\n}\n\n/** The hash that names `page` (0-based), leaving any other keys in `hash` untouched. */\nexport function hashWithPage(hash: string, page: number): string {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n params.set(KEY, String(page + 1));\n return `#${params.toString()}`;\n}\n\nexport interface DeepLinkHandle {\n /** Write the current page into the address bar. */\n push(page: number): void;\n stop(): void;\n}\n\n/**\n * Mirror the reader's page in the URL, and follow it when the URL changes (back/forward, or a\n * pasted link). `onNavigate` fires only for a page the book is not already on.\n */\nexport function bindDeepLink(win: Window, onNavigate: (page: number) => void): DeepLinkHandle {\n // The page the URL is known to describe. Compared against rather than the hash string itself:\n // some environments emit `hashchange` for our own replaceState, and a string comparison there\n // would let the echo through and turn the page a second time.\n let shown: number | null = null;\n const onHashChange = (): void => {\n const page = pageFromHash(win.location.hash);\n if (page === null || page === shown) return; // absent, or already where we are\n shown = page;\n onNavigate(page);\n };\n win.addEventListener('hashchange', onHashChange);\n return {\n push(page) {\n if (page === shown) return;\n shown = page;\n const hash = hashWithPage(win.location.hash, page);\n if (hash === win.location.hash) return;\n // replaceState, not assignment: a reader flipping through fifty pages should not have to\n // press Back fifty times to leave.\n win.history?.replaceState?.(win.history.state, '', hash);\n },\n stop() {\n win.removeEventListener('hashchange', onHashChange);\n },\n };\n}\n","import type { Renderer } from './types';\n\nexport type RendererKind = 'css' | 'webgl2';\n\n/**\n * How the consumer chooses a renderer:\n * - `'auto'` (default): prefer the GPU path (WebGL2), else CSS.\n * - a single kind: force it (`'webgl2'` throws if the device has no WebGL2).\n * - an ordered array: try each in turn, first viable wins.\n * - a `Renderer` instance: use it as-is (custom implementation, §6.1/§11).\n */\nexport type RendererOption = 'auto' | RendererKind | RendererKind[] | Renderer;\n\nexport interface Capabilities {\n /** WebGL2 available — the GPU renderer's only backend. */\n gpu: boolean;\n}\n\ntype RendererLoader = () => Promise<Renderer>;\n\n/**\n * Lazy loaders, one per kind. Each is a dynamic `import()` so the bundler\n * code-splits it into its own chunk and a device fetches only the one selected.\n */\nconst LOADERS: Partial<Record<RendererKind, RendererLoader>> = {\n css: async () => new (await import('./cssRenderer')).CssRenderer(),\n webgl2: async () => new (await import('./webglRenderer')).WebglRenderer(),\n};\n\n// The WebGL2 renderer is auto-selected on GPU-capable devices; CSS is the baseline\n// for non-GPU devices and the fallback for unrecoverable GPU failures (§8.4).\nconst WEBGL2_AUTO = true;\n\nfunction isRenderer(option: RendererOption): option is Renderer {\n return (\n typeof option === 'object' &&\n option !== null &&\n !Array.isArray(option) &&\n typeof (option as Renderer).mount === 'function'\n );\n}\n\nfunction autoOrder(caps: Capabilities): RendererKind[] {\n return caps.gpu && WEBGL2_AUTO ? ['webgl2', 'css'] : ['css'];\n}\n\n/** Detect WebGL2 support — the GPU renderer's only backend (no WebGL1, no WebGPU). */\nexport function detectCapabilities(): Capabilities {\n return { gpu: hasWebGl2() };\n}\n\nfunction hasWebGl2(): boolean {\n try {\n return Boolean(document.createElement('canvas').getContext('webgl2'));\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the `renderer` option to a mounted renderer instance, lazy-loading its\n * chunk. Throws an agent-actionable error if nothing viable can be loaded.\n */\nexport async function selectRenderer(\n option: RendererOption = 'auto',\n caps: Capabilities = detectCapabilities(),\n): Promise<Renderer> {\n if (isRenderer(option)) return option;\n\n const order = Array.isArray(option) ? option : option === 'auto' ? autoOrder(caps) : [option];\n\n for (const kind of order) {\n if (kind === 'webgl2' && !caps.gpu) continue; // skip the GPU path without WebGL2\n const loader = LOADERS[kind];\n if (loader) return loader();\n }\n\n if (order.length === 1 && order[0] === 'webgl2' && !caps.gpu) {\n throw new Error(\n \"renderer: 'webgl2' was forced but this device has no WebGL2. Use 'css' or 'auto'.\",\n );\n }\n throw new Error(\n `renderer: none of [${order.join(', ')}] could be loaded — use 'css' or 'auto'.`,\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { PageRequest } from '../source/types';\n\n/** A rectangle in container pixels. */\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** The current view: the zoom scale and the translate applied about the container origin. */\nexport interface View {\n scale: number;\n tx: number;\n ty: number;\n}\n\n/** One page of the spread, and where it is painted when unzoomed. */\nexport interface TilePage {\n index: number;\n rect: Rect;\n}\n\n/** What to draw: a page's raster region, and the container-space box it belongs in. */\nexport interface TilePlan {\n index: number;\n request: PageRequest;\n /** Where the tile lands on screen, in *untransformed* container pixels. */\n dest: Rect;\n}\n\n/** Intersection of two rectangles, or null when they do not overlap. */\nfunction intersect(a: Rect, b: Rect): Rect | null {\n const x = Math.max(a.x, b.x);\n const y = Math.max(a.y, b.y);\n const right = Math.min(a.x + a.width, b.x + b.width);\n const bottom = Math.min(a.y + a.height, b.y + b.height);\n if (right <= x || bottom <= y) return null;\n return { x, y, width: right - x, height: bottom - y };\n}\n\n/**\n * Work out which part of each page is visible, and at what magnification.\n *\n * The renderer paints a page into `rect` and then scales the whole viewport about the container\n * origin, so a page occupies `rect * scale + t` on screen. Inverting that gives the part of the\n * page inside the viewport, which is the only part worth rasterizing sharply.\n *\n * Returns the plan in untransformed container coordinates: the caller draws it under the same\n * transform the renderer uses, so the tile tracks the page without any extra bookkeeping.\n */\nexport function planTiles(pages: TilePage[], view: View, viewport: Rect): TilePlan[] {\n const { scale, tx, ty } = view;\n if (scale <= 1) return [];\n\n // The viewport in untransformed container space — what the reader can currently see.\n const visible: Rect = {\n x: (viewport.x - tx) / scale,\n y: (viewport.y - ty) / scale,\n width: viewport.width / scale,\n height: viewport.height / scale,\n };\n\n const plans: TilePlan[] = [];\n for (const page of pages) {\n const hit = intersect(page.rect, visible);\n if (!hit || page.rect.width <= 0 || page.rect.height <= 0) continue;\n plans.push({\n index: page.index,\n request: {\n scale,\n region: {\n x: (hit.x - page.rect.x) / page.rect.width,\n y: (hit.y - page.rect.y) / page.rect.height,\n width: hit.width / page.rect.width,\n height: hit.height / page.rect.height,\n },\n },\n dest: hit,\n });\n }\n return plans;\n}\n\n/** Whether two plans ask for the same pixels, so a re-render can be skipped while panning. */\nexport function sameTile(a: TilePlan, b: TilePlan): boolean {\n if (a.index !== b.index || a.request.scale !== b.request.scale) return false;\n const p = a.request.region;\n const q = b.request.region;\n // A fraction of a page-pixel apart is the same tile; exact equality would re-render every frame.\n const EPS = 1e-4;\n return (\n Math.abs(p.x - q.x) < EPS &&\n Math.abs(p.y - q.y) < EPS &&\n Math.abs(p.width - q.width) < EPS &&\n Math.abs(p.height - q.height) < EPS\n );\n}\n\n/**\n * A canvas laid over the book that paints crisp tiles on top of the magnified page.\n *\n * It sits outside the renderer on purpose: both renderers stretch a page raster to a fixed panel,\n * so neither can place a partial-page tile, and teaching them to would mean the same geometry\n * twice. The overlay applies the same transform the renderer does, so the tiles register exactly\n * over the pixels they replace and simply disappear when the reader zooms out.\n */\nexport class ZoomOverlay {\n #canvas: HTMLCanvasElement;\n #ctx: CanvasRenderingContext2D | null;\n #shown: TilePlan[] = [];\n\n constructor(doc: Document, container: HTMLElement) {\n this.#canvas = doc.createElement('canvas');\n this.#canvas.className = 'zine-zoom-overlay';\n // pointer-events:none keeps every gesture reaching the container underneath.\n this.#canvas.style.cssText =\n 'position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity 120ms linear;';\n container.append(this.#canvas);\n this.#ctx = this.#canvas.getContext('2d');\n }\n\n /** Drop every tile and hide the overlay, revealing the renderer's own pixels again. */\n clear(): void {\n if (this.#shown.length === 0) return;\n this.#shown = [];\n this.#canvas.style.opacity = '0';\n const ctx = this.#ctx;\n if (ctx) ctx.clearRect(0, 0, this.#canvas.width, this.#canvas.height);\n }\n\n /** The plans currently painted, so the caller can skip re-rendering an unchanged view. */\n get shown(): readonly TilePlan[] {\n return this.#shown;\n }\n\n /**\n * Paint `tiles` under `view`. Sizes the backing store to the device pixel grid so the tiles are\n * not resampled a second time on the way to the screen.\n */\n draw(tiles: { plan: TilePlan; content: PageContent }[], view: View, viewport: Rect): void {\n const ctx = this.#ctx;\n if (!ctx) return;\n if (tiles.length === 0) {\n this.clear();\n return;\n }\n\n const dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const w = Math.max(1, Math.ceil(viewport.width * dpr));\n const h = Math.max(1, Math.ceil(viewport.height * dpr));\n if (this.#canvas.width !== w || this.#canvas.height !== h) {\n this.#canvas.width = w;\n this.#canvas.height = h;\n }\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, viewport.width, viewport.height);\n // Match the renderer: scale about the container origin, then translate.\n ctx.translate(view.tx, view.ty);\n ctx.scale(view.scale, view.scale);\n for (const { plan, content } of tiles) {\n const d = plan.dest;\n ctx.drawImage(content, d.x, d.y, d.width, d.height);\n }\n\n this.#shown = tiles.map((t) => t.plan);\n this.#canvas.style.opacity = '1';\n }\n\n destroy(): void {\n this.#canvas.remove();\n this.#ctx = null;\n }\n}\n","import type { PageContent } from '../renderer/types';\nimport type { DownloadInfo, OutlineItem, PageRequest, Source } from './types';\n\nexport interface CompositionOptions {\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string | null;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string | null;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string> | null;\n}\n\n/**\n * Wrap `base` so image covers/replacements sit alongside it, or return `base`\n * unchanged when nothing is requested. Covers ADD pages (front prepended, back\n * appended); `pages` REPLACE existing source pages. `pages` indexes the base's own\n * pages, so it is unaffected by an added cover.\n */\nexport function composeSource(base: Source, options: CompositionOptions): Source {\n const hasFront = typeof options.frontCover === 'string';\n const hasBack = typeof options.backCover === 'string';\n const hasPages = options.pages != null && Object.keys(options.pages).length > 0;\n if (!hasFront && !hasBack && !hasPages) return base;\n return new CompositeSource(base, options);\n}\n\nfunction loadImage(url: string): Promise<ImageBitmap> {\n return fetch(url)\n .then((res) => {\n if (!res.ok) throw new Error(`zine: could not load image \"${url}\" (HTTP ${res.status}).`);\n return res.blob();\n })\n .then((blob) => createImageBitmap(blob));\n}\n\nclass CompositeSource implements Source {\n #base: Source;\n #front: string | null;\n #back: string | null;\n #rawPages: Record<number, string>;\n #resolved = new Map<number, string>(); // base-index → url (negatives resolved)\n #cache = new Map<string, Promise<ImageBitmap>>();\n open?: () => Promise<void>;\n onPageUpdate?: (handler: (index: number) => void) => void;\n getText?: (index: number) => Promise<string>;\n getDownload?: () => Promise<DownloadInfo | null>;\n getOutline?: () => Promise<OutlineItem[]>;\n\n constructor(base: Source, options: CompositionOptions) {\n this.#base = base;\n this.#front = typeof options.frontCover === 'string' ? options.frontCover : null;\n this.#back = typeof options.backCover === 'string' ? options.backCover : null;\n this.#rawPages = options.pages ?? {};\n\n if (typeof base.open === 'function') {\n // Async base: page count (and negative indices) known only after open().\n this.open = async (): Promise<void> => {\n await base.open!();\n this.#resolvePages();\n };\n } else {\n this.#resolvePages(); // sync base: count is known now\n }\n\n if (typeof base.onPageUpdate === 'function') {\n // Forward base page updates, shifting to book indices (past the front cover).\n this.onPageUpdate = (handler) => {\n base.onPageUpdate!((src) => handler(src + (this.#front !== null ? 1 : 0)));\n };\n }\n\n if (typeof base.getText === 'function') {\n // Mirrors get(): covers and replaced pages are images, so they have no text of their own —\n // returning the base page's text there would describe content the reader cannot see.\n this.getText = async (index) => {\n const frontOffset = this.#front !== null ? 1 : 0;\n if (this.#front !== null && index === 0) return '';\n if (this.#back !== null && index === this.pageCount - 1) return '';\n const src = index - frontOffset;\n if (this.#resolved.has(src)) return '';\n return base.getText!(src);\n };\n }\n\n if (typeof base.getDownload === 'function') {\n // Covers and replacements change what is displayed, not the original document, so the\n // download is the base source's file either way.\n this.getDownload = () => base.getDownload!();\n }\n\n if (typeof base.getOutline === 'function') {\n // The outline points at base pages; a front cover pushes every one of them along by one.\n this.getOutline = async () => {\n const items = await base.getOutline!();\n const offset = this.#front !== null ? 1 : 0;\n if (offset === 0) return items;\n const shift = (list: OutlineItem[]): OutlineItem[] =>\n list.map((item) => ({\n ...item,\n page: item.page === null ? null : item.page + offset,\n children: shift(item.children),\n }));\n return shift(items);\n };\n }\n }\n\n get pageCount(): number {\n return this.#base.pageCount + (this.#front !== null ? 1 : 0) + (this.#back !== null ? 1 : 0);\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n const frontOffset = this.#front !== null ? 1 : 0;\n // Covers and page overrides are images: they have no detail beyond their own resolution, so\n // the zoom request has nothing to act on and is dropped rather than forwarded.\n if (this.#front !== null && index === 0) return this.#image(this.#front);\n if (this.#back !== null && index === this.pageCount - 1) return this.#image(this.#back);\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n return override !== undefined ? this.#image(override) : this.#base.get(src, opts);\n }\n\n prefetch(indices: number[]): void {\n const frontOffset = this.#front !== null ? 1 : 0;\n const baseIndices: number[] = [];\n for (const index of indices) {\n if (this.#front !== null && index === 0) {\n void this.#image(this.#front).catch(() => {});\n } else if (this.#back !== null && index === this.pageCount - 1) {\n void this.#image(this.#back).catch(() => {});\n } else {\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n if (override !== undefined) void this.#image(override).catch(() => {});\n else baseIndices.push(src);\n }\n }\n this.#base.prefetch(baseIndices);\n }\n\n destroy(): void {\n this.#base.destroy();\n for (const promise of this.#cache.values()) {\n void promise.then((bmp) => bmp.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #resolvePages(): void {\n const n = this.#base.pageCount;\n this.#resolved.clear();\n for (const [key, url] of Object.entries(this.#rawPages)) {\n const raw = Number(key);\n const src = raw < 0 ? n + raw : raw;\n if (Number.isInteger(src) && src >= 0 && src < n) this.#resolved.set(src, url);\n }\n }\n\n #image(url: string): Promise<ImageBitmap> {\n let promise = this.#cache.get(url);\n if (promise === undefined) {\n promise = loadImage(url).catch((error: unknown) => {\n this.#cache.delete(url); // let a later get() retry\n throw error;\n });\n this.#cache.set(url, promise);\n }\n return promise;\n }\n}\n","import {\n buildSpreads,\n shouldSinglePage,\n type Direction,\n type Spread,\n type SpreadMode,\n} from './engine/spread';\nimport { Virtualizer } from './engine/virtualizer';\nimport { Emitter, type ZineEventMap } from './engine/emitter';\nimport { FlipMachine } from './engine/stateMachine';\nimport { PointerRecognizer, PinchRecognizer, bindGestures, type GestureEnd } from './engine/input';\nimport { hitTest } from './geometry/hitTest';\nimport {\n bindDeepLink,\n claimHash,\n hashWithPage,\n pageFromHash,\n releaseHash,\n type DeepLinkHandle,\n} from './engine/deeplink';\nimport { CURL_TYPES, DEFAULT_CURL, type CurlType } from './geometry/curls/types';\nimport { selectRenderer, type RendererOption } from './renderer/select';\nimport type { FlipDirection, PageContent, Renderer, SpreadContent } from './renderer/types';\nimport type { DownloadInfo, OutlineItem, Source } from './source/types';\nimport { planTiles, sameTile, ZoomOverlay, type TilePage } from './engine/zoomTile';\nimport { composeSource } from './source/compose';\nimport type { ControlsOptions } from './controls/types';\n\n/** Grab-zone size as a fraction of the smaller container dimension. */\nconst CORNER_FRACTION = 0.25;\n\n/** Wheel-zoom sensitivity: scale multiplies by exp(-deltaY * this) per wheel event. */\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015;\n\n/** Pointer movement (px) beyond which a press becomes a drag rather than a tap. */\nconst DRAG_THRESHOLD = 6;\n\n/** Window (ms) a single click waits to rule out a double-click before flipping. */\nconst DOUBLE_CLICK_MS = 250;\n\n/** A lone page fills the container, so it sweeps the full width where a spread leaf only covers\n * its half — and it dissolves instead of landing on a facing page. Stretch duration so the\n * peel and fade read at a comparable pace to a spread turn rather than whipping away. */\nconst LONE_PAGE_FLIP_SCALE = 1.55;\n\n/** Characters of surrounding text shown on either side of a search match. */\nconst EXCERPT_PAD = 32;\n\n/** Quiet period before re-rendering the zoomed region, so a pinch or pan does not rasterize\n * on every frame. Long enough to coalesce a gesture, short enough to feel immediate on release. */\nconst ZOOM_TILE_DELAY = 120;\n\n/** One page that matched a {@link Zine.search} query. */\nexport interface SearchHit {\n /** Zero-based page index; pass straight to `flipTo`. */\n page: number;\n /** The match with a little text either side, ellipsised where it was cut. */\n excerpt: string;\n}\n\n/** Pull a readable snippet around a match, collapsing the whitespace PDFs are full of. */\nfunction excerptAround(text: string, at: number, length: number): string {\n const start = Math.max(0, at - EXCERPT_PAD);\n const end = Math.min(text.length, at + length + EXCERPT_PAD);\n const slice = text.slice(start, end).replace(/\\s+/g, ' ').trim();\n return `${start > 0 ? '…' : ''}${slice}${end < text.length ? '…' : ''}`;\n}\n\ninterface DragState {\n direction: FlipDirection;\n targetIndex: number;\n toPage: number;\n toContent: SpreadContent;\n width: number;\n t: number;\n}\n\nexport interface ZoomOptions {\n /** Whether zooming is allowed at all; default true. */\n enabled?: boolean;\n /** Maximum zoom scale; default 4. */\n max?: number;\n /** Zoom on Ctrl/⌘ + wheel (also how trackpad pinch arrives on desktop); default true. */\n wheel?: boolean;\n /** Zoom levels cycled by double-click (wraps to the first); `false` disables. Default [1, 2, 4]. */\n doubleClick?: number[] | false;\n /**\n * Listen for double-click zoom inside the click-to-flip zones. Default: off in 'edge' mode\n * (edges flip instantly, no delay), on in 'half' mode. Turning it on in 'edge' mode makes\n * clicks wait out `clickFlipDelay` so a double-click can preempt them with a zoom.\n */\n doubleClickInFlipZone?: boolean;\n}\n\nexport interface ZineOptions {\n /** Content source (e.g. an ImageSource). Required. */\n source: Source;\n /** Renderer selection; default 'auto'. */\n renderer?: RendererOption;\n /** Page-curl model for the WebGL2 renderer: 'roll' | 'cone' | 'leaf' | 'flick' | 'silk' | 'simple'. Default 'cone'. */\n curl?: CurlType;\n /** Fixed container width in px; omit to let the container/CSS drive the size. */\n width?: number;\n /** Fixed container height in px; omit to let the container/CSS drive the size. */\n height?: number;\n /** Reading direction; default 'ltr'. */\n direction?: Direction;\n /** How pages group into spreads: 'double' | 'single' | 'cover' | 'book'. Default 'cover'. */\n spreadMode?: SpreadMode;\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string>;\n /** Page to open on; default 0. */\n startPage?: number;\n /** Flip animation duration in ms; default 500. */\n flipDuration?: number;\n /** How a tap/click turns pages: near an edge ('edge'), by page half ('half'), or 'off'. Default 'edge'. */\n clickToFlip?: 'edge' | 'half' | 'off';\n /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */\n clickZoneSize?: number;\n /**\n * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom.\n * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active.\n */\n clickFlipDelay?: number;\n /** Zoom behavior. */\n zoom?: ZoomOptions;\n /** Container widths below this (px) switch to one page per spread; default 640. */\n singlePageThreshold?: number;\n /**\n * Whether a narrow container may override `spreadMode` and show one page at a time. Default\n * true.\n *\n * Set false to hold the configured mode at every width — a two-page spread stays two pages on\n * a phone, however small. (`singlePageThreshold: 0` has the same effect; this says it out\n * loud, and can be changed at runtime with {@link Zine.setResponsiveSpread}.)\n */\n responsiveSpread?: boolean;\n /**\n * The built-in toolbar. Shown by default; pass `false` to render none, or an object to choose\n * its position and which controls appear.\n */\n controls?: boolean | ControlsOptions;\n /**\n * Keep the current page in the URL hash (`#page=12`), so a link opens where the reader was and\n * back/forward move through the book. Default true.\n *\n * Only a `page` key is read or written — anything else in the hash is left alone — and updates\n * use `replaceState`, so turning pages does not fill the back button. Pass `false` if your app\n * owns the hash.\n */\n deepLink?: boolean;\n /**\n * Suppress the browser's right-click menu over the book. Default false.\n *\n * A mild deterrent for published documents, not protection: the pages are still in the DOM and\n * reachable by anyone who wants them. It also takes away Inspect and \"Open image in new tab\"\n * for everyone, so it is off unless asked for.\n */\n disableContextMenu?: boolean;\n}\n\n/**\n * The public flipbook. Composes the engine (spreads, virtualizer, events), a\n * lazily-selected renderer, and a content source. Construction is synchronous\n * but setup is async — await `ready` (or listen for the `ready` event) before\n * driving it.\n */\nexport class Zine {\n #container: HTMLElement;\n #source: Source;\n #emitter = new Emitter<ZineEventMap>();\n #virtualizer = new Virtualizer(2);\n #machine = new FlipMachine();\n #direction: Direction;\n #spreadMode: SpreadMode;\n /** The mode the book was built with, so toggling away from one page can return to it — a\n * `cover` book comes back as `cover`, keeping its lone first page, not as plain `double`. */\n #configuredMode: SpreadMode;\n #curl: CurlType;\n #anchorY = 1; // where the last tap/drag grabbed (0=top, 1=bottom); drives anchored curls\n #clickToFlip: 'edge' | 'half' | 'off';\n #clickZoneSize: number;\n #honorDoubleClickInFlipZone: boolean;\n #clickFlipDelayValue: number;\n #singlePageThreshold: number;\n #singlePage = false;\n /** Last layout announced via `spreadChanged`, so a rebuild that changes nothing stays quiet. */\n #effectiveModeShown: SpreadMode | null = null;\n #narrow = false; // responsive: container currently below singlePageThreshold\n #responsiveSpread: boolean;\n #lastAspect = ''; // last container aspect-ratio written (avoids redundant style writes)\n #startPageOption: number | undefined;\n #deepLinkEnabled: boolean;\n #disableContextMenu: boolean;\n #deepLink: DeepLinkHandle | null = null;\n #unsubscribeDeepLink: (() => void) | null = null;\n #spreads: Spread[] = [];\n #current = 0;\n #currentPage = 0;\n #currentContent: SpreadContent = { left: null, right: null };\n #flipDuration: number;\n #zoomEnabled: boolean;\n #wheelZoom: boolean;\n #doubleClickLevels: number[] | null;\n #maxZoom: number;\n #resizeObserver: ResizeObserver | null = null;\n #updateScheduled = false;\n #scale = 1;\n #tx = 0;\n #ty = 0;\n #renderer: Renderer | null = null;\n #fellBack = false;\n #destroyed = false;\n #raf: number | null = null;\n #drag: DragState | null = null;\n #pendingGrab: { direction: FlipDirection; targetIndex: number; toPage: number; width: number } | null = null;\n #press: { x: number; y: number } | null = null;\n #pendingClickTimer: ReturnType<typeof setTimeout> | null = null;\n // Latest flip intent requested while one was already animating; replayed on settle so\n // clicks/keys during a turn aren't dropped and continuous flipping keeps advancing.\n #queuedFlip: (() => void) | null = null;\n // The fold animation currently on screen, so a new flip request can land it instantly\n // (snap to its end pose + commit) and start immediately instead of waiting it out.\n #activeAnim: { toT: number; direction: FlipDirection; onDone: () => void } | null = null;\n // Bumped per flip so an async #runFlip that resumes after being superseded can bail.\n #flipGeneration = 0;\n #pan: { baseTx: number; baseTy: number } | null = null;\n #pinching = false;\n #pinchBaseScale = 1;\n #reducedMotion = false;\n #liveRegion: HTMLElement | null = null;\n #unbindInput: (() => void) | null = null;\n #unbindWheel: (() => void) | null = null;\n #unbindContextMenu: (() => void) | null = null;\n #unbindDblClick: (() => void) | null = null;\n #a11yCleanup: (() => void) | null = null;\n /** Resolved once and reused; see {@link getOutline}. */\n #outline: Promise<OutlineItem[]> | null = null;\n /** Pages whose content improved while a flip was running, to repaint once it settles. */\n #pendingUpgrades = new Set<number>();\n #controlsOption: boolean | ControlsOptions;\n #controlsCleanup: (() => void) | null = null;\n /** Crisp re-renders of the visible region while zoomed in; null until the reader zooms. */\n #zoomOverlay: ZoomOverlay | null = null;\n /** Guards against an out-of-date tile landing after the view has already moved on. */\n #tileGeneration = 0;\n #tileTimer: ReturnType<typeof setTimeout> | null = null;\n #ready: Promise<void>;\n\n /** Stable seek API: drive the fold to a fixed progress without animating (visual regression). */\n readonly debug = {\n setFlipProgress: (t: number, direction: FlipDirection): void => {\n this.#renderer?.setFlipProgress(t, direction);\n },\n };\n\n constructor(container: HTMLElement, options: ZineOptions) {\n validateOptions(container, options);\n this.#container = container;\n if (options.width !== undefined) container.style.width = `${options.width}px`;\n if (options.height !== undefined) container.style.height = `${options.height}px`;\n this.#source = composeSource(options.source, {\n frontCover: options.frontCover,\n backCover: options.backCover,\n pages: options.pages,\n });\n const direction = options.direction ?? 'ltr';\n this.#direction = direction;\n // Set before the spread model is built: that is where a page in the URL is honoured.\n // Claimed before the spread model is built, which is where the hash is read: a second book\n // on the same page must not open on the first one's page.\n this.#deepLinkEnabled = (options.deepLink ?? true) && claimHash();\n this.#disableContextMenu = options.disableContextMenu ?? false;\n this.#spreadMode = options.spreadMode ?? 'cover';\n this.#configuredMode = this.#spreadMode;\n this.#curl = options.curl ?? DEFAULT_CURL;\n this.#clickToFlip = options.clickToFlip ?? 'edge';\n this.#clickZoneSize = options.clickZoneSize ?? 64;\n this.#singlePageThreshold = options.singlePageThreshold ?? 640;\n this.#responsiveSpread = options.responsiveSpread ?? true;\n this.#controlsOption = options.controls ?? true;\n this.#startPageOption = options.startPage;\n // Sync sources (a known page count) build spreads now — so bad pageCount/startPage\n // throw immediately from `new Zine`. Async sources (an `open()`) defer to #init.\n if (typeof this.#source.open !== 'function') {\n this.#buildSpreadModel();\n }\n this.#flipDuration = options.flipDuration ?? 800;\n this.#zoomEnabled = options.zoom?.enabled ?? true;\n this.#wheelZoom = options.zoom?.wheel ?? true;\n const dbl = options.zoom?.doubleClick;\n const levels = dbl === false ? [] : [...(dbl ?? [1, 2, 4])].sort((a, b) => a - b);\n this.#doubleClickLevels = levels.length > 0 ? levels : null;\n this.#maxZoom = options.zoom?.max ?? 4;\n\n // Arbitrate click-to-flip vs double-click zoom inside the flip zones.\n // Listen for double-click zoom in flip zones: default off in 'edge', on in 'half'.\n // clickFlipDelay === 0 hard-disables it. When honored, a delay lets a click wait\n // out a possible double-click; auto default 250 ms, overridable.\n const doubleClickZoomActive = this.#zoomEnabled && this.#doubleClickLevels !== null;\n const listenInFlipZone = options.zoom?.doubleClickInFlipZone ?? (this.#clickToFlip === 'half');\n this.#honorDoubleClickInFlipZone =\n this.#clickToFlip !== 'off' &&\n doubleClickZoomActive &&\n listenInFlipZone &&\n options.clickFlipDelay !== 0;\n this.#clickFlipDelayValue = this.#honorDoubleClickInFlipZone\n ? (options.clickFlipDelay ?? DOUBLE_CLICK_MS)\n : 0;\n\n this.#ready = this.#init(options.renderer ?? 'auto');\n }\n\n /** Resolves once the renderer is mounted and the first spread is painted. */\n get ready(): Promise<void> {\n return this.#ready;\n }\n\n getPageCount(): number {\n return this.#source.pageCount;\n }\n\n getPage(): number {\n return this.#currentPage;\n }\n\n /** The element this flipbook was mounted into. */\n get container(): HTMLElement {\n return this.#container;\n }\n\n /**\n * How pages are currently grouped, one entry per spread — the same model the renderer paints,\n * so it already reflects `spreadMode` and the responsive single-page fallback. Useful for\n * building a page list or thumbnail rail that matches the book.\n */\n getSpreads(): readonly Spread[] {\n return this.#spreads;\n }\n\n /** The spread index currently on screen; indexes into {@link getSpreads}. */\n getSpreadIndex(): number {\n return this.#current;\n }\n\n /** Reading direction, which mirrors each spread's left/right sides. */\n getDirection(): Direction {\n return this.#direction;\n }\n\n /** How pages are grouped: the configured mode, not what a narrow container may be forcing. */\n getSpreadMode(): SpreadMode {\n return this.#spreadMode;\n }\n\n /** Whether the book is showing one page at a time, for whatever reason. */\n isSinglePage(): boolean {\n return this.#singlePage;\n }\n\n /**\n * Regroup the pages, keeping the reader on the page they are looking at.\n *\n * A narrow container may still override this with one page at a time; see\n * {@link setResponsiveSpread}.\n */\n setSpreadMode(mode: SpreadMode): void {\n if (mode === this.#spreadMode) return;\n this.#spreadMode = mode;\n // The narrow measurement was taken under the layout we are leaving, and a book showing one\n // page is narrower than the same book showing two. Holding on to it would let a container\n // that only shrank *because* of one-page mode veto the way back out. Drop it and let the\n // next measurement decide afresh.\n this.#narrow = false;\n this.#rebuildSpreads();\n void this.#renderCurrent();\n }\n\n /**\n * Switch between one page and two, and back again.\n *\n * Returning to two pages restores the mode the book was built with, so a `cover` book gets its\n * lone first page back rather than becoming a plain `double`.\n */\n toggleSpreadMode(): void {\n this.setSpreadMode(\n this.#spreadMode === 'single'\n ? this.#configuredMode === 'single'\n ? 'double' // built as single: there is no other mode to return to\n : this.#configuredMode\n : 'single',\n );\n }\n\n /** Whether a narrow container is allowed to override `spreadMode` with one page at a time. */\n getResponsiveSpread(): boolean {\n return this.#responsiveSpread;\n }\n\n /**\n * Whether the book is showing one page at a time *because the container is narrow*, rather\n * than because it was asked to. False when `responsiveSpread` is off.\n *\n * A book already set to `single` is not being overridden, however narrow it is: asking for one\n * page makes the book itself narrower, so reporting that as an override would strand the reader\n * in a layout they could no longer switch out of.\n */\n isResponsiveSingle(): boolean {\n return this.#narrow && this.#spreadMode !== 'single';\n }\n\n /**\n * Allow or forbid the narrow-container override, and re-lay out at once.\n *\n * Turning it off holds the configured `spreadMode` at every width; turning it back on\n * re-measures, so a book on a narrow screen collapses to single again straight away.\n */\n setResponsiveSpread(enabled: boolean): void {\n if (enabled === this.#responsiveSpread) return;\n this.#responsiveSpread = enabled;\n if (!enabled) {\n if (!this.#narrow) return; // nothing was being overridden\n this.#narrow = false;\n this.#rebuildSpreads();\n } else {\n const width = this.#renderer?.measure().containerWidth;\n if (width === undefined) return;\n this.#narrow = false; // let #applySinglePage see a real change and rebuild\n this.#applySinglePage(width);\n }\n void this.#renderCurrent();\n }\n\n /**\n * Resolve one page's raster, for drawing a thumbnail or exporting. Returns null if the page\n * cannot be decoded — the failure is reported through `sourceError` rather than thrown, as it\n * is for the pages the renderer paints.\n */\n getPageImage(index: number): Promise<PageContent | null> {\n return this.#getPage(index);\n }\n\n /** Whether there is a spread after the current one (false on the last). */\n canFlipNext(): boolean {\n return this.#current + 1 < this.#spreads.length;\n }\n\n /** Whether there is a spread before the current one (false on the first). */\n canFlipPrev(): boolean {\n return this.#current > 0;\n }\n\n /** The ceiling `setZoom` clamps to. */\n getMaxZoom(): number {\n return this.#maxZoom;\n }\n\n /** Whether this book's source can produce text — false for image books. */\n canSearch(): boolean {\n return typeof this.#source.getText === 'function';\n }\n\n /** Whether the original document can be downloaded — true for a PDF from a URL or bytes. */\n canDownload(): boolean {\n return typeof this.#source.getDownload === 'function';\n }\n\n /**\n * Whether the book is a document rather than a set of loose images — true for a PDF, false for\n * an `ImageSource`. Document-shaped features (the page rail, the outline) key off this.\n *\n * Inferred from the source implementing document-only capabilities, so a custom source opts in\n * simply by implementing them.\n */\n isDocument(): boolean {\n return typeof this.#source.getOutline === 'function' || typeof this.#source.getText === 'function';\n }\n\n /** Whether this book can be printed — true when the source has an original document. */\n canPrint(): boolean {\n return typeof this.#source.getDownload === 'function';\n }\n\n /**\n * Print the document.\n *\n * The original file is handed to the browser in an offscreen frame rather than printing the\n * host page: printing the page would capture the toolbar and whatever single spread happens to\n * be on screen, while the browser paginates a PDF properly on its own. Resolves false when\n * there is no original to print — an image book, or a caller-owned pdf.js document.\n */\n async print(): Promise<boolean> {\n const info = await this.getSourceFile();\n const doc = this.#container.ownerDocument;\n if (!info || !doc) return false;\n\n const frame = doc.createElement('iframe');\n // Offscreen rather than display:none: a hidden frame does not always lay out its document,\n // and an unlaid-out frame has nothing to print.\n frame.style.cssText =\n 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0;';\n frame.setAttribute('aria-hidden', 'true');\n frame.src = info.url;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n const finish = (ok: boolean): void => {\n if (settled) return;\n settled = true;\n // Outlive the print dialog, which is modal and synchronous in some browsers; removing\n // the frame while it is open cancels the job.\n setTimeout(() => {\n frame.remove();\n if (info.revoke) URL.revokeObjectURL(info.url);\n }, 60_000);\n resolve(ok);\n };\n frame.addEventListener('load', () => {\n try {\n const win = frame.contentWindow;\n if (!win) return finish(false);\n win.focus();\n win.print();\n finish(true);\n } catch {\n finish(false); // a cross-origin PDF cannot be driven from here\n }\n });\n frame.addEventListener('error', () => finish(false));\n doc.body.appendChild(frame);\n });\n }\n\n /** Whether the source can supply a table of contents. True for PDFs; the document may still\n * turn out to have no outline, in which case {@link getOutline} resolves empty. */\n canOutline(): boolean {\n return typeof this.#source.getOutline === 'function';\n }\n\n /**\n * The document's table of contents, or an empty list when it has none.\n *\n * Memoized: an outline does not change for the life of the document, and resolving one means\n * following every entry's destination to a page. The toolbar asks for it up front to decide\n * whether to offer the outline panel at all, and the panel itself then gets it for free.\n */\n getOutline(): Promise<OutlineItem[]> {\n if (!this.#outline) {\n const getOutline = this.#source.getOutline;\n this.#outline =\n typeof getOutline === 'function'\n ? Promise.resolve(getOutline.call(this.#source)).catch(() => [])\n : Promise.resolve([]);\n }\n return this.#outline;\n }\n\n /**\n * Save the original document. Resolves to false when the source has nothing to hand over\n * (an image book, or a PDF opened from a caller-owned pdf.js document).\n */\n /**\n * Where the original document lives, if the source has one — a PDF's URL or a blob of its\n * bytes. Null for an image book, or a PDF opened from a caller-owned pdf.js document.\n *\n * Saving and printing both start here: the original is better than anything reassembled from\n * page rasters, and the browser already knows how to paginate it.\n */\n async getSourceFile(): Promise<DownloadInfo | null> {\n const getDownload = this.#source.getDownload;\n if (typeof getDownload !== 'function') return null;\n return (await getDownload.call(this.#source)) ?? null;\n }\n\n async download(): Promise<boolean> {\n const info = await this.getSourceFile();\n if (!info) return false;\n const doc = this.#container.ownerDocument;\n if (!doc) return false;\n const link = doc.createElement('a');\n link.href = info.url;\n link.download = info.filename;\n link.rel = 'noopener';\n doc.body.appendChild(link);\n link.click();\n link.remove();\n // An object URL is ours to clean up; give the click a tick to start first.\n if (info.revoke) setTimeout(() => URL.revokeObjectURL(info.url), 10_000);\n return true;\n }\n\n /**\n * Find `query` in the book's text, case-insensitively.\n *\n * Pages are read on demand and cached by the source, so the first search over a long document\n * costs one text extraction per page and later ones are cheap. Returns at most one hit per\n * page, in page order. Always empty when {@link canSearch} is false.\n */\n async search(query: string, options: { limit?: number } = {}): Promise<SearchHit[]> {\n const getText = this.#source.getText;\n const needle = query.trim().toLowerCase();\n if (typeof getText !== 'function' || needle === '') return [];\n const limit = options.limit ?? 50;\n const hits: SearchHit[] = [];\n for (let page = 0; page < this.#source.pageCount && hits.length < limit; page++) {\n let text: string;\n try {\n text = await getText.call(this.#source, page);\n } catch (error) {\n this.#emitter.emit('sourceError', { index: page, error });\n continue;\n }\n const at = text.toLowerCase().indexOf(needle);\n if (at < 0) continue;\n hits.push({ page, excerpt: excerptAround(text, at, needle.length) });\n }\n return hits;\n }\n\n flipNext(): void {\n this.#stepFlip(1);\n }\n\n flipPrev(): void {\n this.#stepFlip(-1);\n }\n\n /** Turn one spread in `step`'s direction, counted from wherever the book lands. Interrupting\n * commits the running turn first, so this steps on from there — repeat taps keep advancing,\n * and a reversal goes to the neighbour of the spread that just landed. */\n #stepFlip(step: 1 | -1): void {\n this.#requestFlip(() => this.#startFlip(this.#current + step));\n }\n\n flipTo(page: number): void {\n const target = clamp(page, 0, Math.max(0, this.#source.pageCount - 1));\n this.#requestFlip(() => this.#startFlip(this.#spreadIndexForPage(target)));\n }\n\n /** Run a flip now. If a fold is animating, land it instantly first so the new turn starts\n * immediately on this click (thunks read `#current` lazily, so it steps on from where the\n * interrupted turn landed). A drag in flight can't be interrupted this way, so its request\n * is queued and replayed on settle instead. */\n #requestFlip(run: () => void): void {\n if (this.#machine.state === 'idle') {\n run();\n } else if (this.#activeAnim) {\n // This request supersedes anything still waiting: landing the current turn commits it,\n // and that commit drains the queue. Without clearing it first, an older tap would fire\n // from inside the commit and take the book somewhere the reader has since changed their\n // mind about — a reversal would be undone by the forward tap it was meant to replace.\n this.#queuedFlip = null;\n this.#finishActiveAnim(); // lands the current turn → machine back to idle\n run();\n } else {\n this.#queuedFlip = run; // dragging: no animation to cut short, so wait for release\n }\n }\n\n /** Snap the in-flight fold to its end pose and commit it now, cancelling its RAF loop. */\n #finishActiveAnim(): void {\n const anim = this.#activeAnim;\n if (!anim) return;\n this.#activeAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#renderer?.setFlipProgress(anim.toT, anim.direction);\n anim.onDone();\n }\n\n /** After an animation settles, fire whatever flip was requested mid-turn. */\n #drainQueuedFlip(): void {\n const queued = this.#queuedFlip;\n if (!queued) {\n // Nothing further to turn to, so this is where the book comes to rest: a good moment to\n // apply any page content that improved while it was moving.\n this.#flushPendingUpgrades();\n return;\n }\n this.#queuedFlip = null;\n queued();\n }\n\n getZoom(): number {\n return this.#scale;\n }\n\n /** Zoom to `scale` (clamped to [1, zoom.max]), keeping `center` (container-local) fixed. */\n setZoom(scale: number, center?: { x: number; y: number }): void {\n if (!this.#renderer || !this.#zoomEnabled) return;\n const s2 = clamp(scale, 1, this.#maxZoom);\n const m = this.#renderer.measure();\n const focal = center ?? { x: m.containerWidth / 2, y: m.containerHeight / 2 };\n const s1 = this.#scale;\n // Solve for the translate that keeps the focal screen point fixed as s1→s2.\n let tx = s2 === 1 ? 0 : focal.x - (s2 / s1) * (focal.x - this.#tx);\n let ty = s2 === 1 ? 0 : focal.y - (s2 / s1) * (focal.y - this.#ty);\n [tx, ty] = this.#clampPan(tx, ty, s2, m.containerWidth, m.containerHeight);\n this.#scale = s2;\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(s2, tx, ty);\n this.#refreshZoomTiles();\n this.#emitter.emit('zoomChanged', { scale: s2 });\n }\n\n resetZoom(): void {\n this.setZoom(1);\n }\n\n #clampPan(tx: number, ty: number, scale: number, w: number, h: number): [number, number] {\n // Keep the scaled content covering the viewport (transform-origin is 0,0).\n return [clamp(tx, w * (1 - scale), 0), clamp(ty, h * (1 - scale), 0)];\n }\n\n on<K extends keyof ZineEventMap>(\n event: K,\n listener: (payload: ZineEventMap[K]) => void,\n ): () => void {\n return this.#emitter.on(event, listener);\n }\n\n destroy(): void {\n this.#destroyed = true;\n if (this.#raf !== null) cancelAnimationFrame(this.#raf);\n this.#activeAnim = null;\n this.#queuedFlip = null;\n this.#clearPendingClickFlip();\n this.#resizeObserver?.disconnect();\n this.#deepLink?.stop();\n this.#deepLink = null;\n this.#unsubscribeDeepLink?.();\n if (this.#deepLinkEnabled) releaseHash(); // a later book may own it now\n this.#controlsCleanup?.();\n this.#a11yCleanup?.();\n this.#unbindWheel?.();\n this.#unbindDblClick?.();\n this.#unbindContextMenu?.();\n this.#unbindInput?.();\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n this.#tileGeneration++; // strand any tile still resolving\n this.#zoomOverlay?.destroy();\n this.#zoomOverlay = null;\n this.#renderer?.destroy();\n this.#renderer = null;\n this.#source.destroy();\n this.#emitter.clear();\n }\n\n #startFlip(targetIndex: number): void {\n if (targetIndex < 0 || targetIndex >= this.#spreads.length || targetIndex === this.#current) {\n return;\n }\n // `flip` is legal only from idle; otherwise a flip/drag is already in flight.\n if (this.#machine.send('flip') === null) return;\n\n const direction: FlipDirection = targetIndex > this.#current ? 'forward' : 'backward';\n const toPage = this.#leadPage(targetIndex);\n // State is already locked (send('flip') above), so re-entrant flips are rejected\n // even though staging the destination content below is async.\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: toPage });\n // Claimed here, synchronously, so this flip owns the run before #runFlip's first await.\n // Bumping inside #runFlip instead let a superseded flip's continuation claim the newest\n // generation and cancel the flip that replaced it.\n void this.#runFlip(targetIndex, direction, toPage, ++this.#flipGeneration);\n }\n\n async #runFlip(\n targetIndex: number,\n direction: FlipDirection,\n toPage: number,\n generation: number,\n ): Promise<void> {\n // Staging the destination is async, so this can resume after a later flip has already\n // superseded it (interrupting mid-turn does exactly that). Without this check the stale\n // continuation would begin animating its own, now-abandoned target and commit it, undoing\n // the turn the reader actually asked for.\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n if (generation !== this.#flipGeneration) return;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n curl: this.#curl,\n anchor: { y: this.#anchorY },\n });\n this.#animateProgress(0, 1, direction, () => this.#commit(targetIndex, toPage, toContent));\n }\n\n /** Animate the fold from `fromT` to `toT`, easing per-frame, then run `onDone`. */\n #animateProgress(\n fromT: number,\n toT: number,\n direction: FlipDirection,\n onDone: () => void,\n ): void {\n const duration = this.#effectiveDuration() * Math.abs(toT - fromT);\n if (duration <= 0) {\n // Reduced motion (or zero-duration): swap without the curl sweep.\n this.#renderer?.setFlipProgress(toT, direction);\n onDone();\n return;\n }\n // Remembered so a mid-turn flip request can #finishActiveAnim() to land it at once.\n // Object identity also tags this run: if #finishActiveAnim (or a new flip) replaces it,\n // a stale frame that still fires bails instead of re-committing or rescheduling itself.\n const anim = { toT, direction, onDone };\n this.#activeAnim = anim;\n const start = performance.now();\n const step = (now: number): void => {\n if (this.#activeAnim !== anim) return; // superseded/interrupted → this frame is void\n // Easing lives here; flipProgressToPose stays linear so seeks are deterministic.\n // Lone pages ease out harder — no facing half to land on, only a dissolve.\n const raw = Math.min(1, (now - start) / duration);\n const eased = this.#singlePage ? easeInOutLone(raw) : easeInOutQuad(raw);\n this.#renderer?.setFlipProgress(fromT + (toT - fromT) * eased, direction);\n if (raw < 1) {\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#raf = null;\n this.#activeAnim = null;\n onDone();\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n #commit(targetIndex: number, toPage: number, toContent: SpreadContent): void {\n const spread = this.#spreads[targetIndex];\n this.#current = targetIndex;\n this.#currentPage = toPage;\n this.#currentContent = toContent;\n // renderSpread paints the landed spread and clears the turning leaf.\n if (spread) this.#paintSpread(spread, toContent);\n this.#prefetchWindow();\n this.#machine.send('settle');\n this.#announce();\n this.#emitter.emit('pageChanged', { page: toPage });\n this.#emitter.emit('flipEnd', { page: toPage });\n this.#drainQueuedFlip();\n }\n\n #leadPage(spreadIndex: number): number {\n const spread = this.#spreads[spreadIndex];\n if (!spread) return this.#currentPage;\n const pages = [spread.left, spread.right].filter((p): p is number => p !== null);\n return pages.length > 0 ? Math.min(...pages) : this.#currentPage;\n }\n\n #onDragStart(clientX: number, clientY: number): void {\n if (this.#pinching || !this.#renderer) return;\n this.#clearPendingClickFlip(); // a new press cancels a click-flip still waiting out its window\n // Zoomed in → a drag pans; at scale 1 → a corner drag flips (§9 mode switch).\n if (this.#scale > 1) {\n if (this.#machine.send('panStart') === null) return;\n this.#pan = { baseTx: this.#tx, baseTy: this.#ty };\n return;\n }\n const b = this.#contentRect();\n const point = this.#toBookPoint(clientX, clientY);\n // Recorded before the busy check below: a tap landing mid-flip is still a real tap, and\n // on release it decides which way to turn. Leaving the previous press in place made that\n // release repeat the last flip's direction instead of honouring the side just tapped.\n this.#press = point; // remembered for tap classification / click-to-flip zone\n // Only the drag machinery needs an idle book — a fold is already on screen, so there is\n // nothing to grab. The tap itself is handled on release, which queues or interrupts.\n if (this.#machine.state !== 'idle') {\n this.#pendingGrab = null;\n return;\n }\n const cornerSize = Math.min(b.width, b.height) * CORNER_FRACTION;\n if (hitTest(point, { width: b.width, height: b.height }, cornerSize) !== 'corner') {\n return; // not a corner — no drag; a tap here may still click-to-flip on release\n }\n this.#anchorY = clamp(point.y / b.height, 0, 1); // fold anchors at the grabbed corner\n // Arm a potential drag; it only becomes a real flip once the pointer moves\n // (so a corner *tap* never fires a spurious flipStart). Right-side corner\n // turns the page forward in LTR; RTL mirrors it.\n const rightSide = point.x > b.width / 2;\n const forward = this.#direction === 'rtl' ? !rightSide : rightSide;\n const targetIndex = this.#current + (forward ? 1 : -1);\n if (targetIndex < 0 || targetIndex >= this.#spreads.length) return;\n this.#pendingGrab = {\n direction: forward ? 'forward' : 'backward',\n targetIndex,\n toPage: this.#leadPage(targetIndex),\n width: b.width,\n };\n }\n\n /** Turn an armed corner press into a live drag flip once the pointer has moved. */\n #promoteGrab(): void {\n const grab = this.#pendingGrab;\n if (!grab) return;\n this.#pendingGrab = null;\n if (this.#machine.send('grab') === null) return;\n this.#drag = {\n direction: grab.direction,\n targetIndex: grab.targetIndex,\n toPage: grab.toPage,\n toContent: { left: null, right: null },\n width: grab.width,\n t: 0,\n };\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: grab.toPage });\n void this.#stageDrag(grab.targetIndex, grab.direction);\n }\n\n async #stageDrag(targetIndex: number, direction: FlipDirection): Promise<void> {\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n // The drag may have ended (or retargeted) while content was resolving.\n if (this.#drag?.targetIndex !== targetIndex) return;\n this.#drag.toContent = toContent;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n curl: this.#curl,\n anchor: { y: this.#anchorY },\n });\n this.#renderer?.setFlipProgress(this.#drag.t, direction);\n }\n\n #onDragMove(dx: number, dy: number): void {\n if (this.#pinching) return;\n if (this.#pan) {\n if (!this.#renderer) return;\n const m = this.#renderer.measure();\n const [tx, ty] = this.#clampPan(\n this.#pan.baseTx + dx,\n this.#pan.baseTy + dy,\n this.#scale,\n m.containerWidth,\n m.containerHeight,\n );\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(this.#scale, tx, ty);\n this.#refreshZoomTiles();\n return;\n }\n // An armed corner press becomes a real flip once it moves past the threshold.\n if (this.#pendingGrab && Math.hypot(dx, dy) > DRAG_THRESHOLD) {\n this.#promoteGrab();\n }\n const drag = this.#drag;\n if (!drag) return;\n const signed = drag.direction === 'forward' ? -dx : dx;\n drag.t = clamp(signed / drag.width, 0, 1);\n this.#renderer?.setFlipProgress(drag.t, drag.direction);\n }\n\n #onDragEnd(gesture: GestureEnd): void {\n if (this.#pinching) return;\n if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n return;\n }\n const drag = this.#drag;\n if (!drag) {\n // Never became a drag → it was a tap; maybe flip via clickToFlip.\n this.#pendingGrab = null;\n this.#maybeClickFlip(gesture);\n return;\n }\n this.#drag = null;\n this.#machine.send('release');\n\n const swiped =\n gesture.swipe &&\n (drag.direction === 'forward' ? gesture.dx < 0 : gesture.dx > 0);\n if (drag.t >= 0.5 || swiped) {\n this.#animateProgress(drag.t, 1, drag.direction, () =>\n this.#commit(drag.targetIndex, drag.toPage, drag.toContent),\n );\n } else {\n this.#animateProgress(drag.t, 0, drag.direction, () => this.#cancelFlip());\n }\n }\n\n #maybeClickFlip(gesture: GestureEnd): void {\n if (this.#clickToFlip === 'off' || this.#scale > 1 || !this.#press) return;\n // A tap barely moves; anything more was a drag we already ignored.\n if (Math.abs(gesture.dx) > DRAG_THRESHOLD || Math.abs(gesture.dy) > DRAG_THRESHOLD) return;\n const direction = this.#clickFlipDirection(this.#press);\n if (!direction) return;\n const step = direction === 'forward' ? 1 : -1;\n if (this.#current + step < 0 || this.#current + step >= this.#spreads.length) return;\n // Anchor the fold at the tapped height (cone/leaf/flick curl from where you tap).\n this.#anchorY = clamp(this.#press.y / this.#contentRect().height, 0, 1);\n // Read `#current` lazily: interrupting commits the running turn first, so this steps on\n // from wherever the book actually landed — one spread back from there is the neighbour\n // the reader is looking at, not the spread they tapped on two turns ago.\n const flip = (): void => this.#startFlip(this.#current + step);\n this.#clearPendingClickFlip();\n if (this.#clickFlipDelayValue <= 0) {\n // No double-click competing here → flip right away (or queue if mid-turn).\n this.#requestFlip(flip);\n return;\n }\n // Wait out the window; a double-click (zoom) cancels this.\n this.#pendingClickTimer = setTimeout(() => {\n this.#pendingClickTimer = null;\n this.#requestFlip(flip);\n }, this.#clickFlipDelayValue);\n }\n\n /** Where the current spread is actually drawn (lone pages centered), as the renderer reports\n * it — the region taps/drags map into. Falls back to book, then the full container. */\n #contentRect(): { x: number; y: number; width: number; height: number } {\n const m = this.#renderer!.measure();\n return m.content ?? m.book ?? { x: 0, y: 0, width: m.containerWidth, height: m.containerHeight };\n }\n\n /** A viewport point in book-local px: relative to the drawn page, not the container. The two\n * differ whenever the book is letterboxed, so every hit test against a zone or corner has to\n * come through here — measuring from the container edge puts the zones off by the bar width. */\n #toBookPoint(clientX: number, clientY: number): { x: number; y: number } {\n const rect = this.#container.getBoundingClientRect();\n const b = this.#contentRect();\n return { x: clientX - rect.left - b.x, y: clientY - rect.top - b.y };\n }\n\n /** Which way a tap at `point` (book-local, via {@link #toBookPoint}) turns the page, or null\n * for a dead zone. */\n #clickFlipDirection(point: { x: number; y: number }): FlipDirection | null {\n if (!this.#renderer) return null;\n const bookWidth = this.#contentRect().width;\n let side: 'left' | 'right' | null;\n if (this.#clickToFlip === 'half') {\n side = point.x > bookWidth / 2 ? 'right' : 'left';\n } else if (point.x <= this.#clickZoneSize) {\n side = 'left';\n } else if (point.x >= bookWidth - this.#clickZoneSize) {\n side = 'right';\n } else {\n side = null; // center dead zone in 'edge' mode\n }\n if (!side) return null;\n const forwardSide = this.#direction === 'rtl' ? 'left' : 'right';\n return side === forwardSide ? 'forward' : 'backward';\n }\n\n #clearPendingClickFlip(): void {\n if (this.#pendingClickTimer !== null) {\n clearTimeout(this.#pendingClickTimer);\n this.#pendingClickTimer = null;\n }\n }\n\n #cancelFlip(): void {\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n this.#machine.send('settle');\n this.#emitter.emit('flipEnd', { page: this.#currentPage });\n this.#drainQueuedFlip();\n }\n\n #onPinchStart(): void {\n // A second finger abandons any single-pointer gesture in flight.\n this.#pendingGrab = null;\n this.#clearPendingClickFlip();\n if (this.#drag) {\n this.#drag = null;\n this.#machine.send('release');\n this.#machine.send('settle');\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n } else if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n }\n this.#pinching = true;\n this.#pinchBaseScale = this.#scale;\n }\n\n #onPinchMove(centerX: number, centerY: number, scale: number): void {\n if (!this.#pinching) return;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(this.#pinchBaseScale * scale, { x: centerX - rect.left, y: centerY - rect.top });\n }\n\n #onPinchEnd(): void {\n this.#pinching = false;\n }\n\n /** Swallow the right-click menu over the book, when the consumer asked for that. */\n #bindContextMenu(): void {\n if (!this.#disableContextMenu) return;\n const onContextMenu = (event: Event): void => event.preventDefault();\n this.#container.addEventListener('contextmenu', onContextMenu);\n this.#unbindContextMenu = () =>\n this.#container.removeEventListener('contextmenu', onContextMenu);\n }\n\n #bindWheelZoom(): void {\n const onWheel = (event: WheelEvent): void => {\n // Ctrl/⌘ + wheel only — this is also what a desktop trackpad pinch emits.\n if (!this.#wheelZoom || !this.#zoomEnabled || !(event.ctrlKey || event.metaKey)) return;\n // Override the browser's native page zoom.\n event.preventDefault();\n const rect = this.#container.getBoundingClientRect();\n const factor = Math.exp(-event.deltaY * WHEEL_ZOOM_SENSITIVITY);\n this.setZoom(this.#scale * factor, {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n });\n };\n this.#container.addEventListener('wheel', onWheel as EventListener, { passive: false });\n this.#unbindWheel = () =>\n this.#container.removeEventListener('wheel', onWheel as EventListener);\n }\n\n #bindDoubleClickZoom(): void {\n const onDoubleClick = (event: MouseEvent): void => {\n const levels = this.#doubleClickLevels;\n if (!this.#zoomEnabled || levels === null || !this.#renderer) return;\n // At scale 1 a double-click inside a flip zone belongs to click-to-flip, not zoom,\n // unless we're honoring double-click zoom there.\n if (this.#scale <= 1 && this.#clickToFlip !== 'off' && !this.#honorDoubleClickInFlipZone) {\n const local = this.#toBookPoint(event.clientX, event.clientY);\n if (this.#clickFlipDirection(local) !== null) return; // in a flip zone → leave it to click-to-flip\n }\n // A double-click means the single-click flip we may have queued was really a zoom.\n this.#clearPendingClickFlip();\n event.preventDefault();\n // Next configured level above the current scale, else wrap to the first.\n const next = levels.find((l) => l > this.#scale + 1e-6) ?? levels[0] ?? this.#scale;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(next, { x: event.clientX - rect.left, y: event.clientY - rect.top });\n };\n this.#container.addEventListener('dblclick', onDoubleClick as EventListener);\n this.#unbindDblClick = () =>\n this.#container.removeEventListener('dblclick', onDoubleClick as EventListener);\n }\n\n async #init(rendererOption: RendererOption): Promise<void> {\n // Load the renderer chunk while opening an async source (e.g. a PDF) in parallel.\n const rendererPromise = selectRenderer(rendererOption);\n if (typeof this.#source.open === 'function') {\n await this.#source.open();\n this.#buildSpreadModel(); // page count known now; validates + builds spreads\n }\n this.#source.prefetch([this.#currentPage]);\n\n const selected = await rendererPromise;\n const renderer = await this.#mountWithFallback(selected);\n this.#renderer = renderer;\n\n // Re-render the current spread when a source upgrades one of its pages in place.\n this.#source.onPageUpdate?.((index) => this.#onPageUpdate(index));\n\n // Now that we can measure, apply single-page mode if the container is narrow.\n this.#applySinglePage(renderer.measure().containerWidth);\n\n const pointer = new PointerRecognizer({\n onStart: (g) => this.#onDragStart(g.x, g.y),\n onMove: (g) => this.#onDragMove(g.dx, g.dy),\n onEnd: (g) => this.#onDragEnd(g),\n });\n const pinch = new PinchRecognizer({\n onPinchStart: () => this.#onPinchStart(),\n onPinchMove: (g) => this.#onPinchMove(g.centerX, g.centerY, g.scale),\n onPinchEnd: () => this.#onPinchEnd(),\n });\n this.#unbindInput = bindGestures(this.#container, { pointer, pinch });\n this.#bindWheelZoom();\n this.#bindDoubleClickZoom();\n this.#bindContextMenu();\n this.#setupReducedMotion();\n this.#a11yCleanup = this.#setupA11y();\n\n await this.#renderCurrent();\n this.#observeResize();\n this.#prefetchWindow();\n this.#announce();\n this.#bindDeepLink();\n await this.#mountControls();\n this.#emitter.emit('ready');\n }\n\n /** Mount `renderer`; if it can't initialize (e.g. no WebGL2 context) fall back to CSS. */\n async #mountWithFallback(renderer: Renderer): Promise<Renderer> {\n try {\n await renderer.mount(this.#container);\n renderer.onFatal?.(() => {\n void this.#fallbackToCss();\n });\n return renderer;\n } catch {\n renderer.destroy();\n const css = await selectRenderer('css');\n await css.mount(this.#container);\n this.#fellBack = true;\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n return css;\n }\n }\n\n /** Runtime fallback: a live renderer signaled an unrecoverable failure (§8.4). */\n async #fallbackToCss(): Promise<void> {\n if (this.#fellBack || this.#destroyed) return;\n this.#fellBack = true;\n this.#renderer?.destroy();\n this.#renderer = null;\n const css = await selectRenderer('css');\n if (this.#destroyed) {\n css.destroy();\n return;\n }\n await css.mount(this.#container);\n this.#renderer = css;\n this.#applySinglePage(css.measure().containerWidth);\n await this.#renderCurrent();\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n }\n\n #setupReducedMotion(): void {\n if (typeof matchMedia !== 'function') return;\n const query = matchMedia('(prefers-reduced-motion: reduce)');\n this.#reducedMotion = query.matches;\n query.addEventListener?.('change', (e) => {\n this.#reducedMotion = e.matches;\n });\n }\n\n /**\n * Build the toolbar, if it is wanted and the container is real DOM.\n *\n * The chunk is fetched lazily so a book with `controls: false` never downloads it. Failure is\n * swallowed on purpose: a toolbar that cannot load should cost the reader a toolbar, not the\n * whole flipbook — `#init` has no error handling of its own, so a throw here would reject\n * `ready`.\n */\n async #mountControls(): Promise<void> {\n if (this.#controlsOption === false || this.#destroyed) return;\n const container = this.#container;\n if (!container.ownerDocument || typeof container.appendChild !== 'function') return;\n try {\n const { mountControls } = await import('./controls/controls');\n if (this.#destroyed) return; // destroyed while the chunk was in flight\n const options = this.#controlsOption === true ? {} : this.#controlsOption;\n this.#controlsCleanup = mountControls(this, container, options);\n } catch {\n // No toolbar; the book itself is unaffected.\n }\n }\n\n /** Mirror the page in the URL, and follow the URL when it changes. */\n #bindDeepLink(): void {\n if (!this.#deepLinkEnabled || typeof globalThis.location === 'undefined') return;\n const win = globalThis as unknown as Window;\n this.#deepLink = bindDeepLink(win, (page) => {\n // Only act on a hash the reader changed — following a link, or pressing Back. A hash we\n // wrote ourselves describes where the book already is.\n if (page !== this.#currentPage) this.flipTo(page);\n });\n this.#deepLink.push(this.#currentPage);\n this.#unsubscribeDeepLink = this.#emitter.on('pageChanged', ({ page }) => {\n this.#deepLink?.push(page);\n });\n }\n\n /** The URL that opens this book at `page` (defaults to the current one). */\n pageLink(page: number = this.#currentPage): string {\n const href = globalThis.location?.href ?? '';\n if (!href) return '';\n const url = new URL(href);\n url.hash = hashWithPage(url.hash, clamp(page, 0, Math.max(0, this.#source.pageCount - 1)));\n return url.toString();\n }\n\n /** Wire keyboard nav + an aria-live page announcer. Skips non-DOM containers. */\n #setupA11y(): (() => void) | null {\n const container = this.#container;\n const doc = container.ownerDocument;\n if (!doc || typeof container.setAttribute !== 'function') return null;\n\n if (!container.hasAttribute('tabindex')) container.tabIndex = 0;\n container.setAttribute('aria-roledescription', 'flipbook');\n const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);\n container.addEventListener('keydown', onKey);\n\n const live = doc.createElement('div');\n live.setAttribute('aria-live', 'polite');\n live.setAttribute('aria-atomic', 'true');\n live.style.cssText =\n 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';\n container.appendChild(live);\n this.#liveRegion = live;\n\n return () => {\n container.removeEventListener('keydown', onKey);\n live.remove();\n this.#liveRegion = null;\n };\n }\n\n #onKeyDown(event: KeyboardEvent): void {\n const forwardKey = this.#direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowLeft':\n if (event.key === forwardKey) this.flipNext();\n else this.flipPrev();\n event.preventDefault();\n break;\n case 'Home':\n this.flipTo(0);\n event.preventDefault();\n break;\n case 'End':\n this.flipTo(this.#source.pageCount - 1);\n event.preventDefault();\n break;\n default:\n break;\n }\n }\n\n #announce(): void {\n if (this.#liveRegion) {\n this.#liveRegion.textContent = `Page ${this.#currentPage + 1} of ${this.#source.pageCount}`;\n }\n }\n\n #effectiveDuration(): number {\n if (this.#reducedMotion) return 0;\n return this.#flipDuration * (this.#singlePage ? LONE_PAGE_FLIP_SCALE : 1);\n }\n\n /** Re-measure and re-render; call after the container resizes. */\n update(): void {\n if (!this.#renderer || this.#machine.state !== 'idle') return;\n this.#applySinglePage(this.#renderer.measure().containerWidth);\n void this.#renderCurrent();\n }\n\n /** The layout to actually use: the responsive narrow fallback forces 'single'. */\n #effectiveMode(): SpreadMode {\n return this.#narrow || this.#spreadMode === 'single' ? 'single' : this.#spreadMode;\n }\n\n #applySinglePage(containerWidth: number): void {\n const narrow =\n this.#responsiveSpread && shouldSinglePage(containerWidth, this.#singlePageThreshold);\n if (narrow === this.#narrow) return;\n this.#narrow = narrow;\n this.#rebuildSpreads();\n }\n\n /** Regroup the pages after something changes the layout, keeping the reader on the page they\n * were looking at even though its spread index may have moved. */\n #rebuildSpreads(): void {\n const mode = this.#effectiveMode();\n const changed = mode !== this.#effectiveModeShown;\n this.#effectiveModeShown = mode;\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(this.#source.pageCount, { direction: this.#direction, mode });\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n // The book's proportions change with the layout, so consumers sizing around it need to know.\n if (changed) this.#emitter.emit('spreadChanged', { mode, singlePage: this.#singlePage });\n }\n\n async #renderCurrent(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!spread || !this.#renderer) return;\n this.#currentContent = await this.#resolveContent(spread);\n this.#paintSpread(spread, this.#currentContent);\n }\n\n #paintSpread(spread: Spread, content: SpreadContent): void {\n this.#renderer?.renderSpread(spread, content, { fill: this.#singlePage });\n this.#applyContainerAspect();\n // Whatever is overlaid was rendered for the pages that just left; drop it and re-request.\n this.#zoomOverlay?.clear();\n this.#refreshZoomTiles();\n }\n\n /** Match the container's aspect-ratio to the book so it fits with no letterbox bars.\n * Non-destructive: `aspect-ratio` only drives whichever dimension the consumer leaves\n * auto (fit-width when they set a width), and is ignored if both are fixed. */\n #applyContainerAspect(): void {\n const b = this.#renderer?.measure().book;\n if (!b || b.width <= 0 || b.height <= 0) return;\n const ratio = (b.width / b.height).toFixed(4);\n if (ratio === this.#lastAspect) return;\n this.#lastAspect = ratio;\n this.#container.style.aspectRatio = ratio;\n }\n\n /**\n * A source upgraded a page's content (e.g. a progressive PDF swapping in the crisp render).\n *\n * Repainting mid-flip would disturb the animation, so an upgrade that lands during one is\n * noted and applied on settle instead of dropped. Dropping it left the page stuck at low\n * resolution for good: the upgrade fires once, and jumping straight to a page — from a search\n * hit, the page field, the outline — is exactly when it tends to arrive.\n */\n #onPageUpdate(index: number): void {\n if (this.#machine.state !== 'idle') {\n this.#pendingUpgrades.add(index);\n return;\n }\n const spread = this.#spreads[this.#current];\n if (spread && (spread.left === index || spread.right === index)) {\n void this.#renderCurrent();\n }\n }\n\n /**\n * Ask the source for a crisp render of whatever is visible, and lay it over the magnified page.\n *\n * Zooming is only a view transform, so without this the reader is magnifying the pixels of a\n * raster made to fit the screen. A vector source can do better; one backed by a fixed-resolution\n * original cannot, and says so by handing back a whole page, which is dropped.\n *\n * Debounced: panning and pinching move the view continuously, and rasterizing a PDF region is\n * far too slow to do per frame. The blurry page stays up in the meantime, so the delay costs\n * sharpness briefly rather than showing a gap.\n */\n #refreshZoomTiles(): void {\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n\n if (this.#scale <= 1) {\n this.#zoomOverlay?.clear();\n this.#tileGeneration++; // abandon anything in flight: it is no longer wanted\n return;\n }\n this.#tileTimer = setTimeout(() => {\n this.#tileTimer = null;\n void this.#renderZoomTiles();\n }, ZOOM_TILE_DELAY);\n }\n\n async #renderZoomTiles(): Promise<void> {\n const renderer = this.#renderer;\n const spread = this.#spreads[this.#current];\n if (!renderer || !spread || this.#scale <= 1) return;\n\n const m = renderer.measure();\n const box = m.content ?? m.book;\n if (!box) return; // no measured page box: nothing to map screen pixels back onto\n\n // In a two-page spread each page owns half the box; a lone page owns all of it.\n const halves: TilePage[] = [];\n const lone = spread.left === null || spread.right === null;\n const width = lone ? box.width : box.width / 2;\n if (spread.left !== null) halves.push({ index: spread.left, rect: { ...box, width } });\n if (spread.right !== null) {\n halves.push({ index: spread.right, rect: { ...box, x: box.x + (lone ? 0 : width), width } });\n }\n\n const view = { scale: this.#scale, tx: this.#tx, ty: this.#ty };\n const viewport = { x: 0, y: 0, width: m.containerWidth, height: m.containerHeight };\n const plans = planTiles(halves, view, viewport);\n\n const current = this.#zoomOverlay?.shown ?? [];\n const unchanged =\n plans.length === current.length && plans.every((p, i) => sameTile(p, current[i]!));\n if (unchanged) return;\n\n const generation = ++this.#tileGeneration;\n const drawn = await Promise.all(\n plans.map(async (plan) => {\n try {\n const content = await this.#source.get(plan.index, plan.request);\n // A source free to ignore the request returns the whole page. Its aspect gives it away:\n // a tile matches the region it was asked for. Drawing a full page into the region's box\n // would squash it, so treat that as \"nothing sharper available\".\n const want = (plan.dest.width / plan.dest.height) * 1;\n const got = content.width / content.height;\n if (Math.abs(want - got) / want > 0.02) return null;\n return { plan, content };\n } catch (error) {\n // A tile is an enhancement: the readable, blurry page is still on screen underneath.\n this.#emitter.emit('sourceError', { index: plan.index, error });\n return null;\n }\n }),\n );\n if (generation !== this.#tileGeneration || this.#scale <= 1) return; // the view moved on\n const tiles = drawn.filter((t): t is NonNullable<typeof t> => t !== null);\n if (tiles.length === 0) return; // leave whatever is up; clearing would only flicker\n\n this.#zoomOverlay ??= this.#createZoomOverlay();\n this.#zoomOverlay?.draw(tiles, view, viewport);\n }\n\n #createZoomOverlay(): ZoomOverlay | null {\n const doc = this.#container.ownerDocument as Document | undefined;\n // Same guard as #setupA11y: tests mount against bare EventTargets with no real DOM.\n if (!doc || typeof this.#container.append !== 'function') return null;\n return new ZoomOverlay(doc, this.#container);\n }\n\n /** Apply any upgrade that arrived mid-flip, now that the book has settled. */\n #flushPendingUpgrades(): void {\n if (this.#pendingUpgrades.size === 0) return;\n const spread = this.#spreads[this.#current];\n const onScreen =\n spread !== undefined &&\n [spread.left, spread.right].some((p) => p !== null && this.#pendingUpgrades.has(p));\n this.#pendingUpgrades.clear();\n if (onScreen) void this.#renderCurrent();\n }\n\n #observeResize(): void {\n if (typeof ResizeObserver === 'undefined') return;\n this.#resizeObserver = new ResizeObserver(() => this.#scheduleUpdate());\n this.#resizeObserver.observe(this.#container);\n }\n\n #scheduleUpdate(): void {\n if (this.#updateScheduled) return;\n this.#updateScheduled = true;\n requestAnimationFrame(() => {\n this.#updateScheduled = false;\n this.update();\n });\n }\n\n async #resolveContent(spread: Spread): Promise<SpreadContent> {\n const [left, right] = await Promise.all([this.#getPage(spread.left), this.#getPage(spread.right)]);\n return { left, right };\n }\n\n /** Decode one page; on failure emit `sourceError` and degrade to a blank (null) page. */\n async #getPage(index: number | null): Promise<PageContent | null> {\n if (index === null) return null;\n try {\n return await this.#source.get(index);\n } catch (error) {\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }\n\n #prefetchWindow(): void {\n const { toMount } = this.#virtualizer.update(this.#current, this.#spreads.length);\n const pages: number[] = [];\n for (const index of toMount) {\n const spread = this.#spreads[index];\n if (spread?.left != null) pages.push(spread.left);\n if (spread?.right != null) pages.push(spread.right);\n }\n this.#source.prefetch(pages);\n }\n\n #spreadIndexForPage(page: number): number {\n const index = this.#spreads.findIndex((s) => s.left === page || s.right === page);\n return index === -1 ? 0 : index;\n }\n\n /** Validate the (now-known) page count + startPage and build the spread model. */\n #buildSpreadModel(): void {\n const pageCount = this.#source.pageCount;\n if (!Number.isInteger(pageCount) || pageCount < 1) {\n throw new Error(`Zine: source has ${pageCount} pages; a Source must have at least 1 page.`);\n }\n const startPage = this.#startPageOption;\n if (\n startPage !== undefined &&\n (!Number.isInteger(startPage) || startPage < 0 || startPage >= pageCount)\n ) {\n throw new Error(\n `Zine: startPage ${JSON.stringify(startPage)} is out of range for a ${pageCount}-page book (valid 0..${pageCount - 1}).`,\n );\n }\n const mode = this.#effectiveMode();\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(pageCount, { direction: this.#direction, mode });\n // A page in the URL wins over `startPage`: the reader followed a link to it, which is a\n // later and more specific intent than the page the author configured.\n const linked = this.#deepLinkEnabled ? pageFromHash(globalThis.location?.hash ?? '') : null;\n this.#currentPage = clamp(linked ?? startPage ?? 0, 0, pageCount - 1);\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\n/** Quadratic ease-in-out. Deliberately gentler than a cubic: a cubic leaves the page nearly\n * motionless for the first quarter of the turn and then spikes through the middle at roughly\n * double the peak speed, which reads as a lurch rather than a sheet of paper being turned. */\nfunction easeInOutQuad(t: number): number {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n}\n\n/** Lone-page turn: same gentle start as the spread ease, then a longer decelerating finish.\n * Without a facing half the landing is a dissolve — front-loading a bit of progress and\n * stretching the tail keeps that fade from reading as a whip. */\nfunction easeInOutLone(t: number): number {\n const x = easeInOutQuad(t);\n return 1 - Math.pow(1 - x, 1.3);\n}\n\nfunction typeName(v: unknown): string {\n if (v === null) return 'null';\n if (Array.isArray(v)) return 'array';\n return typeof v;\n}\n\n/** Validate constructor inputs up front, throwing agent-actionable errors. */\nfunction validateOptions(container: unknown, options: unknown): void {\n if (\n typeof container !== 'object' ||\n container === null ||\n typeof (container as { appendChild?: unknown }).appendChild !== 'function' ||\n typeof (container as { addEventListener?: unknown }).addEventListener !== 'function'\n ) {\n throw new Error(`Zine: container must be a DOM element; received ${typeName(container)}.`);\n }\n if (typeof options !== 'object' || options === null) {\n throw new Error('Zine: an options object with a `source` is required.');\n }\n const o = options as Record<string, unknown>;\n\n const source = o.source as { get?: unknown; pageCount?: unknown } | undefined;\n if (\n typeof source !== 'object' ||\n source === null ||\n typeof source.get !== 'function' ||\n typeof source.pageCount !== 'number'\n ) {\n throw new Error('Zine: `source` is required and must be a Source, e.g. new ImageSource(urls).');\n }\n // Note: pageCount >= 1 and startPage-in-range are validated in #buildSpreadModel —\n // synchronously for a sync source, or after open() for an async one (e.g. PdfSource).\n\n const startPage = o.startPage;\n if (startPage !== undefined && (typeof startPage !== 'number' || !Number.isInteger(startPage) || startPage < 0)) {\n throw new Error(`Zine: startPage must be a non-negative integer; got ${JSON.stringify(startPage)}.`);\n }\n\n const direction = o.direction;\n if (direction !== undefined && direction !== 'ltr' && direction !== 'rtl') {\n throw new Error(`Zine: direction must be 'ltr' or 'rtl'; got ${JSON.stringify(direction)}.`);\n }\n\n const clickToFlip = o.clickToFlip;\n if (clickToFlip !== undefined && !['edge', 'half', 'off'].includes(clickToFlip as string)) {\n throw new Error(`Zine: clickToFlip must be 'edge', 'half', or 'off'; got ${JSON.stringify(clickToFlip)}.`);\n }\n\n const curl = o.curl;\n if (curl !== undefined && !CURL_TYPES.includes(curl as CurlType)) {\n throw new Error(`Zine: curl must be one of ${CURL_TYPES.join(', ')}; got ${JSON.stringify(curl)}.`);\n }\n\n for (const flag of ['deepLink', 'disableContextMenu', 'responsiveSpread'] as const) {\n if (o[flag] !== undefined && typeof o[flag] !== 'boolean') {\n throw new Error(`Zine: ${flag} must be a boolean; got ${typeName(o[flag])}.`);\n }\n }\n\n const controls = o.controls;\n if (controls !== undefined && typeof controls !== 'boolean' && (typeof controls !== 'object' || controls === null)) {\n throw new Error(\n `Zine: controls must be a boolean or an options object; got ${typeName(controls)}.`,\n );\n }\n const position = (controls as { position?: unknown } | undefined)?.position;\n if (position !== undefined && !['top', 'bottom', 'left', 'right'].includes(position as string)) {\n throw new Error(\n `Zine: controls.position must be 'top', 'bottom', 'left', or 'right'; got ${JSON.stringify(position)}.`,\n );\n }\n\n const spreadMode = o.spreadMode;\n if (spreadMode !== undefined && !['double', 'single', 'cover', 'book'].includes(spreadMode as string)) {\n throw new Error(\n `Zine: spreadMode must be 'double', 'single', 'cover', or 'book'; got ${JSON.stringify(spreadMode)}.`,\n );\n }\n\n for (const key of ['frontCover', 'backCover'] as const) {\n const value = o[key];\n if (value !== undefined && typeof value !== 'string') {\n throw new Error(`Zine: ${key} must be an image URL string; got ${JSON.stringify(value)}.`);\n }\n }\n const pages = o.pages;\n if (pages !== undefined && (typeof pages !== 'object' || pages === null || Array.isArray(pages))) {\n throw new Error('Zine: pages must be an object mapping page indices to image URLs.');\n }\n\n assertMin(o.width, 'width', 1);\n assertMin(o.height, 'height', 1);\n assertMin(o.flipDuration, 'flipDuration', 0);\n assertMin(o.clickZoneSize, 'clickZoneSize', 0);\n assertMin(o.clickFlipDelay, 'clickFlipDelay', 0);\n assertMin(o.singlePageThreshold, 'singlePageThreshold', 0);\n validateZoomOption(o.zoom);\n validateRendererOption(o.renderer);\n}\n\nfunction assertMin(value: unknown, name: string, min: number): void {\n if (value === undefined) return;\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {\n throw new Error(`Zine: ${name} must be a number >= ${min}; got ${JSON.stringify(value)}.`);\n }\n}\n\nfunction validateZoomOption(zoom: unknown): void {\n if (zoom === undefined) return;\n if (typeof zoom !== 'object' || zoom === null || Array.isArray(zoom)) {\n throw new Error('Zine: zoom must be an object, e.g. { max: 4 }.');\n }\n const z = zoom as Record<string, unknown>;\n if (z.enabled !== undefined && typeof z.enabled !== 'boolean') {\n throw new Error(`Zine: zoom.enabled must be a boolean; got ${JSON.stringify(z.enabled)}.`);\n }\n if (z.wheel !== undefined && typeof z.wheel !== 'boolean') {\n throw new Error(`Zine: zoom.wheel must be a boolean; got ${JSON.stringify(z.wheel)}.`);\n }\n if (z.doubleClickInFlipZone !== undefined && typeof z.doubleClickInFlipZone !== 'boolean') {\n throw new Error(\n `Zine: zoom.doubleClickInFlipZone must be a boolean; got ${JSON.stringify(z.doubleClickInFlipZone)}.`,\n );\n }\n if (z.doubleClick !== undefined && z.doubleClick !== false) {\n const levels = z.doubleClick;\n if (\n !Array.isArray(levels) ||\n levels.some((n) => typeof n !== 'number' || !Number.isFinite(n) || n < 1)\n ) {\n throw new Error(\n 'Zine: zoom.doubleClick must be false or an array of zoom levels >= 1, e.g. [1, 2, 4].',\n );\n }\n }\n assertMin(z.max, 'zoom.max', 1);\n}\n\nfunction validateRendererOption(renderer: unknown): void {\n if (renderer === undefined) return;\n if (typeof renderer === 'string') {\n if (renderer !== 'auto' && renderer !== 'css' && renderer !== 'webgl2') {\n throw new Error(\n `Zine: renderer '${renderer}' is not recognized; use 'auto', 'css', 'webgl2', an array of those, or a custom Renderer.`,\n );\n }\n return;\n }\n if (Array.isArray(renderer)) {\n for (const kind of renderer) {\n if (kind !== 'css' && kind !== 'webgl2') {\n throw new Error(\n `Zine: renderer order array may only contain 'css' or 'webgl2'; got ${JSON.stringify(kind)}.`,\n );\n }\n }\n return;\n }\n if (\n typeof renderer === 'object' &&\n renderer !== null &&\n typeof (renderer as { mount?: unknown }).mount === 'function'\n ) {\n return;\n }\n throw new Error(\n \"Zine: renderer must be 'auto', 'css', 'webgl2', an array of those, or a custom Renderer instance.\",\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { Source } from './types';\n\nexport type ImageFit = 'contain' | 'cover';\n\nexport interface ImageSourceOptions {\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** How each image fits its page; a hint consumed by the render layer. Default 'contain'. */\n fit?: ImageFit;\n}\n\n/**\n * Default content source for image books: decodes a list of image URLs to\n * ImageBitmaps, memoizes them, and prefetches neighbors on access.\n */\nexport class ImageSource implements Source {\n readonly pageCount: number;\n readonly fit: ImageFit;\n #urls: string[];\n #preload: number;\n #cache = new Map<number, Promise<ImageBitmap>>();\n\n constructor(urls: string[], options: ImageSourceOptions = {}) {\n this.#urls = urls;\n this.pageCount = urls.length;\n this.#preload = options.preload ?? 1;\n this.fit = options.fit ?? 'contain';\n }\n\n get(index: number): Promise<PageContent> {\n const decoded = this.#decode(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#decode(i).catch(() => {}); // swallow: prefetch failures surface on real get()\n }\n }\n }\n\n destroy(): void {\n for (const pending of this.#cache.values()) {\n void pending.then((bitmap) => bitmap.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #decode(index: number): Promise<ImageBitmap> {\n const cached = this.#cache.get(index);\n if (cached) return cached;\n\n const url = this.#urls[index];\n if (url === undefined) {\n return Promise.reject(\n new RangeError(`ImageSource: page ${index} is out of range (0..${this.pageCount - 1}).`),\n );\n }\n\n const promise = (async () => {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`ImageSource: failed to fetch page ${index} (${url}) — HTTP ${res.status}.`);\n }\n return createImageBitmap(await res.blob());\n })().catch((err: unknown) => {\n this.#cache.delete(index); // drop the failed entry so a later get() can retry\n throw err;\n });\n\n this.#cache.set(index, promise);\n return promise;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#radius","#mounted","#listeners","#state","#handlers","#activeId","#start","#prev","#last","#end","#reset","#pinching","#points","#pair","#startDistance","#remove","#base","#front","#back","#rawPages","#resolvePages","#resolved","#image","#cache","#renderer","#container","#source","#direction","#deepLinkEnabled","#disableContextMenu","#spreadMode","#configuredMode","#curl","#clickToFlip","#clickZoneSize","#singlePageThreshold","#responsiveSpread","#controlsOption","#startPageOption","#buildSpreadModel","#flipDuration","#zoomEnabled","#wheelZoom","#doubleClickLevels","#maxZoom","#honorDoubleClickInFlipZone","#clickFlipDelayValue","#ready","#init","#currentPage","#spreads","#current","#singlePage","#narrow","#rebuildSpreads","#renderCurrent","#applySinglePage","#getPage","#outline","#emitter","#stepFlip","#requestFlip","#startFlip","#spreadIndexForPage","#machine","#activeAnim","#queuedFlip","#finishActiveAnim","#raf","#flushPendingUpgrades","#scale","#tx","#ty","#clampPan","#refreshZoomTiles","#destroyed","#clearPendingClickFlip","#resizeObserver","#deepLink","#unsubscribeDeepLink","#controlsCleanup","#a11yCleanup","#unbindWheel","#unbindDblClick","#unbindContextMenu","#unbindInput","#tileTimer","#tileGeneration","#leadPage","#runFlip","#flipGeneration","#resolveContent","#currentContent","#effectiveCurl","#anchorY","#animateProgress","#commit","#effectiveDuration","#paintSpread","#prefetchWindow","#announce","#drainQueuedFlip","#pinching","#pan","#contentRect","#toBookPoint","#press","#pendingGrab","#drag","#stageDrag","#promoteGrab","#maybeClickFlip","#cancelFlip","#clickFlipDirection","#pendingClickTimer","#pinchBaseScale","#mountWithFallback","#onPageUpdate","#onDragStart","#onDragMove","#onDragEnd","#onPinchStart","#onPinchMove","#onPinchEnd","#bindWheelZoom","#bindDoubleClickZoom","#bindContextMenu","#setupReducedMotion","#setupA11y","#observeResize","#bindDeepLink","#mountControls","#fallbackToCss","#fellBack","#reducedMotion","#onKeyDown","#liveRegion","#effectiveMode","#effectiveModeShown","#applyContainerAspect","#zoomedAt","#lastAspect","#pendingUpgrades","#upgradeScale","#renderZoomTiles","#scheduleUpdate","#updateScheduled","#virtualizer","#urls","#preload","#decode","#cache"],"sources":["../src/engine/spread.ts","../src/engine/virtualizer.ts","../src/engine/emitter.ts","../src/engine/stateMachine.ts","../src/engine/input.ts","../src/geometry/hitTest.ts","../src/engine/deeplink.ts","../src/renderer/select.ts","../src/source/compose.ts","../src/zine.ts","../src/source/imageSource.ts"],"sourcesContent":["export type Direction = 'ltr' | 'rtl';\n\n/**\n * How pages group into spreads:\n * - `double`: paired from the start — `[0|1] [2|3] …`\n * - `single`: one page per spread — `[0] [1] …`\n * - `cover`: first page alone, then paired — `[·|0] [1|2] …`\n * - `book`: first AND last alone (front + back cover) — `[·|0] [1|2] … [N-1|·]`\n */\nexport type SpreadMode = 'double' | 'single' | 'cover' | 'book';\n\nexport interface Spread {\n /** Page index shown on the left, or null for a blank side. */\n left: number | null;\n /** Page index shown on the right, or null for a blank side. */\n right: number | null;\n}\n\nexport interface SpreadOptions {\n direction?: Direction;\n mode?: SpreadMode;\n}\n\n/**\n * Map a flat page list (indices 0..pageCount-1) into display spreads.\n *\n * Reading order is always preserved; `direction` only mirrors each spread's\n * left/right positions, so RTL is just an LTR layout with the sides swapped.\n * Out-of-range slots are `null` (a blank half on odd counts / a lone cover).\n */\nexport function buildSpreads(pageCount: number, options: SpreadOptions = {}): Spread[] {\n const { direction = 'ltr', mode = 'cover' } = options;\n if (pageCount <= 0) return [];\n\n const page = (i: number): number | null => (i >= 0 && i < pageCount ? i : null);\n const spreads: Spread[] = [];\n\n if (mode === 'single') {\n for (let i = 0; i < pageCount; i++) spreads.push({ left: null, right: i });\n } else if (mode === 'double') {\n for (let i = 0; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else if (mode === 'cover') {\n spreads.push({ left: null, right: 0 });\n for (let i = 1; i < pageCount; i += 2) spreads.push({ left: page(i), right: page(i + 1) });\n } else {\n // book: front cover alone, interior paired, back cover alone\n spreads.push({ left: null, right: 0 });\n if (pageCount > 1) {\n for (let i = 1; i <= pageCount - 2; i += 2) {\n spreads.push({ left: page(i), right: i + 1 <= pageCount - 2 ? page(i + 1) : null });\n }\n spreads.push({ left: pageCount - 1, right: null });\n }\n }\n\n return direction === 'rtl'\n ? spreads.map((s) => ({ left: s.right, right: s.left }))\n : spreads;\n}\n\n/** Whether a container this wide should fall back to one page per spread. */\nexport function shouldSinglePage(containerWidth: number, threshold: number): boolean {\n return containerWidth < threshold;\n}\n","/** Spread indices that should be mounted around `current`, clamped to the book. */\nexport function mountWindow(current: number, spreadCount: number, radius: number): number[] {\n const window: number[] = [];\n const from = Math.max(0, current - radius);\n const to = Math.min(spreadCount - 1, current + radius);\n for (let i = from; i <= to; i++) {\n window.push(i);\n }\n return window;\n}\n\nexport interface VirtualizerDelta {\n toMount: number[];\n toEvict: number[];\n}\n\n/**\n * Tracks which spreads are mounted and, on each move, reports the difference:\n * spreads entering the ±radius window (`toMount`) and those leaving it\n * (`toEvict`). Default radius 2 = current spread ±2.\n */\nexport class Virtualizer {\n #radius: number;\n #mounted = new Set<number>();\n\n constructor(radius = 2) {\n this.#radius = radius;\n }\n\n get mounted(): number[] {\n return [...this.#mounted].sort((a, b) => a - b);\n }\n\n /** Recenter on `current`; mounts/evicts to match the window and returns the delta. */\n update(current: number, spreadCount: number): VirtualizerDelta {\n const window = new Set(mountWindow(current, spreadCount, this.#radius));\n const toMount = [...window].filter((i) => !this.#mounted.has(i)).sort((a, b) => a - b);\n const toEvict = [...this.#mounted].filter((i) => !window.has(i)).sort((a, b) => a - b);\n for (const i of toEvict) this.#mounted.delete(i);\n for (const i of toMount) this.#mounted.add(i);\n return { toMount, toEvict };\n }\n}\n","import type { SpreadMode } from './spread.js';\n\nexport type Listener<T> = (payload: T) => void;\n\n/** Events typed as `void` are emitted with no payload argument. */\ntype PayloadArgs<T> = [T] extends [void] ? [] : [payload: T];\n\n/** Minimal typed event emitter keyed by an event map. */\nexport class Emitter<Events> {\n #listeners = new Map<keyof Events, Set<Listener<unknown>>>();\n\n /** Subscribe; returns an unsubscribe function. */\n on<K extends keyof Events>(type: K, listener: Listener<Events[K]>): () => void {\n let set = this.#listeners.get(type);\n if (!set) {\n set = new Set();\n this.#listeners.set(type, set);\n }\n set.add(listener as Listener<unknown>);\n return () => this.off(type, listener);\n }\n\n off<K extends keyof Events>(type: K, listener: Listener<Events[K]>): void {\n this.#listeners.get(type)?.delete(listener as Listener<unknown>);\n }\n\n emit<K extends keyof Events>(type: K, ...args: PayloadArgs<Events[K]>): void {\n const payload = args[0] as Events[K];\n // Copy so a listener that unsubscribes mid-emit doesn't disturb iteration.\n const set = this.#listeners.get(type);\n if (!set) return;\n for (const listener of [...set]) {\n (listener as Listener<Events[K]>)(payload);\n }\n }\n\n clear(): void {\n this.#listeners.clear();\n }\n}\n\n/**\n * Public event surface of a Zine instance. Payloads are provisional and will\n * firm up as the engine wires the emitters that fire them.\n */\nexport interface ZineEventMap {\n ready: void;\n flipStart: { from: number; to: number };\n flipEnd: { page: number };\n pageChanged: { page: number };\n /** Pages regrouped: the spread mode changed, or a narrow container forced one page. */\n spreadChanged: { mode: SpreadMode; singlePage: boolean };\n zoomChanged: { scale: number };\n sourceError: { index: number; error: unknown };\n rendererFallback: { from: string; to: string };\n}\n\nexport type ZineEmitter = Emitter<ZineEventMap>;\n","export type FlipState = 'idle' | 'dragging' | 'animating' | 'zoomed-panning';\n\nexport type FlipEvent = 'grab' | 'release' | 'settle' | 'panStart' | 'panEnd' | 'flip';\n\n/**\n * Legal transitions. A flip drag runs idle → dragging → animating → idle; a\n * programmatic flip jumps idle → animating directly via `flip`; zoom panning\n * (only reachable when zoomed in) is a separate branch idle → zoomed-panning →\n * idle. Any (state, event) pair absent here is illegal.\n */\nconst TRANSITIONS: Record<FlipState, Partial<Record<FlipEvent, FlipState>>> = {\n idle: { grab: 'dragging', panStart: 'zoomed-panning', flip: 'animating' },\n dragging: { release: 'animating' },\n animating: { settle: 'idle' },\n 'zoomed-panning': { panEnd: 'idle' },\n};\n\n/** Pure transition: the next state, or null if `event` is illegal from `state`. */\nexport function nextState(state: FlipState, event: FlipEvent): FlipState | null {\n return TRANSITIONS[state][event] ?? null;\n}\n\n/** Stateful holder around `nextState`; illegal events are ignored. */\nexport class FlipMachine {\n #state: FlipState;\n\n constructor(initial: FlipState = 'idle') {\n this.#state = initial;\n }\n\n get state(): FlipState {\n return this.#state;\n }\n\n /** Apply `event`; returns the new state, or null (no-op) if the event was illegal. */\n send(event: FlipEvent): FlipState | null {\n const next = nextState(this.#state, event);\n if (next !== null) {\n this.#state = next;\n }\n return next;\n }\n}\n","export interface GestureStart {\n x: number;\n y: number;\n t: number;\n}\n\nexport interface GestureMove {\n x: number;\n y: number;\n /** Displacement from the start point. */\n dx: number;\n dy: number;\n}\n\nexport interface GestureEnd {\n x: number;\n y: number;\n dx: number;\n dy: number;\n /** Elapsed time from start to end (ms). */\n dt: number;\n /** Velocity of the final move segment (px/ms). */\n vx: number;\n vy: number;\n /** A fast flick: final speed exceeded the swipe threshold and the gesture was not canceled. */\n swipe: boolean;\n /** The pointer was canceled (e.g. pointercancel) rather than lifted. */\n canceled: boolean;\n}\n\nexport interface PointerHandlers {\n onStart?: (g: GestureStart) => void;\n onMove?: (g: GestureMove) => void;\n onEnd?: (g: GestureEnd) => void;\n}\n\n/** Flick threshold in px/ms; a release faster than this is a swipe. */\nconst SWIPE_VELOCITY = 0.3;\n\ninterface Sample {\n x: number;\n y: number;\n t: number;\n}\n\n/**\n * Recognizes a single-pointer drag from raw pointer samples and reports normalized\n * gestures. DOM-agnostic on purpose: `bindPointerInput` maps real PointerEvents onto\n * it, but the kinematics stay pure and unit-testable. Extra pointers are ignored\n * (multi-touch pinch is a separate recognizer).\n */\nexport class PointerRecognizer {\n #handlers: PointerHandlers;\n #activeId: number | null = null;\n #start: Sample | null = null;\n #prev: Sample | null = null;\n #last: Sample | null = null;\n\n constructor(handlers: PointerHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get active(): boolean {\n return this.#activeId !== null;\n }\n\n down(id: number, x: number, y: number, t: number): void {\n if (this.#activeId !== null) return; // single-pointer; ignore extra pointers\n this.#activeId = id;\n const s: Sample = { x, y, t };\n this.#start = s;\n this.#prev = s;\n this.#last = s;\n this.#handlers.onStart?.({ x, y, t });\n }\n\n move(id: number, x: number, y: number, t: number): void {\n if (id !== this.#activeId || this.#start === null) return;\n this.#prev = this.#last;\n this.#last = { x, y, t };\n this.#handlers.onMove?.({ x, y, dx: x - this.#start.x, dy: y - this.#start.y });\n }\n\n up(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, false);\n }\n\n cancel(id: number, x: number, y: number, t: number): void {\n this.#end(id, x, y, t, true);\n }\n\n #end(id: number, x: number, y: number, t: number, canceled: boolean): void {\n if (id !== this.#activeId || this.#start === null) return;\n const start = this.#start;\n // Velocity comes from the last move segment, not the release point — on lift the\n // pointer usually stops, so release-relative velocity would kill every flick.\n const last = this.#last ?? start;\n const prev = this.#prev ?? start;\n const segDt = last.t - prev.t;\n const vx = segDt > 0 ? (last.x - prev.x) / segDt : 0;\n const vy = segDt > 0 ? (last.y - prev.y) / segDt : 0;\n const speed = Math.hypot(vx, vy);\n this.#reset();\n this.#handlers.onEnd?.({\n x,\n y,\n dx: x - start.x,\n dy: y - start.y,\n dt: t - start.t,\n vx,\n vy,\n swipe: !canceled && speed > SWIPE_VELOCITY,\n canceled,\n });\n }\n\n #reset(): void {\n this.#activeId = null;\n this.#start = null;\n this.#prev = null;\n this.#last = null;\n }\n}\n\n/**\n * Wire real PointerEvents from `target` into a recognizer. Returns an unbind\n * function that removes every listener. Coordinates are passed through as\n * client-space; mapping to page-local space is the engine's job.\n */\nexport function bindPointerInput(target: EventTarget, recognizer: PointerRecognizer): () => void {\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n recognizer.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\n/**\n * Wire PointerEvents from `target` into a drag recognizer and/or a pinch\n * recognizer at once, so a single pointer stream feeds both. Returns an unbind.\n */\nexport function bindGestures(\n target: EventTarget,\n recognizers: { pointer?: PointerRecognizer; pinch?: PinchRecognizer },\n): () => void {\n const { pointer, pinch } = recognizers;\n const down = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.down(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.down(p.pointerId, p.clientX, p.clientY);\n };\n const move = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.move(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.move(p.pointerId, p.clientX, p.clientY);\n };\n const up = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.up(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.up(p.pointerId);\n };\n const cancel = (e: Event): void => {\n const p = e as PointerEvent;\n pointer?.cancel(p.pointerId, p.clientX, p.clientY, p.timeStamp);\n pinch?.cancel(p.pointerId);\n };\n target.addEventListener('pointerdown', down);\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n target.addEventListener('pointercancel', cancel);\n return () => {\n target.removeEventListener('pointerdown', down);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n target.removeEventListener('pointercancel', cancel);\n };\n}\n\nexport interface PinchStart {\n centerX: number;\n centerY: number;\n /** Distance between the two pointers at pinch start. */\n distance: number;\n}\n\nexport interface PinchMove {\n centerX: number;\n centerY: number;\n /** Current distance / start distance. */\n scale: number;\n}\n\nexport interface PinchHandlers {\n onPinchStart?: (g: PinchStart) => void;\n onPinchMove?: (g: PinchMove) => void;\n onPinchEnd?: () => void;\n}\n\ninterface Pt {\n x: number;\n y: number;\n}\n\nfunction distance(a: Pt, b: Pt): number {\n return Math.hypot(a.x - b.x, a.y - b.y);\n}\n\n/**\n * Recognizes a two-pointer pinch and reports zoom scale (relative to the start\n * distance) and the live center point. No timestamps: pinch drives zoom, not a\n * flick. A third pointer is ignored; lifting either tracked pointer ends the pinch.\n * Coordinating this with the drag recognizer (a second finger should suspend a\n * single-finger drag) is the engine's job.\n */\nexport class PinchRecognizer {\n #handlers: PinchHandlers;\n #points = new Map<number, Pt>();\n #startDistance = 0;\n #pinching = false;\n\n constructor(handlers: PinchHandlers = {}) {\n this.#handlers = handlers;\n }\n\n get pinching(): boolean {\n return this.#pinching;\n }\n\n down(id: number, x: number, y: number): void {\n if (this.#points.has(id) || this.#points.size >= 2) return; // track only two\n this.#points.set(id, { x, y });\n if (this.#points.size === 2) {\n const { a, b } = this.#pair();\n this.#startDistance = distance(a, b);\n this.#pinching = true;\n this.#handlers.onPinchStart?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n distance: this.#startDistance,\n });\n }\n }\n\n move(id: number, x: number, y: number): void {\n const p = this.#points.get(id);\n if (!p) return;\n p.x = x;\n p.y = y;\n if (!this.#pinching) return;\n const { a, b } = this.#pair();\n this.#handlers.onPinchMove?.({\n centerX: (a.x + b.x) / 2,\n centerY: (a.y + b.y) / 2,\n scale: this.#startDistance > 0 ? distance(a, b) / this.#startDistance : 1,\n });\n }\n\n up(id: number): void {\n this.#remove(id);\n }\n\n cancel(id: number): void {\n this.#remove(id);\n }\n\n #remove(id: number): void {\n if (!this.#points.delete(id)) return;\n if (this.#pinching && this.#points.size < 2) {\n this.#pinching = false;\n this.#startDistance = 0;\n this.#handlers.onPinchEnd?.();\n }\n }\n\n #pair(): { a: Pt; b: Pt } {\n const values = [...this.#points.values()];\n const a = values[0];\n const b = values[1];\n if (!a || !b) throw new Error('pinch requires two active pointers');\n return { a, b };\n }\n}\n","import type { PageRect, Vec2 } from './foldFromPointer';\n\nexport type HitRegion = 'corner' | 'page' | 'outside';\n\n/**\n * Classify a page-local point into a hit region.\n *\n * Coordinates are page-local (see `foldFromPointer`): (0,0) top-left,\n * page spanning (0,0)..(width,height). `cornerSize` is the width/height of each\n * grabbable corner box. All four corners report `corner`; the engine decides\n * which are actually grabbable for the current spread layout (LTR/RTL, cover).\n */\nexport function hitTest(point: Vec2, pageRect: PageRect, cornerSize: number): HitRegion {\n const { width, height } = pageRect;\n if (point.x < 0 || point.x > width || point.y < 0 || point.y > height) {\n return 'outside';\n }\n const nearX = point.x <= cornerSize || point.x >= width - cornerSize;\n const nearY = point.y <= cornerSize || point.y >= height - cornerSize;\n return nearX && nearY ? 'corner' : 'page';\n}\n","/**\n * Keeping the page in the URL, so a link opens where the reader was.\n *\n * The hash is treated as shared ground rather than ours: only a `page` key is read or written,\n * anything else in it is preserved, and updates go through `replaceState` so turning pages does\n * not fill the reader's back button with history entries.\n */\nconst KEY = 'page';\n\n/**\n * Whether some book on this page already owns the hash.\n *\n * Two books sharing one URL would each overwrite the other's page, and the second to load would\n * open on the first one's page. Only the first claims it; the rest still work, they just do not\n * appear in the address bar.\n */\nlet claimed = false;\n\n/** Take the hash if it is going, and report whether this caller got it. */\nexport function claimHash(): boolean {\n if (claimed) return false;\n claimed = true;\n return true;\n}\n\n/** Give the hash back, so a book that replaces this one can own it. */\nexport function releaseHash(): void {\n claimed = false;\n}\n\n/** Read the 1-based page from a hash, as a 0-based index. Null when it carries no page. */\nexport function pageFromHash(hash: string): number | null {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n const raw = params.get(KEY);\n if (raw === null) return null;\n const page = Number(raw);\n return Number.isInteger(page) && page >= 1 ? page - 1 : null;\n}\n\n/** The hash that names `page` (0-based), leaving any other keys in `hash` untouched. */\nexport function hashWithPage(hash: string, page: number): string {\n const params = new URLSearchParams(hash.replace(/^#/, ''));\n params.set(KEY, String(page + 1));\n return `#${params.toString()}`;\n}\n\nexport interface DeepLinkHandle {\n /** Write the current page into the address bar. */\n push(page: number): void;\n stop(): void;\n}\n\n/**\n * Mirror the reader's page in the URL, and follow it when the URL changes (back/forward, or a\n * pasted link). `onNavigate` fires only for a page the book is not already on.\n */\nexport function bindDeepLink(win: Window, onNavigate: (page: number) => void): DeepLinkHandle {\n // The page the URL is known to describe. Compared against rather than the hash string itself:\n // some environments emit `hashchange` for our own replaceState, and a string comparison there\n // would let the echo through and turn the page a second time.\n let shown: number | null = null;\n const onHashChange = (): void => {\n const page = pageFromHash(win.location.hash);\n if (page === null || page === shown) return; // absent, or already where we are\n shown = page;\n onNavigate(page);\n };\n win.addEventListener('hashchange', onHashChange);\n return {\n push(page) {\n if (page === shown) return;\n shown = page;\n const hash = hashWithPage(win.location.hash, page);\n if (hash === win.location.hash) return;\n // replaceState, not assignment: a reader flipping through fifty pages should not have to\n // press Back fifty times to leave.\n win.history?.replaceState?.(win.history.state, '', hash);\n },\n stop() {\n win.removeEventListener('hashchange', onHashChange);\n },\n };\n}\n","import type { Renderer } from './types';\n\nexport type RendererKind = 'css' | 'webgl2';\n\n/**\n * How the consumer chooses a renderer:\n * - `'auto'` (default): prefer the GPU path (WebGL2), else CSS.\n * - a single kind: force it (`'webgl2'` throws if the device has no WebGL2).\n * - an ordered array: try each in turn, first viable wins.\n * - a `Renderer` instance: use it as-is (custom implementation, §6.1/§11).\n */\nexport type RendererOption = 'auto' | RendererKind | RendererKind[] | Renderer;\n\nexport interface Capabilities {\n /** WebGL2 available — the GPU renderer's only backend. */\n gpu: boolean;\n}\n\ntype RendererLoader = () => Promise<Renderer>;\n\n/**\n * Lazy loaders, one per kind. Each is a dynamic `import()` so the bundler\n * code-splits it into its own chunk and a device fetches only the one selected.\n */\nconst LOADERS: Partial<Record<RendererKind, RendererLoader>> = {\n css: async () => new (await import('./cssRenderer')).CssRenderer(),\n webgl2: async () => new (await import('./webglRenderer')).WebglRenderer(),\n};\n\n// The WebGL2 renderer is auto-selected on GPU-capable devices; CSS is the baseline\n// for non-GPU devices and the fallback for unrecoverable GPU failures (§8.4).\nconst WEBGL2_AUTO = true;\n\nfunction isRenderer(option: RendererOption): option is Renderer {\n return (\n typeof option === 'object' &&\n option !== null &&\n !Array.isArray(option) &&\n typeof (option as Renderer).mount === 'function'\n );\n}\n\nfunction autoOrder(caps: Capabilities): RendererKind[] {\n return caps.gpu && WEBGL2_AUTO ? ['webgl2', 'css'] : ['css'];\n}\n\n/** Detect WebGL2 support — the GPU renderer's only backend (no WebGL1, no WebGPU). */\nexport function detectCapabilities(): Capabilities {\n return { gpu: hasWebGl2() };\n}\n\nfunction hasWebGl2(): boolean {\n try {\n return Boolean(document.createElement('canvas').getContext('webgl2'));\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the `renderer` option to a mounted renderer instance, lazy-loading its\n * chunk. Throws an agent-actionable error if nothing viable can be loaded.\n */\nexport async function selectRenderer(\n option: RendererOption = 'auto',\n caps: Capabilities = detectCapabilities(),\n): Promise<Renderer> {\n if (isRenderer(option)) return option;\n\n const order = Array.isArray(option) ? option : option === 'auto' ? autoOrder(caps) : [option];\n\n for (const kind of order) {\n if (kind === 'webgl2' && !caps.gpu) continue; // skip the GPU path without WebGL2\n const loader = LOADERS[kind];\n if (loader) return loader();\n }\n\n if (order.length === 1 && order[0] === 'webgl2' && !caps.gpu) {\n throw new Error(\n \"renderer: 'webgl2' was forced but this device has no WebGL2. Use 'css' or 'auto'.\",\n );\n }\n throw new Error(\n `renderer: none of [${order.join(', ')}] could be loaded — use 'css' or 'auto'.`,\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { DownloadInfo, OutlineItem, PageRequest, Source } from './types';\n\nexport interface CompositionOptions {\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string | null;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string | null;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string> | null;\n}\n\n/**\n * Wrap `base` so image covers/replacements sit alongside it, or return `base`\n * unchanged when nothing is requested. Covers ADD pages (front prepended, back\n * appended); `pages` REPLACE existing source pages. `pages` indexes the base's own\n * pages, so it is unaffected by an added cover.\n */\nexport function composeSource(base: Source, options: CompositionOptions): Source {\n const hasFront = typeof options.frontCover === 'string';\n const hasBack = typeof options.backCover === 'string';\n const hasPages = options.pages != null && Object.keys(options.pages).length > 0;\n if (!hasFront && !hasBack && !hasPages) return base;\n return new CompositeSource(base, options);\n}\n\nfunction loadImage(url: string): Promise<ImageBitmap> {\n return fetch(url)\n .then((res) => {\n if (!res.ok) throw new Error(`zine: could not load image \"${url}\" (HTTP ${res.status}).`);\n return res.blob();\n })\n .then((blob) => createImageBitmap(blob));\n}\n\nclass CompositeSource implements Source {\n #base: Source;\n #front: string | null;\n #back: string | null;\n #rawPages: Record<number, string>;\n #resolved = new Map<number, string>(); // base-index → url (negatives resolved)\n #cache = new Map<string, Promise<ImageBitmap>>();\n open?: () => Promise<void>;\n onPageUpdate?: (handler: (index: number) => void) => void;\n getText?: (index: number) => Promise<string>;\n getDownload?: () => Promise<DownloadInfo | null>;\n getOutline?: () => Promise<OutlineItem[]>;\n\n constructor(base: Source, options: CompositionOptions) {\n this.#base = base;\n this.#front = typeof options.frontCover === 'string' ? options.frontCover : null;\n this.#back = typeof options.backCover === 'string' ? options.backCover : null;\n this.#rawPages = options.pages ?? {};\n\n if (typeof base.open === 'function') {\n // Async base: page count (and negative indices) known only after open().\n this.open = async (): Promise<void> => {\n await base.open!();\n this.#resolvePages();\n };\n } else {\n this.#resolvePages(); // sync base: count is known now\n }\n\n if (typeof base.onPageUpdate === 'function') {\n // Forward base page updates, shifting to book indices (past the front cover).\n this.onPageUpdate = (handler) => {\n base.onPageUpdate!((src) => handler(src + (this.#front !== null ? 1 : 0)));\n };\n }\n\n if (typeof base.getText === 'function') {\n // Mirrors get(): covers and replaced pages are images, so they have no text of their own —\n // returning the base page's text there would describe content the reader cannot see.\n this.getText = async (index) => {\n const frontOffset = this.#front !== null ? 1 : 0;\n if (this.#front !== null && index === 0) return '';\n if (this.#back !== null && index === this.pageCount - 1) return '';\n const src = index - frontOffset;\n if (this.#resolved.has(src)) return '';\n return base.getText!(src);\n };\n }\n\n if (typeof base.getDownload === 'function') {\n // Covers and replacements change what is displayed, not the original document, so the\n // download is the base source's file either way.\n this.getDownload = () => base.getDownload!();\n }\n\n if (typeof base.getOutline === 'function') {\n // The outline points at base pages; a front cover pushes every one of them along by one.\n this.getOutline = async () => {\n const items = await base.getOutline!();\n const offset = this.#front !== null ? 1 : 0;\n if (offset === 0) return items;\n const shift = (list: OutlineItem[]): OutlineItem[] =>\n list.map((item) => ({\n ...item,\n page: item.page === null ? null : item.page + offset,\n children: shift(item.children),\n }));\n return shift(items);\n };\n }\n }\n\n get pageCount(): number {\n return this.#base.pageCount + (this.#front !== null ? 1 : 0) + (this.#back !== null ? 1 : 0);\n }\n\n get(index: number, opts?: PageRequest): Promise<PageContent> {\n const frontOffset = this.#front !== null ? 1 : 0;\n // Covers and page overrides are images: they have no detail beyond their own resolution, so\n // the zoom request has nothing to act on and is dropped rather than forwarded.\n if (this.#front !== null && index === 0) return this.#image(this.#front);\n if (this.#back !== null && index === this.pageCount - 1) return this.#image(this.#back);\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n return override !== undefined ? this.#image(override) : this.#base.get(src, opts);\n }\n\n prefetch(indices: number[]): void {\n const frontOffset = this.#front !== null ? 1 : 0;\n const baseIndices: number[] = [];\n for (const index of indices) {\n if (this.#front !== null && index === 0) {\n void this.#image(this.#front).catch(() => {});\n } else if (this.#back !== null && index === this.pageCount - 1) {\n void this.#image(this.#back).catch(() => {});\n } else {\n const src = index - frontOffset;\n const override = this.#resolved.get(src);\n if (override !== undefined) void this.#image(override).catch(() => {});\n else baseIndices.push(src);\n }\n }\n this.#base.prefetch(baseIndices);\n }\n\n destroy(): void {\n this.#base.destroy();\n for (const promise of this.#cache.values()) {\n void promise.then((bmp) => bmp.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #resolvePages(): void {\n const n = this.#base.pageCount;\n this.#resolved.clear();\n for (const [key, url] of Object.entries(this.#rawPages)) {\n const raw = Number(key);\n const src = raw < 0 ? n + raw : raw;\n if (Number.isInteger(src) && src >= 0 && src < n) this.#resolved.set(src, url);\n }\n }\n\n #image(url: string): Promise<ImageBitmap> {\n let promise = this.#cache.get(url);\n if (promise === undefined) {\n promise = loadImage(url).catch((error: unknown) => {\n this.#cache.delete(url); // let a later get() retry\n throw error;\n });\n this.#cache.set(url, promise);\n }\n return promise;\n }\n}\n","import {\n buildSpreads,\n shouldSinglePage,\n type Direction,\n type Spread,\n type SpreadMode,\n} from './engine/spread';\nimport { Virtualizer } from './engine/virtualizer';\nimport { Emitter, type ZineEventMap } from './engine/emitter';\nimport { FlipMachine } from './engine/stateMachine';\nimport { PointerRecognizer, PinchRecognizer, bindGestures, type GestureEnd } from './engine/input';\nimport { hitTest } from './geometry/hitTest';\nimport {\n bindDeepLink,\n claimHash,\n hashWithPage,\n pageFromHash,\n releaseHash,\n type DeepLinkHandle,\n} from './engine/deeplink';\nimport {\n CURL_TYPES,\n DEFAULT_CURL,\n IMPORTABLE_CURLS,\n type CurlSpec,\n type CurlType,\n} from './geometry/curls/types';\nimport { selectRenderer, type RendererOption } from './renderer/select';\nimport type { FlipDirection, PageContent, Renderer, SpreadContent } from './renderer/types';\nimport type { DownloadInfo, OutlineItem, Source } from './source/types';\nimport { composeSource } from './source/compose';\nimport type { ControlsOptions } from './controls/types';\n\n/** Grab-zone size as a fraction of the smaller container dimension. */\nconst CORNER_FRACTION = 0.25;\n\n/** Wheel-zoom sensitivity: scale multiplies by exp(-deltaY * this) per wheel event. */\nconst WHEEL_ZOOM_SENSITIVITY = 0.0015;\n\n/** Pointer movement (px) beyond which a press becomes a drag rather than a tap. */\nconst DRAG_THRESHOLD = 6;\n\n/** Window (ms) a single click waits to rule out a double-click before flipping. */\nconst DOUBLE_CLICK_MS = 250;\n\n/** A lone page fills the container, so it sweeps the full width where a spread leaf only covers\n * its half — and it dissolves instead of landing on a facing page. Stretch duration so the\n * peel and fade read at a comparable pace to a spread turn rather than whipping away. */\nconst LONE_PAGE_FLIP_SCALE = 1.55;\n\n/** Characters of surrounding text shown on either side of a search match. */\nconst EXCERPT_PAD = 32;\n\n/** Quiet period before re-rasterizing at a new zoom, so a pinch does not rasterize every frame. */\nconst ZOOM_TILE_DELAY = 60;\n\n/** Ceiling on the zoomed page raster. Memory grows with the square of this, so a letter page at\n * 4x would run past 100 MB; 2.5x keeps a deep zoom far sharper than fit-to-screen for a\n * fraction of that. */\nconst MAX_PAGE_UPGRADE = 2.5;\n\n/** Flip time under `prefers-reduced-motion`. Short enough not to read as animation, long enough\n * to show which way the page went — an instant swap leaves the reader guessing. */\nconst REDUCED_MOTION_DURATION = 120;\n\n/** One page that matched a {@link Zine.search} query. */\nexport interface SearchHit {\n /** Zero-based page index; pass straight to `flipTo`. */\n page: number;\n /** The match with a little text either side, ellipsised where it was cut. */\n excerpt: string;\n}\n\n/** Pull a readable snippet around a match, collapsing the whitespace PDFs are full of. */\nfunction excerptAround(text: string, at: number, length: number): string {\n const start = Math.max(0, at - EXCERPT_PAD);\n const end = Math.min(text.length, at + length + EXCERPT_PAD);\n const slice = text.slice(start, end).replace(/\\s+/g, ' ').trim();\n return `${start > 0 ? '…' : ''}${slice}${end < text.length ? '…' : ''}`;\n}\n\ninterface DragState {\n direction: FlipDirection;\n targetIndex: number;\n toPage: number;\n toContent: SpreadContent;\n width: number;\n t: number;\n}\n\nexport interface ZoomOptions {\n /** Whether zooming is allowed at all; default true. */\n enabled?: boolean;\n /** Maximum zoom scale; default 4. */\n max?: number;\n /** Zoom on Ctrl/⌘ + wheel (also how trackpad pinch arrives on desktop); default true. */\n wheel?: boolean;\n /** Zoom levels cycled by double-click (wraps to the first); `false` disables. Default [1, 2, 4]. */\n doubleClick?: number[] | false;\n /**\n * Listen for double-click zoom inside the click-to-flip zones. Default: off in 'edge' mode\n * (edges flip instantly, no delay), on in 'half' mode. Turning it on in 'edge' mode makes\n * clicks wait out `clickFlipDelay` so a double-click can preempt them with a zoom.\n */\n doubleClickInFlipZone?: boolean;\n}\n\nexport interface ZineOptions {\n /** Content source (e.g. an ImageSource). Required. */\n source: Source;\n /** Renderer selection; default 'auto'. */\n renderer?: RendererOption;\n /**\n * Page-curl model for the WebGL2 renderer. Default `'cone'`.\n *\n * `'cone'` and `'simple'` are bundled and can be named directly. The others are imported and\n * passed as models, so a book that never uses them never carries their math:\n *\n * ```ts\n * import { silk } from '@zinejs/core/curls';\n * new Zine(el, { source, curl: silk });\n * ```\n *\n * Any object of the same shape works, so a custom curl needs no registration.\n */\n curl?: CurlSpec;\n /** Fixed container width in px; omit to let the container/CSS drive the size. */\n width?: number;\n /** Fixed container height in px; omit to let the container/CSS drive the size. */\n height?: number;\n /** Reading direction; default 'ltr'. */\n direction?: Direction;\n /** How pages group into spreads: 'double' | 'single' | 'cover' | 'book'. Default 'cover'. */\n spreadMode?: SpreadMode;\n /** Image URL prepended as a lone front cover (adds a page). */\n frontCover?: string;\n /** Image URL appended as a lone back cover (adds a page). */\n backCover?: string;\n /** Replace source pages with image URLs, keyed by 0-based source index (negative = from the end). */\n pages?: Record<number, string>;\n /** Page to open on; default 0. */\n startPage?: number;\n /** Flip animation duration in ms; default 500. */\n flipDuration?: number;\n /** How a tap/click turns pages: near an edge ('edge'), by page half ('half'), or 'off'. Default 'edge'. */\n clickToFlip?: 'edge' | 'half' | 'off';\n /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */\n clickZoneSize?: number;\n /**\n * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom.\n * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active.\n */\n clickFlipDelay?: number;\n /** Zoom behavior. */\n zoom?: ZoomOptions;\n /** Container widths below this (px) switch to one page per spread; default 640. */\n singlePageThreshold?: number;\n /**\n * Whether a narrow container may override `spreadMode` and show one page at a time. Default\n * true.\n *\n * Set false to hold the configured mode at every width — a two-page spread stays two pages on\n * a phone, however small. (`singlePageThreshold: 0` has the same effect; this says it out\n * loud, and can be changed at runtime with {@link Zine.setResponsiveSpread}.)\n */\n responsiveSpread?: boolean;\n /**\n * The built-in toolbar. Shown by default; pass `false` to render none, or an object to choose\n * its position and which controls appear.\n */\n controls?: boolean | ControlsOptions;\n /**\n * Keep the current page in the URL hash (`#page=12`), so a link opens where the reader was and\n * back/forward move through the book. Default true.\n *\n * Only a `page` key is read or written — anything else in the hash is left alone — and updates\n * use `replaceState`, so turning pages does not fill the back button. Pass `false` if your app\n * owns the hash.\n */\n deepLink?: boolean;\n /**\n * Suppress the browser's right-click menu over the book. Default false.\n *\n * A mild deterrent for published documents, not protection: the pages are still in the DOM and\n * reachable by anyone who wants them. It also takes away Inspect and \"Open image in new tab\"\n * for everyone, so it is off unless asked for.\n */\n disableContextMenu?: boolean;\n}\n\n/**\n * The public flipbook. Composes the engine (spreads, virtualizer, events), a\n * lazily-selected renderer, and a content source. Construction is synchronous\n * but setup is async — await `ready` (or listen for the `ready` event) before\n * driving it.\n */\nexport class Zine {\n #container: HTMLElement;\n #source: Source;\n #emitter = new Emitter<ZineEventMap>();\n #virtualizer = new Virtualizer(2);\n #machine = new FlipMachine();\n #direction: Direction;\n #spreadMode: SpreadMode;\n /** The mode the book was built with, so toggling away from one page can return to it — a\n * `cover` book comes back as `cover`, keeping its lone first page, not as plain `double`. */\n #configuredMode: SpreadMode;\n #curl: CurlSpec;\n #anchorY = 1; // where the last tap/drag grabbed (0=top, 1=bottom); drives anchored curls\n #clickToFlip: 'edge' | 'half' | 'off';\n #clickZoneSize: number;\n #honorDoubleClickInFlipZone: boolean;\n #clickFlipDelayValue: number;\n #singlePageThreshold: number;\n #singlePage = false;\n /** Last layout announced via `spreadChanged`, so a rebuild that changes nothing stays quiet. */\n #effectiveModeShown: SpreadMode | null = null;\n #narrow = false; // responsive: container currently below singlePageThreshold\n #responsiveSpread: boolean;\n #lastAspect = ''; // last container aspect-ratio written (avoids redundant style writes)\n #startPageOption: number | undefined;\n #deepLinkEnabled: boolean;\n #disableContextMenu: boolean;\n #deepLink: DeepLinkHandle | null = null;\n #unsubscribeDeepLink: (() => void) | null = null;\n #spreads: Spread[] = [];\n #current = 0;\n #currentPage = 0;\n #currentContent: SpreadContent = { left: null, right: null };\n #flipDuration: number;\n #zoomEnabled: boolean;\n #wheelZoom: boolean;\n #doubleClickLevels: number[] | null;\n #maxZoom: number;\n #resizeObserver: ResizeObserver | null = null;\n #updateScheduled = false;\n #scale = 1;\n #tx = 0;\n #ty = 0;\n #renderer: Renderer | null = null;\n #fellBack = false;\n #destroyed = false;\n #raf: number | null = null;\n #drag: DragState | null = null;\n #pendingGrab: { direction: FlipDirection; targetIndex: number; toPage: number; width: number } | null = null;\n #press: { x: number; y: number } | null = null;\n #pendingClickTimer: ReturnType<typeof setTimeout> | null = null;\n // Latest flip intent requested while one was already animating; replayed on settle so\n // clicks/keys during a turn aren't dropped and continuous flipping keeps advancing.\n #queuedFlip: (() => void) | null = null;\n // The fold animation currently on screen, so a new flip request can land it instantly\n // (snap to its end pose + commit) and start immediately instead of waiting it out.\n #activeAnim: { toT: number; direction: FlipDirection; onDone: () => void } | null = null;\n // Bumped per flip so an async #runFlip that resumes after being superseded can bail.\n #flipGeneration = 0;\n #pan: { baseTx: number; baseTy: number } | null = null;\n #pinching = false;\n #pinchBaseScale = 1;\n #reducedMotion = false;\n #liveRegion: HTMLElement | null = null;\n #unbindInput: (() => void) | null = null;\n #unbindWheel: (() => void) | null = null;\n #unbindContextMenu: (() => void) | null = null;\n #unbindDblClick: (() => void) | null = null;\n #a11yCleanup: (() => void) | null = null;\n /** Resolved once and reused; see {@link getOutline}. */\n #outline: Promise<OutlineItem[]> | null = null;\n /** Pages whose content improved while a flip was running, to repaint once it settles. */\n #pendingUpgrades = new Set<number>();\n #controlsOption: boolean | ControlsOptions;\n #controlsCleanup: (() => void) | null = null;\n /** Resolution multiplier the current spread is rasterized at; 1 while not zoomed. */\n #zoomedAt = 1;\n /** Guards against an out-of-date tile landing after the view has already moved on. */\n #tileGeneration = 0;\n #tileTimer: ReturnType<typeof setTimeout> | null = null;\n #ready: Promise<void>;\n\n /** Stable seek API: drive the fold to a fixed progress without animating (visual regression). */\n readonly debug = {\n setFlipProgress: (t: number, direction: FlipDirection): void => {\n this.#renderer?.setFlipProgress(t, direction);\n },\n };\n\n constructor(container: HTMLElement, options: ZineOptions) {\n validateOptions(container, options);\n this.#container = container;\n if (options.width !== undefined) container.style.width = `${options.width}px`;\n if (options.height !== undefined) container.style.height = `${options.height}px`;\n this.#source = composeSource(options.source, {\n frontCover: options.frontCover,\n backCover: options.backCover,\n pages: options.pages,\n });\n const direction = options.direction ?? 'ltr';\n this.#direction = direction;\n // Set before the spread model is built: that is where a page in the URL is honoured.\n // Claimed before the spread model is built, which is where the hash is read: a second book\n // on the same page must not open on the first one's page.\n this.#deepLinkEnabled = (options.deepLink ?? true) && claimHash();\n this.#disableContextMenu = options.disableContextMenu ?? false;\n this.#spreadMode = options.spreadMode ?? 'cover';\n this.#configuredMode = this.#spreadMode;\n this.#curl = options.curl ?? DEFAULT_CURL;\n this.#clickToFlip = options.clickToFlip ?? 'edge';\n this.#clickZoneSize = options.clickZoneSize ?? 64;\n this.#singlePageThreshold = options.singlePageThreshold ?? 640;\n this.#responsiveSpread = options.responsiveSpread ?? true;\n this.#controlsOption = options.controls ?? true;\n this.#startPageOption = options.startPage;\n // Sync sources (a known page count) build spreads now — so bad pageCount/startPage\n // throw immediately from `new Zine`. Async sources (an `open()`) defer to #init.\n if (typeof this.#source.open !== 'function') {\n this.#buildSpreadModel();\n }\n this.#flipDuration = options.flipDuration ?? 800;\n this.#zoomEnabled = options.zoom?.enabled ?? true;\n this.#wheelZoom = options.zoom?.wheel ?? true;\n const dbl = options.zoom?.doubleClick;\n const levels = dbl === false ? [] : [...(dbl ?? [1, 2, 4])].sort((a, b) => a - b);\n this.#doubleClickLevels = levels.length > 0 ? levels : null;\n this.#maxZoom = options.zoom?.max ?? 4;\n\n // Arbitrate click-to-flip vs double-click zoom inside the flip zones.\n // Listen for double-click zoom in flip zones: default off in 'edge', on in 'half'.\n // clickFlipDelay === 0 hard-disables it. When honored, a delay lets a click wait\n // out a possible double-click; auto default 250 ms, overridable.\n const doubleClickZoomActive = this.#zoomEnabled && this.#doubleClickLevels !== null;\n const listenInFlipZone = options.zoom?.doubleClickInFlipZone ?? (this.#clickToFlip === 'half');\n this.#honorDoubleClickInFlipZone =\n this.#clickToFlip !== 'off' &&\n doubleClickZoomActive &&\n listenInFlipZone &&\n options.clickFlipDelay !== 0;\n this.#clickFlipDelayValue = this.#honorDoubleClickInFlipZone\n ? (options.clickFlipDelay ?? DOUBLE_CLICK_MS)\n : 0;\n\n this.#ready = this.#init(options.renderer ?? 'auto');\n }\n\n /** Resolves once the renderer is mounted and the first spread is painted. */\n get ready(): Promise<void> {\n return this.#ready;\n }\n\n getPageCount(): number {\n return this.#source.pageCount;\n }\n\n getPage(): number {\n return this.#currentPage;\n }\n\n /** The element this flipbook was mounted into. */\n get container(): HTMLElement {\n return this.#container;\n }\n\n /**\n * How pages are currently grouped, one entry per spread — the same model the renderer paints,\n * so it already reflects `spreadMode` and the responsive single-page fallback. Useful for\n * building a page list or thumbnail rail that matches the book.\n */\n getSpreads(): readonly Spread[] {\n return this.#spreads;\n }\n\n /** The spread index currently on screen; indexes into {@link getSpreads}. */\n getSpreadIndex(): number {\n return this.#current;\n }\n\n /** Reading direction, which mirrors each spread's left/right sides. */\n getDirection(): Direction {\n return this.#direction;\n }\n\n /** How pages are grouped: the configured mode, not what a narrow container may be forcing. */\n getSpreadMode(): SpreadMode {\n return this.#spreadMode;\n }\n\n /** Whether the book is showing one page at a time, for whatever reason. */\n isSinglePage(): boolean {\n return this.#singlePage;\n }\n\n /**\n * Regroup the pages, keeping the reader on the page they are looking at.\n *\n * A narrow container may still override this with one page at a time; see\n * {@link setResponsiveSpread}.\n */\n setSpreadMode(mode: SpreadMode): void {\n if (mode === this.#spreadMode) return;\n this.#spreadMode = mode;\n // The narrow measurement was taken under the layout we are leaving, and a book showing one\n // page is narrower than the same book showing two. Holding on to it would let a container\n // that only shrank *because* of one-page mode veto the way back out. Drop it and let the\n // next measurement decide afresh.\n this.#narrow = false;\n this.#rebuildSpreads();\n void this.#renderCurrent();\n }\n\n /**\n * Switch between one page and two, and back again.\n *\n * Returning to two pages restores the mode the book was built with, so a `cover` book gets its\n * lone first page back rather than becoming a plain `double`.\n */\n toggleSpreadMode(): void {\n this.setSpreadMode(\n this.#spreadMode === 'single'\n ? this.#configuredMode === 'single'\n ? 'double' // built as single: there is no other mode to return to\n : this.#configuredMode\n : 'single',\n );\n }\n\n /** Whether a narrow container is allowed to override `spreadMode` with one page at a time. */\n getResponsiveSpread(): boolean {\n return this.#responsiveSpread;\n }\n\n /**\n * Whether the book is showing one page at a time *because the container is narrow*, rather\n * than because it was asked to. False when `responsiveSpread` is off.\n *\n * A book already set to `single` is not being overridden, however narrow it is: asking for one\n * page makes the book itself narrower, so reporting that as an override would strand the reader\n * in a layout they could no longer switch out of.\n */\n isResponsiveSingle(): boolean {\n return this.#narrow && this.#spreadMode !== 'single';\n }\n\n /**\n * Allow or forbid the narrow-container override, and re-lay out at once.\n *\n * Turning it off holds the configured `spreadMode` at every width; turning it back on\n * re-measures, so a book on a narrow screen collapses to single again straight away.\n */\n setResponsiveSpread(enabled: boolean): void {\n if (enabled === this.#responsiveSpread) return;\n this.#responsiveSpread = enabled;\n if (!enabled) {\n if (!this.#narrow) return; // nothing was being overridden\n this.#narrow = false;\n this.#rebuildSpreads();\n } else {\n const width = this.#renderer?.measure().containerWidth;\n if (width === undefined) return;\n this.#narrow = false; // let #applySinglePage see a real change and rebuild\n this.#applySinglePage(width);\n }\n void this.#renderCurrent();\n }\n\n /**\n * Resolve one page's raster, for drawing a thumbnail or exporting. Returns null if the page\n * cannot be decoded — the failure is reported through `sourceError` rather than thrown, as it\n * is for the pages the renderer paints.\n */\n getPageImage(index: number): Promise<PageContent | null> {\n return this.#getPage(index);\n }\n\n /** Whether there is a spread after the current one (false on the last). */\n canFlipNext(): boolean {\n return this.#current + 1 < this.#spreads.length;\n }\n\n /** Whether there is a spread before the current one (false on the first). */\n canFlipPrev(): boolean {\n return this.#current > 0;\n }\n\n /** The ceiling `setZoom` clamps to. */\n getMaxZoom(): number {\n return this.#maxZoom;\n }\n\n /** Whether this book's source can produce text — false for image books. */\n canSearch(): boolean {\n return typeof this.#source.getText === 'function';\n }\n\n /** Whether the original document can be downloaded — true for a PDF from a URL or bytes. */\n canDownload(): boolean {\n return typeof this.#source.getDownload === 'function';\n }\n\n /**\n * Whether the book is a document rather than a set of loose images — true for a PDF, false for\n * an `ImageSource`. Document-shaped features (the page rail, the outline) key off this.\n *\n * Inferred from the source implementing document-only capabilities, so a custom source opts in\n * simply by implementing them.\n */\n isDocument(): boolean {\n return typeof this.#source.getOutline === 'function' || typeof this.#source.getText === 'function';\n }\n\n /** Whether this book can be printed — true when the source has an original document. */\n canPrint(): boolean {\n return typeof this.#source.getDownload === 'function';\n }\n\n /**\n * Print the document.\n *\n * The original file is handed to the browser in an offscreen frame rather than printing the\n * host page: printing the page would capture the toolbar and whatever single spread happens to\n * be on screen, while the browser paginates a PDF properly on its own. Resolves false when\n * there is no original to print — an image book, or a caller-owned pdf.js document.\n */\n async print(): Promise<boolean> {\n const info = await this.getSourceFile();\n const doc = this.#container.ownerDocument;\n if (!info || !doc) return false;\n\n const frame = doc.createElement('iframe');\n // Offscreen rather than display:none: a hidden frame does not always lay out its document,\n // and an unlaid-out frame has nothing to print.\n frame.style.cssText =\n 'position:fixed;right:0;bottom:0;width:1px;height:1px;opacity:0;border:0;';\n frame.setAttribute('aria-hidden', 'true');\n frame.src = info.url;\n\n return new Promise<boolean>((resolve) => {\n let settled = false;\n const finish = (ok: boolean): void => {\n if (settled) return;\n settled = true;\n // Outlive the print dialog, which is modal and synchronous in some browsers; removing\n // the frame while it is open cancels the job.\n setTimeout(() => {\n frame.remove();\n if (info.revoke) URL.revokeObjectURL(info.url);\n }, 60_000);\n resolve(ok);\n };\n frame.addEventListener('load', () => {\n try {\n const win = frame.contentWindow;\n if (!win) return finish(false);\n win.focus();\n win.print();\n finish(true);\n } catch {\n finish(false); // a cross-origin PDF cannot be driven from here\n }\n });\n frame.addEventListener('error', () => finish(false));\n doc.body.appendChild(frame);\n });\n }\n\n /** Whether the source can supply a table of contents. True for PDFs; the document may still\n * turn out to have no outline, in which case {@link getOutline} resolves empty. */\n canOutline(): boolean {\n return typeof this.#source.getOutline === 'function';\n }\n\n /**\n * The document's table of contents, or an empty list when it has none.\n *\n * Memoized: an outline does not change for the life of the document, and resolving one means\n * following every entry's destination to a page. The toolbar asks for it up front to decide\n * whether to offer the outline panel at all, and the panel itself then gets it for free.\n */\n getOutline(): Promise<OutlineItem[]> {\n if (!this.#outline) {\n const getOutline = this.#source.getOutline;\n this.#outline =\n typeof getOutline === 'function'\n ? Promise.resolve(getOutline.call(this.#source)).catch(() => [])\n : Promise.resolve([]);\n }\n return this.#outline;\n }\n\n /**\n * Save the original document. Resolves to false when the source has nothing to hand over\n * (an image book, or a PDF opened from a caller-owned pdf.js document).\n */\n /**\n * Where the original document lives, if the source has one — a PDF's URL or a blob of its\n * bytes. Null for an image book, or a PDF opened from a caller-owned pdf.js document.\n *\n * Saving and printing both start here: the original is better than anything reassembled from\n * page rasters, and the browser already knows how to paginate it.\n */\n async getSourceFile(): Promise<DownloadInfo | null> {\n const getDownload = this.#source.getDownload;\n if (typeof getDownload !== 'function') return null;\n return (await getDownload.call(this.#source)) ?? null;\n }\n\n async download(): Promise<boolean> {\n const info = await this.getSourceFile();\n if (!info) return false;\n const doc = this.#container.ownerDocument;\n if (!doc) return false;\n const link = doc.createElement('a');\n link.href = info.url;\n link.download = info.filename;\n link.rel = 'noopener';\n doc.body.appendChild(link);\n link.click();\n link.remove();\n // An object URL is ours to clean up; give the click a tick to start first.\n if (info.revoke) setTimeout(() => URL.revokeObjectURL(info.url), 10_000);\n return true;\n }\n\n /**\n * Find `query` in the book's text, case-insensitively.\n *\n * Pages are read on demand and cached by the source, so the first search over a long document\n * costs one text extraction per page and later ones are cheap. Returns at most one hit per\n * page, in page order. Always empty when {@link canSearch} is false.\n */\n async search(query: string, options: { limit?: number } = {}): Promise<SearchHit[]> {\n const getText = this.#source.getText;\n const needle = query.trim().toLowerCase();\n if (typeof getText !== 'function' || needle === '') return [];\n const limit = options.limit ?? 50;\n const hits: SearchHit[] = [];\n for (let page = 0; page < this.#source.pageCount && hits.length < limit; page++) {\n let text: string;\n try {\n text = await getText.call(this.#source, page);\n } catch (error) {\n this.#emitter.emit('sourceError', { index: page, error });\n continue;\n }\n const at = text.toLowerCase().indexOf(needle);\n if (at < 0) continue;\n hits.push({ page, excerpt: excerptAround(text, at, needle.length) });\n }\n return hits;\n }\n\n flipNext(): void {\n this.#stepFlip(1);\n }\n\n flipPrev(): void {\n this.#stepFlip(-1);\n }\n\n /** Turn one spread in `step`'s direction, counted from wherever the book lands. Interrupting\n * commits the running turn first, so this steps on from there — repeat taps keep advancing,\n * and a reversal goes to the neighbour of the spread that just landed. */\n #stepFlip(step: 1 | -1): void {\n this.#requestFlip(() => this.#startFlip(this.#current + step));\n }\n\n flipTo(page: number): void {\n const target = clamp(page, 0, Math.max(0, this.#source.pageCount - 1));\n this.#requestFlip(() => this.#startFlip(this.#spreadIndexForPage(target)));\n }\n\n /** Run a flip now. If a fold is animating, land it instantly first so the new turn starts\n * immediately on this click (thunks read `#current` lazily, so it steps on from where the\n * interrupted turn landed). A drag in flight can't be interrupted this way, so its request\n * is queued and replayed on settle instead. */\n #requestFlip(run: () => void): void {\n if (this.#machine.state === 'idle') {\n run();\n } else if (this.#activeAnim) {\n // This request supersedes anything still waiting: landing the current turn commits it,\n // and that commit drains the queue. Without clearing it first, an older tap would fire\n // from inside the commit and take the book somewhere the reader has since changed their\n // mind about — a reversal would be undone by the forward tap it was meant to replace.\n this.#queuedFlip = null;\n this.#finishActiveAnim(); // lands the current turn → machine back to idle\n run();\n } else {\n this.#queuedFlip = run; // dragging: no animation to cut short, so wait for release\n }\n }\n\n /** Snap the in-flight fold to its end pose and commit it now, cancelling its RAF loop. */\n #finishActiveAnim(): void {\n const anim = this.#activeAnim;\n if (!anim) return;\n this.#activeAnim = null;\n if (this.#raf !== null) {\n cancelAnimationFrame(this.#raf);\n this.#raf = null;\n }\n this.#renderer?.setFlipProgress(anim.toT, anim.direction);\n anim.onDone();\n }\n\n /** After an animation settles, fire whatever flip was requested mid-turn. */\n #drainQueuedFlip(): void {\n const queued = this.#queuedFlip;\n if (!queued) {\n // Nothing further to turn to, so this is where the book comes to rest: a good moment to\n // apply any page content that improved while it was moving.\n this.#flushPendingUpgrades();\n return;\n }\n this.#queuedFlip = null;\n queued();\n }\n\n getZoom(): number {\n return this.#scale;\n }\n\n /** Zoom to `scale` (clamped to [1, zoom.max]), keeping `center` (container-local) fixed. */\n setZoom(scale: number, center?: { x: number; y: number }): void {\n if (!this.#renderer || !this.#zoomEnabled) return;\n const s2 = clamp(scale, 1, this.#maxZoom);\n const m = this.#renderer.measure();\n // Default to the middle of the pages, not of the container: on a lone page those differ, and\n // zooming toward the container centre would drift the page off toward the gutter.\n const box = m.screenAt?.(this.#scale);\n const focal = center ??\n (box\n ? { x: box.x + this.#tx + box.width / 2, y: box.y + this.#ty + box.height / 2 }\n : null) ?? { x: m.containerWidth / 2, y: m.containerHeight / 2 };\n const s1 = this.#scale;\n // Solve for the translate that keeps the focal screen point fixed as s1→s2.\n let tx = s2 === 1 ? 0 : focal.x - (s2 / s1) * (focal.x - this.#tx);\n let ty = s2 === 1 ? 0 : focal.y - (s2 / s1) * (focal.y - this.#ty);\n [tx, ty] = this.#clampPan(tx, ty, s2, m.containerWidth, m.containerHeight);\n this.#scale = s2;\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(s2, tx, ty);\n this.#refreshZoomTiles();\n this.#emitter.emit('zoomChanged', { scale: s2 });\n }\n\n resetZoom(): void {\n this.setZoom(1);\n }\n\n #clampPan(tx: number, ty: number, scale: number, w: number, h: number): [number, number] {\n // Bound the pan by where the pages actually are, not by the container. A lone page (a cover,\n // or the back of a book-mode spread) is painted in the middle half, and letterboxing insets\n // the book on any container whose aspect differs; clamping to the container would let the\n // reader drag the page off to one side and stare at the space beside it.\n //\n // `screen` is where the renderer says the pages are under the *current* view, so the offset\n // between it and the translate that produced it converts a wanted translate into a page\n // position. The renderers place a lone page differently (one shifts outside the view scale,\n // one inside), which is why this asks rather than deriving it.\n // Land on whole device pixels. A fractional offset makes every screen pixel a different\n // bilinear blend of the same texels, so as the page slides the strokes of each glyph thicken\n // and thin: the text appears to shimmer and change weight rather than simply move.\n const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n // `|| 0` normalises -0, which Object.is and strict deep-equality treat as distinct from 0.\n const snap = (v: number): number => Math.round(v * dpr) / dpr || 0;\n\n // Asked for `scale` rather than read from the current view, because setZoom clamps before it\n // commits and would otherwise be bounded by the scale it is leaving.\n // Falls back to the container, which is what the pages fill on a renderer that does not\n // report a box: keeps a bare Renderer implementation clamped rather than free to pan away.\n const box = this.#renderer?.measure().screenAt?.(scale) ?? {\n x: 0,\n y: 0,\n width: w * scale,\n height: h * scale,\n };\n const [minX, maxX] = axisPanRange(box.x, box.width, w);\n const [minY, maxY] = axisPanRange(box.y, box.height, h);\n return [snap(clamp(tx, minX, maxX)), snap(clamp(ty, minY, maxY))];\n }\n\n on<K extends keyof ZineEventMap>(\n event: K,\n listener: (payload: ZineEventMap[K]) => void,\n ): () => void {\n return this.#emitter.on(event, listener);\n }\n\n destroy(): void {\n this.#destroyed = true;\n if (this.#raf !== null) cancelAnimationFrame(this.#raf);\n this.#activeAnim = null;\n this.#queuedFlip = null;\n this.#clearPendingClickFlip();\n this.#resizeObserver?.disconnect();\n this.#deepLink?.stop();\n this.#deepLink = null;\n this.#unsubscribeDeepLink?.();\n if (this.#deepLinkEnabled) releaseHash(); // a later book may own it now\n this.#controlsCleanup?.();\n this.#a11yCleanup?.();\n this.#unbindWheel?.();\n this.#unbindDblClick?.();\n this.#unbindContextMenu?.();\n this.#unbindInput?.();\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n this.#tileGeneration++; // strand any raster still resolving\n this.#renderer?.destroy();\n this.#renderer = null;\n this.#source.destroy();\n this.#emitter.clear();\n }\n\n #startFlip(targetIndex: number): void {\n if (targetIndex < 0 || targetIndex >= this.#spreads.length || targetIndex === this.#current) {\n return;\n }\n // `flip` is legal only from idle; otherwise a flip/drag is already in flight.\n if (this.#machine.send('flip') === null) return;\n\n const direction: FlipDirection = targetIndex > this.#current ? 'forward' : 'backward';\n const toPage = this.#leadPage(targetIndex);\n // State is already locked (send('flip') above), so re-entrant flips are rejected\n // even though staging the destination content below is async.\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: toPage });\n // Claimed here, synchronously, so this flip owns the run before #runFlip's first await.\n // Bumping inside #runFlip instead let a superseded flip's continuation claim the newest\n // generation and cancel the flip that replaced it.\n void this.#runFlip(targetIndex, direction, toPage, ++this.#flipGeneration);\n }\n\n async #runFlip(\n targetIndex: number,\n direction: FlipDirection,\n toPage: number,\n generation: number,\n ): Promise<void> {\n // Staging the destination is async, so this can resume after a later flip has already\n // superseded it (interrupting mid-turn does exactly that). Without this check the stale\n // continuation would begin animating its own, now-abandoned target and commit it, undoing\n // the turn the reader actually asked for.\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n if (generation !== this.#flipGeneration) return;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n this.#animateProgress(0, 1, direction, () => this.#commit(targetIndex, toPage, toContent));\n }\n\n /** Animate the fold from `fromT` to `toT`, easing per-frame, then run `onDone`. */\n #animateProgress(\n fromT: number,\n toT: number,\n direction: FlipDirection,\n onDone: () => void,\n ): void {\n const duration = this.#effectiveDuration() * Math.abs(toT - fromT);\n if (duration <= 0) {\n // Reduced motion (or zero-duration): swap without the curl sweep.\n this.#renderer?.setFlipProgress(toT, direction);\n onDone();\n return;\n }\n // Remembered so a mid-turn flip request can #finishActiveAnim() to land it at once.\n // Object identity also tags this run: if #finishActiveAnim (or a new flip) replaces it,\n // a stale frame that still fires bails instead of re-committing or rescheduling itself.\n const anim = { toT, direction, onDone };\n this.#activeAnim = anim;\n const start = performance.now();\n const step = (now: number): void => {\n if (this.#activeAnim !== anim) return; // superseded/interrupted → this frame is void\n // Easing lives here; flipProgressToPose stays linear so seeks are deterministic.\n // Lone pages ease out harder — no facing half to land on, only a dissolve.\n const raw = Math.min(1, (now - start) / duration);\n const eased = this.#singlePage ? easeInOutLone(raw) : easeInOutQuad(raw);\n this.#renderer?.setFlipProgress(fromT + (toT - fromT) * eased, direction);\n if (raw < 1) {\n this.#raf = requestAnimationFrame(step);\n } else {\n this.#raf = null;\n this.#activeAnim = null;\n onDone();\n }\n };\n this.#raf = requestAnimationFrame(step);\n }\n\n #commit(targetIndex: number, toPage: number, toContent: SpreadContent): void {\n const spread = this.#spreads[targetIndex];\n this.#current = targetIndex;\n this.#currentPage = toPage;\n this.#currentContent = toContent;\n // renderSpread paints the landed spread and clears the turning leaf.\n if (spread) this.#paintSpread(spread, toContent);\n this.#prefetchWindow();\n this.#machine.send('settle');\n this.#announce();\n this.#emitter.emit('pageChanged', { page: toPage });\n this.#emitter.emit('flipEnd', { page: toPage });\n this.#drainQueuedFlip();\n }\n\n #leadPage(spreadIndex: number): number {\n const spread = this.#spreads[spreadIndex];\n if (!spread) return this.#currentPage;\n const pages = [spread.left, spread.right].filter((p): p is number => p !== null);\n return pages.length > 0 ? Math.min(...pages) : this.#currentPage;\n }\n\n #onDragStart(clientX: number, clientY: number): void {\n if (this.#pinching || !this.#renderer) return;\n this.#clearPendingClickFlip(); // a new press cancels a click-flip still waiting out its window\n // Zoomed in → a drag pans; at scale 1 → a corner drag flips (§9 mode switch).\n if (this.#scale > 1) {\n if (this.#machine.send('panStart') === null) return;\n this.#pan = { baseTx: this.#tx, baseTy: this.#ty };\n return;\n }\n const b = this.#contentRect();\n const point = this.#toBookPoint(clientX, clientY);\n // Recorded before the busy check below: a tap landing mid-flip is still a real tap, and\n // on release it decides which way to turn. Leaving the previous press in place made that\n // release repeat the last flip's direction instead of honouring the side just tapped.\n this.#press = point; // remembered for tap classification / click-to-flip zone\n // Only the drag machinery needs an idle book — a fold is already on screen, so there is\n // nothing to grab. The tap itself is handled on release, which queues or interrupts.\n if (this.#machine.state !== 'idle') {\n this.#pendingGrab = null;\n return;\n }\n const cornerSize = Math.min(b.width, b.height) * CORNER_FRACTION;\n if (hitTest(point, { width: b.width, height: b.height }, cornerSize) !== 'corner') {\n return; // not a corner — no drag; a tap here may still click-to-flip on release\n }\n this.#anchorY = clamp(point.y / b.height, 0, 1); // fold anchors at the grabbed corner\n // Arm a potential drag; it only becomes a real flip once the pointer moves\n // (so a corner *tap* never fires a spurious flipStart). Right-side corner\n // turns the page forward in LTR; RTL mirrors it.\n const rightSide = point.x > b.width / 2;\n const forward = this.#direction === 'rtl' ? !rightSide : rightSide;\n const targetIndex = this.#current + (forward ? 1 : -1);\n if (targetIndex < 0 || targetIndex >= this.#spreads.length) return;\n this.#pendingGrab = {\n direction: forward ? 'forward' : 'backward',\n targetIndex,\n toPage: this.#leadPage(targetIndex),\n width: b.width,\n };\n }\n\n /** Turn an armed corner press into a live drag flip once the pointer has moved. */\n #promoteGrab(): void {\n const grab = this.#pendingGrab;\n if (!grab) return;\n this.#pendingGrab = null;\n if (this.#machine.send('grab') === null) return;\n this.#drag = {\n direction: grab.direction,\n targetIndex: grab.targetIndex,\n toPage: grab.toPage,\n toContent: { left: null, right: null },\n width: grab.width,\n t: 0,\n };\n this.#emitter.emit('flipStart', { from: this.#currentPage, to: grab.toPage });\n void this.#stageDrag(grab.targetIndex, grab.direction);\n }\n\n async #stageDrag(targetIndex: number, direction: FlipDirection): Promise<void> {\n const toSpread = this.#spreads[targetIndex];\n const toContent: SpreadContent = toSpread\n ? await this.#resolveContent(toSpread)\n : { left: null, right: null };\n // The drag may have ended (or retargeted) while content was resolving.\n if (this.#drag?.targetIndex !== targetIndex) return;\n this.#drag.toContent = toContent;\n this.#renderer?.beginFlip(this.#currentContent, toContent, direction, {\n fill: this.#singlePage,\n curl: this.#effectiveCurl(),\n anchor: { y: this.#anchorY },\n });\n this.#renderer?.setFlipProgress(this.#drag.t, direction);\n }\n\n #onDragMove(dx: number, dy: number): void {\n if (this.#pinching) return;\n if (this.#pan) {\n if (!this.#renderer) return;\n const m = this.#renderer.measure();\n const [tx, ty] = this.#clampPan(\n this.#pan.baseTx + dx,\n this.#pan.baseTy + dy,\n this.#scale,\n m.containerWidth,\n m.containerHeight,\n );\n this.#tx = tx;\n this.#ty = ty;\n this.#renderer.setViewTransform(this.#scale, tx, ty);\n this.#refreshZoomTiles();\n return;\n }\n // An armed corner press becomes a real flip once it moves past the threshold.\n if (this.#pendingGrab && Math.hypot(dx, dy) > DRAG_THRESHOLD) {\n this.#promoteGrab();\n }\n const drag = this.#drag;\n if (!drag) return;\n const signed = drag.direction === 'forward' ? -dx : dx;\n drag.t = clamp(signed / drag.width, 0, 1);\n this.#renderer?.setFlipProgress(drag.t, drag.direction);\n }\n\n #onDragEnd(gesture: GestureEnd): void {\n if (this.#pinching) return;\n if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n return;\n }\n const drag = this.#drag;\n if (!drag) {\n // Never became a drag → it was a tap; maybe flip via clickToFlip.\n this.#pendingGrab = null;\n this.#maybeClickFlip(gesture);\n return;\n }\n this.#drag = null;\n this.#machine.send('release');\n\n const swiped =\n gesture.swipe &&\n (drag.direction === 'forward' ? gesture.dx < 0 : gesture.dx > 0);\n if (drag.t >= 0.5 || swiped) {\n this.#animateProgress(drag.t, 1, drag.direction, () =>\n this.#commit(drag.targetIndex, drag.toPage, drag.toContent),\n );\n } else {\n this.#animateProgress(drag.t, 0, drag.direction, () => this.#cancelFlip());\n }\n }\n\n #maybeClickFlip(gesture: GestureEnd): void {\n if (this.#clickToFlip === 'off' || this.#scale > 1 || !this.#press) return;\n // A tap barely moves; anything more was a drag we already ignored.\n if (Math.abs(gesture.dx) > DRAG_THRESHOLD || Math.abs(gesture.dy) > DRAG_THRESHOLD) return;\n const direction = this.#clickFlipDirection(this.#press);\n if (!direction) return;\n const step = direction === 'forward' ? 1 : -1;\n if (this.#current + step < 0 || this.#current + step >= this.#spreads.length) return;\n // Anchor the fold at the tapped height (cone/leaf/flick curl from where you tap).\n this.#anchorY = clamp(this.#press.y / this.#contentRect().height, 0, 1);\n // Read `#current` lazily: interrupting commits the running turn first, so this steps on\n // from wherever the book actually landed — one spread back from there is the neighbour\n // the reader is looking at, not the spread they tapped on two turns ago.\n const flip = (): void => this.#startFlip(this.#current + step);\n this.#clearPendingClickFlip();\n if (this.#clickFlipDelayValue <= 0) {\n // No double-click competing here → flip right away (or queue if mid-turn).\n this.#requestFlip(flip);\n return;\n }\n // Wait out the window; a double-click (zoom) cancels this.\n this.#pendingClickTimer = setTimeout(() => {\n this.#pendingClickTimer = null;\n this.#requestFlip(flip);\n }, this.#clickFlipDelayValue);\n }\n\n /** Where the current spread is actually drawn (lone pages centered), as the renderer reports\n * it — the region taps/drags map into. Falls back to book, then the full container. */\n #contentRect(): { x: number; y: number; width: number; height: number } {\n const m = this.#renderer!.measure();\n return m.content ?? m.book ?? { x: 0, y: 0, width: m.containerWidth, height: m.containerHeight };\n }\n\n /** A viewport point in book-local px: relative to the drawn page, not the container. The two\n * differ whenever the book is letterboxed, so every hit test against a zone or corner has to\n * come through here — measuring from the container edge puts the zones off by the bar width. */\n #toBookPoint(clientX: number, clientY: number): { x: number; y: number } {\n const rect = this.#container.getBoundingClientRect();\n const b = this.#contentRect();\n return { x: clientX - rect.left - b.x, y: clientY - rect.top - b.y };\n }\n\n /** Which way a tap at `point` (book-local, via {@link #toBookPoint}) turns the page, or null\n * for a dead zone. */\n #clickFlipDirection(point: { x: number; y: number }): FlipDirection | null {\n if (!this.#renderer) return null;\n const bookWidth = this.#contentRect().width;\n let side: 'left' | 'right' | null;\n if (this.#clickToFlip === 'half') {\n side = point.x > bookWidth / 2 ? 'right' : 'left';\n } else if (point.x <= this.#clickZoneSize) {\n side = 'left';\n } else if (point.x >= bookWidth - this.#clickZoneSize) {\n side = 'right';\n } else {\n side = null; // center dead zone in 'edge' mode\n }\n if (!side) return null;\n const forwardSide = this.#direction === 'rtl' ? 'left' : 'right';\n return side === forwardSide ? 'forward' : 'backward';\n }\n\n #clearPendingClickFlip(): void {\n if (this.#pendingClickTimer !== null) {\n clearTimeout(this.#pendingClickTimer);\n this.#pendingClickTimer = null;\n }\n }\n\n #cancelFlip(): void {\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n this.#machine.send('settle');\n this.#emitter.emit('flipEnd', { page: this.#currentPage });\n this.#drainQueuedFlip();\n }\n\n #onPinchStart(): void {\n // A second finger abandons any single-pointer gesture in flight.\n this.#pendingGrab = null;\n this.#clearPendingClickFlip();\n if (this.#drag) {\n this.#drag = null;\n this.#machine.send('release');\n this.#machine.send('settle');\n const spread = this.#spreads[this.#current];\n if (spread) this.#paintSpread(spread, this.#currentContent);\n } else if (this.#pan) {\n this.#pan = null;\n this.#machine.send('panEnd');\n }\n this.#pinching = true;\n this.#pinchBaseScale = this.#scale;\n }\n\n #onPinchMove(centerX: number, centerY: number, scale: number): void {\n if (!this.#pinching) return;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(this.#pinchBaseScale * scale, { x: centerX - rect.left, y: centerY - rect.top });\n }\n\n #onPinchEnd(): void {\n this.#pinching = false;\n }\n\n /** Swallow the right-click menu over the book, when the consumer asked for that. */\n #bindContextMenu(): void {\n if (!this.#disableContextMenu) return;\n const onContextMenu = (event: Event): void => event.preventDefault();\n this.#container.addEventListener('contextmenu', onContextMenu);\n this.#unbindContextMenu = () =>\n this.#container.removeEventListener('contextmenu', onContextMenu);\n }\n\n #bindWheelZoom(): void {\n const onWheel = (event: WheelEvent): void => {\n // Ctrl/⌘ + wheel only — this is also what a desktop trackpad pinch emits.\n if (!this.#wheelZoom || !this.#zoomEnabled || !(event.ctrlKey || event.metaKey)) return;\n // Override the browser's native page zoom.\n event.preventDefault();\n const rect = this.#container.getBoundingClientRect();\n const factor = Math.exp(-event.deltaY * WHEEL_ZOOM_SENSITIVITY);\n this.setZoom(this.#scale * factor, {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n });\n };\n this.#container.addEventListener('wheel', onWheel as EventListener, { passive: false });\n this.#unbindWheel = () =>\n this.#container.removeEventListener('wheel', onWheel as EventListener);\n }\n\n #bindDoubleClickZoom(): void {\n const onDoubleClick = (event: MouseEvent): void => {\n const levels = this.#doubleClickLevels;\n if (!this.#zoomEnabled || levels === null || !this.#renderer) return;\n // At scale 1 a double-click inside a flip zone belongs to click-to-flip, not zoom,\n // unless we're honoring double-click zoom there.\n if (this.#scale <= 1 && this.#clickToFlip !== 'off' && !this.#honorDoubleClickInFlipZone) {\n const local = this.#toBookPoint(event.clientX, event.clientY);\n if (this.#clickFlipDirection(local) !== null) return; // in a flip zone → leave it to click-to-flip\n }\n // A double-click means the single-click flip we may have queued was really a zoom.\n this.#clearPendingClickFlip();\n event.preventDefault();\n // Next configured level above the current scale, else wrap to the first.\n const next = levels.find((l) => l > this.#scale + 1e-6) ?? levels[0] ?? this.#scale;\n const rect = this.#container.getBoundingClientRect();\n this.setZoom(next, { x: event.clientX - rect.left, y: event.clientY - rect.top });\n };\n this.#container.addEventListener('dblclick', onDoubleClick as EventListener);\n this.#unbindDblClick = () =>\n this.#container.removeEventListener('dblclick', onDoubleClick as EventListener);\n }\n\n async #init(rendererOption: RendererOption): Promise<void> {\n // Load the renderer chunk while opening an async source (e.g. a PDF) in parallel.\n const rendererPromise = selectRenderer(rendererOption);\n if (typeof this.#source.open === 'function') {\n await this.#source.open();\n this.#buildSpreadModel(); // page count known now; validates + builds spreads\n }\n this.#source.prefetch([this.#currentPage]);\n\n const selected = await rendererPromise;\n const renderer = await this.#mountWithFallback(selected);\n this.#renderer = renderer;\n\n // Re-render the current spread when a source upgrades one of its pages in place.\n this.#source.onPageUpdate?.((index) => this.#onPageUpdate(index));\n\n // Now that we can measure, apply single-page mode if the container is narrow.\n this.#applySinglePage(renderer.measure().containerWidth);\n\n const pointer = new PointerRecognizer({\n onStart: (g) => this.#onDragStart(g.x, g.y),\n onMove: (g) => this.#onDragMove(g.dx, g.dy),\n onEnd: (g) => this.#onDragEnd(g),\n });\n const pinch = new PinchRecognizer({\n onPinchStart: () => this.#onPinchStart(),\n onPinchMove: (g) => this.#onPinchMove(g.centerX, g.centerY, g.scale),\n onPinchEnd: () => this.#onPinchEnd(),\n });\n this.#unbindInput = bindGestures(this.#container, { pointer, pinch });\n this.#bindWheelZoom();\n this.#bindDoubleClickZoom();\n this.#bindContextMenu();\n this.#setupReducedMotion();\n this.#a11yCleanup = this.#setupA11y();\n\n await this.#renderCurrent();\n this.#observeResize();\n this.#prefetchWindow();\n this.#announce();\n this.#bindDeepLink();\n await this.#mountControls();\n this.#emitter.emit('ready');\n }\n\n /** Mount `renderer`; if it can't initialize (e.g. no WebGL2 context) fall back to CSS. */\n async #mountWithFallback(renderer: Renderer): Promise<Renderer> {\n try {\n await renderer.mount(this.#container);\n renderer.onFatal?.(() => {\n void this.#fallbackToCss();\n });\n return renderer;\n } catch {\n renderer.destroy();\n const css = await selectRenderer('css');\n await css.mount(this.#container);\n this.#fellBack = true;\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n return css;\n }\n }\n\n /** Runtime fallback: a live renderer signaled an unrecoverable failure (§8.4). */\n async #fallbackToCss(): Promise<void> {\n if (this.#fellBack || this.#destroyed) return;\n this.#fellBack = true;\n this.#renderer?.destroy();\n this.#renderer = null;\n const css = await selectRenderer('css');\n if (this.#destroyed) {\n css.destroy();\n return;\n }\n await css.mount(this.#container);\n this.#renderer = css;\n this.#applySinglePage(css.measure().containerWidth);\n await this.#renderCurrent();\n this.#emitter.emit('rendererFallback', { from: 'webgl2', to: 'css' });\n }\n\n #setupReducedMotion(): void {\n if (typeof matchMedia !== 'function') return;\n const query = matchMedia('(prefers-reduced-motion: reduce)');\n this.#reducedMotion = query.matches;\n query.addEventListener?.('change', (e) => {\n this.#reducedMotion = e.matches;\n });\n }\n\n /**\n * Build the toolbar, if it is wanted and the container is real DOM.\n *\n * The chunk is fetched lazily so a book with `controls: false` never downloads it. Failure is\n * swallowed on purpose: a toolbar that cannot load should cost the reader a toolbar, not the\n * whole flipbook — `#init` has no error handling of its own, so a throw here would reject\n * `ready`.\n */\n async #mountControls(): Promise<void> {\n if (this.#controlsOption === false || this.#destroyed) return;\n const container = this.#container;\n if (!container.ownerDocument || typeof container.appendChild !== 'function') return;\n try {\n const { mountControls } = await import('./controls/controls');\n if (this.#destroyed) return; // destroyed while the chunk was in flight\n const options = this.#controlsOption === true ? {} : this.#controlsOption;\n this.#controlsCleanup = mountControls(this, container, options);\n } catch {\n // No toolbar; the book itself is unaffected.\n }\n }\n\n /** Mirror the page in the URL, and follow the URL when it changes. */\n #bindDeepLink(): void {\n if (!this.#deepLinkEnabled || typeof globalThis.location === 'undefined') return;\n const win = globalThis as unknown as Window;\n this.#deepLink = bindDeepLink(win, (page) => {\n // Only act on a hash the reader changed — following a link, or pressing Back. A hash we\n // wrote ourselves describes where the book already is.\n if (page !== this.#currentPage) this.flipTo(page);\n });\n this.#deepLink.push(this.#currentPage);\n this.#unsubscribeDeepLink = this.#emitter.on('pageChanged', ({ page }) => {\n this.#deepLink?.push(page);\n });\n }\n\n /** The URL that opens this book at `page` (defaults to the current one). */\n pageLink(page: number = this.#currentPage): string {\n const href = globalThis.location?.href ?? '';\n if (!href) return '';\n const url = new URL(href);\n url.hash = hashWithPage(url.hash, clamp(page, 0, Math.max(0, this.#source.pageCount - 1)));\n return url.toString();\n }\n\n /** Wire keyboard nav + an aria-live page announcer. Skips non-DOM containers. */\n #setupA11y(): (() => void) | null {\n const container = this.#container;\n const doc = container.ownerDocument;\n if (!doc || typeof container.setAttribute !== 'function') return null;\n\n if (!container.hasAttribute('tabindex')) container.tabIndex = 0;\n container.setAttribute('aria-roledescription', 'flipbook');\n const onKey = (e: KeyboardEvent): void => this.#onKeyDown(e);\n container.addEventListener('keydown', onKey);\n\n const live = doc.createElement('div');\n live.setAttribute('aria-live', 'polite');\n live.setAttribute('aria-atomic', 'true');\n live.style.cssText =\n 'position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;';\n container.appendChild(live);\n this.#liveRegion = live;\n\n return () => {\n container.removeEventListener('keydown', onKey);\n live.remove();\n this.#liveRegion = null;\n };\n }\n\n #onKeyDown(event: KeyboardEvent): void {\n const forwardKey = this.#direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight';\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowLeft':\n if (event.key === forwardKey) this.flipNext();\n else this.flipPrev();\n event.preventDefault();\n break;\n case 'Home':\n this.flipTo(0);\n event.preventDefault();\n break;\n case 'End':\n this.flipTo(this.#source.pageCount - 1);\n event.preventDefault();\n break;\n default:\n break;\n }\n }\n\n #announce(): void {\n if (this.#liveRegion) {\n this.#liveRegion.textContent = `Page ${this.#currentPage + 1} of ${this.#source.pageCount}`;\n }\n }\n\n #effectiveDuration(): number {\n // Reduced motion still turns the page, just briefly and flat (see #effectiveCurl): a page\n // that teleports gives the reader no sense of which way the book moved.\n if (this.#reducedMotion) return REDUCED_MOTION_DURATION;\n return this.#flipDuration * (this.#singlePage ? LONE_PAGE_FLIP_SCALE : 1);\n }\n\n /** The curl to turn with: reduced motion flattens the sheet, whatever the book asked for. */\n #effectiveCurl(): CurlSpec {\n return this.#reducedMotion ? 'simple' : this.#curl;\n }\n\n /** Re-measure and re-render; call after the container resizes. */\n update(): void {\n if (!this.#renderer || this.#machine.state !== 'idle') return;\n this.#applySinglePage(this.#renderer.measure().containerWidth);\n void this.#renderCurrent();\n }\n\n /** The layout to actually use: the responsive narrow fallback forces 'single'. */\n #effectiveMode(): SpreadMode {\n return this.#narrow || this.#spreadMode === 'single' ? 'single' : this.#spreadMode;\n }\n\n #applySinglePage(containerWidth: number): void {\n const narrow =\n this.#responsiveSpread && shouldSinglePage(containerWidth, this.#singlePageThreshold);\n if (narrow === this.#narrow) return;\n this.#narrow = narrow;\n this.#rebuildSpreads();\n }\n\n /** Regroup the pages after something changes the layout, keeping the reader on the page they\n * were looking at even though its spread index may have moved. */\n #rebuildSpreads(): void {\n const mode = this.#effectiveMode();\n const changed = mode !== this.#effectiveModeShown;\n this.#effectiveModeShown = mode;\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(this.#source.pageCount, { direction: this.#direction, mode });\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n // The book's proportions change with the layout, so consumers sizing around it need to know.\n if (changed) this.#emitter.emit('spreadChanged', { mode, singlePage: this.#singlePage });\n }\n\n async #renderCurrent(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!spread || !this.#renderer) return;\n this.#currentContent = await this.#resolveContent(spread);\n this.#paintSpread(spread, this.#currentContent);\n }\n\n #paintSpread(spread: Spread, content: SpreadContent): void {\n this.#renderer?.renderSpread(spread, content, { fill: this.#singlePage });\n this.#applyContainerAspect();\n // These pages were resolved at fit-to-screen; if the reader is zoomed, ask for them sharper.\n this.#zoomedAt = 1;\n this.#refreshZoomTiles();\n }\n\n /** Match the container's aspect-ratio to the book so it fits with no letterbox bars.\n * Non-destructive: `aspect-ratio` only drives whichever dimension the consumer leaves\n * auto (fit-width when they set a width), and is ignored if both are fixed. */\n #applyContainerAspect(): void {\n const b = this.#renderer?.measure().book;\n if (!b || b.width <= 0 || b.height <= 0) return;\n const ratio = (b.width / b.height).toFixed(4);\n if (ratio === this.#lastAspect) return;\n this.#lastAspect = ratio;\n this.#container.style.aspectRatio = ratio;\n }\n\n /**\n * A source upgraded a page's content (e.g. a progressive PDF swapping in the crisp render).\n *\n * Repainting mid-flip would disturb the animation, so an upgrade that lands during one is\n * noted and applied on settle instead of dropped. Dropping it left the page stuck at low\n * resolution for good: the upgrade fires once, and jumping straight to a page — from a search\n * hit, the page field, the outline — is exactly when it tends to arrive.\n */\n #onPageUpdate(index: number): void {\n if (this.#machine.state !== 'idle') {\n this.#pendingUpgrades.add(index);\n return;\n }\n const spread = this.#spreads[this.#current];\n if (spread && (spread.left === index || spread.right === index)) {\n void this.#renderCurrent();\n }\n }\n\n /**\n * Ask the source for a crisp render of whatever is visible, and lay it over the magnified page.\n *\n * Zooming is only a view transform, so without this the reader is magnifying the pixels of a\n * raster made to fit the screen. A vector source can do better; one backed by a fixed-resolution\n * original cannot, and says so by handing back a whole page, which is dropped.\n *\n * Debounced, and hidden while the view moves. A tile is crisp for one view only, so during a\n * pan it is stale by definition; showing it anyway puts two versions of the same text on screen\n * at once. Hiding leaves the reader with the uniformly magnified page an image book already\n * shows, which is smooth because there is nothing to disagree with it. The tile returns once\n * the view settles.\n */\n /**\n * Re-rasterize the visible pages at the magnification being viewed, and hand them to the\n * renderer as ordinary page content.\n *\n * Zoom is a view transform, so the renderer magnifies a raster made to fit the screen and text\n * goes soft. Giving it a sharper raster for the same page fixes that at the root: panning stays\n * a pure view transform over crisp pixels, which is why an image book pans smoothly and why an\n * overlay painted only between gestures never could.\n *\n * Debounced, since rasterizing a PDF page is far too slow to do per frame.\n */\n #refreshZoomTiles(): void {\n if (this.#tileTimer !== null) clearTimeout(this.#tileTimer);\n const wanted = this.#upgradeScale();\n if (wanted === this.#zoomedAt) return; // already showing this resolution\n if (wanted === 1) {\n this.#tileGeneration++; // abandon anything in flight: it is no longer wanted\n this.#zoomedAt = 1;\n void this.#renderCurrent(); // back to the fit-to-screen raster\n return;\n }\n this.#tileTimer = setTimeout(() => {\n this.#tileTimer = null;\n void this.#renderZoomTiles();\n }, ZOOM_TILE_DELAY);\n }\n\n /**\n * The resolution multiplier to rasterize the visible pages at.\n *\n * Capped because a page costs the square of this in memory: at 4x a letter page is over 100 MB,\n * which would dwarf the source's whole cache. The cap keeps a deep zoom sharper than the\n * fit-to-screen raster without trying to match it pixel for pixel.\n */\n #upgradeScale(): number {\n if (this.#scale <= 1) return 1;\n return Math.min(Math.ceil(this.#scale * 2) / 2, MAX_PAGE_UPGRADE);\n }\n\n async #renderZoomTiles(): Promise<void> {\n const spread = this.#spreads[this.#current];\n if (!this.#renderer || !spread) return;\n const scale = this.#upgradeScale();\n if (scale === 1 || scale === this.#zoomedAt) return;\n\n const generation = ++this.#tileGeneration;\n const whole = { x: 0, y: 0, width: 1, height: 1 };\n const pages = await Promise.all(\n [spread.left, spread.right].map(async (index) => {\n if (index === null) return null;\n try {\n return await this.#source.get(index, { scale, region: whole });\n } catch (error) {\n // An upgrade is an enhancement: the readable, softer page stays on screen.\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }),\n );\n // Rasterizing is slow and the reader may have moved on, or zoomed somewhere else entirely.\n if (generation !== this.#tileGeneration || this.#upgradeScale() !== scale) return;\n const left = pages[0] ?? null;\n const right = pages[1] ?? null;\n // Nothing sharper came back: a source with a fixed-resolution original hands over the raster\n // already on screen, and repainting it would be pure churn.\n if (left === this.#currentContent.left && right === this.#currentContent.right) return;\n\n this.#zoomedAt = scale;\n this.#currentContent = { left, right };\n this.#renderer.renderSpread(spread, this.#currentContent, { fill: this.#singlePage });\n }\n\n /** Apply any upgrade that arrived mid-flip, now that the book has settled. */\n #flushPendingUpgrades(): void {\n if (this.#pendingUpgrades.size === 0) return;\n const spread = this.#spreads[this.#current];\n const onScreen =\n spread !== undefined &&\n [spread.left, spread.right].some((p) => p !== null && this.#pendingUpgrades.has(p));\n this.#pendingUpgrades.clear();\n if (onScreen) void this.#renderCurrent();\n }\n\n #observeResize(): void {\n if (typeof ResizeObserver === 'undefined') return;\n this.#resizeObserver = new ResizeObserver(() => this.#scheduleUpdate());\n this.#resizeObserver.observe(this.#container);\n }\n\n #scheduleUpdate(): void {\n if (this.#updateScheduled) return;\n this.#updateScheduled = true;\n requestAnimationFrame(() => {\n this.#updateScheduled = false;\n this.update();\n });\n }\n\n async #resolveContent(spread: Spread): Promise<SpreadContent> {\n const [left, right] = await Promise.all([this.#getPage(spread.left), this.#getPage(spread.right)]);\n return { left, right };\n }\n\n /** Decode one page; on failure emit `sourceError` and degrade to a blank (null) page. */\n async #getPage(index: number | null): Promise<PageContent | null> {\n if (index === null) return null;\n try {\n return await this.#source.get(index);\n } catch (error) {\n this.#emitter.emit('sourceError', { index, error });\n return null;\n }\n }\n\n #prefetchWindow(): void {\n const { toMount } = this.#virtualizer.update(this.#current, this.#spreads.length);\n const pages: number[] = [];\n for (const index of toMount) {\n const spread = this.#spreads[index];\n if (spread?.left != null) pages.push(spread.left);\n if (spread?.right != null) pages.push(spread.right);\n }\n this.#source.prefetch(pages);\n }\n\n #spreadIndexForPage(page: number): number {\n const index = this.#spreads.findIndex((s) => s.left === page || s.right === page);\n return index === -1 ? 0 : index;\n }\n\n /** Validate the (now-known) page count + startPage and build the spread model. */\n #buildSpreadModel(): void {\n const pageCount = this.#source.pageCount;\n if (!Number.isInteger(pageCount) || pageCount < 1) {\n throw new Error(`Zine: source has ${pageCount} pages; a Source must have at least 1 page.`);\n }\n const startPage = this.#startPageOption;\n if (\n startPage !== undefined &&\n (!Number.isInteger(startPage) || startPage < 0 || startPage >= pageCount)\n ) {\n throw new Error(\n `Zine: startPage ${JSON.stringify(startPage)} is out of range for a ${pageCount}-page book (valid 0..${pageCount - 1}).`,\n );\n }\n const mode = this.#effectiveMode();\n this.#singlePage = mode === 'single';\n this.#spreads = buildSpreads(pageCount, { direction: this.#direction, mode });\n // A page in the URL wins over `startPage`: the reader followed a link to it, which is a\n // later and more specific intent than the page the author configured.\n const linked = this.#deepLinkEnabled ? pageFromHash(globalThis.location?.hash ?? '') : null;\n this.#currentPage = clamp(linked ?? startPage ?? 0, 0, pageCount - 1);\n this.#current = this.#spreadIndexForPage(this.#currentPage);\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return value < min ? min : value > max ? max : value;\n}\n\n/**\n * The translate range that keeps content within the viewport along one axis.\n *\n * `pos`/`size` are where the pages sit with no pan applied, so the view puts them at `pos + t`.\n * When they are larger than the viewport the range spans from their far edge to their near one,\n * so the reader can reach every part. When smaller there is nothing to explore, so the range\n * collapses to the single value that centres them: a lone page at 2x fills the width exactly, and\n * any freedom there would only let it drift off to one side.\n */\nfunction axisPanRange(pos: number, size: number, viewport: number): [min: number, max: number] {\n if (size <= viewport) {\n const centered = (viewport - size) / 2 - pos;\n return [centered, centered];\n }\n return [viewport - (pos + size), -pos];\n}\n\n/** Quadratic ease-in-out. Deliberately gentler than a cubic: a cubic leaves the page nearly\n * motionless for the first quarter of the turn and then spikes through the middle at roughly\n * double the peak speed, which reads as a lurch rather than a sheet of paper being turned. */\nfunction easeInOutQuad(t: number): number {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n}\n\n/** Lone-page turn: same gentle start as the spread ease, then a longer decelerating finish.\n * Without a facing half the landing is a dissolve — front-loading a bit of progress and\n * stretching the tail keeps that fade from reading as a whip. */\nfunction easeInOutLone(t: number): number {\n const x = easeInOutQuad(t);\n return 1 - Math.pow(1 - x, 1.3);\n}\n\nfunction typeName(v: unknown): string {\n if (v === null) return 'null';\n if (Array.isArray(v)) return 'array';\n return typeof v;\n}\n\n/** Validate constructor inputs up front, throwing agent-actionable errors. */\nfunction validateOptions(container: unknown, options: unknown): void {\n if (\n typeof container !== 'object' ||\n container === null ||\n typeof (container as { appendChild?: unknown }).appendChild !== 'function' ||\n typeof (container as { addEventListener?: unknown }).addEventListener !== 'function'\n ) {\n throw new Error(`Zine: container must be a DOM element; received ${typeName(container)}.`);\n }\n if (typeof options !== 'object' || options === null) {\n throw new Error('Zine: an options object with a `source` is required.');\n }\n const o = options as Record<string, unknown>;\n\n const source = o.source as { get?: unknown; pageCount?: unknown } | undefined;\n if (\n typeof source !== 'object' ||\n source === null ||\n typeof source.get !== 'function' ||\n typeof source.pageCount !== 'number'\n ) {\n throw new Error('Zine: `source` is required and must be a Source, e.g. new ImageSource(urls).');\n }\n // Note: pageCount >= 1 and startPage-in-range are validated in #buildSpreadModel —\n // synchronously for a sync source, or after open() for an async one (e.g. PdfSource).\n\n const startPage = o.startPage;\n if (startPage !== undefined && (typeof startPage !== 'number' || !Number.isInteger(startPage) || startPage < 0)) {\n throw new Error(`Zine: startPage must be a non-negative integer; got ${JSON.stringify(startPage)}.`);\n }\n\n const direction = o.direction;\n if (direction !== undefined && direction !== 'ltr' && direction !== 'rtl') {\n throw new Error(`Zine: direction must be 'ltr' or 'rtl'; got ${JSON.stringify(direction)}.`);\n }\n\n const clickToFlip = o.clickToFlip;\n if (clickToFlip !== undefined && !['edge', 'half', 'off'].includes(clickToFlip as string)) {\n throw new Error(`Zine: clickToFlip must be 'edge', 'half', or 'off'; got ${JSON.stringify(clickToFlip)}.`);\n }\n\n const curl = o.curl;\n if (curl !== undefined) {\n if (typeof curl === 'object' && curl !== null) {\n // An imported or custom model. Check the one member the renderer calls every frame, so a\n // wrong shape fails here rather than as an undefined call mid-flip.\n if (typeof (curl as { deform?: unknown }).deform !== 'function') {\n throw new Error('Zine: a curl model must have a deform() function.');\n }\n } else if (IMPORTABLE_CURLS.includes(curl as string)) {\n // A real curl, just not bundled: say how to get it rather than calling it invalid.\n throw new Error(\n `Zine: the '${String(curl)}' curl is not bundled. Import it and pass the model: ` +\n `import { ${String(curl)} } from '@zinejs/core/curls' → curl: ${String(curl)}`,\n );\n } else if (!CURL_TYPES.includes(curl as CurlType)) {\n throw new Error(\n `Zine: curl must be ${CURL_TYPES.join(' or ')}, or a model imported from ` +\n `'@zinejs/core/curls' (${IMPORTABLE_CURLS.join(', ')}); got ${JSON.stringify(curl)}.`,\n );\n }\n }\n\n for (const flag of ['deepLink', 'disableContextMenu', 'responsiveSpread'] as const) {\n if (o[flag] !== undefined && typeof o[flag] !== 'boolean') {\n throw new Error(`Zine: ${flag} must be a boolean; got ${typeName(o[flag])}.`);\n }\n }\n\n const controls = o.controls;\n if (controls !== undefined && typeof controls !== 'boolean' && (typeof controls !== 'object' || controls === null)) {\n throw new Error(\n `Zine: controls must be a boolean or an options object; got ${typeName(controls)}.`,\n );\n }\n const position = (controls as { position?: unknown } | undefined)?.position;\n if (position !== undefined && !['top', 'bottom', 'left', 'right'].includes(position as string)) {\n throw new Error(\n `Zine: controls.position must be 'top', 'bottom', 'left', or 'right'; got ${JSON.stringify(position)}.`,\n );\n }\n\n const spreadMode = o.spreadMode;\n if (spreadMode !== undefined && !['double', 'single', 'cover', 'book'].includes(spreadMode as string)) {\n throw new Error(\n `Zine: spreadMode must be 'double', 'single', 'cover', or 'book'; got ${JSON.stringify(spreadMode)}.`,\n );\n }\n\n for (const key of ['frontCover', 'backCover'] as const) {\n const value = o[key];\n if (value !== undefined && typeof value !== 'string') {\n throw new Error(`Zine: ${key} must be an image URL string; got ${JSON.stringify(value)}.`);\n }\n }\n const pages = o.pages;\n if (pages !== undefined && (typeof pages !== 'object' || pages === null || Array.isArray(pages))) {\n throw new Error('Zine: pages must be an object mapping page indices to image URLs.');\n }\n\n assertMin(o.width, 'width', 1);\n assertMin(o.height, 'height', 1);\n assertMin(o.flipDuration, 'flipDuration', 0);\n assertMin(o.clickZoneSize, 'clickZoneSize', 0);\n assertMin(o.clickFlipDelay, 'clickFlipDelay', 0);\n assertMin(o.singlePageThreshold, 'singlePageThreshold', 0);\n validateZoomOption(o.zoom);\n validateRendererOption(o.renderer);\n}\n\nfunction assertMin(value: unknown, name: string, min: number): void {\n if (value === undefined) return;\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min) {\n throw new Error(`Zine: ${name} must be a number >= ${min}; got ${JSON.stringify(value)}.`);\n }\n}\n\nfunction validateZoomOption(zoom: unknown): void {\n if (zoom === undefined) return;\n if (typeof zoom !== 'object' || zoom === null || Array.isArray(zoom)) {\n throw new Error('Zine: zoom must be an object, e.g. { max: 4 }.');\n }\n const z = zoom as Record<string, unknown>;\n if (z.enabled !== undefined && typeof z.enabled !== 'boolean') {\n throw new Error(`Zine: zoom.enabled must be a boolean; got ${JSON.stringify(z.enabled)}.`);\n }\n if (z.wheel !== undefined && typeof z.wheel !== 'boolean') {\n throw new Error(`Zine: zoom.wheel must be a boolean; got ${JSON.stringify(z.wheel)}.`);\n }\n if (z.doubleClickInFlipZone !== undefined && typeof z.doubleClickInFlipZone !== 'boolean') {\n throw new Error(\n `Zine: zoom.doubleClickInFlipZone must be a boolean; got ${JSON.stringify(z.doubleClickInFlipZone)}.`,\n );\n }\n if (z.doubleClick !== undefined && z.doubleClick !== false) {\n const levels = z.doubleClick;\n if (\n !Array.isArray(levels) ||\n levels.some((n) => typeof n !== 'number' || !Number.isFinite(n) || n < 1)\n ) {\n throw new Error(\n 'Zine: zoom.doubleClick must be false or an array of zoom levels >= 1, e.g. [1, 2, 4].',\n );\n }\n }\n assertMin(z.max, 'zoom.max', 1);\n}\n\nfunction validateRendererOption(renderer: unknown): void {\n if (renderer === undefined) return;\n if (typeof renderer === 'string') {\n if (renderer !== 'auto' && renderer !== 'css' && renderer !== 'webgl2') {\n throw new Error(\n `Zine: renderer '${renderer}' is not recognized; use 'auto', 'css', 'webgl2', an array of those, or a custom Renderer.`,\n );\n }\n return;\n }\n if (Array.isArray(renderer)) {\n for (const kind of renderer) {\n if (kind !== 'css' && kind !== 'webgl2') {\n throw new Error(\n `Zine: renderer order array may only contain 'css' or 'webgl2'; got ${JSON.stringify(kind)}.`,\n );\n }\n }\n return;\n }\n if (\n typeof renderer === 'object' &&\n renderer !== null &&\n typeof (renderer as { mount?: unknown }).mount === 'function'\n ) {\n return;\n }\n throw new Error(\n \"Zine: renderer must be 'auto', 'css', 'webgl2', an array of those, or a custom Renderer instance.\",\n );\n}\n","import type { PageContent } from '../renderer/types';\nimport type { Source } from './types';\n\nexport type ImageFit = 'contain' | 'cover';\n\nexport interface ImageSourceOptions {\n /** How many adjacent pages to prefetch around a requested page. Default 1. */\n preload?: number;\n /** How each image fits its page; a hint consumed by the render layer. Default 'contain'. */\n fit?: ImageFit;\n}\n\n/**\n * Default content source for image books: decodes a list of image URLs to\n * ImageBitmaps, memoizes them, and prefetches neighbors on access.\n */\nexport class ImageSource implements Source {\n readonly pageCount: number;\n readonly fit: ImageFit;\n #urls: string[];\n #preload: number;\n #cache = new Map<number, Promise<ImageBitmap>>();\n\n constructor(urls: string[], options: ImageSourceOptions = {}) {\n this.#urls = urls;\n this.pageCount = urls.length;\n this.#preload = options.preload ?? 1;\n this.fit = options.fit ?? 'contain';\n }\n\n get(index: number): Promise<PageContent> {\n const decoded = this.#decode(index);\n for (let d = 1; d <= this.#preload; d++) {\n this.prefetch([index - d, index + d]);\n }\n return decoded;\n }\n\n prefetch(indices: number[]): void {\n for (const i of indices) {\n if (i >= 0 && i < this.pageCount) {\n void this.#decode(i).catch(() => {}); // swallow: prefetch failures surface on real get()\n }\n }\n }\n\n destroy(): void {\n for (const pending of this.#cache.values()) {\n void pending.then((bitmap) => bitmap.close()).catch(() => {});\n }\n this.#cache.clear();\n }\n\n #decode(index: number): Promise<ImageBitmap> {\n const cached = this.#cache.get(index);\n if (cached) return cached;\n\n const url = this.#urls[index];\n if (url === undefined) {\n return Promise.reject(\n new RangeError(`ImageSource: page ${index} is out of range (0..${this.pageCount - 1}).`),\n );\n }\n\n const promise = (async () => {\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`ImageSource: failed to fetch page ${index} (${url}) — HTTP ${res.status}.`);\n }\n return createImageBitmap(await res.blob());\n })().catch((err: unknown) => {\n this.#cache.delete(index); // drop the failed entry so a later get() can retry\n throw err;\n });\n\n this.#cache.set(index, promise);\n return promise;\n }\n}\n"],"mappings":"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"}