@zinejs/core 0.9.2 → 1.0.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/{cssRenderer-CTaM4cAj.js → cssRenderer-DAW29lU2.js} +39 -22
- package/dist/cssRenderer-DAW29lU2.js.map +1 -0
- package/dist/engine/gutter.d.ts +12 -0
- package/dist/gutter-C3H2CDT2.js +43 -0
- package/dist/gutter-C3H2CDT2.js.map +1 -0
- package/dist/index.js +550 -526
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +14 -9
- package/dist/index.umd.js.map +1 -1
- package/dist/renderer/cssRenderer.d.ts +1 -1
- package/dist/renderer/types.d.ts +13 -0
- package/dist/renderer/webglRenderer.d.ts +1 -1
- package/dist/types-DYqgosP1.js +9 -0
- package/dist/types-DYqgosP1.js.map +1 -0
- package/dist/{webglRenderer-DQn4pc1S.js → webglRenderer-9RcNHAy-.js} +74 -49
- package/dist/webglRenderer-9RcNHAy-.js.map +1 -0
- package/dist/zine.d.ts +6 -0
- package/options.schema.json +8 -0
- package/package.json +1 -1
- package/dist/cssRenderer-CTaM4cAj.js.map +0 -1
- package/dist/webglRenderer-DQn4pc1S.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Spread } from '../engine/spread';
|
|
2
|
-
import type
|
|
2
|
+
import { type FlipDirection, type LayoutMetrics, type Renderer, type RenderOptions, type SpreadContent } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages
|
|
5
5
|
* are <canvas> elements; the turning leaf (front + back faces, backface-hidden)
|
package/dist/renderer/types.d.ts
CHANGED
|
@@ -6,7 +6,20 @@ export type PageContent = ImageBitmap | HTMLCanvasElement;
|
|
|
6
6
|
export interface SpreadContent {
|
|
7
7
|
left: PageContent | null;
|
|
8
8
|
right: PageContent | null;
|
|
9
|
+
/** Fraction of each page's width hidden at its spine edge, removing a print-gutter duplicate
|
|
10
|
+
* (see `gutterOverlap`). Honoured only on a full two-page spread; 0 or omitted = none. */
|
|
11
|
+
spineTrim?: number;
|
|
9
12
|
}
|
|
13
|
+
/** The spine trim actually applied: only a full two-page spread is trimmed, never a lone page or
|
|
14
|
+
* single-page (fill) mode, where there is no facing page to duplicate. */
|
|
15
|
+
export declare function spineTrimOf(content: SpreadContent, fill: boolean): number;
|
|
16
|
+
/** The visible horizontal part of a page's raster, as [start, end] fractions of its width. */
|
|
17
|
+
export type PageSpan = readonly [number, number];
|
|
18
|
+
export declare const FULL_SPAN: PageSpan;
|
|
19
|
+
/** A left-hand page's spine is its right edge, so a spine trim narrows it from the right. */
|
|
20
|
+
export declare const leftSpan: (trim: number) => PageSpan;
|
|
21
|
+
/** A right-hand page's spine is its left edge. */
|
|
22
|
+
export declare const rightSpan: (trim: number) => PageSpan;
|
|
10
23
|
export type FlipDirection = 'forward' | 'backward';
|
|
11
24
|
export interface RenderOptions {
|
|
12
25
|
/** Render a lone page filling the container (single-page mode), not a half panel. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Spread } from '../engine/spread';
|
|
2
|
-
import type
|
|
2
|
+
import { type FlipDirection, type LayoutMetrics, type Renderer, type RenderOptions, type SpreadContent } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* GPU renderer (WebGL2, hand-written). The turning leaf is a dense grid deformed on
|
|
5
5
|
* the CPU by the selected curl model (see geometry/curls) and
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/renderer/types.ts
|
|
2
|
+
function e(e, t) {
|
|
3
|
+
return !t && e.left !== null && e.right !== null ? e.spineTrim ?? 0 : 0;
|
|
4
|
+
}
|
|
5
|
+
var t = [0, 1], n = (e) => [0, 1 - e], r = (e) => [e, 1];
|
|
6
|
+
//#endregion
|
|
7
|
+
export { e as i, n, r, t };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=types-DYqgosP1.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-DYqgosP1.js","names":[],"sources":["../src/renderer/types.ts"],"sourcesContent":["import type { Spread } from '../engine/spread';\nimport type { CurlSpec, CurlAnchor } from '../geometry/curls/types';\n\n/** A decoded, ready-to-paint raster for one page (produced by a content source). */\nexport type PageContent = ImageBitmap | HTMLCanvasElement;\n\n/** Resolved content for the two halves of a spread; null = blank side. */\nexport interface SpreadContent {\n left: PageContent | null;\n right: PageContent | null;\n /** Fraction of each page's width hidden at its spine edge, removing a print-gutter duplicate\n * (see `gutterOverlap`). Honoured only on a full two-page spread; 0 or omitted = none. */\n spineTrim?: number;\n}\n\n/** The spine trim actually applied: only a full two-page spread is trimmed, never a lone page or\n * single-page (fill) mode, where there is no facing page to duplicate. */\nexport function spineTrimOf(content: SpreadContent, fill: boolean): number {\n return !fill && content.left !== null && content.right !== null ? (content.spineTrim ?? 0) : 0;\n}\n\n/** The visible horizontal part of a page's raster, as [start, end] fractions of its width. */\nexport type PageSpan = readonly [number, number];\nexport const FULL_SPAN: PageSpan = [0, 1];\n/** A left-hand page's spine is its right edge, so a spine trim narrows it from the right. */\nexport const leftSpan = (trim: number): PageSpan => [0, 1 - trim];\n/** A right-hand page's spine is its left edge. */\nexport const rightSpan = (trim: number): PageSpan => [trim, 1];\n\nexport type FlipDirection = 'forward' | 'backward';\n\nexport interface RenderOptions {\n /** Render a lone page filling the container (single-page mode), not a half panel. */\n fill?: boolean;\n /** Which page-curl model the WebGL2 renderer bends the leaf with (ignored by CSS). */\n curl?: CurlSpec;\n /** Corner the fold originates from for anchored curls; y in [0,1] (where the reader tapped). */\n anchor?: CurlAnchor;\n /** How a page whose size differs from the book's fits: `contain` (default) keeps its aspect and\n * centers it (a margin appears); `fill` stretches it to the book's shape. Book-global. */\n fit?: 'contain' | 'fill';\n}\n\n/** Layout metrics the engine needs for geometry and mode decisions. */\nexport interface LayoutMetrics {\n containerWidth: number;\n containerHeight: number;\n pageWidth: number;\n pageHeight: number;\n /** The letterboxed book area (aspect-fitted, centered) in container px — a stable 2-page\n * (or 1-page in fill) region used for aspect sizing. Omit to fall back to the full container. */\n book?: { x: number; y: number; width: number; height: number };\n /** Where the current spread is actually painted, in container px — equal to `book` for a\n * full spread, but the centered half for a lone page (cover / book front-back). This is the\n * region the engine hit-tests against so taps track the visible page. Omit to fall back. */\n content?: { x: number; y: number; width: number; height: number };\n /** The book's stable aspect (width / height) for the whole document, from the first page rather\n * than the current spread. The container is sized to this so a document with off-size pages does\n * not resize (layout shift) as you navigate. Omit before the first paint. */\n containerAspect?: number;\n /**\n * Where `content` would land at zoom `scale` with no pan, in container px.\n *\n * Reported by the renderer rather than derived by the engine because the two place a lone page\n * differently: WebGL2 offsets it outside the view scale, so the offset is fixed in screen space,\n * while CSS applies it inside and it magnifies. The engine adds the translate to this to bound\n * panning; without it a lone page can be dragged until its content is off screen. Omit to fall\n * back to the container.\n */\n screenAt?(scale: number): { x: number; y: number; width: number; height: number };\n}\n\n/**\n * The contract every renderer implements (CSS baseline, WebGL2/GPU). The engine\n * drives it and never reaches past it, so the renderers are interchangeable and\n * behaviorally identical (PRD §8). One is selected at init; a device fetches one.\n */\nexport interface Renderer {\n /** Attach to a container. Async to accommodate GPU context init (WebGL2). */\n mount(container: HTMLElement): Promise<void>;\n\n /** Tear down: remove DOM/listeners, release any GPU context. */\n destroy(): void;\n\n /** Paint a spread from already-resolved page rasters (static, no flip). */\n renderSpread(spread: Spread, content: SpreadContent, options?: RenderOptions): void;\n\n /**\n * Set up a flip: the turning leaf's front is `from`'s leading page and its back\n * is `to`'s facing page, with the destination revealed underneath. Call once at\n * flip start, then drive `setFlipProgress`.\n */\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void;\n\n /**\n * Drive the turning page to progress `t` (0..1) in `direction`. Also the stable\n * seek API used by visual-regression tests (call `beginFlip` first to stage content).\n */\n setFlipProgress(t: number, direction: FlipDirection): void;\n\n /** Apply zoom/pan: uniform `scale` about the viewport, translated by (x, y). */\n setViewTransform(scale: number, x: number, y: number): void;\n\n /** Current layout metrics (container + page dimensions). */\n measure(): LayoutMetrics;\n\n /**\n * Largest texture dimension (px) the renderer can accept, when it has a hard cap (WebGL2's\n * MAX_TEXTURE_SIZE). Sources read it to avoid rasterizing a zoom tile the GPU would reject and\n * paint black. Omit when unbounded (the CSS renderer).\n */\n readonly maxTextureSize?: number;\n\n /**\n * Optional: register a handler the renderer calls when it hits an unrecoverable\n * state (e.g. a GPU context that cannot be restored), so the engine can swap to\n * the CSS fallback (§8.4). Renderers that cannot fail this way omit it.\n */\n onFatal?(handler: () => void): void;\n}\n"],"mappings":";AAiBA,SAAgB,EAAY,GAAwB,GAAuB;CACzE,OAAO,CAAC,KAAQ,EAAQ,SAAS,QAAQ,EAAQ,UAAU,OAAQ,EAAQ,aAAa,IAAK;AAC/F;AAIA,IAAa,IAAsB,CAAC,GAAG,CAAC,GAE3B,KAAY,MAA2B,CAAC,GAAG,IAAI,CAAI,GAEnD,KAAa,MAA2B,CAAC,GAAM,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { n as e } from "./types-DEz3mRLI.js";
|
|
2
2
|
import { o as t, r as n, t as r } from "./bundled-ESapkNsS.js";
|
|
3
|
+
import { i, n as a, r as o, t as s } from "./types-DYqgosP1.js";
|
|
3
4
|
//#region src/renderer/gl/context.ts
|
|
4
|
-
function
|
|
5
|
+
function c(e) {
|
|
5
6
|
let t = e.getContext("webgl2", {
|
|
6
7
|
alpha: !0,
|
|
7
8
|
premultipliedAlpha: !0,
|
|
@@ -15,17 +16,17 @@ function i(e) {
|
|
|
15
16
|
}
|
|
16
17
|
//#endregion
|
|
17
18
|
//#region src/renderer/gl/program.ts
|
|
18
|
-
function
|
|
19
|
+
function l(e, t, n) {
|
|
19
20
|
let r = e.createProgram();
|
|
20
21
|
if (r === null) throw Error("WebglRenderer: could not create a program.");
|
|
21
|
-
let i =
|
|
22
|
+
let i = u(e, e.VERTEX_SHADER, t), a = u(e, e.FRAGMENT_SHADER, n);
|
|
22
23
|
if (e.attachShader(r, i), e.attachShader(r, a), e.linkProgram(r), e.deleteShader(i), e.deleteShader(a), !e.getProgramParameter(r, e.LINK_STATUS)) {
|
|
23
24
|
let t = e.getProgramInfoLog(r);
|
|
24
25
|
throw e.deleteProgram(r), Error(`WebglRenderer: program link failed: ${t ?? "unknown error"}`);
|
|
25
26
|
}
|
|
26
27
|
return r;
|
|
27
28
|
}
|
|
28
|
-
function
|
|
29
|
+
function u(e, t, n) {
|
|
29
30
|
let r = e.createShader(t);
|
|
30
31
|
if (r === null) throw Error("WebglRenderer: could not create a shader.");
|
|
31
32
|
if (e.shaderSource(r, n), e.compileShader(r), !e.getShaderParameter(r, e.COMPILE_STATUS)) {
|
|
@@ -36,19 +37,19 @@ function o(e, t, n) {
|
|
|
36
37
|
}
|
|
37
38
|
//#endregion
|
|
38
39
|
//#region src/renderer/gl/texture.ts
|
|
39
|
-
function
|
|
40
|
+
function d(e) {
|
|
40
41
|
let t = e.createTexture();
|
|
41
42
|
if (t === null) throw Error("WebglRenderer: could not create a texture.");
|
|
42
43
|
return e.bindTexture(e.TEXTURE_2D, t), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_S, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_WRAP_T, e.CLAMP_TO_EDGE), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MIN_FILTER, e.LINEAR), e.texParameteri(e.TEXTURE_2D, e.TEXTURE_MAG_FILTER, e.LINEAR), t;
|
|
43
44
|
}
|
|
44
|
-
function
|
|
45
|
-
let r =
|
|
45
|
+
function f(e, t, n) {
|
|
46
|
+
let r = p(n, e.getParameter(e.MAX_TEXTURE_SIZE));
|
|
46
47
|
return e.bindTexture(e.TEXTURE_2D, t), e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL, !1), e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL, !0), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, e.RGBA, e.UNSIGNED_BYTE, r), {
|
|
47
48
|
width: r.width,
|
|
48
49
|
height: r.height
|
|
49
50
|
};
|
|
50
51
|
}
|
|
51
|
-
function
|
|
52
|
+
function p(e, t) {
|
|
52
53
|
let n = e, r = Math.max(n.width, n.height);
|
|
53
54
|
if (!Number.isFinite(t) || t <= 0 || r <= t) return n;
|
|
54
55
|
let i = t / r, a = document.createElement("canvas");
|
|
@@ -56,11 +57,11 @@ function l(e, t) {
|
|
|
56
57
|
}
|
|
57
58
|
//#endregion
|
|
58
59
|
//#region src/renderer/webglRenderer.ts
|
|
59
|
-
var
|
|
60
|
-
function
|
|
60
|
+
var m = "#version 300 es\nin vec2 aUnit;\nuniform vec2 uViewport;\nuniform vec4 uRect;\nuniform vec3 uView;\nout vec2 vUv;\nvoid main() {\n vec2 px = uRect.xy + aUnit * uRect.zw;\n px = px * uView.x + uView.yz;\n vec2 clip = px / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUnit;\n}", h = "#version 300 es\nprecision mediump float;\nin vec2 vUv;\nuniform sampler2D uTex;\nuniform float uGutterSide; // +1 = spine at right edge, -1 = at left edge, 0 = none\nuniform vec2 uSpan; // visible u-range of the page texture (a spine trim narrows it)\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vec2(mix(uSpan.x, uSpan.y, vUv.x), vUv.y));\n if (uGutterSide != 0.0) {\n float d = uGutterSide > 0.0 ? (1.0 - vUv.x) : vUv.x;\n c.rgb *= mix(0.72, 1.0, smoothstep(0.0, 0.10, d));\n }\n outColor = c;\n}", g = "#version 300 es\nin vec3 aPos; // leaf-local device px: x = bent offset from spine, y 0..H, z depth\nin vec3 aNormal;\nin vec2 aUv;\nuniform vec2 uViewport;\nuniform vec3 uView; // scale, tx, ty\nuniform float uOriginX; // spine x in device px\nuniform float uDir; // +1 leaf to the right of spine, -1 to the left\nuniform float uLeafW; // leaf width in device px (perspective scale)\nout vec2 vUv;\nout float vFacing;\nout float vU;\nvoid main() {\n float bookX = uOriginX + uDir * aPos.x;\n float bookY = aPos.y;\n float Z = aPos.z;\n // Eye distance, in leaf widths. Keeps the perspective resolution-independent. 5.3 puts the\n // roll's deepest point ~14% larger than flat; stronger than that (a nearer eye) balloons the\n // turning sheet and reads more like a fisheye than a page lifting.\n float D = uLeafW * 5.3;\n float persp = D / max(D - Z, 1.0);\n float cx = uViewport.x * 0.5;\n float cy = uViewport.y * 0.5;\n float px = cx + (bookX - cx) * persp;\n float py = cy + (bookY - cy) * min(persp, 1.0); // Y only shrinks (top-safe)\n vec2 p = vec2(px, py) * uView.x + uView.yz;\n vec2 clip = p / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUv;\n vFacing = aNormal.z;\n vU = aUv.x;\n}", _ = "#version 300 es\nprecision mediump float;\nin vec2 vUv;\nin float vFacing;\nin float vU;\nuniform sampler2D uFront;\nuniform sampler2D uBack;\nuniform vec2 uFrontSpan; // visible u-ranges of the two faces (a spine trim narrows them)\nuniform vec2 uBackSpan;\nuniform highp float uDir;\nuniform float uGloss; // 0 disables the specular highlight (e.g. the flat 'simple' curl)\nuniform float uAlpha; // leaf opacity; a lone page dissolves into the page landing beneath it\nuniform float uCrease; // spine-crease strength: 1 matches a spread's gutter; ramps from 0 for a lone leaf\nout vec4 outColor;\nvoid main() {\n // The turn mesh carries its own facing in the normal; the front points at the viewer\n // (normal.z > 0) until the leaf passes vertical, for both flip directions. u->x still\n // flips with uDir so a left-hand leaf reads spine-inward.\n bool showFront = vFacing > 0.0;\n float fx = uDir > 0.0 ? vUv.x : 1.0 - vUv.x;\n vec4 c = showFront\n ? texture(uFront, vec2(mix(uFrontSpan.x, uFrontSpan.y, fx), vUv.y))\n : texture(uBack, vec2(mix(uBackSpan.x, uBackSpan.y, 1.0 - fx), vUv.y));\n\n // Fold shading: the sheet darkens sharply where it curves away from the viewer, so the\n // rolled edge reads as a deep crease with a soft highlight riding the ridge.\n float diff = clamp(abs(vFacing), 0.0, 1.0);\n float lit = mix(0.42, 1.0, diff);\n // Spine crease matches the gutter shadow on a spread. A lone page has no gutter, so uCrease\n // ramps it in from 0 as the leaf lifts — otherwise a still-flat leaf paints a shadow band on\n // the anchor edge before the turn even begins.\n lit *= mix(1.0, mix(0.7, 1.0, smoothstep(0.0, 0.12, vU)), uCrease);\n\n // Glossy specular: brightest where the surface tilts ~30 deg from facing the viewer,\n // so a thin glossy highlight rides across the sheet as it rolls.\n float spec = pow(max(cos(acos(diff) - 0.52), 0.0), 220.0) * 0.22 * uGloss;\n\n c.rgb = clamp(c.rgb * lit + spec, 0.0, 1.0);\n // Premultiplied alpha (blendFunc ONE, ONE_MINUS_SRC_ALPHA): scale colour as well as alpha,\n // or fading would brighten the sheet additively instead of dissolving it.\n outColor = vec4(c.rgb * uAlpha, c.a * uAlpha);\n}", v = 28, y = 36, b = .7, x = .78, S = 4e3, C = 256 * 1024 * 1024;
|
|
61
|
+
function w(e) {
|
|
61
62
|
return e.left === null == (e.right === null) ? 0 : e.right === null ? 1 : -1;
|
|
62
63
|
}
|
|
63
|
-
var
|
|
64
|
+
var T = class {
|
|
64
65
|
#e = null;
|
|
65
66
|
#t = null;
|
|
66
67
|
#n = null;
|
|
@@ -71,7 +72,7 @@ var x = class {
|
|
|
71
72
|
#s = null;
|
|
72
73
|
#c = null;
|
|
73
74
|
#l = 0;
|
|
74
|
-
#u = t(
|
|
75
|
+
#u = t(v, y);
|
|
75
76
|
#d = /* @__PURE__ */ new Map();
|
|
76
77
|
#f = 0;
|
|
77
78
|
#p = null;
|
|
@@ -105,7 +106,7 @@ var x = class {
|
|
|
105
106
|
mount(e) {
|
|
106
107
|
this.#e = e;
|
|
107
108
|
let t = e.ownerDocument, n = t.createElement("canvas");
|
|
108
|
-
return n.style.cssText = "display:block;width:100%;height:100%;", e.append(n), this.#t = n, n.addEventListener("webglcontextlost", this.#V), n.addEventListener("webglcontextrestored", this.#H), t.addEventListener("visibilitychange", this.#U), this.#n =
|
|
109
|
+
return n.style.cssText = "display:block;width:100%;height:100%;", e.append(n), this.#t = n, n.addEventListener("webglcontextlost", this.#V), n.addEventListener("webglcontextrestored", this.#H), t.addEventListener("visibilitychange", this.#U), this.#n = c(n), this.#m = this.#n.getParameter(this.#n.MAX_TEXTURE_SIZE), this.#K(), Promise.resolve();
|
|
109
110
|
}
|
|
110
111
|
destroy() {
|
|
111
112
|
this.#G();
|
|
@@ -125,23 +126,32 @@ var x = class {
|
|
|
125
126
|
return this.#m || void 0;
|
|
126
127
|
}
|
|
127
128
|
renderSpread(e, t, n) {
|
|
128
|
-
this.#y = t, this.#b = n?.fill ?? !1, n?.fit && (this.#A = n.fit), this.#x = null, this.#L(t), this.#Y();
|
|
129
|
+
this.#y = t, this.#b = n?.fill ?? !1, n?.fit && (this.#A = n.fit), this.#x = null, this.#L(t, this.#b), this.#Y();
|
|
129
130
|
}
|
|
130
|
-
#L(e) {
|
|
131
|
-
let
|
|
132
|
-
|
|
131
|
+
#L(e, t) {
|
|
132
|
+
let n = e.left ?? e.right;
|
|
133
|
+
if (n && n.height) {
|
|
134
|
+
let r = n.width / n.height;
|
|
135
|
+
this.#O = r * (1 - i(e, t)), this.#k === 0 && (this.#k = r);
|
|
136
|
+
}
|
|
133
137
|
}
|
|
134
138
|
#R() {
|
|
135
139
|
return this.#A === "fill" && this.#k > 0 ? this.#k : this.#O;
|
|
136
140
|
}
|
|
137
|
-
beginFlip(e, t, r,
|
|
138
|
-
let
|
|
139
|
-
|
|
141
|
+
beginFlip(e, t, r, c) {
|
|
142
|
+
let l = c?.fill ?? !1;
|
|
143
|
+
c?.fit && (this.#A = c.fit), this.#S = 0, c?.curl && (this.#C = n(c.curl)), c?.anchor && (this.#w = c.anchor), this.#E = l ? 0 : w(e), this.#D = l ? 0 : w(t), this.#L(t, l);
|
|
144
|
+
let u = i(e, l), d = i(t, l);
|
|
145
|
+
l ? this.#x = {
|
|
140
146
|
underLeft: null,
|
|
141
147
|
underRight: null,
|
|
142
148
|
underFull: t.right ?? t.left,
|
|
143
149
|
front: e.right ?? e.left,
|
|
144
150
|
back: e.right ?? e.left,
|
|
151
|
+
underLeftSpan: s,
|
|
152
|
+
underRightSpan: s,
|
|
153
|
+
frontSpan: s,
|
|
154
|
+
backSpan: s,
|
|
145
155
|
dir: r === "forward" ? 1 : -1,
|
|
146
156
|
fill: !0
|
|
147
157
|
} : r === "forward" ? this.#x = {
|
|
@@ -150,6 +160,10 @@ var x = class {
|
|
|
150
160
|
underFull: null,
|
|
151
161
|
front: e.right,
|
|
152
162
|
back: t.left,
|
|
163
|
+
underLeftSpan: a(u),
|
|
164
|
+
underRightSpan: o(d),
|
|
165
|
+
frontSpan: o(u),
|
|
166
|
+
backSpan: a(d),
|
|
153
167
|
dir: 1,
|
|
154
168
|
fill: !1
|
|
155
169
|
} : this.#x = {
|
|
@@ -158,6 +172,10 @@ var x = class {
|
|
|
158
172
|
underFull: null,
|
|
159
173
|
front: e.left,
|
|
160
174
|
back: t.right,
|
|
175
|
+
underLeftSpan: a(d),
|
|
176
|
+
underRightSpan: o(u),
|
|
177
|
+
frontSpan: a(u),
|
|
178
|
+
backSpan: o(d),
|
|
161
179
|
dir: -1,
|
|
162
180
|
fill: !1
|
|
163
181
|
}, this.#Y();
|
|
@@ -211,7 +229,7 @@ var x = class {
|
|
|
211
229
|
}
|
|
212
230
|
#B() {
|
|
213
231
|
let e = this.#z();
|
|
214
|
-
return !this.#b && this.#y !== null &&
|
|
232
|
+
return !this.#b && this.#y !== null && w(this.#y) !== 0 ? {
|
|
215
233
|
x: e.x + e.width / 4,
|
|
216
234
|
y: e.y,
|
|
217
235
|
width: e.width / 2,
|
|
@@ -219,7 +237,7 @@ var x = class {
|
|
|
219
237
|
} : e;
|
|
220
238
|
}
|
|
221
239
|
#V = (e) => {
|
|
222
|
-
e.preventDefault(), this.#N = !0, this.#F === null && (this.#F = setTimeout(() => this.#W(),
|
|
240
|
+
e.preventDefault(), this.#N = !0, this.#F === null && (this.#F = setTimeout(() => this.#W(), S));
|
|
223
241
|
};
|
|
224
242
|
#H = () => {
|
|
225
243
|
this.#G(), this.#N = !1, this.#K(), this.#Y();
|
|
@@ -236,13 +254,14 @@ var x = class {
|
|
|
236
254
|
#K() {
|
|
237
255
|
let e = this.#n;
|
|
238
256
|
if (e !== null) {
|
|
239
|
-
this.#r =
|
|
257
|
+
this.#r = l(e, m, h), this.#i = l(e, g, _);
|
|
240
258
|
for (let t of [
|
|
241
259
|
"uViewport",
|
|
242
260
|
"uRect",
|
|
243
261
|
"uView",
|
|
244
262
|
"uTex",
|
|
245
|
-
"uGutterSide"
|
|
263
|
+
"uGutterSide",
|
|
264
|
+
"uSpan"
|
|
246
265
|
]) this.#h[t] = e.getUniformLocation(this.#r, t);
|
|
247
266
|
for (let t of [
|
|
248
267
|
"uViewport",
|
|
@@ -252,6 +271,8 @@ var x = class {
|
|
|
252
271
|
"uLeafW",
|
|
253
272
|
"uFront",
|
|
254
273
|
"uBack",
|
|
274
|
+
"uFrontSpan",
|
|
275
|
+
"uBackSpan",
|
|
255
276
|
"uGloss",
|
|
256
277
|
"uAlpha",
|
|
257
278
|
"uCrease"
|
|
@@ -277,7 +298,7 @@ var x = class {
|
|
|
277
298
|
}
|
|
278
299
|
#J(e, t) {
|
|
279
300
|
let n = [];
|
|
280
|
-
for (let e = 0; e <
|
|
301
|
+
for (let e = 0; e < y; e++) for (let t = 0; t < v; t++) {
|
|
281
302
|
let r = e * 29 + t, i = r + 1, a = r + 29, o = a + 1;
|
|
282
303
|
n.push(r, a, i, i, a, o);
|
|
283
304
|
}
|
|
@@ -309,22 +330,26 @@ var x = class {
|
|
|
309
330
|
}
|
|
310
331
|
#Z() {
|
|
311
332
|
let e = this.#j, t = this.#M, n = this.#y;
|
|
312
|
-
this.#T = this.#b ? 0 :
|
|
333
|
+
if (this.#T = this.#b ? 0 : w(n) * (e / 4), this.#ee(), this.#b) this.#te({
|
|
313
334
|
x: 0,
|
|
314
335
|
y: 0,
|
|
315
336
|
w: e,
|
|
316
337
|
h: t
|
|
317
|
-
}, n.right ?? n.left)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
338
|
+
}, n.right ?? n.left);
|
|
339
|
+
else {
|
|
340
|
+
let r = i(n, !1);
|
|
341
|
+
this.#te({
|
|
342
|
+
x: 0,
|
|
343
|
+
y: 0,
|
|
344
|
+
w: e / 2,
|
|
345
|
+
h: t
|
|
346
|
+
}, n.left, +!!n.right, a(r)), this.#te({
|
|
347
|
+
x: e / 2,
|
|
348
|
+
y: 0,
|
|
349
|
+
w: e / 2,
|
|
350
|
+
h: t
|
|
351
|
+
}, n.right, n.left ? -1 : 0, o(r));
|
|
352
|
+
}
|
|
328
353
|
}
|
|
329
354
|
#Q() {
|
|
330
355
|
let e = this.#n, t = this.#j, n = this.#M, r = this.#x;
|
|
@@ -338,29 +363,29 @@ var x = class {
|
|
|
338
363
|
y: 0,
|
|
339
364
|
w: t / 2,
|
|
340
365
|
h: n
|
|
341
|
-
}, r.underLeft, 1), this.#te({
|
|
366
|
+
}, r.underLeft, 1, r.underLeftSpan), this.#te({
|
|
342
367
|
x: t / 2,
|
|
343
368
|
y: 0,
|
|
344
369
|
w: t / 2,
|
|
345
370
|
h: n
|
|
346
|
-
}, r.underRight, -1)), r.front === null && r.back === null) return;
|
|
371
|
+
}, r.underRight, -1, r.underRightSpan)), r.front === null && r.back === null) return;
|
|
347
372
|
let i = r.fill ? t : t / 2, a = r.fill ? r.dir > 0 ? 0 : t : t / 2;
|
|
348
373
|
this.#C.deform(this.#u, i, n, this.#S, {
|
|
349
374
|
y: this.#w.y,
|
|
350
375
|
fill: r.fill
|
|
351
|
-
}), e.bindBuffer(e.ARRAY_BUFFER, this.#s), e.bufferSubData(e.ARRAY_BUFFER, 0, this.#u.positions), e.bindBuffer(e.ARRAY_BUFFER, this.#c), e.bufferSubData(e.ARRAY_BUFFER, 0, this.#u.normals), this.#ne(e.TEXTURE0, r.front), this.#ne(e.TEXTURE1, r.back), e.activeTexture(e.TEXTURE0), e.useProgram(this.#i), e.bindVertexArray(this.#o), e.uniform2f(this.#g.uViewport ?? null, t, n), e.uniform3f(this.#g.uView ?? null, this.#_.scale, this.#_.tx * this.#v + this.#T, this.#_.ty * this.#v), e.uniform1f(this.#g.uOriginX ?? null, a), e.uniform1f(this.#g.uDir ?? null, r.dir), e.uniform1f(this.#g.uLeafW ?? null, i), e.uniform1f(this.#g.uGloss ?? null, this.#C.gloss === !1 ? 0 : 1), e.uniform1f(this.#g.uAlpha ?? null, r.fill ? this.#$() : 1), e.uniform1f(this.#g.uCrease ?? null, r.fill ? Math.min(1, this.#S / .2) : 1), e.drawElements(e.TRIANGLES, this.#l, e.UNSIGNED_SHORT, 0);
|
|
376
|
+
}), e.bindBuffer(e.ARRAY_BUFFER, this.#s), e.bufferSubData(e.ARRAY_BUFFER, 0, this.#u.positions), e.bindBuffer(e.ARRAY_BUFFER, this.#c), e.bufferSubData(e.ARRAY_BUFFER, 0, this.#u.normals), this.#ne(e.TEXTURE0, r.front), this.#ne(e.TEXTURE1, r.back), e.activeTexture(e.TEXTURE0), e.useProgram(this.#i), e.bindVertexArray(this.#o), e.uniform2f(this.#g.uViewport ?? null, t, n), e.uniform3f(this.#g.uView ?? null, this.#_.scale, this.#_.tx * this.#v + this.#T, this.#_.ty * this.#v), e.uniform1f(this.#g.uOriginX ?? null, a), e.uniform1f(this.#g.uDir ?? null, r.dir), e.uniform1f(this.#g.uLeafW ?? null, i), e.uniform2f(this.#g.uFrontSpan ?? null, r.frontSpan[0], r.frontSpan[1]), e.uniform2f(this.#g.uBackSpan ?? null, r.backSpan[0], r.backSpan[1]), e.uniform1f(this.#g.uGloss ?? null, this.#C.gloss === !1 ? 0 : 1), e.uniform1f(this.#g.uAlpha ?? null, r.fill ? this.#$() : 1), e.uniform1f(this.#g.uCrease ?? null, r.fill ? Math.min(1, this.#S / .2) : 1), e.drawElements(e.TRIANGLES, this.#l, e.UNSIGNED_SHORT, 0);
|
|
352
377
|
}
|
|
353
378
|
#$() {
|
|
354
|
-
let e = this.#C.flat ?
|
|
379
|
+
let e = this.#C.flat ? b : x, t = (this.#S - e) / (1 - e);
|
|
355
380
|
return t <= 0 ? 1 : t >= 1 ? 0 : (1 - t) ** 1.5;
|
|
356
381
|
}
|
|
357
382
|
#ee() {
|
|
358
383
|
let e = this.#n;
|
|
359
384
|
e.useProgram(this.#r), e.bindVertexArray(this.#a), e.activeTexture(e.TEXTURE0), e.uniform2f(this.#h.uViewport ?? null, this.#j, this.#M), e.uniform3f(this.#h.uView ?? null, this.#_.scale, this.#_.tx * this.#v + this.#T, this.#_.ty * this.#v);
|
|
360
385
|
}
|
|
361
|
-
#te(e, t, n = 0) {
|
|
362
|
-
let
|
|
363
|
-
t !== null && (this.#ne(
|
|
386
|
+
#te(e, t, n = 0, r = s) {
|
|
387
|
+
let i = this.#n;
|
|
388
|
+
t !== null && (this.#ne(i.TEXTURE0, t), i.uniform4f(this.#h.uRect ?? null, e.x, e.y, e.w, e.h), i.uniform1f(this.#h.uGutterSide ?? null, n), i.uniform2f(this.#h.uSpan ?? null, r[0], r[1]), i.drawArrays(i.TRIANGLE_STRIP, 0, 4));
|
|
364
389
|
}
|
|
365
390
|
#ne(e, t) {
|
|
366
391
|
let n = this.#n;
|
|
@@ -369,7 +394,7 @@ var x = class {
|
|
|
369
394
|
#re(e) {
|
|
370
395
|
let t = this.#n, n = this.#d.get(e);
|
|
371
396
|
if (n !== void 0) return this.#d.delete(e), this.#d.set(e, n), n.tex;
|
|
372
|
-
let r =
|
|
397
|
+
let r = d(t), i = f(t, r, e), a = i.width * i.height * 4;
|
|
373
398
|
return this.#d.set(e, {
|
|
374
399
|
tex: r,
|
|
375
400
|
bytes: a
|
|
@@ -378,12 +403,12 @@ var x = class {
|
|
|
378
403
|
#ie() {
|
|
379
404
|
let e = this.#n;
|
|
380
405
|
for (let [t, n] of this.#d) {
|
|
381
|
-
if (this.#f <=
|
|
406
|
+
if (this.#f <= C || this.#d.size <= 4) break;
|
|
382
407
|
this.#d.delete(t), this.#f -= n.bytes, e.deleteTexture(n.tex);
|
|
383
408
|
}
|
|
384
409
|
}
|
|
385
410
|
#ae(e) {
|
|
386
|
-
let t =
|
|
411
|
+
let t = d(e);
|
|
387
412
|
return e.bindTexture(e.TEXTURE_2D, t), e.texImage2D(e.TEXTURE_2D, 0, e.RGBA, 1, 1, 0, e.RGBA, e.UNSIGNED_BYTE, new Uint8Array([
|
|
388
413
|
0,
|
|
389
414
|
0,
|
|
@@ -400,6 +425,6 @@ var x = class {
|
|
|
400
425
|
}
|
|
401
426
|
};
|
|
402
427
|
//#endregion
|
|
403
|
-
export {
|
|
428
|
+
export { T as WebglRenderer };
|
|
404
429
|
|
|
405
|
-
//# sourceMappingURL=webglRenderer-
|
|
430
|
+
//# sourceMappingURL=webglRenderer-9RcNHAy-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webglRenderer-9RcNHAy-.js","names":["#container","#canvas","#onContextLost","#onContextRestored","#onVisibilityChange","#gl","#maxTextureSize","#buildGlResources","#clearRestoreTimer","#texCache","#blankTex","#quadVao","#curlVao","#flat","#curl","#cacheBytes","#content","#flip","#fatalHandler","#fill","#fit","#trackAspect","#render","#pageAspect","#docAspect","#flipT","#curlModel","#anchor","#fromShiftUnit","#toShiftUnit","#view","#bookBox","#contentBox","#shiftX","#dpr","#drawAspect","#contextLost","#restoreTimer","#fatal","#fatalFired","#flatU","#curlU","#makeQuadVao","#buildCurlMesh","#makeBlankTexture","#idxCount","#posBuf","#normBuf","#mesh","#resize","#fitBook","#drawFlip","#drawStatic","#bookW","#bookH","#useFlat","#drawQuad","#bindFace","#fillFade","#textureFor","#evictTextures"],"sources":["../src/renderer/gl/context.ts","../src/renderer/gl/program.ts","../src/renderer/gl/texture.ts","../src/renderer/webglRenderer.ts"],"sourcesContent":["/** Create a WebGL2 context, or throw so the caller can fall back to CSS (§8.4). */\nexport function createGlContext(canvas: HTMLCanvasElement): WebGL2RenderingContext {\n const gl = canvas.getContext('webgl2', {\n alpha: true,\n premultipliedAlpha: true,\n antialias: true,\n depth: false,\n stencil: false,\n // We render on demand (not every frame), so the buffer must persist between\n // composites — otherwise the settled page flickers once the flip animation\n // stops and nothing is redrawing each frame.\n preserveDrawingBuffer: true,\n });\n if (gl === null) {\n throw new Error('WebglRenderer: could not create a WebGL2 context.');\n }\n return gl;\n}\n","/** Compile + link a shader program; throws with the driver log on failure. */\nexport function createProgram(\n gl: WebGL2RenderingContext,\n vertexSrc: string,\n fragmentSrc: string,\n): WebGLProgram {\n const program = gl.createProgram();\n if (program === null) throw new Error('WebglRenderer: could not create a program.');\n\n const vs = compileShader(gl, gl.VERTEX_SHADER, vertexSrc);\n const fs = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSrc);\n gl.attachShader(program, vs);\n gl.attachShader(program, fs);\n gl.linkProgram(program);\n\n // Shaders can be detached/deleted once linked.\n gl.deleteShader(vs);\n gl.deleteShader(fs);\n\n if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n const log = gl.getProgramInfoLog(program);\n gl.deleteProgram(program);\n throw new Error(`WebglRenderer: program link failed: ${log ?? 'unknown error'}`);\n }\n return program;\n}\n\nfunction compileShader(gl: WebGL2RenderingContext, type: number, src: string): WebGLShader {\n const shader = gl.createShader(type);\n if (shader === null) throw new Error('WebglRenderer: could not create a shader.');\n gl.shaderSource(shader, src);\n gl.compileShader(shader);\n if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n const log = gl.getShaderInfoLog(shader);\n gl.deleteShader(shader);\n throw new Error(`WebglRenderer: shader compile failed: ${log ?? 'unknown error'}`);\n }\n return shader;\n}\n","/** Create a page texture with clamp + linear filtering (no mipmaps — §8.3). */\nexport function createTexture(gl: WebGL2RenderingContext): WebGLTexture {\n const tex = gl.createTexture();\n if (tex === null) throw new Error('WebglRenderer: could not create a texture.');\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n return tex;\n}\n\n/**\n * Upload a decoded page raster (ImageBitmap or canvas) into `tex`, returning the dimensions actually\n * uploaded (which differ from the source when it was too big for the GPU).\n *\n * A raster larger than `gl.MAX_TEXTURE_SIZE` on either axis cannot be uploaded: `texImage2D` fails\n * and leaves the texture incomplete, which samples as solid black. A zoomed page on a high-DPR phone\n * (small MAX_TEXTURE_SIZE, e.g. 4096) hits this readily. Downscale over-large rasters to fit,\n * preserving aspect, so a slightly-softer page beats a black one.\n */\nexport function uploadTexture(\n gl: WebGL2RenderingContext,\n tex: WebGLTexture,\n source: TexImageSource,\n): { width: number; height: number } {\n const max = gl.getParameter(gl.MAX_TEXTURE_SIZE) as number;\n const upload = fitToLimit(source, max);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n // No Y flip: the vertex shader maps aUnit.y=0 to the page top and samples the\n // source's top row there. UNPACK_FLIP_Y is honored for <canvas> sources but\n // IGNORED for ImageBitmap, so flipping would invert PDF (canvas) pages while\n // leaving image (ImageBitmap) pages upright. Off keeps both consistent.\n gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);\n gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, upload);\n return { width: upload.width, height: upload.height };\n}\n\n/** Return `source` unchanged if it fits `max`, else a canvas holding it scaled down to fit. */\nfunction fitToLimit(source: TexImageSource, max: number): TexImageSource & { width: number; height: number } {\n const dims = source as unknown as { width: number; height: number };\n const longest = Math.max(dims.width, dims.height);\n if (!Number.isFinite(max) || max <= 0 || longest <= max) {\n return dims as TexImageSource & { width: number; height: number };\n }\n const factor = max / longest;\n const canvas = document.createElement('canvas');\n canvas.width = Math.max(1, Math.floor(dims.width * factor));\n canvas.height = Math.max(1, Math.floor(dims.height * factor));\n canvas.getContext('2d')?.drawImage(source as CanvasImageSource, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n","// Imported from the leaf modules, not the barrel: the barrel pulls in every curl, which would\n// defeat the point of only bundling the two the engine can name.\nimport { createPageMesh, type PageMesh } from '../geometry/curls/mesh';\nimport { BUNDLED_CURLS, resolveCurl } from '../geometry/curls/bundled';\nimport { DEFAULT_CURL, type CurlAnchor, type CurlModel, type CurlSpec } from '../geometry/curls/types';\nimport type { Spread } from '../engine/spread';\nimport {\n FULL_SPAN,\n leftSpan,\n rightSpan,\n spineTrimOf,\n type FlipDirection,\n type LayoutMetrics,\n type PageSpan,\n type Renderer,\n type RenderOptions,\n type SpreadContent,\n} from './types';\nimport { createGlContext } from './gl/context';\nimport { createProgram } from './gl/program';\nimport { createTexture, uploadTexture } from './gl/texture';\n\n// Flat program: a page rect (device px, top-left origin) with zoom/pan and a soft\n// permanent gutter shadow toward the spine so the turning leaf's shadow lands into it.\nconst FLAT_VERT = `#version 300 es\nin vec2 aUnit;\nuniform vec2 uViewport;\nuniform vec4 uRect;\nuniform vec3 uView;\nout vec2 vUv;\nvoid main() {\n vec2 px = uRect.xy + aUnit * uRect.zw;\n px = px * uView.x + uView.yz;\n vec2 clip = px / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUnit;\n}`;\n\nconst FLAT_FRAG = `#version 300 es\nprecision mediump float;\nin vec2 vUv;\nuniform sampler2D uTex;\nuniform float uGutterSide; // +1 = spine at right edge, -1 = at left edge, 0 = none\nuniform vec2 uSpan; // visible u-range of the page texture (a spine trim narrows it)\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vec2(mix(uSpan.x, uSpan.y, vUv.x), vUv.y));\n if (uGutterSide != 0.0) {\n float d = uGutterSide > 0.0 ? (1.0 - vUv.x) : vUv.x;\n c.rgb *= mix(0.72, 1.0, smoothstep(0.0, 0.10, d));\n }\n outColor = c;\n}`;\n\n// Curl program: a thin pass-through for the CPU-deformed page mesh (positions and\n// normals come from the selected curl model). It places the leaf, applies a top-safe depth\n// perspective, and lights from the true surface normal.\nconst CURL_VERT = `#version 300 es\nin vec3 aPos; // leaf-local device px: x = bent offset from spine, y 0..H, z depth\nin vec3 aNormal;\nin vec2 aUv;\nuniform vec2 uViewport;\nuniform vec3 uView; // scale, tx, ty\nuniform float uOriginX; // spine x in device px\nuniform float uDir; // +1 leaf to the right of spine, -1 to the left\nuniform float uLeafW; // leaf width in device px (perspective scale)\nout vec2 vUv;\nout float vFacing;\nout float vU;\nvoid main() {\n float bookX = uOriginX + uDir * aPos.x;\n float bookY = aPos.y;\n float Z = aPos.z;\n // Eye distance, in leaf widths. Keeps the perspective resolution-independent. 5.3 puts the\n // roll's deepest point ~14% larger than flat; stronger than that (a nearer eye) balloons the\n // turning sheet and reads more like a fisheye than a page lifting.\n float D = uLeafW * 5.3;\n float persp = D / max(D - Z, 1.0);\n float cx = uViewport.x * 0.5;\n float cy = uViewport.y * 0.5;\n float px = cx + (bookX - cx) * persp;\n float py = cy + (bookY - cy) * min(persp, 1.0); // Y only shrinks (top-safe)\n vec2 p = vec2(px, py) * uView.x + uView.yz;\n vec2 clip = p / uViewport * 2.0 - 1.0;\n gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);\n vUv = aUv;\n vFacing = aNormal.z;\n vU = aUv.x;\n}`;\n\nconst CURL_FRAG = `#version 300 es\nprecision mediump float;\nin vec2 vUv;\nin float vFacing;\nin float vU;\nuniform sampler2D uFront;\nuniform sampler2D uBack;\nuniform vec2 uFrontSpan; // visible u-ranges of the two faces (a spine trim narrows them)\nuniform vec2 uBackSpan;\nuniform highp float uDir;\nuniform float uGloss; // 0 disables the specular highlight (e.g. the flat 'simple' curl)\nuniform float uAlpha; // leaf opacity; a lone page dissolves into the page landing beneath it\nuniform float uCrease; // spine-crease strength: 1 matches a spread's gutter; ramps from 0 for a lone leaf\nout vec4 outColor;\nvoid main() {\n // The turn mesh carries its own facing in the normal; the front points at the viewer\n // (normal.z > 0) until the leaf passes vertical, for both flip directions. u->x still\n // flips with uDir so a left-hand leaf reads spine-inward.\n bool showFront = vFacing > 0.0;\n float fx = uDir > 0.0 ? vUv.x : 1.0 - vUv.x;\n vec4 c = showFront\n ? texture(uFront, vec2(mix(uFrontSpan.x, uFrontSpan.y, fx), vUv.y))\n : texture(uBack, vec2(mix(uBackSpan.x, uBackSpan.y, 1.0 - fx), vUv.y));\n\n // Fold shading: the sheet darkens sharply where it curves away from the viewer, so the\n // rolled edge reads as a deep crease with a soft highlight riding the ridge.\n float diff = clamp(abs(vFacing), 0.0, 1.0);\n float lit = mix(0.42, 1.0, diff);\n // Spine crease matches the gutter shadow on a spread. A lone page has no gutter, so uCrease\n // ramps it in from 0 as the leaf lifts — otherwise a still-flat leaf paints a shadow band on\n // the anchor edge before the turn even begins.\n lit *= mix(1.0, mix(0.7, 1.0, smoothstep(0.0, 0.12, vU)), uCrease);\n\n // Glossy specular: brightest where the surface tilts ~30 deg from facing the viewer,\n // so a thin glossy highlight rides across the sheet as it rolls.\n float spec = pow(max(cos(acos(diff) - 0.52), 0.0), 220.0) * 0.22 * uGloss;\n\n c.rgb = clamp(c.rgb * lit + spec, 0.0, 1.0);\n // Premultiplied alpha (blendFunc ONE, ONE_MINUS_SRC_ALPHA): scale colour as well as alpha,\n // or fading would brighten the sheet additively instead of dissolving it.\n outColor = vec4(c.rgb * uAlpha, c.a * uAlpha);\n}`;\n\nconst GRID_COLS = 28;\nconst GRID_ROWS = 36;\n/** Progress at which a lone page starts dissolving. A full-width leaf has no facing half to\n * flop onto, so instead of landing on empty space it fades into the page arriving beneath it\n * over the rest of the turn. Roll is already flat by mid-turn; curling models stay bent\n * longer, so they dissolve later. Timed so the fade covers the still landed pose (curl RHO_END)\n * rather than washing out a sheet that is still swinging. */\nconst FILL_FADE_START_ROLL = 0.7;\nconst FILL_FADE_START_CURL = 0.78;\nconst RESTORE_TIMEOUT_MS = 4000;\nconst MAX_TEXTURE_CACHE_BYTES = 256 * 1024 * 1024;\n\ninterface Rect {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\ninterface FlipState {\n underLeft: TexImageSource | null;\n underRight: TexImageSource | null;\n underFull: TexImageSource | null; // fill mode\n front: TexImageSource | null;\n back: TexImageSource | null;\n // Visible spans: each page keeps the spine trim of the spread it belongs to.\n underLeftSpan: PageSpan;\n underRightSpan: PageSpan;\n frontSpan: PageSpan;\n backSpan: PageSpan;\n dir: number; // +1 forward (leaf right of spine), -1 backward (left)\n fill: boolean;\n}\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * GPU renderer (WebGL2, hand-written). The turning leaf is a dense grid deformed on\n * the CPU by the selected curl model (see geometry/curls) and\n * uploaded each frame, lit from real normals; the static pages are flat quads with a\n * gutter shadow. Restores\n * GL state on context loss; signals onFatal (→ CSS fallback) when unrecoverable.\n */\nexport class WebglRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #canvas: HTMLCanvasElement | null = null;\n #gl: WebGL2RenderingContext | null = null;\n\n #flat: WebGLProgram | null = null;\n #curl: WebGLProgram | null = null;\n #quadVao: WebGLVertexArrayObject | null = null;\n #curlVao: WebGLVertexArrayObject | null = null;\n #posBuf: WebGLBuffer | null = null;\n #normBuf: WebGLBuffer | null = null;\n #idxCount = 0;\n #mesh: PageMesh = createPageMesh(GRID_COLS, GRID_ROWS);\n\n #texCache = new Map<TexImageSource, { tex: WebGLTexture; bytes: number }>();\n #cacheBytes = 0;\n #blankTex: WebGLTexture | null = null;\n // The GPU's largest accepted texture dimension; a zoom tile past it would upload black (see\n // uploadTexture). Read once at mount and exposed so the source can cap its rasters to fit.\n #maxTextureSize = 0;\n\n #flatU: Record<string, WebGLUniformLocation | null> = {};\n #curlU: Record<string, WebGLUniformLocation | null> = {};\n\n #view = { scale: 1, tx: 0, ty: 0 };\n #dpr = 1;\n #content: SpreadContent | null = null;\n #fill = false;\n #flip: FlipState | null = null;\n #flipT = 0;\n #curlModel: CurlModel = BUNDLED_CURLS[DEFAULT_CURL];\n #anchor: CurlAnchor = { y: 0.5 };\n // A lone page (cover / book front-back) sits on one half; these center it by shifting\n // the view a quarter-width, interpolated across a flip so the open/close doesn't jump.\n #shiftX = 0;\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the 2-page spread is letterboxed to the pages' aspect, centered\n // in the container, so pages fill their halves without stretching. 0 = aspect unknown.\n #pageAspect = 0;\n // The first page's aspect, latched: the container is sized to this for the whole document so\n // off-size pages do not resize it (layout shift). `contain` letterboxes them; `fill` stretches.\n #docAspect = 0;\n #fit: 'contain' | 'fill' = 'contain';\n #bookW = 0;\n #bookH = 0;\n\n #contextLost = false;\n #fatalFired = false;\n #restoreTimer: ReturnType<typeof setTimeout> | null = null;\n #fatalHandler: (() => void) | null = null;\n\n mount(container: HTMLElement): Promise<void> {\n this.#container = container;\n const doc = container.ownerDocument;\n const canvas = doc.createElement('canvas');\n canvas.style.cssText = 'display:block;width:100%;height:100%;';\n container.append(canvas);\n this.#canvas = canvas;\n\n canvas.addEventListener('webglcontextlost', this.#onContextLost as EventListener);\n canvas.addEventListener('webglcontextrestored', this.#onContextRestored as EventListener);\n doc.addEventListener('visibilitychange', this.#onVisibilityChange);\n\n this.#gl = createGlContext(canvas); // throws if unavailable -> engine falls back (§8.4)\n this.#maxTextureSize = this.#gl.getParameter(this.#gl.MAX_TEXTURE_SIZE) as number;\n this.#buildGlResources();\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clearRestoreTimer();\n const canvas = this.#canvas;\n if (canvas !== null) {\n canvas.removeEventListener('webglcontextlost', this.#onContextLost as EventListener);\n canvas.removeEventListener('webglcontextrestored', this.#onContextRestored as EventListener);\n canvas.ownerDocument.removeEventListener('visibilitychange', this.#onVisibilityChange);\n }\n const gl = this.#gl;\n if (gl !== null) {\n for (const cached of this.#texCache.values()) gl.deleteTexture(cached.tex);\n if (this.#blankTex !== null) gl.deleteTexture(this.#blankTex);\n if (this.#quadVao !== null) gl.deleteVertexArray(this.#quadVao);\n if (this.#curlVao !== null) gl.deleteVertexArray(this.#curlVao);\n if (this.#flat !== null) gl.deleteProgram(this.#flat);\n if (this.#curl !== null) gl.deleteProgram(this.#curl);\n gl.getExtension('WEBGL_lose_context')?.loseContext();\n }\n canvas?.remove();\n this.#texCache.clear();\n this.#cacheBytes = 0;\n this.#blankTex = null;\n this.#container = null;\n this.#canvas = null;\n this.#gl = null;\n this.#content = null;\n this.#flip = null;\n }\n\n onFatal(handler: () => void): void {\n this.#fatalHandler = handler;\n }\n\n get maxTextureSize(): number | undefined {\n return this.#maxTextureSize || undefined;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n this.#content = content;\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#flip = null;\n this.#trackAspect(content, this.#fill);\n this.#render();\n }\n\n /** Remember the current spread's page aspect (for drawing; narrowed by any spine trim) and latch\n * the first page's untrimmed aspect (for the container). The container follows #docAspect so\n * off-size or trimmed pages never resize it, whichever spread the reader opened on. */\n #trackAspect(content: SpreadContent, fill: boolean): void {\n const p = content.left ?? content.right;\n if (p && p.height) {\n const raw = p.width / p.height;\n this.#pageAspect = raw * (1 - spineTrimOf(content, fill));\n if (this.#docAspect === 0) this.#docAspect = raw;\n }\n }\n\n /** Aspect the book is drawn at: the current page under `contain` (letterboxed within the stable\n * container), or the document aspect under `fill` (fills it, stretching off-size pages). */\n #drawAspect(): number {\n return this.#fit === 'fill' && this.#docAspect > 0 ? this.#docAspect : this.#pageAspect;\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n const fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#flipT = 0;\n if (options?.curl) this.#curlModel = resolveCurl(options.curl);\n if (options?.anchor) this.#anchor = options.anchor;\n this.#fromShiftUnit = fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = fill ? 0 : shiftUnit(to);\n this.#trackAspect(to, fill);\n // Each page keeps the trim of its own spread, so the leaf's front matches `from` and its back\n // matches `to`: the spine edge stays hidden through the whole turn.\n const fromTrim = spineTrimOf(from, fill);\n const toTrim = spineTrimOf(to, fill);\n if (fill) {\n this.#flip = {\n underLeft: null,\n underRight: null,\n underFull: to.right ?? to.left,\n front: from.right ?? from.left,\n // A lone page has no facing \"next\" leaf on its back; show the same page mirrored\n // (the shader flips the back UV) so it reads as the sheet's own reverse side.\n back: from.right ?? from.left,\n underLeftSpan: FULL_SPAN,\n underRightSpan: FULL_SPAN,\n frontSpan: FULL_SPAN,\n backSpan: FULL_SPAN,\n dir: direction === 'forward' ? 1 : -1,\n fill: true,\n };\n } else if (direction === 'forward') {\n this.#flip = {\n underLeft: from.left,\n underRight: to.right,\n underFull: null,\n front: from.right,\n back: to.left,\n underLeftSpan: leftSpan(fromTrim),\n underRightSpan: rightSpan(toTrim),\n frontSpan: rightSpan(fromTrim),\n backSpan: leftSpan(toTrim),\n dir: 1,\n fill: false,\n };\n } else {\n this.#flip = {\n underLeft: to.left,\n underRight: from.right,\n underFull: null,\n front: from.left,\n back: to.right,\n underLeftSpan: leftSpan(toTrim),\n underRightSpan: rightSpan(fromTrim),\n frontSpan: leftSpan(fromTrim),\n backSpan: rightSpan(toTrim),\n dir: -1,\n fill: false,\n };\n }\n this.#render();\n }\n\n setFlipProgress(t: number, _direction: FlipDirection): void {\n this.#flipT = t;\n this.#render();\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#view = { scale, tx: x, ty: y };\n this.#render();\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => {\n // The shader applies `px * scale + view.t + shiftX`, so the lone-page shift sits\n // *outside* the scale: a fixed screen offset, not part of the magnified geometry.\n const shiftCss = this.#shiftX / this.#dpr;\n return {\n x: (content.x - shiftCss) * scale + shiftCss,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n };\n }\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n containerAspect: this.#docAspect > 0 ? (this.#fill ? 1 : 2) * this.#docAspect : undefined,\n };\n }\n\n /** The fitted book rect in container CSS px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const el = this.#container;\n const cw = el?.clientWidth ?? 0;\n const ch = el?.clientHeight ?? 0;\n const aspect = this.#drawAspect();\n if (aspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * aspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors the draw-time #shiftX centering). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n const lone = !this.#fill && this.#content !== null && shiftUnit(this.#content) !== 0;\n return lone ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n // --- context loss / restore ---------------------------------------------------\n\n #onContextLost = (event: Event): void => {\n event.preventDefault();\n this.#contextLost = true;\n if (this.#restoreTimer === null) {\n this.#restoreTimer = setTimeout(() => this.#fatal(), RESTORE_TIMEOUT_MS);\n }\n };\n\n #onContextRestored = (): void => {\n this.#clearRestoreTimer();\n this.#contextLost = false;\n this.#buildGlResources();\n this.#render();\n };\n\n #onVisibilityChange = (): void => {\n if (this.#canvas?.ownerDocument.visibilityState === 'visible') this.#render();\n };\n\n #fatal(): void {\n if (this.#fatalFired) return;\n this.#fatalFired = true;\n this.#fatalHandler?.();\n }\n\n #clearRestoreTimer(): void {\n if (this.#restoreTimer !== null) {\n clearTimeout(this.#restoreTimer);\n this.#restoreTimer = null;\n }\n }\n\n #buildGlResources(): void {\n const gl = this.#gl;\n if (gl === null) return;\n\n this.#flat = createProgram(gl, FLAT_VERT, FLAT_FRAG);\n this.#curl = createProgram(gl, CURL_VERT, CURL_FRAG);\n for (const name of ['uViewport', 'uRect', 'uView', 'uTex', 'uGutterSide', 'uSpan']) {\n this.#flatU[name] = gl.getUniformLocation(this.#flat, name);\n }\n for (const name of ['uViewport', 'uView', 'uOriginX', 'uDir', 'uLeafW', 'uFront', 'uBack', 'uFrontSpan', 'uBackSpan', 'uGloss', 'uAlpha', 'uCrease']) {\n this.#curlU[name] = gl.getUniformLocation(this.#curl, name);\n }\n\n this.#quadVao = this.#makeQuadVao(gl, this.#flat);\n this.#buildCurlMesh(gl, this.#curl);\n\n this.#texCache.clear();\n this.#cacheBytes = 0;\n this.#blankTex = this.#makeBlankTexture(gl);\n gl.useProgram(this.#flat);\n gl.uniform1i(this.#flatU.uTex ?? null, 0);\n gl.useProgram(this.#curl);\n gl.uniform1i(this.#curlU.uFront ?? null, 0);\n gl.uniform1i(this.#curlU.uBack ?? null, 1);\n gl.clearColor(0, 0, 0, 0);\n gl.enable(gl.BLEND);\n gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); // premultiplied alpha\n }\n\n #makeQuadVao(gl: WebGL2RenderingContext, program: WebGLProgram): WebGLVertexArrayObject {\n const vao = gl.createVertexArray();\n const buffer = gl.createBuffer();\n if (vao === null || buffer === null) throw new Error('WebglRenderer: could not create buffers.');\n gl.bindVertexArray(vao);\n gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);\n const aUnit = gl.getAttribLocation(program, 'aUnit');\n gl.enableVertexAttribArray(aUnit);\n gl.vertexAttribPointer(aUnit, 2, gl.FLOAT, false, 0, 0);\n gl.bindVertexArray(null);\n return vao;\n }\n\n #buildCurlMesh(gl: WebGL2RenderingContext, program: WebGLProgram): void {\n const nx = GRID_COLS + 1;\n const indices: number[] = [];\n for (let j = 0; j < GRID_ROWS; j++) {\n for (let i = 0; i < GRID_COLS; i++) {\n const a = j * nx + i;\n const b = a + 1;\n const c = a + nx;\n const d = c + 1;\n indices.push(a, c, b, b, c, d);\n }\n }\n this.#idxCount = indices.length;\n\n const vao = gl.createVertexArray();\n this.#posBuf = gl.createBuffer();\n this.#normBuf = gl.createBuffer();\n const uvBuf = gl.createBuffer();\n const idxBuf = gl.createBuffer();\n if (vao === null || this.#posBuf === null || this.#normBuf === null || uvBuf === null || idxBuf === null) {\n throw new Error('WebglRenderer: could not create mesh buffers.');\n }\n gl.bindVertexArray(vao);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.positions, gl.DYNAMIC_DRAW);\n const aPos = gl.getAttribLocation(program, 'aPos');\n gl.enableVertexAttribArray(aPos);\n gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#normBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.normals, gl.DYNAMIC_DRAW);\n const aNormal = gl.getAttribLocation(program, 'aNormal');\n gl.enableVertexAttribArray(aNormal);\n gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ARRAY_BUFFER, uvBuf);\n gl.bufferData(gl.ARRAY_BUFFER, this.#mesh.uvs, gl.STATIC_DRAW);\n const aUv = gl.getAttribLocation(program, 'aUv');\n gl.enableVertexAttribArray(aUv);\n gl.vertexAttribPointer(aUv, 2, gl.FLOAT, false, 0, 0);\n\n gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, idxBuf);\n gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);\n\n gl.bindVertexArray(null);\n this.#curlVao = vao;\n }\n\n // --- drawing ------------------------------------------------------------------\n\n #render(): void {\n const gl = this.#gl;\n const canvas = this.#canvas;\n if (gl === null || canvas === null || this.#contextLost || gl.isContextLost()) return;\n this.#resize();\n gl.clear(gl.COLOR_BUFFER_BIT);\n this.#fitBook();\n if (this.#flip !== null) this.#drawFlip();\n else if (this.#content !== null) this.#drawStatic();\n }\n\n /** Letterbox the 2-page book to the page aspect, centered; restricts the GL viewport\n * so pages fill their halves without stretching (the surround stays cleared). */\n #fitBook(): void {\n const gl = this.#gl!;\n const canvas = this.#canvas!;\n const cw = canvas.width;\n const ch = canvas.height;\n const aspect = this.#drawAspect();\n if (aspect > 0) {\n const bookAspect = (this.#fill ? 1 : 2) * aspect;\n if (bookAspect > cw / ch) {\n this.#bookW = cw;\n this.#bookH = Math.round(cw / bookAspect);\n } else {\n this.#bookH = ch;\n this.#bookW = Math.round(ch * bookAspect);\n }\n } else {\n this.#bookW = cw;\n this.#bookH = ch;\n }\n const bx = Math.round((cw - this.#bookW) / 2);\n const by = Math.round((ch - this.#bookH) / 2); // centered, so GL's bottom offset == by\n gl.viewport(bx, by, this.#bookW, this.#bookH);\n }\n\n #drawStatic(): void {\n const w = this.#bookW;\n const h = this.#bookH;\n const content = this.#content!;\n this.#shiftX = this.#fill ? 0 : shiftUnit(content) * (w / 4);\n this.#useFlat();\n if (this.#fill) {\n this.#drawQuad({ x: 0, y: 0, w, h }, content.right ?? content.left);\n } else {\n // A lone page (one side null) gets no gutter shadow — it's a standalone, centered page.\n const trim = spineTrimOf(content, false);\n this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, content.left, content.right ? 1 : 0, leftSpan(trim));\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, content.right, content.left ? -1 : 0, rightSpan(trim));\n }\n }\n\n #drawFlip(): void {\n const gl = this.#gl!;\n const w = this.#bookW;\n const h = this.#bookH;\n const flip = this.#flip!;\n\n // Interpolate the lone-page centering across the flip so an opening cover (or a\n // closing back page) slides between centered and spread instead of jumping.\n this.#shiftX = flip.fill\n ? 0\n : (this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * this.#flipT) * (w / 4);\n\n // Static pages underneath (with gutter shadow).\n this.#useFlat();\n if (flip.fill) {\n this.#drawQuad({ x: 0, y: 0, w, h }, flip.underFull);\n } else {\n this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, flip.underLeft, 1, flip.underLeftSpan);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, flip.underRight, -1, flip.underRightSpan);\n }\n\n if (flip.front === null && flip.back === null) return;\n\n // Deform the leaf mesh with the selected curl model on the CPU, upload it, then draw.\n const leafW = flip.fill ? w : w / 2;\n const originX = flip.fill ? (flip.dir > 0 ? 0 : w) : w / 2;\n this.#curlModel.deform(this.#mesh, leafW, h, this.#flipT, {\n y: this.#anchor.y,\n fill: flip.fill,\n });\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#posBuf);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.#mesh.positions);\n gl.bindBuffer(gl.ARRAY_BUFFER, this.#normBuf);\n gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.#mesh.normals);\n\n this.#bindFace(gl.TEXTURE0, flip.front);\n this.#bindFace(gl.TEXTURE1, flip.back);\n gl.activeTexture(gl.TEXTURE0);\n\n gl.useProgram(this.#curl);\n gl.bindVertexArray(this.#curlVao);\n gl.uniform2f(this.#curlU.uViewport ?? null, w, h);\n gl.uniform3f(\n this.#curlU.uView ?? null,\n this.#view.scale,\n this.#view.tx * this.#dpr + this.#shiftX,\n this.#view.ty * this.#dpr,\n );\n gl.uniform1f(this.#curlU.uOriginX ?? null, originX);\n gl.uniform1f(this.#curlU.uDir ?? null, flip.dir);\n gl.uniform1f(this.#curlU.uLeafW ?? null, leafW);\n gl.uniform2f(this.#curlU.uFrontSpan ?? null, flip.frontSpan[0], flip.frontSpan[1]);\n gl.uniform2f(this.#curlU.uBackSpan ?? null, flip.backSpan[0], flip.backSpan[1]);\n gl.uniform1f(this.#curlU.uGloss ?? null, this.#curlModel.gloss === false ? 0 : 1);\n gl.uniform1f(this.#curlU.uAlpha ?? null, flip.fill ? this.#fillFade() : 1);\n // Spreads carry the full gutter crease; a lone leaf ramps it in over the first fifth of the\n // turn so a still-flat sheet shows no shadow band at its anchor before it lifts.\n gl.uniform1f(this.#curlU.uCrease ?? null, flip.fill ? Math.min(1, this.#flipT / 0.2) : 1);\n gl.drawElements(gl.TRIANGLES, this.#idxCount, gl.UNSIGNED_SHORT, 0);\n }\n\n /** Leaf opacity for a lone page: solid until the curl-appropriate fade start, then\n * dissolving to 0 as it lands, so the turn resolves into the destination page rather\n * than onto blank space. The fade eases so opacity hangs longer then finishes cleanly. */\n #fillFade(): number {\n const start = this.#curlModel.flat ? FILL_FADE_START_ROLL : FILL_FADE_START_CURL;\n const over = (this.#flipT - start) / (1 - start);\n if (over <= 0) return 1;\n if (over >= 1) return 0;\n // Ease-out the remaining opacity: stay readable through most of the window, then settle.\n return Math.pow(1 - over, 1.5);\n }\n\n #useFlat(): void {\n const gl = this.#gl!;\n gl.useProgram(this.#flat);\n gl.bindVertexArray(this.#quadVao);\n gl.activeTexture(gl.TEXTURE0);\n gl.uniform2f(this.#flatU.uViewport ?? null, this.#bookW, this.#bookH);\n gl.uniform3f(\n this.#flatU.uView ?? null,\n this.#view.scale,\n this.#view.tx * this.#dpr + this.#shiftX,\n this.#view.ty * this.#dpr,\n );\n }\n\n #drawQuad(rect: Rect, source: TexImageSource | null, gutterSide = 0, span: PageSpan = FULL_SPAN): void {\n const gl = this.#gl!;\n if (source === null) return;\n this.#bindFace(gl.TEXTURE0, source);\n gl.uniform4f(this.#flatU.uRect ?? null, rect.x, rect.y, rect.w, rect.h);\n gl.uniform1f(this.#flatU.uGutterSide ?? null, gutterSide);\n gl.uniform2f(this.#flatU.uSpan ?? null, span[0], span[1]);\n gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n }\n\n #bindFace(unit: number, source: TexImageSource | null): void {\n const gl = this.#gl!;\n gl.activeTexture(unit);\n gl.bindTexture(gl.TEXTURE_2D, source !== null ? this.#textureFor(source) : this.#blankTex);\n }\n\n #textureFor(source: TexImageSource): WebGLTexture {\n const gl = this.#gl!;\n const existing = this.#texCache.get(source);\n if (existing !== undefined) {\n this.#texCache.delete(source);\n this.#texCache.set(source, existing);\n return existing.tex;\n }\n const tex = createTexture(gl);\n // uploadTexture may downscale a raster too large for the GPU; account for what it really uploaded.\n const uploaded = uploadTexture(gl, tex, source);\n const bytes = uploaded.width * uploaded.height * 4;\n this.#texCache.set(source, { tex, bytes });\n this.#cacheBytes += bytes;\n this.#evictTextures();\n return tex;\n }\n\n #evictTextures(): void {\n const gl = this.#gl!;\n for (const [src, cached] of this.#texCache) {\n if (this.#cacheBytes <= MAX_TEXTURE_CACHE_BYTES || this.#texCache.size <= 4) break;\n this.#texCache.delete(src);\n this.#cacheBytes -= cached.bytes;\n gl.deleteTexture(cached.tex);\n }\n }\n\n #makeBlankTexture(gl: WebGL2RenderingContext): WebGLTexture {\n const tex = createTexture(gl);\n gl.bindTexture(gl.TEXTURE_2D, tex);\n gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));\n return tex;\n }\n\n #resize(): void {\n const gl = this.#gl;\n const canvas = this.#canvas;\n const el = this.#container;\n if (gl === null || canvas === null || el === null) return;\n this.#dpr = typeof devicePixelRatio === 'number' ? devicePixelRatio : 1;\n const w = Math.max(1, Math.round(el.clientWidth * this.#dpr));\n const h = Math.max(1, Math.round(el.clientHeight * this.#dpr));\n if (canvas.width !== w || canvas.height !== h) {\n canvas.width = w;\n canvas.height = h;\n gl.viewport(0, 0, w, h);\n }\n }\n}\n"],"mappings":";;;;AACA,SAAgB,EAAgB,GAAmD;CACjF,IAAM,IAAK,EAAO,WAAW,UAAU;EACrC,OAAO;EACP,oBAAoB;EACpB,WAAW;EACX,OAAO;EACP,SAAS;EAIT,uBAAuB;CACzB,CAAC;CACD,IAAI,MAAO,MACT,MAAU,MAAM,mDAAmD;CAErE,OAAO;AACT;;;AChBA,SAAgB,EACd,GACA,GACA,GACc;CACd,IAAM,IAAU,EAAG,cAAc;CACjC,IAAI,MAAY,MAAM,MAAU,MAAM,4CAA4C;CAElF,IAAM,IAAK,EAAc,GAAI,EAAG,eAAe,CAAS,GAClD,IAAK,EAAc,GAAI,EAAG,iBAAiB,CAAW;CAS5D,IARA,EAAG,aAAa,GAAS,CAAE,GAC3B,EAAG,aAAa,GAAS,CAAE,GAC3B,EAAG,YAAY,CAAO,GAGtB,EAAG,aAAa,CAAE,GAClB,EAAG,aAAa,CAAE,GAEd,CAAC,EAAG,oBAAoB,GAAS,EAAG,WAAW,GAAG;EACpD,IAAM,IAAM,EAAG,kBAAkB,CAAO;EAExC,MADA,EAAG,cAAc,CAAO,GACd,MAAM,uCAAuC,KAAO,iBAAiB;CACjF;CACA,OAAO;AACT;AAEA,SAAS,EAAc,GAA4B,GAAc,GAA0B;CACzF,IAAM,IAAS,EAAG,aAAa,CAAI;CACnC,IAAI,MAAW,MAAM,MAAU,MAAM,2CAA2C;CAGhF,IAFA,EAAG,aAAa,GAAQ,CAAG,GAC3B,EAAG,cAAc,CAAM,GACnB,CAAC,EAAG,mBAAmB,GAAQ,EAAG,cAAc,GAAG;EACrD,IAAM,IAAM,EAAG,iBAAiB,CAAM;EAEtC,MADA,EAAG,aAAa,CAAM,GACZ,MAAM,yCAAyC,KAAO,iBAAiB;CACnF;CACA,OAAO;AACT;;;ACrCA,SAAgB,EAAc,GAA0C;CACtE,IAAM,IAAM,EAAG,cAAc;CAC7B,IAAI,MAAQ,MAAM,MAAU,MAAM,4CAA4C;CAM9E,OALA,EAAG,YAAY,EAAG,YAAY,CAAG,GACjC,EAAG,cAAc,EAAG,YAAY,EAAG,gBAAgB,EAAG,aAAa,GACnE,EAAG,cAAc,EAAG,YAAY,EAAG,gBAAgB,EAAG,aAAa,GACnE,EAAG,cAAc,EAAG,YAAY,EAAG,oBAAoB,EAAG,MAAM,GAChE,EAAG,cAAc,EAAG,YAAY,EAAG,oBAAoB,EAAG,MAAM,GACzD;AACT;AAWA,SAAgB,EACd,GACA,GACA,GACmC;CAEnC,IAAM,IAAS,EAAW,GADd,EAAG,aAAa,EAAG,gBACG,CAAG;CASrC,OARA,EAAG,YAAY,EAAG,YAAY,CAAG,GAKjC,EAAG,YAAY,EAAG,qBAAqB,EAAK,GAC5C,EAAG,YAAY,EAAG,gCAAgC,EAAI,GACtD,EAAG,WAAW,EAAG,YAAY,GAAG,EAAG,MAAM,EAAG,MAAM,EAAG,eAAe,CAAM,GACnE;EAAE,OAAO,EAAO;EAAO,QAAQ,EAAO;CAAO;AACtD;AAGA,SAAS,EAAW,GAAwB,GAAiE;CAC3G,IAAM,IAAO,GACP,IAAU,KAAK,IAAI,EAAK,OAAO,EAAK,MAAM;CAChD,IAAI,CAAC,OAAO,SAAS,CAAG,KAAK,KAAO,KAAK,KAAW,GAClD,OAAO;CAET,IAAM,IAAS,IAAM,GACf,IAAS,SAAS,cAAc,QAAQ;CAI9C,OAHA,EAAO,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,EAAK,QAAQ,CAAM,CAAC,GAC1D,EAAO,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,EAAK,SAAS,CAAM,CAAC,GAC5D,EAAO,WAAW,IAAI,CAAC,EAAE,UAAU,GAA6B,GAAG,GAAG,EAAO,OAAO,EAAO,MAAM,GAC1F;AACT;;;AC5BA,IAAM,IAAY,+TAcZ,IAAY,ghBAmBZ,IAAY,8uCAiCZ,IAAY,upEA2CZ,IAAY,IACZ,IAAY,IAMZ,IAAuB,IACvB,IAAuB,KACvB,IAAqB,KACrB,IAA0B,MAAM,OAAO;AA0B7C,SAAS,EAAU,GAAgC;CAEjD,OADc,EAAQ,SAAS,SAAW,EAAQ,UAAU,QACV,IAAnC,EAAQ,UAAU,OAAY,IAAL;AAC1C;AASA,IAAa,IAAb,MAA+C;CAC7C,KAAiC;CACjC,KAAoC;CACpC,KAAqC;CAErC,KAA6B;CAC7B,KAA6B;CAC7B,KAA0C;CAC1C,KAA0C;CAC1C,KAA8B;CAC9B,KAA+B;CAC/B,KAAY;CACZ,KAAkB,EAAe,GAAW,CAAS;CAErD,qBAAY,IAAI,IAA0D;CAC1E,KAAc;CACd,KAAiC;CAGjC,KAAkB;CAElB,KAAsD,CAAC;CACvD,KAAsD,CAAC;CAEvD,KAAQ;EAAE,OAAO;EAAG,IAAI;EAAG,IAAI;CAAE;CACjC,KAAO;CACP,KAAiC;CACjC,KAAQ;CACR,KAA0B;CAC1B,KAAS;CACT,KAAwB,EAAc;CACtC,KAAsB,EAAE,GAAG,GAAI;CAG/B,KAAU;CACV,KAAiB;CACjB,KAAe;CAGf,KAAc;CAGd,KAAa;CACb,KAA2B;CAC3B,KAAS;CACT,KAAS;CAET,KAAe;CACf,KAAc;CACd,KAAsD;CACtD,KAAqC;CAErC,MAAM,GAAuC;EAC3C,KAAKA,KAAa;EAClB,IAAM,IAAM,EAAU,eAChB,IAAS,EAAI,cAAc,QAAQ;EAYzC,OAXA,EAAO,MAAM,UAAU,yCACvB,EAAU,OAAO,CAAM,GACvB,KAAKC,KAAU,GAEf,EAAO,iBAAiB,oBAAoB,KAAKC,EAA+B,GAChF,EAAO,iBAAiB,wBAAwB,KAAKC,EAAmC,GACxF,EAAI,iBAAiB,oBAAoB,KAAKC,EAAmB,GAEjE,KAAKC,KAAM,EAAgB,CAAM,GACjC,KAAKC,KAAkB,KAAKD,GAAI,aAAa,KAAKA,GAAI,gBAAgB,GACtE,KAAKE,GAAkB,GAChB,QAAQ,QAAQ;CACzB;CAEA,UAAgB;EACd,KAAKC,GAAmB;EACxB,IAAM,IAAS,KAAKP;EACpB,AAAI,MAAW,SACb,EAAO,oBAAoB,oBAAoB,KAAKC,EAA+B,GACnF,EAAO,oBAAoB,wBAAwB,KAAKC,EAAmC,GAC3F,EAAO,cAAc,oBAAoB,oBAAoB,KAAKC,EAAmB;EAEvF,IAAM,IAAK,KAAKC;EAChB,IAAI,MAAO,MAAM;GACf,KAAK,IAAM,KAAU,KAAKI,GAAU,OAAO,GAAG,EAAG,cAAc,EAAO,GAAG;GAMzE,AALI,KAAKC,OAAc,QAAM,EAAG,cAAc,KAAKA,EAAS,GACxD,KAAKC,OAAa,QAAM,EAAG,kBAAkB,KAAKA,EAAQ,GAC1D,KAAKC,OAAa,QAAM,EAAG,kBAAkB,KAAKA,EAAQ,GAC1D,KAAKC,OAAU,QAAM,EAAG,cAAc,KAAKA,EAAK,GAChD,KAAKC,OAAU,QAAM,EAAG,cAAc,KAAKA,EAAK,GACpD,EAAG,aAAa,oBAAoB,CAAC,EAAE,YAAY;EACrD;EASA,AARA,GAAQ,OAAO,GACf,KAAKL,GAAU,MAAM,GACrB,KAAKM,KAAc,GACnB,KAAKL,KAAY,MACjB,KAAKV,KAAa,MAClB,KAAKC,KAAU,MACf,KAAKI,KAAM,MACX,KAAKW,KAAW,MAChB,KAAKC,KAAQ;CACf;CAEA,QAAQ,GAA2B;EACjC,KAAKC,KAAgB;CACvB;CAEA,IAAI,iBAAqC;EACvC,OAAO,KAAKZ,MAAmB,KAAA;CACjC;CAEA,aAAa,GAAiB,GAAwB,GAA+B;EAMnF,AALA,KAAKU,KAAW,GAChB,KAAKG,KAAQ,GAAS,QAAQ,IAC1B,GAAS,QAAK,KAAKC,KAAO,EAAQ,MACtC,KAAKH,KAAQ,MACb,KAAKI,GAAa,GAAS,KAAKF,EAAK,GACrC,KAAKG,GAAQ;CACf;CAKA,GAAa,GAAwB,GAAqB;EACxD,IAAM,IAAI,EAAQ,QAAQ,EAAQ;EAClC,IAAI,KAAK,EAAE,QAAQ;GACjB,IAAM,IAAM,EAAE,QAAQ,EAAE;GAExB,AADA,KAAKC,KAAc,KAAO,IAAI,EAAY,GAAS,CAAI,IACnD,KAAKC,OAAe,MAAG,KAAKA,KAAa;EAC/C;CACF;CAIA,KAAsB;EACpB,OAAO,KAAKJ,OAAS,UAAU,KAAKI,KAAa,IAAI,KAAKA,KAAa,KAAKD;CAC9E;CAEA,UACE,GACA,GACA,GACA,GACM;EACN,IAAM,IAAO,GAAS,QAAQ;EAO9B,AANI,GAAS,QAAK,KAAKH,KAAO,EAAQ,MACtC,KAAKK,KAAS,GACV,GAAS,SAAM,KAAKC,KAAa,EAAY,EAAQ,IAAI,IACzD,GAAS,WAAQ,KAAKC,KAAU,EAAQ,SAC5C,KAAKC,KAAiB,IAAO,IAAI,EAAU,CAAI,GAC/C,KAAKC,KAAe,IAAO,IAAI,EAAU,CAAE,GAC3C,KAAKR,GAAa,GAAI,CAAI;EAG1B,IAAM,IAAW,EAAY,GAAM,CAAI,GACjC,IAAS,EAAY,GAAI,CAAI;EA8CnC,AA7CI,IACF,KAAKJ,KAAQ;GACX,WAAW;GACX,YAAY;GACZ,WAAW,EAAG,SAAS,EAAG;GAC1B,OAAO,EAAK,SAAS,EAAK;GAG1B,MAAM,EAAK,SAAS,EAAK;GACzB,eAAe;GACf,gBAAgB;GAChB,WAAW;GACX,UAAU;GACV,KAAK,MAAc,YAAY,IAAI;GACnC,MAAM;EACR,IACS,MAAc,YACvB,KAAKA,KAAQ;GACX,WAAW,EAAK;GAChB,YAAY,EAAG;GACf,WAAW;GACX,OAAO,EAAK;GACZ,MAAM,EAAG;GACT,eAAe,EAAS,CAAQ;GAChC,gBAAgB,EAAU,CAAM;GAChC,WAAW,EAAU,CAAQ;GAC7B,UAAU,EAAS,CAAM;GACzB,KAAK;GACL,MAAM;EACR,IAEA,KAAKA,KAAQ;GACX,WAAW,EAAG;GACd,YAAY,EAAK;GACjB,WAAW;GACX,OAAO,EAAK;GACZ,MAAM,EAAG;GACT,eAAe,EAAS,CAAM;GAC9B,gBAAgB,EAAU,CAAQ;GAClC,WAAW,EAAS,CAAQ;GAC5B,UAAU,EAAU,CAAM;GAC1B,KAAK;GACL,MAAM;EACR,GAEF,KAAKK,GAAQ;CACf;CAEA,gBAAgB,GAAW,GAAiC;EAE1D,AADA,KAAKG,KAAS,GACd,KAAKH,GAAQ;CACf;CAEA,iBAAiB,GAAe,GAAW,GAAiB;EAE1D,AADA,KAAKQ,KAAQ;GAAE;GAAO,IAAI;GAAG,IAAI;EAAE,GACnC,KAAKR,GAAQ;CACf;CAEA,UAAyB;EACvB,IAAM,IAAK,KAAKtB,IACV,IAAI,GAAI,eAAe,GACvB,IAAI,GAAI,gBAAgB,GACxB,IAAQ,KAAKuB,KAAc,GAC3B,IAAO,IAAQ,KAAKQ,GAAS,IAAI,KAAA,GACjC,IAAU,IAAQ,KAAKC,GAAY,IAAI,KAAA,GACvC,IAAW,KACZ,MAA2E;GAG1E,IAAM,IAAW,KAAKC,KAAU,KAAKC;GACrC,OAAO;IACL,IAAI,EAAQ,IAAI,KAAY,IAAQ;IACpC,GAAG,EAAQ,IAAI;IACf,OAAO,EAAQ,QAAQ;IACvB,QAAQ,EAAQ,SAAS;GAC3B;EACF,IACA,KAAA;EACJ,OAAO;GACL,gBAAgB;GAChB,iBAAiB;GACjB,WAAW,IAAI;GACf,YAAY;GACZ;GACA;GACA;GACA,iBAAiB,KAAKV,KAAa,KAAK,KAAKL,KAAQ,IAAI,KAAK,KAAKK,KAAa,KAAA;EAClF;CACF;CAGA,KAAoE;EAClE,IAAM,IAAK,KAAKxB,IACV,IAAK,GAAI,eAAe,GACxB,IAAK,GAAI,gBAAgB,GACzB,IAAS,KAAKmC,GAAY;EAChC,IAAI,KAAU,KAAK,KAAM,KAAK,KAAM,GAAG,OAAO;GAAE,GAAG;GAAG,GAAG;GAAG,OAAO;GAAI,QAAQ;EAAG;EAClF,IAAM,KAAM,KAAKhB,KAAQ,IAAI,KAAK,GAC9B,IAAK,GACL,IAAK;EAGT,OAFI,IAAK,IAAK,IAAI,IAAK,IAAK,IACvB,IAAK,IAAK,GACR;GAAE,IAAI,IAAK,KAAM;GAAG,IAAI,IAAK,KAAM;GAAG,OAAO;GAAI,QAAQ;EAAG;CACrE;CAIA,KAAuE;EACrE,IAAM,IAAI,KAAKY,GAAS;EAExB,OADa,CAAC,KAAKZ,MAAS,KAAKH,OAAa,QAAQ,EAAU,KAAKA,EAAQ,MAAM,IACrE;GAAE,GAAG,EAAE,IAAI,EAAE,QAAQ;GAAG,GAAG,EAAE;GAAG,OAAO,EAAE,QAAQ;GAAG,QAAQ,EAAE;EAAO,IAAI;CACzF;CAIA,MAAkB,MAAuB;EAGvC,AAFA,EAAM,eAAe,GACrB,KAAKoB,KAAe,IAChB,KAAKC,OAAkB,SACzB,KAAKA,KAAgB,iBAAiB,KAAKC,GAAO,GAAG,CAAkB;CAE3E;CAEA,WAAiC;EAI/B,AAHA,KAAK9B,GAAmB,GACxB,KAAK4B,KAAe,IACpB,KAAK7B,GAAkB,GACvB,KAAKe,GAAQ;CACf;CAEA,WAAkC;EAChC,AAAI,KAAKrB,IAAS,cAAc,oBAAoB,aAAW,KAAKqB,GAAQ;CAC9E;CAEA,KAAe;EACT,KAAKiB,OACT,KAAKA,KAAc,IACnB,KAAKrB,KAAgB;CACvB;CAEA,KAA2B;EACzB,AAAI,KAAKmB,OAAkB,SACzB,aAAa,KAAKA,EAAa,GAC/B,KAAKA,KAAgB;CAEzB;CAEA,KAA0B;EACxB,IAAM,IAAK,KAAKhC;EACZ,UAAO,MAGX;GADA,KAAKQ,KAAQ,EAAc,GAAI,GAAW,CAAS,GACnD,KAAKC,KAAQ,EAAc,GAAI,GAAW,CAAS;GACnD,KAAK,IAAM,KAAQ;IAAC;IAAa;IAAS;IAAS;IAAQ;IAAe;GAAO,GAC/E,KAAK0B,GAAO,KAAQ,EAAG,mBAAmB,KAAK3B,IAAO,CAAI;GAE5D,KAAK,IAAM,KAAQ;IAAC;IAAa;IAAS;IAAY;IAAQ;IAAU;IAAU;IAAS;IAAc;IAAa;IAAU;IAAU;GAAS,GACjJ,KAAK4B,GAAO,KAAQ,EAAG,mBAAmB,KAAK3B,IAAO,CAAI;GAgB5D,AAbA,KAAKH,KAAW,KAAK+B,GAAa,GAAI,KAAK7B,EAAK,GAChD,KAAK8B,GAAe,GAAI,KAAK7B,EAAK,GAElC,KAAKL,GAAU,MAAM,GACrB,KAAKM,KAAc,GACnB,KAAKL,KAAY,KAAKkC,IAAkB,CAAE,GAC1C,EAAG,WAAW,KAAK/B,EAAK,GACxB,EAAG,UAAU,KAAK2B,GAAO,QAAQ,MAAM,CAAC,GACxC,EAAG,WAAW,KAAK1B,EAAK,GACxB,EAAG,UAAU,KAAK2B,GAAO,UAAU,MAAM,CAAC,GAC1C,EAAG,UAAU,KAAKA,GAAO,SAAS,MAAM,CAAC,GACzC,EAAG,WAAW,GAAG,GAAG,GAAG,CAAC,GACxB,EAAG,OAAO,EAAG,KAAK,GAClB,EAAG,UAAU,EAAG,KAAK,EAAG,mBAAmB;EArBQ;CAsBrD;CAEA,GAAa,GAA4B,GAA+C;EACtF,IAAM,IAAM,EAAG,kBAAkB,GAC3B,IAAS,EAAG,aAAa;EAC/B,IAAI,MAAQ,QAAQ,MAAW,MAAM,MAAU,MAAM,0CAA0C;EAG/F,AAFA,EAAG,gBAAgB,CAAG,GACtB,EAAG,WAAW,EAAG,cAAc,CAAM,GACrC,EAAG,WAAW,EAAG,cAAc,IAAI,aAAa;GAAC;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;GAAG;EAAC,CAAC,GAAG,EAAG,WAAW;EACzF,IAAM,IAAQ,EAAG,kBAAkB,GAAS,OAAO;EAInD,OAHA,EAAG,wBAAwB,CAAK,GAChC,EAAG,oBAAoB,GAAO,GAAG,EAAG,OAAO,IAAO,GAAG,CAAC,GACtD,EAAG,gBAAgB,IAAI,GAChB;CACT;CAEA,GAAe,GAA4B,GAA6B;EACtE,IACM,IAAoB,CAAC;EAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAW,KAC7B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAW,KAAK;GAClC,IAAM,IAAI,IAAI,KAAK,GACb,IAAI,IAAI,GACR,IAAI,IAAI,IACR,IAAI,IAAI;GACd,EAAQ,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;EAC/B;EAEF,KAAKI,KAAY,EAAQ;EAEzB,IAAM,IAAM,EAAG,kBAAkB;EAEjC,AADA,KAAKC,KAAU,EAAG,aAAa,GAC/B,KAAKC,KAAW,EAAG,aAAa;EAChC,IAAM,IAAQ,EAAG,aAAa,GACxB,IAAS,EAAG,aAAa;EAC/B,IAAI,MAAQ,QAAQ,KAAKD,OAAY,QAAQ,KAAKC,OAAa,QAAQ,MAAU,QAAQ,MAAW,MAClG,MAAU,MAAM,+CAA+C;EAKjE,AAHA,EAAG,gBAAgB,CAAG,GAEtB,EAAG,WAAW,EAAG,cAAc,KAAKD,EAAO,GAC3C,EAAG,WAAW,EAAG,cAAc,KAAKE,GAAM,WAAW,EAAG,YAAY;EACpE,IAAM,IAAO,EAAG,kBAAkB,GAAS,MAAM;EAKjD,AAJA,EAAG,wBAAwB,CAAI,GAC/B,EAAG,oBAAoB,GAAM,GAAG,EAAG,OAAO,IAAO,GAAG,CAAC,GAErD,EAAG,WAAW,EAAG,cAAc,KAAKD,EAAQ,GAC5C,EAAG,WAAW,EAAG,cAAc,KAAKC,GAAM,SAAS,EAAG,YAAY;EAClE,IAAM,IAAU,EAAG,kBAAkB,GAAS,SAAS;EAKvD,AAJA,EAAG,wBAAwB,CAAO,GAClC,EAAG,oBAAoB,GAAS,GAAG,EAAG,OAAO,IAAO,GAAG,CAAC,GAExD,EAAG,WAAW,EAAG,cAAc,CAAK,GACpC,EAAG,WAAW,EAAG,cAAc,KAAKA,GAAM,KAAK,EAAG,WAAW;EAC7D,IAAM,IAAM,EAAG,kBAAkB,GAAS,KAAK;EAQ/C,AAPA,EAAG,wBAAwB,CAAG,GAC9B,EAAG,oBAAoB,GAAK,GAAG,EAAG,OAAO,IAAO,GAAG,CAAC,GAEpD,EAAG,WAAW,EAAG,sBAAsB,CAAM,GAC7C,EAAG,WAAW,EAAG,sBAAsB,IAAI,YAAY,CAAO,GAAG,EAAG,WAAW,GAE/E,EAAG,gBAAgB,IAAI,GACvB,KAAKpC,KAAW;CAClB;CAIA,KAAgB;EACd,IAAM,IAAK,KAAKP,IACV,IAAS,KAAKJ;EAChB,MAAO,QAAQ,MAAW,QAAQ,KAAKmC,MAAgB,EAAG,cAAc,MAC5E,KAAKa,IAAQ,GACb,EAAG,MAAM,EAAG,gBAAgB,GAC5B,KAAKC,GAAS,GACV,KAAKjC,OAAU,OACV,KAAKD,OAAa,QAAM,KAAKoC,GAAY,IADzB,KAAKD,GAAU;CAE1C;CAIA,KAAiB;EACf,IAAM,IAAK,KAAK9C,IACV,IAAS,KAAKJ,IACd,IAAK,EAAO,OACZ,IAAK,EAAO,QACZ,IAAS,KAAKkC,GAAY;EAChC,IAAI,IAAS,GAAG;GACd,IAAM,KAAc,KAAKhB,KAAQ,IAAI,KAAK;GAC1C,AAAI,IAAa,IAAK,KACpB,KAAKkC,KAAS,GACd,KAAKC,KAAS,KAAK,MAAM,IAAK,CAAU,MAExC,KAAKA,KAAS,GACd,KAAKD,KAAS,KAAK,MAAM,IAAK,CAAU;EAE5C,OAEE,AADA,KAAKA,KAAS,GACd,KAAKC,KAAS;EAEhB,IAAM,IAAK,KAAK,OAAO,IAAK,KAAKD,MAAU,CAAC,GACtC,IAAK,KAAK,OAAO,IAAK,KAAKC,MAAU,CAAC;EAC5C,EAAG,SAAS,GAAI,GAAI,KAAKD,IAAQ,KAAKC,EAAM;CAC9C;CAEA,KAAoB;EAClB,IAAM,IAAI,KAAKD,IACT,IAAI,KAAKC,IACT,IAAU,KAAKtC;EAGrB,IAFA,KAAKiB,KAAU,KAAKd,KAAQ,IAAI,EAAU,CAAO,KAAK,IAAI,IAC1D,KAAKoC,IAAS,GACV,KAAKpC,IACP,KAAKqC,IAAU;GAAE,GAAG;GAAG,GAAG;GAAG;GAAG;EAAE,GAAG,EAAQ,SAAS,EAAQ,IAAI;OAC7D;GAEL,IAAM,IAAO,EAAY,GAAS,EAAK;GAEvC,AADA,KAAKA,IAAU;IAAE,GAAG;IAAG,GAAG;IAAG,GAAG,IAAI;IAAG;GAAE,GAAG,EAAQ,MAAM,KAAQ,OAAe,EAAS,CAAI,CAAC,GAC/F,KAAKA,IAAU;IAAE,GAAG,IAAI;IAAG,GAAG;IAAG,GAAG,IAAI;IAAG;GAAE,GAAG,EAAQ,OAAO,EAAQ,OAAO,KAAK,GAAG,EAAU,CAAI,CAAC;EACvG;CACF;CAEA,KAAkB;EAChB,IAAM,IAAK,KAAKnD,IACV,IAAI,KAAKgD,IACT,IAAI,KAAKC,IACT,IAAO,KAAKrC;EAiBlB,IAbA,KAAKgB,KAAU,EAAK,OAChB,KACC,KAAKL,MAAkB,KAAKC,KAAe,KAAKD,MAAkB,KAAKH,OAAW,IAAI,IAG3F,KAAK8B,IAAS,GACV,EAAK,OACP,KAAKC,IAAU;GAAE,GAAG;GAAG,GAAG;GAAG;GAAG;EAAE,GAAG,EAAK,SAAS,KAEnD,KAAKA,IAAU;GAAE,GAAG;GAAG,GAAG;GAAG,GAAG,IAAI;GAAG;EAAE,GAAG,EAAK,WAAW,GAAG,EAAK,aAAa,GACjF,KAAKA,IAAU;GAAE,GAAG,IAAI;GAAG,GAAG;GAAG,GAAG,IAAI;GAAG;EAAE,GAAG,EAAK,YAAY,IAAI,EAAK,cAAc,IAGtF,EAAK,UAAU,QAAQ,EAAK,SAAS,MAAM;EAG/C,IAAM,IAAQ,EAAK,OAAO,IAAI,IAAI,GAC5B,IAAU,EAAK,OAAQ,EAAK,MAAM,IAAI,IAAI,IAAK,IAAI;EAiCzD,AAhCA,KAAK9B,GAAW,OAAO,KAAKsB,IAAO,GAAO,GAAG,KAAKvB,IAAQ;GACxD,GAAG,KAAKE,GAAQ;GAChB,MAAM,EAAK;EACb,CAAC,GACD,EAAG,WAAW,EAAG,cAAc,KAAKmB,EAAO,GAC3C,EAAG,cAAc,EAAG,cAAc,GAAG,KAAKE,GAAM,SAAS,GACzD,EAAG,WAAW,EAAG,cAAc,KAAKD,EAAQ,GAC5C,EAAG,cAAc,EAAG,cAAc,GAAG,KAAKC,GAAM,OAAO,GAEvD,KAAKS,IAAU,EAAG,UAAU,EAAK,KAAK,GACtC,KAAKA,IAAU,EAAG,UAAU,EAAK,IAAI,GACrC,EAAG,cAAc,EAAG,QAAQ,GAE5B,EAAG,WAAW,KAAK3C,EAAK,GACxB,EAAG,gBAAgB,KAAKF,EAAQ,GAChC,EAAG,UAAU,KAAK6B,GAAO,aAAa,MAAM,GAAG,CAAC,GAChD,EAAG,UACD,KAAKA,GAAO,SAAS,MACrB,KAAKX,GAAM,OACX,KAAKA,GAAM,KAAK,KAAKI,KAAO,KAAKD,IACjC,KAAKH,GAAM,KAAK,KAAKI,EACvB,GACA,EAAG,UAAU,KAAKO,GAAO,YAAY,MAAM,CAAO,GAClD,EAAG,UAAU,KAAKA,GAAO,QAAQ,MAAM,EAAK,GAAG,GAC/C,EAAG,UAAU,KAAKA,GAAO,UAAU,MAAM,CAAK,GAC9C,EAAG,UAAU,KAAKA,GAAO,cAAc,MAAM,EAAK,UAAU,IAAI,EAAK,UAAU,EAAE,GACjF,EAAG,UAAU,KAAKA,GAAO,aAAa,MAAM,EAAK,SAAS,IAAI,EAAK,SAAS,EAAE,GAC9E,EAAG,UAAU,KAAKA,GAAO,UAAU,MAAM,KAAKf,GAAW,UAAU,KAAQ,IAAI,CAAC,GAChF,EAAG,UAAU,KAAKe,GAAO,UAAU,MAAM,EAAK,OAAO,KAAKiB,GAAU,IAAI,CAAC,GAGzE,EAAG,UAAU,KAAKjB,GAAO,WAAW,MAAM,EAAK,OAAO,KAAK,IAAI,GAAG,KAAKhB,KAAS,EAAG,IAAI,CAAC,GACxF,EAAG,aAAa,EAAG,WAAW,KAAKoB,IAAW,EAAG,gBAAgB,CAAC;CACpE;CAKA,KAAoB;EAClB,IAAM,IAAQ,KAAKnB,GAAW,OAAO,IAAuB,GACtD,KAAQ,KAAKD,KAAS,MAAU,IAAI;EAI1C,OAHI,KAAQ,IAAU,IAClB,KAAQ,IAAU,KAEN,IAAI,MAAM;CAC5B;CAEA,MAAiB;EACf,IAAM,IAAK,KAAKpB;EAKhB,AAJA,EAAG,WAAW,KAAKQ,EAAK,GACxB,EAAG,gBAAgB,KAAKF,EAAQ,GAChC,EAAG,cAAc,EAAG,QAAQ,GAC5B,EAAG,UAAU,KAAK6B,GAAO,aAAa,MAAM,KAAKa,IAAQ,KAAKC,EAAM,GACpE,EAAG,UACD,KAAKd,GAAO,SAAS,MACrB,KAAKV,GAAM,OACX,KAAKA,GAAM,KAAK,KAAKI,KAAO,KAAKD,IACjC,KAAKH,GAAM,KAAK,KAAKI,EACvB;CACF;CAEA,IAAU,GAAY,GAA+B,IAAa,GAAG,IAAiB,GAAiB;EACrG,IAAM,IAAK,KAAK7B;EACZ,MAAW,SACf,KAAKoD,IAAU,EAAG,UAAU,CAAM,GAClC,EAAG,UAAU,KAAKjB,GAAO,SAAS,MAAM,EAAK,GAAG,EAAK,GAAG,EAAK,GAAG,EAAK,CAAC,GACtE,EAAG,UAAU,KAAKA,GAAO,eAAe,MAAM,CAAU,GACxD,EAAG,UAAU,KAAKA,GAAO,SAAS,MAAM,EAAK,IAAI,EAAK,EAAE,GACxD,EAAG,WAAW,EAAG,gBAAgB,GAAG,CAAC;CACvC;CAEA,IAAU,GAAc,GAAqC;EAC3D,IAAM,IAAK,KAAKnC;EAEhB,AADA,EAAG,cAAc,CAAI,GACrB,EAAG,YAAY,EAAG,YAAY,MAAW,OAAkC,KAAKK,KAAhC,KAAKiD,IAAY,CAAM,CAAkB;CAC3F;CAEA,IAAY,GAAsC;EAChD,IAAM,IAAK,KAAKtD,IACV,IAAW,KAAKI,GAAU,IAAI,CAAM;EAC1C,IAAI,MAAa,KAAA,GAGf,OAFA,KAAKA,GAAU,OAAO,CAAM,GAC5B,KAAKA,GAAU,IAAI,GAAQ,CAAQ,GAC5B,EAAS;EAElB,IAAM,IAAM,EAAc,CAAE,GAEtB,IAAW,EAAc,GAAI,GAAK,CAAM,GACxC,IAAQ,EAAS,QAAQ,EAAS,SAAS;EAIjD,OAHA,KAAKA,GAAU,IAAI,GAAQ;GAAE;GAAK;EAAM,CAAC,GACzC,KAAKM,MAAe,GACpB,KAAK6C,IAAe,GACb;CACT;CAEA,MAAuB;EACrB,IAAM,IAAK,KAAKvD;EAChB,KAAK,IAAM,CAAC,GAAK,MAAW,KAAKI,IAAW;GAC1C,IAAI,KAAKM,MAAe,KAA2B,KAAKN,GAAU,QAAQ,GAAG;GAG7E,AAFA,KAAKA,GAAU,OAAO,CAAG,GACzB,KAAKM,MAAe,EAAO,OAC3B,EAAG,cAAc,EAAO,GAAG;EAC7B;CACF;CAEA,IAAkB,GAA0C;EAC1D,IAAM,IAAM,EAAc,CAAE;EAG5B,OAFA,EAAG,YAAY,EAAG,YAAY,CAAG,GACjC,EAAG,WAAW,EAAG,YAAY,GAAG,EAAG,MAAM,GAAG,GAAG,GAAG,EAAG,MAAM,EAAG,eAAe,IAAI,WAAW;GAAC;GAAG;GAAG;GAAG;EAAC,CAAC,CAAC,GAClG;CACT;CAEA,MAAgB;EACd,IAAM,IAAK,KAAKV,IACV,IAAS,KAAKJ,IACd,IAAK,KAAKD;EAChB,IAAI,MAAO,QAAQ,MAAW,QAAQ,MAAO,MAAM;EACnD,KAAKkC,KAAO,OAAO,oBAAqB,WAAW,mBAAmB;EACtE,IAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,EAAG,cAAc,KAAKA,EAAI,CAAC,GACtD,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,EAAG,eAAe,KAAKA,EAAI,CAAC;EAC7D,CAAI,EAAO,UAAU,KAAK,EAAO,WAAW,OAC1C,EAAO,QAAQ,GACf,EAAO,SAAS,GAChB,EAAG,SAAS,GAAG,GAAG,GAAG,CAAC;CAE1B;AACF"}
|
package/dist/zine.d.ts
CHANGED
|
@@ -72,6 +72,12 @@ export interface ZineOptions {
|
|
|
72
72
|
* centers it (a small margin shows); 'fill' stretches it to the book's shape. Either way the
|
|
73
73
|
* book keeps one shape for the whole document, so mixed-size pages never cause a layout shift. */
|
|
74
74
|
fit?: 'contain' | 'fill';
|
|
75
|
+
/** Hide a print-gutter duplicate: print PDFs sometimes repeat a strip of artwork on both sides
|
|
76
|
+
* of the spine so nothing is lost in the binding, which shows twice when the spread lies flat.
|
|
77
|
+
* A number is the overlap as a fraction of page width (e.g. 0.02 = 2%), removed from every
|
|
78
|
+
* two-page spread, half from each page's spine edge. 'auto' finds the repeat per spread and only
|
|
79
|
+
* trims spreads that have one. Lone pages are never trimmed. Default 0 (off). */
|
|
80
|
+
gutterOverlap?: 'auto' | number;
|
|
75
81
|
/** Override any reader-facing text (control labels, screen-reader announcements, panel and
|
|
76
82
|
* loading text) for localization. English by default; pass only the keys you want to change.
|
|
77
83
|
* Interpolated entries (e.g. the page announcement) are functions. See {@link ZineStrings}. */
|
package/options.schema.json
CHANGED
|
@@ -89,6 +89,14 @@
|
|
|
89
89
|
"default": "contain",
|
|
90
90
|
"description": "How a page whose size differs from the book fits: contain keeps its aspect and centers it (a margin shows); fill stretches it. The book keeps one shape document-wide, so mixed-size pages never shift layout."
|
|
91
91
|
},
|
|
92
|
+
"gutterOverlap": {
|
|
93
|
+
"oneOf": [
|
|
94
|
+
{ "const": "auto" },
|
|
95
|
+
{ "type": "number", "minimum": 0, "maximum": 0.5 }
|
|
96
|
+
],
|
|
97
|
+
"default": 0,
|
|
98
|
+
"description": "Hide a print-gutter duplicate (artwork repeated on both sides of the spine for binding). A number is the overlap as a fraction of page width, removed from every two-page spread, half from each page. 'auto' finds the repeat per spread and only trims spreads that have one. Lone pages are never trimmed. 0 = off."
|
|
99
|
+
},
|
|
92
100
|
"strings": {
|
|
93
101
|
"type": "object",
|
|
94
102
|
"description": "Override any reader-facing text (control labels, screen-reader announcements, panel and loading text) for localization. English by default; pass only the keys to change. Interpolated entries are functions, so this is set in code, not JSON."
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cssRenderer-CTaM4cAj.js","names":["#container","#clip","#viewport","#book","#pageLeft","#makeCanvas","#pageRight","#leaf","#leafFront","#leafBack","#leafShadow","#paint","#fill","#fit","#contentShift","#trackAspect","#layoutBook","#applyShift","#fromShiftUnit","#toShiftUnit","#bookW","#pageAspect","#docAspect","#bookBox","#drawAspect","#contentBox"],"sources":["../src/geometry/flipProgressToPose.ts","../src/renderer/cssRenderer.ts"],"sourcesContent":["export interface FlipPose {\n /** Turning-page rotation about the spine, radians: 0 (flat/open) → π (fully turned). */\n angle: number;\n /** Mesh cylinder-wrap curl, 0..1 — flat at the ends, peaks edge-on at t=0.5. CSS renderer ignores it. */\n curl: number;\n /** Fold-shadow intensity, 0..1 — peaks edge-on at t=0.5. */\n shadowAlpha: number;\n}\n\n/**\n * Map a 0→1 flip progress to a renderer-neutral fold pose.\n *\n * Linear in `t` on purpose — easing belongs to the animator that produces `t`,\n * so seeking to a fixed progress (e.g. the 0/.25/.5/.75/1 visual-regression stops)\n * is deterministic. The CSS renderer consumes `angle` as a spine `rotateY`; the\n * WebGL2 renderer additionally bends its mesh by `curl`.\n */\nexport function flipProgressToPose(t: number): FlipPose {\n const clamped = t < 0 ? 0 : t > 1 ? 1 : t;\n const arc = Math.sin(clamped * Math.PI);\n return {\n angle: clamped * Math.PI,\n curl: arc,\n shadowAlpha: arc,\n };\n}\n","import { flipProgressToPose } from '../geometry/flipProgressToPose';\nimport type { Spread } from '../engine/spread';\nimport type {\n FlipDirection,\n LayoutMetrics,\n PageContent,\n Renderer,\n RenderOptions,\n SpreadContent,\n} from './types';\n\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * Layered gradients painted over the turning leaf to fake paper depth on the flat\n * fold: a crease shadow at the spine, a soft curve sheen, and a free-edge shade.\n * The spine is the leaf's pivot edge — left for a forward flip, right for backward.\n */\nfunction foldOverlay(direction: FlipDirection): string {\n const dir = direction === 'forward' ? 'to right' : 'to left';\n return [\n `linear-gradient(${dir}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,\n `linear-gradient(${dir}, transparent 40%, rgba(255,255,255,0.32) 66%, transparent 90%)`,\n `linear-gradient(${dir}, transparent 66%, rgba(0,0,0,0.35))`,\n ].join(',');\n}\n\n/**\n * Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages\n * are <canvas> elements; the turning leaf (front + back faces, backface-hidden)\n * rotates about the spine driven by flipProgressToPose(), with a gradient shadow\n * overlay for depth. `beginFlip` stages the surfaces; `setFlipProgress` animates.\n */\nexport class CssRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #clip!: HTMLDivElement;\n #viewport!: HTMLDivElement;\n #book!: HTMLDivElement;\n #pageLeft!: HTMLCanvasElement;\n #pageRight!: HTMLCanvasElement;\n #leaf!: HTMLDivElement;\n #leafFront!: HTMLCanvasElement;\n #leafBack!: HTMLCanvasElement;\n #leafShadow!: HTMLDivElement;\n // Lone-page centering, interpolated across a flip (see the WebGL renderer).\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the spread is letterboxed to the page aspect (see the WebGL renderer).\n #pageAspect = 0;\n // First page's aspect, latched: the container is sized to this document-wide so off-size pages\n // do not resize it. `contain` letterboxes them within it; `fill` stretches them to it.\n #docAspect = 0;\n #fit: 'contain' | 'fill' = 'contain';\n #bookW = 0;\n #fill = false;\n #contentShift = 0; // shiftUnit of the current static spread (nonzero = lone page, centered)\n\n mount(container: HTMLElement): Promise<void> {\n const doc = container.ownerDocument;\n this.#container = container;\n\n // Fixed-size clip box; NOT transformed, so zoom can't scale the crop region.\n this.#clip = doc.createElement('div');\n this.#clip.className = 'zine-clip';\n this.#clip.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;';\n\n // Transform target for zoom/pan; lives inside the clip.\n this.#viewport = doc.createElement('div');\n this.#viewport.className = 'zine-viewport';\n this.#viewport.style.cssText = 'position:absolute;inset:0;transform-origin:0 0;';\n\n this.#book = doc.createElement('div');\n this.#book.className = 'zine-book';\n this.#book.style.cssText =\n 'position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;';\n\n this.#pageLeft = this.#makeCanvas(doc, 'zine-page-left', 'left:0;');\n this.#pageRight = this.#makeCanvas(doc, 'zine-page-right', 'right:0;');\n\n this.#leaf = doc.createElement('div');\n this.#leaf.className = 'zine-leaf';\n this.#leaf.style.cssText =\n 'position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;';\n\n this.#leafFront = doc.createElement('canvas');\n this.#leafFront.className = 'zine-leaf-front';\n this.#leafFront.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;';\n\n this.#leafBack = doc.createElement('canvas');\n this.#leafBack.className = 'zine-leaf-back';\n this.#leafBack.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);';\n\n this.#leafShadow = doc.createElement('div');\n this.#leafShadow.className = 'zine-leaf-shadow';\n this.#leafShadow.style.cssText = 'position:absolute;inset:0;opacity:0;pointer-events:none;';\n this.#leafShadow.style.background = foldOverlay('forward'); // default; beginFlip sets per direction\n\n this.#leaf.append(this.#leafFront, this.#leafBack, this.#leafShadow);\n this.#book.append(this.#pageLeft, this.#pageRight, this.#leaf);\n this.#viewport.append(this.#book);\n this.#clip.append(this.#viewport);\n container.append(this.#clip);\n\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clip?.remove();\n this.#container = null;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n if (options?.fill) {\n // Single-page mode: the lone page fills the container; hide the other panel.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#paint(this.#pageLeft, content.right ?? content.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#paint(this.#pageLeft, content.left);\n this.#paint(this.#pageRight, content.right);\n }\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#contentShift = this.#fill ? 0 : shiftUnit(content);\n this.#trackAspect(content);\n this.#layoutBook();\n this.#applyShift(this.#contentShift);\n // A fresh spread cancels any in-progress flip.\n this.#leaf.style.display = 'none';\n this.#leaf.style.transform = '';\n this.#leafShadow.style.opacity = '0';\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n if (options?.fill) {\n // Single-page: a full-width leaf turns about one edge, revealing `to` underneath.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#leaf.style.width = '100%';\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = direction === 'forward' ? 'left center' : 'right center';\n this.#paint(this.#pageLeft, to.right ?? to.left);\n this.#paint(this.#leafFront, from.right ?? from.left);\n this.#paint(this.#leafBack, to.right ?? to.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#leaf.style.width = '50%';\n if (direction === 'forward') {\n // Right page lifts and swings left about the spine. Left stays; right reveals `to`.\n this.#paint(this.#pageLeft, from.left);\n this.#paint(this.#pageRight, to.right);\n this.#paint(this.#leafFront, from.right);\n this.#paint(this.#leafBack, to.left);\n this.#leaf.style.left = '50%';\n this.#leaf.style.transformOrigin = 'left center';\n } else {\n // Left page swings right about its right edge. Right stays; left reveals `to`.\n this.#paint(this.#pageRight, from.right);\n this.#paint(this.#pageLeft, to.left);\n this.#paint(this.#leafFront, from.left);\n this.#paint(this.#leafBack, to.right);\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = 'right center';\n }\n }\n this.#fromShiftUnit = options?.fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = options?.fill ? 0 : shiftUnit(to);\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#trackAspect(to);\n this.#layoutBook();\n this.#applyShift(this.#fromShiftUnit);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = 'rotateY(0deg)';\n this.#leafShadow.style.background = foldOverlay(direction);\n this.#leafShadow.style.opacity = '0';\n }\n\n setFlipProgress(t: number, direction: FlipDirection): void {\n const pose = flipProgressToPose(t);\n const deg = pose.angle * RAD_TO_DEG;\n this.#applyShift(this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * t);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = `rotateY(${direction === 'forward' ? -deg : deg}deg)`;\n this.#leafShadow.style.opacity = String(pose.shadowAlpha);\n }\n\n #applyShift(unit: number): void {\n const px = unit * (this.#bookW / 4);\n this.#book.style.transform = px ? `translateX(${px}px)` : '';\n }\n\n /** Remember the current spread's page aspect (for drawing) and latch the first page's aspect (for\n * the container). The container follows #docAspect so off-size pages do not resize it. */\n #trackAspect(content: SpreadContent): void {\n const p = content.left ?? content.right;\n if (p && p.height) {\n this.#pageAspect = p.width / p.height;\n if (this.#docAspect === 0) this.#docAspect = this.#pageAspect;\n }\n }\n\n /** Aspect the book is drawn at: the current page under `contain` (letterboxed within the stable\n * container), or the document aspect under `fill` (fills it, stretching off-size pages). */\n #drawAspect(): number {\n return this.#fit === 'fill' && this.#docAspect > 0 ? this.#docAspect : this.#pageAspect;\n }\n\n /** Letterbox the 2-page book to the page aspect, centered in the container, so pages\n * fill their halves without stretching; the surround is the container background. */\n #layoutBook(): void {\n const b = this.#bookBox();\n this.#bookW = b.width;\n const s = this.#book.style;\n s.inset = 'auto';\n s.left = `${b.x}px`;\n s.top = `${b.y}px`;\n s.width = `${b.width}px`;\n s.height = `${b.height}px`;\n }\n\n /** The fitted book rect in container px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const cw = this.#container?.clientWidth ?? 0;\n const ch = this.#container?.clientHeight ?? 0;\n const aspect = this.#drawAspect();\n if (aspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * aspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors #applyShift). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n return this.#contentShift !== 0 ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#viewport.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n // The shift lives on #book, inside the scaled #viewport, so unlike the WebGL2 renderer it\n // magnifies along with everything else and the whole box simply scales.\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => ({\n x: content.x * scale,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n })\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n containerAspect: this.#docAspect > 0 ? (this.#fill ? 1 : 2) * this.#docAspect : undefined,\n };\n }\n\n #makeCanvas(doc: Document, className: string, extra: string): HTMLCanvasElement {\n const c = doc.createElement('canvas');\n c.className = className;\n c.style.cssText = `position:absolute;top:0;width:50%;height:100%;${extra}`;\n return c;\n }\n\n #paint(canvas: HTMLCanvasElement, content: PageContent | null): void {\n const ctx = canvas.getContext('2d');\n if (content === null) {\n ctx?.clearRect(0, 0, canvas.width, canvas.height);\n return;\n }\n canvas.width = content.width;\n canvas.height = content.height;\n ctx?.drawImage(content, 0, 0);\n }\n}\n"],"mappings":"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"}
|