@zinejs/core 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +27 -49
  2. package/dist/{bundled-Cp43MIt0.js → bundled-ESapkNsS.js} +34 -34
  3. package/dist/bundled-ESapkNsS.js.map +1 -0
  4. package/dist/controls/contextMenu.d.ts +16 -0
  5. package/dist/controls/controls.d.ts +10 -3
  6. package/dist/controls/icons.d.ts +2 -2
  7. package/dist/controls/menu.d.ts +68 -0
  8. package/dist/controls/search.d.ts +15 -2
  9. package/dist/controls/styles.d.ts +1 -1
  10. package/dist/controls/toolbar.d.ts +1 -1
  11. package/dist/controls/types.d.ts +13 -2
  12. package/dist/{controls-Djjk8Yac.js → controls-CcOu_19g.js} +355 -199
  13. package/dist/controls-CcOu_19g.js.map +1 -0
  14. package/dist/curls.js +1 -1
  15. package/dist/geometry/curls/cone.d.ts +4 -3
  16. package/dist/index.js +644 -450
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.umd.js +61 -13
  19. package/dist/index.umd.js.map +1 -1
  20. package/dist/loading/loading.d.ts +1 -1
  21. package/dist/{loading-C0qbANt2.js → loading-DF88K9yJ.js} +2 -2
  22. package/dist/loading-DF88K9yJ.js.map +1 -0
  23. package/dist/renderer/gl/texture.d.ts +13 -2
  24. package/dist/renderer/types.d.ts +6 -0
  25. package/dist/renderer/webglRenderer.d.ts +1 -0
  26. package/dist/{share-DXAfK9rJ.js → share-BrQa6aYm.js} +2 -2
  27. package/dist/{share-DXAfK9rJ.js.map → share-BrQa6aYm.js.map} +1 -1
  28. package/dist/source/types.d.ts +5 -0
  29. package/dist/styles-D_zOGk2q.js +40 -0
  30. package/dist/styles-D_zOGk2q.js.map +1 -0
  31. package/dist/{webglRenderer-nGRruZk2.js → webglRenderer-CmQ_mUrX.js} +113 -101
  32. package/dist/webglRenderer-CmQ_mUrX.js.map +1 -0
  33. package/dist/zine.d.ts +52 -1
  34. package/options.schema.json +49 -0
  35. package/package.json +2 -2
  36. package/dist/bundled-Cp43MIt0.js.map +0 -1
  37. package/dist/controls-Djjk8Yac.js.map +0 -1
  38. package/dist/loading-C0qbANt2.js.map +0 -1
  39. package/dist/styles-CgCzjFUk.js +0 -40
  40. package/dist/styles-CgCzjFUk.js.map +0 -1
  41. package/dist/webglRenderer-nGRruZk2.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { n as e } from "./types-DEz3mRLI.js";
2
- import { o as t, r as n, t as r } from "./bundled-Cp43MIt0.js";
2
+ import { o as t, r as n, t as r } from "./bundled-ESapkNsS.js";
3
3
  //#region src/renderer/gl/context.ts
4
4
  function i(e) {
5
5
  let t = e.getContext("webgl2", {
@@ -42,15 +42,25 @@ function s(e) {
42
42
  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
43
  }
44
44
  function c(e, t, n) {
45
- 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, n);
45
+ let r = l(n, e.getParameter(e.MAX_TEXTURE_SIZE));
46
+ 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
+ width: r.width,
48
+ height: r.height
49
+ };
50
+ }
51
+ function l(e, t) {
52
+ let n = e, r = Math.max(n.width, n.height);
53
+ if (!Number.isFinite(t) || t <= 0 || r <= t) return n;
54
+ let i = t / r, a = document.createElement("canvas");
55
+ return a.width = Math.max(1, Math.floor(n.width * i)), a.height = Math.max(1, Math.floor(n.height * i)), a.getContext("2d")?.drawImage(e, 0, 0, a.width, a.height), a;
46
56
  }
47
57
  //#endregion
48
58
  //#region src/renderer/webglRenderer.ts
49
- var l = "#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}", u = "#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\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vUv);\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}", d = "#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}", f = "#version 300 es\nprecision mediump float;\nin vec2 vUv;\nin float vFacing;\nin float vU;\nuniform sampler2D uFront;\nuniform sampler2D uBack;\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 ? texture(uFront, vec2(fx, vUv.y)) : texture(uBack, vec2(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}", p = 28, m = 36, h = .7, g = .8, _ = 4e3, v = 256 * 1024 * 1024;
50
- function y(e) {
59
+ var u = "#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}", d = "#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\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vUv);\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}", f = "#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}", p = "#version 300 es\nprecision mediump float;\nin vec2 vUv;\nin float vFacing;\nin float vU;\nuniform sampler2D uFront;\nuniform sampler2D uBack;\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 ? texture(uFront, vec2(fx, vUv.y)) : texture(uBack, vec2(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}", m = 28, h = 36, g = .7, _ = .78, v = 4e3, y = 256 * 1024 * 1024;
60
+ function b(e) {
51
61
  return e.left === null == (e.right === null) ? 0 : e.right === null ? 1 : -1;
52
62
  }
53
- var b = class {
63
+ var x = class {
54
64
  #e = null;
55
65
  #t = null;
56
66
  #n = null;
@@ -61,63 +71,67 @@ var b = class {
61
71
  #s = null;
62
72
  #c = null;
63
73
  #l = 0;
64
- #u = t(p, m);
74
+ #u = t(m, h);
65
75
  #d = /* @__PURE__ */ new Map();
66
76
  #f = 0;
67
77
  #p = null;
68
- #m = {};
78
+ #m = 0;
69
79
  #h = {};
70
- #g = {
80
+ #g = {};
81
+ #_ = {
71
82
  scale: 1,
72
83
  tx: 0,
73
84
  ty: 0
74
85
  };
75
- #_ = 1;
76
- #v = null;
77
- #y = !1;
78
- #b = null;
79
- #x = 0;
80
- #S = r[e];
81
- #C = { y: .5 };
82
- #w = 0;
86
+ #v = 1;
87
+ #y = null;
88
+ #b = !1;
89
+ #x = null;
90
+ #S = 0;
91
+ #C = r[e];
92
+ #w = { y: .5 };
83
93
  #T = 0;
84
94
  #E = 0;
85
95
  #D = 0;
86
96
  #O = 0;
87
97
  #k = 0;
88
- #A = !1;
98
+ #A = 0;
89
99
  #j = !1;
90
- #M = null;
100
+ #M = !1;
91
101
  #N = null;
102
+ #P = null;
92
103
  mount(e) {
93
104
  this.#e = e;
94
105
  let t = e.ownerDocument, n = t.createElement("canvas");
95
- return n.style.cssText = "display:block;width:100%;height:100%;", e.append(n), this.#t = n, n.addEventListener("webglcontextlost", this.#L), n.addEventListener("webglcontextrestored", this.#R), t.addEventListener("visibilitychange", this.#z), this.#n = i(n), this.#H(), Promise.resolve();
106
+ return n.style.cssText = "display:block;width:100%;height:100%;", e.append(n), this.#t = n, n.addEventListener("webglcontextlost", this.#R), n.addEventListener("webglcontextrestored", this.#z), t.addEventListener("visibilitychange", this.#B), this.#n = i(n), this.#m = this.#n.getParameter(this.#n.MAX_TEXTURE_SIZE), this.#U(), Promise.resolve();
96
107
  }
97
108
  destroy() {
98
- this.#V();
109
+ this.#H();
99
110
  let e = this.#t;
100
- e !== null && (e.removeEventListener("webglcontextlost", this.#L), e.removeEventListener("webglcontextrestored", this.#R), e.ownerDocument.removeEventListener("visibilitychange", this.#z));
111
+ e !== null && (e.removeEventListener("webglcontextlost", this.#R), e.removeEventListener("webglcontextrestored", this.#z), e.ownerDocument.removeEventListener("visibilitychange", this.#B));
101
112
  let t = this.#n;
102
113
  if (t !== null) {
103
114
  for (let e of this.#d.values()) t.deleteTexture(e.tex);
104
115
  this.#p !== null && t.deleteTexture(this.#p), this.#a !== null && t.deleteVertexArray(this.#a), this.#o !== null && t.deleteVertexArray(this.#o), this.#r !== null && t.deleteProgram(this.#r), this.#i !== null && t.deleteProgram(this.#i), t.getExtension("WEBGL_lose_context")?.loseContext();
105
116
  }
106
- e?.remove(), this.#d.clear(), this.#f = 0, this.#p = null, this.#e = null, this.#t = null, this.#n = null, this.#v = null, this.#b = null;
117
+ e?.remove(), this.#d.clear(), this.#f = 0, this.#p = null, this.#e = null, this.#t = null, this.#n = null, this.#y = null, this.#x = null;
107
118
  }
108
119
  onFatal(e) {
109
- this.#N = e;
120
+ this.#P = e;
121
+ }
122
+ get maxTextureSize() {
123
+ return this.#m || void 0;
110
124
  }
111
125
  renderSpread(e, t, n) {
112
- this.#v = t, this.#y = n?.fill ?? !1, this.#b = null, this.#P(t), this.#G();
126
+ this.#y = t, this.#b = n?.fill ?? !1, this.#x = null, this.#F(t), this.#K();
113
127
  }
114
- #P(e) {
128
+ #F(e) {
115
129
  let t = e.left ?? e.right;
116
- t && t.height && (this.#D = t.width / t.height);
130
+ t && t.height && (this.#O = t.width / t.height);
117
131
  }
118
132
  beginFlip(e, t, r, i) {
119
133
  let a = i?.fill ?? !1;
120
- this.#x = 0, i?.curl && (this.#S = n(i.curl)), i?.anchor && (this.#C = i.anchor), this.#T = a ? 0 : y(e), this.#E = a ? 0 : y(t), this.#P(t), a ? this.#b = {
134
+ this.#S = 0, i?.curl && (this.#C = n(i.curl)), i?.anchor && (this.#w = i.anchor), this.#E = a ? 0 : b(e), this.#D = a ? 0 : b(t), this.#F(t), a ? this.#x = {
121
135
  underLeft: null,
122
136
  underRight: null,
123
137
  underFull: t.right ?? t.left,
@@ -125,7 +139,7 @@ var b = class {
125
139
  back: e.right ?? e.left,
126
140
  dir: r === "forward" ? 1 : -1,
127
141
  fill: !0
128
- } : r === "forward" ? this.#b = {
142
+ } : r === "forward" ? this.#x = {
129
143
  underLeft: e.left,
130
144
  underRight: t.right,
131
145
  underFull: null,
@@ -133,7 +147,7 @@ var b = class {
133
147
  back: t.left,
134
148
  dir: 1,
135
149
  fill: !1
136
- } : this.#b = {
150
+ } : this.#x = {
137
151
  underLeft: t.left,
138
152
  underRight: e.right,
139
153
  underFull: null,
@@ -141,21 +155,21 @@ var b = class {
141
155
  back: t.right,
142
156
  dir: -1,
143
157
  fill: !1
144
- }, this.#G();
158
+ }, this.#K();
145
159
  }
146
160
  setFlipProgress(e, t) {
147
- this.#x = e, this.#G();
161
+ this.#S = e, this.#K();
148
162
  }
149
163
  setViewTransform(e, t, n) {
150
- this.#g = {
164
+ this.#_ = {
151
165
  scale: e,
152
166
  tx: t,
153
167
  ty: n
154
- }, this.#G();
168
+ }, this.#K();
155
169
  }
156
170
  measure() {
157
- let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0, r = this.#D > 0, i = r ? this.#F() : void 0, a = r ? this.#I() : void 0, o = a ? (e) => {
158
- let t = this.#w / this.#_;
171
+ let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0, r = this.#O > 0, i = r ? this.#I() : void 0, a = r ? this.#L() : void 0, o = a ? (e) => {
172
+ let t = this.#T / this.#v;
159
173
  return {
160
174
  x: (a.x - t) * e + t,
161
175
  y: a.y * e,
@@ -173,15 +187,15 @@ var b = class {
173
187
  screenAt: o
174
188
  };
175
189
  }
176
- #F() {
190
+ #I() {
177
191
  let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0;
178
- if (this.#D <= 0 || t <= 0 || n <= 0) return {
192
+ if (this.#O <= 0 || t <= 0 || n <= 0) return {
179
193
  x: 0,
180
194
  y: 0,
181
195
  width: t,
182
196
  height: n
183
197
  };
184
- let r = (this.#y ? 1 : 2) * this.#D, i = t, a = n;
198
+ let r = (this.#b ? 1 : 2) * this.#O, i = t, a = n;
185
199
  return r > t / n ? a = t / r : i = n * r, {
186
200
  x: (t - i) / 2,
187
201
  y: (n - a) / 2,
@@ -189,41 +203,41 @@ var b = class {
189
203
  height: a
190
204
  };
191
205
  }
192
- #I() {
193
- let e = this.#F();
194
- return !this.#y && this.#v !== null && y(this.#v) !== 0 ? {
206
+ #L() {
207
+ let e = this.#I();
208
+ return !this.#b && this.#y !== null && b(this.#y) !== 0 ? {
195
209
  x: e.x + e.width / 4,
196
210
  y: e.y,
197
211
  width: e.width / 2,
198
212
  height: e.height
199
213
  } : e;
200
214
  }
201
- #L = (e) => {
202
- e.preventDefault(), this.#A = !0, this.#M === null && (this.#M = setTimeout(() => this.#B(), _));
203
- };
204
- #R = () => {
205
- this.#V(), this.#A = !1, this.#H(), this.#G();
215
+ #R = (e) => {
216
+ e.preventDefault(), this.#j = !0, this.#N === null && (this.#N = setTimeout(() => this.#V(), v));
206
217
  };
207
218
  #z = () => {
208
- this.#t?.ownerDocument.visibilityState === "visible" && this.#G();
219
+ this.#H(), this.#j = !1, this.#U(), this.#K();
220
+ };
221
+ #B = () => {
222
+ this.#t?.ownerDocument.visibilityState === "visible" && this.#K();
209
223
  };
210
- #B() {
211
- this.#j || (this.#j = !0, this.#N?.());
212
- }
213
224
  #V() {
214
- this.#M !== null && (clearTimeout(this.#M), this.#M = null);
225
+ this.#M || (this.#M = !0, this.#P?.());
215
226
  }
216
227
  #H() {
228
+ this.#N !== null && (clearTimeout(this.#N), this.#N = null);
229
+ }
230
+ #U() {
217
231
  let e = this.#n;
218
232
  if (e !== null) {
219
- this.#r = a(e, l, u), this.#i = a(e, d, f);
233
+ this.#r = a(e, u, d), this.#i = a(e, f, p);
220
234
  for (let t of [
221
235
  "uViewport",
222
236
  "uRect",
223
237
  "uView",
224
238
  "uTex",
225
239
  "uGutterSide"
226
- ]) this.#m[t] = e.getUniformLocation(this.#r, t);
240
+ ]) this.#h[t] = e.getUniformLocation(this.#r, t);
227
241
  for (let t of [
228
242
  "uViewport",
229
243
  "uView",
@@ -235,11 +249,11 @@ var b = class {
235
249
  "uGloss",
236
250
  "uAlpha",
237
251
  "uCrease"
238
- ]) this.#h[t] = e.getUniformLocation(this.#i, t);
239
- this.#a = this.#U(e, this.#r), this.#W(e, this.#i), this.#d.clear(), this.#f = 0, this.#p = this.#te(e), e.useProgram(this.#r), e.uniform1i(this.#m.uTex ?? null, 0), e.useProgram(this.#i), e.uniform1i(this.#h.uFront ?? null, 0), e.uniform1i(this.#h.uBack ?? null, 1), e.clearColor(0, 0, 0, 0), e.enable(e.BLEND), e.blendFunc(e.ONE, e.ONE_MINUS_SRC_ALPHA);
252
+ ]) this.#g[t] = e.getUniformLocation(this.#i, t);
253
+ this.#a = this.#W(e, this.#r), this.#G(e, this.#i), this.#d.clear(), this.#f = 0, this.#p = this.#ne(e), e.useProgram(this.#r), e.uniform1i(this.#h.uTex ?? null, 0), e.useProgram(this.#i), e.uniform1i(this.#g.uFront ?? null, 0), e.uniform1i(this.#g.uBack ?? null, 1), e.clearColor(0, 0, 0, 0), e.enable(e.BLEND), e.blendFunc(e.ONE, e.ONE_MINUS_SRC_ALPHA);
240
254
  }
241
255
  }
242
- #U(e, t) {
256
+ #W(e, t) {
243
257
  let n = e.createVertexArray(), r = e.createBuffer();
244
258
  if (n === null || r === null) throw Error("WebglRenderer: could not create buffers.");
245
259
  e.bindVertexArray(n), e.bindBuffer(e.ARRAY_BUFFER, r), e.bufferData(e.ARRAY_BUFFER, new Float32Array([
@@ -255,9 +269,9 @@ var b = class {
255
269
  let i = e.getAttribLocation(t, "aUnit");
256
270
  return e.enableVertexAttribArray(i), e.vertexAttribPointer(i, 2, e.FLOAT, !1, 0, 0), e.bindVertexArray(null), n;
257
271
  }
258
- #W(e, t) {
272
+ #G(e, t) {
259
273
  let n = [];
260
- for (let e = 0; e < m; e++) for (let t = 0; t < p; t++) {
274
+ for (let e = 0; e < h; e++) for (let t = 0; t < m; t++) {
261
275
  let r = e * 29 + t, i = r + 1, a = r + 29, o = a + 1;
262
276
  n.push(r, a, i, i, a, o);
263
277
  }
@@ -274,97 +288,95 @@ var b = class {
274
288
  let c = e.getAttribLocation(t, "aUv");
275
289
  e.enableVertexAttribArray(c), e.vertexAttribPointer(c, 2, e.FLOAT, !1, 0, 0), e.bindBuffer(e.ELEMENT_ARRAY_BUFFER, a), e.bufferData(e.ELEMENT_ARRAY_BUFFER, new Uint16Array(n), e.STATIC_DRAW), e.bindVertexArray(null), this.#o = r;
276
290
  }
277
- #G() {
291
+ #K() {
278
292
  let e = this.#n, t = this.#t;
279
- e === null || t === null || this.#A || e.isContextLost() || (this.#ne(), e.clear(e.COLOR_BUFFER_BIT), this.#K(), this.#b === null ? this.#v !== null && this.#q() : this.#J());
293
+ e === null || t === null || this.#j || e.isContextLost() || (this.#re(), e.clear(e.COLOR_BUFFER_BIT), this.#q(), this.#x === null ? this.#y !== null && this.#J() : this.#Y());
280
294
  }
281
- #K() {
295
+ #q() {
282
296
  let e = this.#n, t = this.#t, n = t.width, r = t.height;
283
- if (this.#D > 0) {
284
- let e = (this.#y ? 1 : 2) * this.#D;
285
- e > n / r ? (this.#O = n, this.#k = Math.round(n / e)) : (this.#k = r, this.#O = Math.round(r * e));
286
- } else this.#O = n, this.#k = r;
287
- let i = Math.round((n - this.#O) / 2), a = Math.round((r - this.#k) / 2);
288
- e.viewport(i, a, this.#O, this.#k);
297
+ if (this.#O > 0) {
298
+ let e = (this.#b ? 1 : 2) * this.#O;
299
+ e > n / r ? (this.#k = n, this.#A = Math.round(n / e)) : (this.#A = r, this.#k = Math.round(r * e));
300
+ } else this.#k = n, this.#A = r;
301
+ let i = Math.round((n - this.#k) / 2), a = Math.round((r - this.#A) / 2);
302
+ e.viewport(i, a, this.#k, this.#A);
289
303
  }
290
- #q() {
291
- let e = this.#O, t = this.#k, n = this.#v;
292
- this.#w = this.#y ? 0 : y(n) * (e / 4), this.#X(), this.#y ? this.#Z({
304
+ #J() {
305
+ let e = this.#k, t = this.#A, n = this.#y;
306
+ this.#T = this.#b ? 0 : b(n) * (e / 4), this.#Z(), this.#b ? this.#Q({
293
307
  x: 0,
294
308
  y: 0,
295
309
  w: e,
296
310
  h: t
297
- }, n.right ?? n.left) : (this.#Z({
311
+ }, n.right ?? n.left) : (this.#Q({
298
312
  x: 0,
299
313
  y: 0,
300
314
  w: e / 2,
301
315
  h: t
302
- }, n.left, +!!n.right), this.#Z({
316
+ }, n.left, +!!n.right), this.#Q({
303
317
  x: e / 2,
304
318
  y: 0,
305
319
  w: e / 2,
306
320
  h: t
307
321
  }, n.right, n.left ? -1 : 0));
308
322
  }
309
- #J() {
310
- let e = this.#n, t = this.#O, n = this.#k, r = this.#b;
311
- if (this.#w = r.fill ? 0 : (this.#T + (this.#E - this.#T) * this.#x) * (t / 4), this.#X(), r.fill ? this.#Z({
323
+ #Y() {
324
+ let e = this.#n, t = this.#k, n = this.#A, r = this.#x;
325
+ if (this.#T = r.fill ? 0 : (this.#E + (this.#D - this.#E) * this.#S) * (t / 4), this.#Z(), r.fill ? this.#Q({
312
326
  x: 0,
313
327
  y: 0,
314
328
  w: t,
315
329
  h: n
316
- }, r.underFull) : (this.#Z({
330
+ }, r.underFull) : (this.#Q({
317
331
  x: 0,
318
332
  y: 0,
319
333
  w: t / 2,
320
334
  h: n
321
- }, r.underLeft, 1), this.#Z({
335
+ }, r.underLeft, 1), this.#Q({
322
336
  x: t / 2,
323
337
  y: 0,
324
338
  w: t / 2,
325
339
  h: n
326
340
  }, r.underRight, -1)), r.front === null && r.back === null) return;
327
341
  let i = r.fill ? t : t / 2, a = r.fill ? r.dir > 0 ? 0 : t : t / 2;
328
- this.#S.deform(this.#u, i, n, this.#x, {
329
- y: this.#C.y,
342
+ this.#C.deform(this.#u, i, n, this.#S, {
343
+ y: this.#w.y,
330
344
  fill: r.fill
331
- }), 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.#Q(e.TEXTURE0, r.front), this.#Q(e.TEXTURE1, r.back), e.activeTexture(e.TEXTURE0), e.useProgram(this.#i), e.bindVertexArray(this.#o), e.uniform2f(this.#h.uViewport ?? null, t, n), e.uniform3f(this.#h.uView ?? null, this.#g.scale, this.#g.tx * this.#_ + this.#w, this.#g.ty * this.#_), e.uniform1f(this.#h.uOriginX ?? null, a), e.uniform1f(this.#h.uDir ?? null, r.dir), e.uniform1f(this.#h.uLeafW ?? null, i), e.uniform1f(this.#h.uGloss ?? null, this.#S.gloss === !1 ? 0 : 1), e.uniform1f(this.#h.uAlpha ?? null, r.fill ? this.#Y() : 1), e.uniform1f(this.#h.uCrease ?? null, r.fill ? Math.min(1, this.#x / .2) : 1), e.drawElements(e.TRIANGLES, this.#l, e.UNSIGNED_SHORT, 0);
345
+ }), 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.#$(e.TEXTURE0, r.front), this.#$(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.#X() : 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);
332
346
  }
333
- #Y() {
334
- let e = this.#S.flat ? h : g, t = (this.#x - e) / (1 - e);
347
+ #X() {
348
+ let e = this.#C.flat ? g : _, t = (this.#S - e) / (1 - e);
335
349
  return t <= 0 ? 1 : t >= 1 ? 0 : (1 - t) ** 1.5;
336
350
  }
337
- #X() {
351
+ #Z() {
338
352
  let e = this.#n;
339
- e.useProgram(this.#r), e.bindVertexArray(this.#a), e.activeTexture(e.TEXTURE0), e.uniform2f(this.#m.uViewport ?? null, this.#O, this.#k), e.uniform3f(this.#m.uView ?? null, this.#g.scale, this.#g.tx * this.#_ + this.#w, this.#g.ty * this.#_);
353
+ e.useProgram(this.#r), e.bindVertexArray(this.#a), e.activeTexture(e.TEXTURE0), e.uniform2f(this.#h.uViewport ?? null, this.#k, this.#A), e.uniform3f(this.#h.uView ?? null, this.#_.scale, this.#_.tx * this.#v + this.#T, this.#_.ty * this.#v);
340
354
  }
341
- #Z(e, t, n = 0) {
355
+ #Q(e, t, n = 0) {
342
356
  let r = this.#n;
343
- t !== null && (this.#Q(r.TEXTURE0, t), r.uniform4f(this.#m.uRect ?? null, e.x, e.y, e.w, e.h), r.uniform1f(this.#m.uGutterSide ?? null, n), r.drawArrays(r.TRIANGLE_STRIP, 0, 4));
357
+ t !== null && (this.#$(r.TEXTURE0, t), r.uniform4f(this.#h.uRect ?? null, e.x, e.y, e.w, e.h), r.uniform1f(this.#h.uGutterSide ?? null, n), r.drawArrays(r.TRIANGLE_STRIP, 0, 4));
344
358
  }
345
- #Q(e, t) {
359
+ #$(e, t) {
346
360
  let n = this.#n;
347
- n.activeTexture(e), n.bindTexture(n.TEXTURE_2D, t === null ? this.#p : this.#$(t));
361
+ n.activeTexture(e), n.bindTexture(n.TEXTURE_2D, t === null ? this.#p : this.#ee(t));
348
362
  }
349
- #$(e) {
363
+ #ee(e) {
350
364
  let t = this.#n, n = this.#d.get(e);
351
365
  if (n !== void 0) return this.#d.delete(e), this.#d.set(e, n), n.tex;
352
- let r = s(t);
353
- c(t, r, e);
354
- let i = e, a = i.width * i.height * 4;
366
+ let r = s(t), i = c(t, r, e), a = i.width * i.height * 4;
355
367
  return this.#d.set(e, {
356
368
  tex: r,
357
369
  bytes: a
358
- }), this.#f += a, this.#ee(), r;
370
+ }), this.#f += a, this.#te(), r;
359
371
  }
360
- #ee() {
372
+ #te() {
361
373
  let e = this.#n;
362
374
  for (let [t, n] of this.#d) {
363
- if (this.#f <= v || this.#d.size <= 4) break;
375
+ if (this.#f <= y || this.#d.size <= 4) break;
364
376
  this.#d.delete(t), this.#f -= n.bytes, e.deleteTexture(n.tex);
365
377
  }
366
378
  }
367
- #te(e) {
379
+ #ne(e) {
368
380
  let t = s(e);
369
381
  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([
370
382
  0,
@@ -373,15 +385,15 @@ var b = class {
373
385
  0
374
386
  ])), t;
375
387
  }
376
- #ne() {
388
+ #re() {
377
389
  let e = this.#n, t = this.#t, n = this.#e;
378
390
  if (e === null || t === null || n === null) return;
379
- this.#_ = typeof devicePixelRatio == "number" ? devicePixelRatio : 1;
380
- let r = Math.max(1, Math.round(n.clientWidth * this.#_)), i = Math.max(1, Math.round(n.clientHeight * this.#_));
391
+ this.#v = typeof devicePixelRatio == "number" ? devicePixelRatio : 1;
392
+ let r = Math.max(1, Math.round(n.clientWidth * this.#v)), i = Math.max(1, Math.round(n.clientHeight * this.#v));
381
393
  (t.width !== r || t.height !== i) && (t.width = r, t.height = i, e.viewport(0, 0, r, i));
382
394
  }
383
395
  };
384
396
  //#endregion
385
- export { b as WebglRenderer };
397
+ export { x as WebglRenderer };
386
398
 
387
- //# sourceMappingURL=webglRenderer-nGRruZk2.js.map
399
+ //# sourceMappingURL=webglRenderer-CmQ_mUrX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webglRenderer-CmQ_mUrX.js","names":["#container","#canvas","#onContextLost","#onContextRestored","#onVisibilityChange","#gl","#maxTextureSize","#buildGlResources","#clearRestoreTimer","#texCache","#blankTex","#quadVao","#curlVao","#flat","#curl","#cacheBytes","#content","#flip","#fatalHandler","#fill","#trackAspect","#render","#pageAspect","#flipT","#curlModel","#anchor","#fromShiftUnit","#toShiftUnit","#view","#bookBox","#contentBox","#shiftX","#dpr","#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 type {\n FlipDirection,\n LayoutMetrics,\n Renderer,\n RenderOptions,\n 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\nout vec4 outColor;\nvoid main() {\n vec4 c = texture(uTex, vUv);\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 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 ? texture(uFront, vec2(fx, vUv.y)) : texture(uBack, vec2(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 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 #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 this.#flip = null;\n this.#trackAspect(content);\n this.#render();\n }\n\n /** Remember the page aspect (width/height) so the book can be letterboxed to it. */\n #trackAspect(content: SpreadContent): void {\n const p = content.left ?? content.right;\n if (p && p.height) this.#pageAspect = p.width / p.height;\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 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);\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 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 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 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 };\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 if (this.#pageAspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * this.#pageAspect;\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']) {\n this.#flatU[name] = gl.getUniformLocation(this.#flat, name);\n }\n for (const name of ['uViewport', 'uView', 'uOriginX', 'uDir', 'uLeafW', 'uFront', 'uBack', '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 if (this.#pageAspect > 0) {\n const bookAspect = (this.#fill ? 1 : 2) * this.#pageAspect;\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 this.#drawQuad({ x: 0, y: 0, w: w / 2, h }, content.left, content.right ? 1 : 0);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, content.right, content.left ? -1 : 0);\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);\n this.#drawQuad({ x: w / 2, y: 0, w: w / 2, h }, flip.underRight, -1);\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.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): 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.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":"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"}
package/dist/zine.d.ts CHANGED
@@ -4,7 +4,7 @@ import { type CurlSpec } from './geometry/curls/types';
4
4
  import { type RendererOption } from './renderer/select';
5
5
  import type { FlipDirection, PageContent } from './renderer/types';
6
6
  import type { DownloadInfo, OutlineItem, Source } from './source/types';
7
- import type { ControlsOptions } from './controls/types';
7
+ import type { ContextMenuOptions, ControlsOptions } from './controls/types';
8
8
  /** One page that matched a {@link Zine.search} query. */
9
9
  export interface SearchHit {
10
10
  /** Zero-based page index; pass straight to `flipTo`. */
@@ -69,6 +69,29 @@ export interface ZineOptions {
69
69
  clickToFlip?: 'edge' | 'half' | 'off';
70
70
  /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */
71
71
  clickZoneSize?: number;
72
+ /**
73
+ * On a mouse, change the cursor over the book to hint what a press would do: `pointer` where a
74
+ * click turns the page, `zoom-in` where a double-click zooms (including the edge peel band, so a
75
+ * sweep stays calm), `grab` over the whole page once zoomed (a drag pans), and `grabbing` while a
76
+ * peel or pan is actually in flight. Default true. Set false when a changing cursor would be noise
77
+ * in the embedding design. No effect on touch.
78
+ */
79
+ cursorHints?: boolean;
80
+ /**
81
+ * Subtle, one-shot discoverability hints shown on the book itself (so they reach touch readers,
82
+ * unlike {@link cursorHints}): the leading page corner peeks and settles on first open, nudges
83
+ * once more if the reader sits idle without turning a page, a small "drag to move" caption appears
84
+ * the first time they zoom in, and (on a mouse) a "double-click or Ctrl/⌘-scroll to zoom" caption
85
+ * appears if a lone click lands in a dead zone where only a double-click would do anything. Each
86
+ * hint stops for good the moment the reader performs the gesture it teaches. Honors
87
+ * `prefers-reduced-motion` (the motion cues are skipped). Default true. Pass an object to keep what
88
+ * the reader has learned across visits: `{ persist: true }` records it in localStorage so a
89
+ * returning reader is not re-taught (default is in-memory, per page load).
90
+ */
91
+ hints?: boolean | {
92
+ enabled?: boolean;
93
+ persist?: boolean;
94
+ };
72
95
  /**
73
96
  * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom.
74
97
  * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active.
@@ -115,8 +138,36 @@ export interface ZineOptions {
115
138
  * A mild deterrent for published documents, not protection: the pages are still in the DOM and
116
139
  * reachable by anyone who wants them. It also takes away Inspect and "Open image in new tab"
117
140
  * for everyone, so it is off unless asked for.
141
+ *
142
+ * Mutually exclusive with {@link contextMenu}: one replaces the native menu, the other only
143
+ * removes it, so enabling both is a contradiction and throws.
118
144
  */
119
145
  disableContextMenu?: boolean;
146
+ /**
147
+ * A right-click menu of reading controls (zoom, page turns, fullscreen, print, download, share),
148
+ * opened at the cursor over the book. On by default. Over a canvas the native menu offers only
149
+ * the generic page actions (Back, Reload, View source), so replacing it with the book's own
150
+ * controls is the better trade; pass `false` to keep the browser's menu.
151
+ *
152
+ * Reuses the toolbar's icons and actions, so the same controls appear whether reached from the
153
+ * toolbar or a right-click, and it works even with `controls: false` — a bare book can still
154
+ * offer one. `true` uses the default set; an object with `items` replaces the layout. Mouse
155
+ * affordance only; touch never raises it.
156
+ *
157
+ * Turned off automatically when {@link disableContextMenu} is set (that option removes the native
158
+ * menu with nothing in its place); setting both explicitly throws.
159
+ */
160
+ contextMenu?: boolean | ContextMenuOptions;
161
+ /**
162
+ * Control ids to hide from both surfaces at once: off the toolbar (bar and overflow menu) and out
163
+ * of the right-click menu. `['print', 'download']` removes those two everywhere, wherever they are
164
+ * nested, without having to edit `controls.items` and `contextMenu.items` separately.
165
+ *
166
+ * A blunt cross-cutting filter: it only removes, and it applies to whatever layout each surface
167
+ * ends up with (default or custom). To rearrange or add controls, set the per-surface `items`
168
+ * instead.
169
+ */
170
+ hideControls?: readonly string[];
120
171
  }
121
172
  /**
122
173
  * The public flipbook. Composes the engine (spreads, virtualizer, events), a