@zinejs/core 0.9.0 → 0.9.2

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 (37) hide show
  1. package/LICENSE +73 -0
  2. package/dist/controls/icons.d.ts +4 -0
  3. package/dist/controls/sidebar.d.ts +2 -0
  4. package/dist/controls/types.d.ts +3 -0
  5. package/dist/{controls-CcOu_19g.js → controls-Dh1okIx6.js} +60 -44
  6. package/dist/controls-Dh1okIx6.js.map +1 -0
  7. package/dist/{cssRenderer-Deskm2ND.js → cssRenderer-CTaM4cAj.js} +35 -29
  8. package/dist/cssRenderer-CTaM4cAj.js.map +1 -0
  9. package/dist/flipSound-BFXSkWBo.js +78 -0
  10. package/dist/flipSound-BFXSkWBo.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +709 -569
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.umd.js +4 -4
  15. package/dist/index.umd.js.map +1 -1
  16. package/dist/loading/loading.d.ts +2 -1
  17. package/dist/{loading-DF88K9yJ.js → loading-BgbqJz5k.js} +7 -8
  18. package/dist/loading-BgbqJz5k.js.map +1 -0
  19. package/dist/renderer/types.d.ts +7 -0
  20. package/dist/{share-BrQa6aYm.js → share-CZorhnKI.js} +25 -22
  21. package/dist/share-CZorhnKI.js.map +1 -0
  22. package/dist/sound/clip.d.ts +9 -0
  23. package/dist/sound/flipSound.d.ts +30 -0
  24. package/dist/strings.d.ts +74 -0
  25. package/dist/{styles-D_zOGk2q.js → styles-C1Qp5pby.js} +4 -2
  26. package/dist/styles-C1Qp5pby.js.map +1 -0
  27. package/dist/{webglRenderer-CmQ_mUrX.js → webglRenderer-DQn4pc1S.js} +85 -79
  28. package/dist/webglRenderer-DQn4pc1S.js.map +1 -0
  29. package/dist/zine.d.ts +35 -0
  30. package/options.schema.json +27 -0
  31. package/package.json +12 -10
  32. package/dist/controls-CcOu_19g.js.map +0 -1
  33. package/dist/cssRenderer-Deskm2ND.js.map +0 -1
  34. package/dist/loading-DF88K9yJ.js.map +0 -1
  35. package/dist/share-BrQa6aYm.js.map +0 -1
  36. package/dist/styles-D_zOGk2q.js.map +0 -1
  37. package/dist/webglRenderer-CmQ_mUrX.js.map +0 -1
@@ -95,20 +95,22 @@ var x = class {
95
95
  #D = 0;
96
96
  #O = 0;
97
97
  #k = 0;
98
- #A = 0;
99
- #j = !1;
100
- #M = !1;
101
- #N = null;
102
- #P = null;
98
+ #A = "contain";
99
+ #j = 0;
100
+ #M = 0;
101
+ #N = !1;
102
+ #P = !1;
103
+ #F = null;
104
+ #I = null;
103
105
  mount(e) {
104
106
  this.#e = e;
105
107
  let t = e.ownerDocument, n = t.createElement("canvas");
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();
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 = i(n), this.#m = this.#n.getParameter(this.#n.MAX_TEXTURE_SIZE), this.#K(), Promise.resolve();
107
109
  }
108
110
  destroy() {
109
- this.#H();
111
+ this.#G();
110
112
  let e = this.#t;
111
- e !== null && (e.removeEventListener("webglcontextlost", this.#R), e.removeEventListener("webglcontextrestored", this.#z), e.ownerDocument.removeEventListener("visibilitychange", this.#B));
113
+ e !== null && (e.removeEventListener("webglcontextlost", this.#V), e.removeEventListener("webglcontextrestored", this.#H), e.ownerDocument.removeEventListener("visibilitychange", this.#U));
112
114
  let t = this.#n;
113
115
  if (t !== null) {
114
116
  for (let e of this.#d.values()) t.deleteTexture(e.tex);
@@ -117,21 +119,24 @@ var x = class {
117
119
  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;
118
120
  }
119
121
  onFatal(e) {
120
- this.#P = e;
122
+ this.#I = e;
121
123
  }
122
124
  get maxTextureSize() {
123
125
  return this.#m || void 0;
124
126
  }
125
127
  renderSpread(e, t, n) {
126
- this.#y = t, this.#b = n?.fill ?? !1, this.#x = null, this.#F(t), this.#K();
128
+ this.#y = t, this.#b = n?.fill ?? !1, n?.fit && (this.#A = n.fit), this.#x = null, this.#L(t), this.#Y();
127
129
  }
128
- #F(e) {
130
+ #L(e) {
129
131
  let t = e.left ?? e.right;
130
- t && t.height && (this.#O = t.width / t.height);
132
+ t && t.height && (this.#O = t.width / t.height, this.#k === 0 && (this.#k = this.#O));
133
+ }
134
+ #R() {
135
+ return this.#A === "fill" && this.#k > 0 ? this.#k : this.#O;
131
136
  }
132
137
  beginFlip(e, t, r, i) {
133
138
  let a = i?.fill ?? !1;
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 = {
139
+ i?.fit && (this.#A = i.fit), 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.#L(t), a ? this.#x = {
135
140
  underLeft: null,
136
141
  underRight: null,
137
142
  underFull: t.right ?? t.left,
@@ -155,20 +160,20 @@ var x = class {
155
160
  back: t.right,
156
161
  dir: -1,
157
162
  fill: !1
158
- }, this.#K();
163
+ }, this.#Y();
159
164
  }
160
165
  setFlipProgress(e, t) {
161
- this.#S = e, this.#K();
166
+ this.#S = e, this.#Y();
162
167
  }
163
168
  setViewTransform(e, t, n) {
164
169
  this.#_ = {
165
170
  scale: e,
166
171
  tx: t,
167
172
  ty: n
168
- }, this.#K();
173
+ }, this.#Y();
169
174
  }
170
175
  measure() {
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) => {
176
+ let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0, r = this.#O > 0, i = r ? this.#z() : void 0, a = r ? this.#B() : void 0, o = a ? (e) => {
172
177
  let t = this.#T / this.#v;
173
178
  return {
174
179
  x: (a.x - t) * e + t,
@@ -184,27 +189,28 @@ var x = class {
184
189
  pageHeight: n,
185
190
  book: i,
186
191
  content: a,
187
- screenAt: o
192
+ screenAt: o,
193
+ containerAspect: this.#k > 0 ? (this.#b ? 1 : 2) * this.#k : void 0
188
194
  };
189
195
  }
190
- #I() {
191
- let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0;
192
- if (this.#O <= 0 || t <= 0 || n <= 0) return {
196
+ #z() {
197
+ let e = this.#e, t = e?.clientWidth ?? 0, n = e?.clientHeight ?? 0, r = this.#R();
198
+ if (r <= 0 || t <= 0 || n <= 0) return {
193
199
  x: 0,
194
200
  y: 0,
195
201
  width: t,
196
202
  height: n
197
203
  };
198
- let r = (this.#b ? 1 : 2) * this.#O, i = t, a = n;
199
- return r > t / n ? a = t / r : i = n * r, {
200
- x: (t - i) / 2,
201
- y: (n - a) / 2,
202
- width: i,
203
- height: a
204
+ let i = (this.#b ? 1 : 2) * r, a = t, o = n;
205
+ return i > t / n ? o = t / i : a = n * i, {
206
+ x: (t - a) / 2,
207
+ y: (n - o) / 2,
208
+ width: a,
209
+ height: o
204
210
  };
205
211
  }
206
- #L() {
207
- let e = this.#I();
212
+ #B() {
213
+ let e = this.#z();
208
214
  return !this.#b && this.#y !== null && b(this.#y) !== 0 ? {
209
215
  x: e.x + e.width / 4,
210
216
  y: e.y,
@@ -212,22 +218,22 @@ var x = class {
212
218
  height: e.height
213
219
  } : e;
214
220
  }
215
- #R = (e) => {
216
- e.preventDefault(), this.#j = !0, this.#N === null && (this.#N = setTimeout(() => this.#V(), v));
221
+ #V = (e) => {
222
+ e.preventDefault(), this.#N = !0, this.#F === null && (this.#F = setTimeout(() => this.#W(), v));
217
223
  };
218
- #z = () => {
219
- this.#H(), this.#j = !1, this.#U(), this.#K();
224
+ #H = () => {
225
+ this.#G(), this.#N = !1, this.#K(), this.#Y();
220
226
  };
221
- #B = () => {
222
- this.#t?.ownerDocument.visibilityState === "visible" && this.#K();
227
+ #U = () => {
228
+ this.#t?.ownerDocument.visibilityState === "visible" && this.#Y();
223
229
  };
224
- #V() {
225
- this.#M || (this.#M = !0, this.#P?.());
230
+ #W() {
231
+ this.#P || (this.#P = !0, this.#I?.());
226
232
  }
227
- #H() {
228
- this.#N !== null && (clearTimeout(this.#N), this.#N = null);
233
+ #G() {
234
+ this.#F !== null && (clearTimeout(this.#F), this.#F = null);
229
235
  }
230
- #U() {
236
+ #K() {
231
237
  let e = this.#n;
232
238
  if (e !== null) {
233
239
  this.#r = a(e, u, d), this.#i = a(e, f, p);
@@ -250,10 +256,10 @@ var x = class {
250
256
  "uAlpha",
251
257
  "uCrease"
252
258
  ]) 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);
259
+ this.#a = this.#q(e, this.#r), this.#J(e, this.#i), this.#d.clear(), this.#f = 0, this.#p = this.#ae(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);
254
260
  }
255
261
  }
256
- #W(e, t) {
262
+ #q(e, t) {
257
263
  let n = e.createVertexArray(), r = e.createBuffer();
258
264
  if (n === null || r === null) throw Error("WebglRenderer: could not create buffers.");
259
265
  e.bindVertexArray(n), e.bindBuffer(e.ARRAY_BUFFER, r), e.bufferData(e.ARRAY_BUFFER, new Float32Array([
@@ -269,7 +275,7 @@ var x = class {
269
275
  let i = e.getAttribLocation(t, "aUnit");
270
276
  return e.enableVertexAttribArray(i), e.vertexAttribPointer(i, 2, e.FLOAT, !1, 0, 0), e.bindVertexArray(null), n;
271
277
  }
272
- #G(e, t) {
278
+ #J(e, t) {
273
279
  let n = [];
274
280
  for (let e = 0; e < h; e++) for (let t = 0; t < m; t++) {
275
281
  let r = e * 29 + t, i = r + 1, a = r + 29, o = a + 1;
@@ -288,51 +294,51 @@ var x = class {
288
294
  let c = e.getAttribLocation(t, "aUv");
289
295
  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;
290
296
  }
291
- #K() {
297
+ #Y() {
292
298
  let e = this.#n, t = this.#t;
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());
294
- }
295
- #q() {
296
- let e = this.#n, t = this.#t, n = t.width, r = t.height;
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);
303
- }
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({
299
+ e === null || t === null || this.#N || e.isContextLost() || (this.#oe(), e.clear(e.COLOR_BUFFER_BIT), this.#X(), this.#x === null ? this.#y !== null && this.#Z() : this.#Q());
300
+ }
301
+ #X() {
302
+ let e = this.#n, t = this.#t, n = t.width, r = t.height, i = this.#R();
303
+ if (i > 0) {
304
+ let e = (this.#b ? 1 : 2) * i;
305
+ e > n / r ? (this.#j = n, this.#M = Math.round(n / e)) : (this.#M = r, this.#j = Math.round(r * e));
306
+ } else this.#j = n, this.#M = r;
307
+ let a = Math.round((n - this.#j) / 2), o = Math.round((r - this.#M) / 2);
308
+ e.viewport(a, o, this.#j, this.#M);
309
+ }
310
+ #Z() {
311
+ let e = this.#j, t = this.#M, n = this.#y;
312
+ this.#T = this.#b ? 0 : b(n) * (e / 4), this.#ee(), this.#b ? this.#te({
307
313
  x: 0,
308
314
  y: 0,
309
315
  w: e,
310
316
  h: t
311
- }, n.right ?? n.left) : (this.#Q({
317
+ }, n.right ?? n.left) : (this.#te({
312
318
  x: 0,
313
319
  y: 0,
314
320
  w: e / 2,
315
321
  h: t
316
- }, n.left, +!!n.right), this.#Q({
322
+ }, n.left, +!!n.right), this.#te({
317
323
  x: e / 2,
318
324
  y: 0,
319
325
  w: e / 2,
320
326
  h: t
321
327
  }, n.right, n.left ? -1 : 0));
322
328
  }
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({
329
+ #Q() {
330
+ let e = this.#n, t = this.#j, n = this.#M, r = this.#x;
331
+ if (this.#T = r.fill ? 0 : (this.#E + (this.#D - this.#E) * this.#S) * (t / 4), this.#ee(), r.fill ? this.#te({
326
332
  x: 0,
327
333
  y: 0,
328
334
  w: t,
329
335
  h: n
330
- }, r.underFull) : (this.#Q({
336
+ }, r.underFull) : (this.#te({
331
337
  x: 0,
332
338
  y: 0,
333
339
  w: t / 2,
334
340
  h: n
335
- }, r.underLeft, 1), this.#Q({
341
+ }, r.underLeft, 1), this.#te({
336
342
  x: t / 2,
337
343
  y: 0,
338
344
  w: t / 2,
@@ -342,41 +348,41 @@ var x = class {
342
348
  this.#C.deform(this.#u, i, n, this.#S, {
343
349
  y: this.#w.y,
344
350
  fill: r.fill
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);
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);
346
352
  }
347
- #X() {
353
+ #$() {
348
354
  let e = this.#C.flat ? g : _, t = (this.#S - e) / (1 - e);
349
355
  return t <= 0 ? 1 : t >= 1 ? 0 : (1 - t) ** 1.5;
350
356
  }
351
- #Z() {
357
+ #ee() {
352
358
  let e = this.#n;
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);
359
+ 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);
354
360
  }
355
- #Q(e, t, n = 0) {
361
+ #te(e, t, n = 0) {
356
362
  let r = this.#n;
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));
363
+ t !== null && (this.#ne(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));
358
364
  }
359
- #$(e, t) {
365
+ #ne(e, t) {
360
366
  let n = this.#n;
361
- n.activeTexture(e), n.bindTexture(n.TEXTURE_2D, t === null ? this.#p : this.#ee(t));
367
+ n.activeTexture(e), n.bindTexture(n.TEXTURE_2D, t === null ? this.#p : this.#re(t));
362
368
  }
363
- #ee(e) {
369
+ #re(e) {
364
370
  let t = this.#n, n = this.#d.get(e);
365
371
  if (n !== void 0) return this.#d.delete(e), this.#d.set(e, n), n.tex;
366
372
  let r = s(t), i = c(t, r, e), a = i.width * i.height * 4;
367
373
  return this.#d.set(e, {
368
374
  tex: r,
369
375
  bytes: a
370
- }), this.#f += a, this.#te(), r;
376
+ }), this.#f += a, this.#ie(), r;
371
377
  }
372
- #te() {
378
+ #ie() {
373
379
  let e = this.#n;
374
380
  for (let [t, n] of this.#d) {
375
381
  if (this.#f <= y || this.#d.size <= 4) break;
376
382
  this.#d.delete(t), this.#f -= n.bytes, e.deleteTexture(n.tex);
377
383
  }
378
384
  }
379
- #ne(e) {
385
+ #ae(e) {
380
386
  let t = s(e);
381
387
  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([
382
388
  0,
@@ -385,7 +391,7 @@ var x = class {
385
391
  0
386
392
  ])), t;
387
393
  }
388
- #re() {
394
+ #oe() {
389
395
  let e = this.#n, t = this.#t, n = this.#e;
390
396
  if (e === null || t === null || n === null) return;
391
397
  this.#v = typeof devicePixelRatio == "number" ? devicePixelRatio : 1;
@@ -396,4 +402,4 @@ var x = class {
396
402
  //#endregion
397
403
  export { x as WebglRenderer };
398
404
 
399
- //# sourceMappingURL=webglRenderer-CmQ_mUrX.js.map
405
+ //# sourceMappingURL=webglRenderer-DQn4pc1S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webglRenderer-DQn4pc1S.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 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 // 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);\n this.#render();\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 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);\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 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']) {\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 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 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
@@ -5,6 +5,7 @@ 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
7
  import type { ContextMenuOptions, ControlsOptions } from './controls/types';
8
+ import { type ZineStrings } from './strings';
8
9
  /** One page that matched a {@link Zine.search} query. */
9
10
  export interface SearchHit {
10
11
  /** Zero-based page index; pass straight to `flipTo`. */
@@ -67,6 +68,14 @@ export interface ZineOptions {
67
68
  flipDuration?: number;
68
69
  /** How a tap/click turns pages: near an edge ('edge'), by page half ('half'), or 'off'. Default 'edge'. */
69
70
  clickToFlip?: 'edge' | 'half' | 'off';
71
+ /** How a page whose size differs from the book fits: 'contain' (default) keeps its aspect and
72
+ * centers it (a small margin shows); 'fill' stretches it to the book's shape. Either way the
73
+ * book keeps one shape for the whole document, so mixed-size pages never cause a layout shift. */
74
+ fit?: 'contain' | 'fill';
75
+ /** Override any reader-facing text (control labels, screen-reader announcements, panel and
76
+ * loading text) for localization. English by default; pass only the keys you want to change.
77
+ * Interpolated entries (e.g. the page announcement) are functions. See {@link ZineStrings}. */
78
+ strings?: Partial<ZineStrings>;
70
79
  /** Edge-zone size in px per side, used when clickToFlip is 'edge'. Default 64. */
71
80
  clickZoneSize?: number;
72
81
  /**
@@ -92,6 +101,22 @@ export interface ZineOptions {
92
101
  enabled?: boolean;
93
102
  persist?: boolean;
94
103
  };
104
+ /**
105
+ * Play a short sound on each page turn. Off by default — web audio is unexpected, so it is
106
+ * strictly opt-in. `true` uses the bundled clip at the default volume; an object overrides it:
107
+ * `url` points at your own clip (fetched at runtime), `volume` is 0..1, and `muted: true` offers
108
+ * the sound but starts it silent so the reader turns it on themselves (via the `mute` control),
109
+ * and `persist: true` remembers the reader's mute choice across visits in localStorage (otherwise
110
+ * it resets each load). Playback is best-effort and never throws: a browser that gates audio behind
111
+ * a gesture, lacks Web Audio, or cannot decode the clip just stays silent. Whenever sound is
112
+ * enabled — audible or muted — a `mute` control appears in the toolbar and right-click menu.
113
+ */
114
+ sound?: boolean | {
115
+ url?: string;
116
+ volume?: number;
117
+ muted?: boolean;
118
+ persist?: boolean;
119
+ };
95
120
  /**
96
121
  * Delay (ms) a click waits before flipping, so a double-click can preempt it with a zoom.
97
122
  * Omit for auto: 0 when double-click zoom is inactive, 250 when it's active.
@@ -332,6 +357,16 @@ export declare class Zine {
332
357
  y: number;
333
358
  }): void;
334
359
  resetZoom(): void;
360
+ /** The resolved reader-facing text (English defaults merged with the `strings` option). Read by
361
+ * the controls and panels so a translation reaches every label and announcement. */
362
+ get strings(): ZineStrings;
363
+ /** Whether page-flip sound is enabled for this book (the `sound` option was set). */
364
+ isSoundEnabled(): boolean;
365
+ /** Whether flip sound is currently muted. Always false when sound is not enabled. */
366
+ isSoundMuted(): boolean;
367
+ /** Mute or unmute the flip sound. Remembered even if the sound controller is still loading, and
368
+ * a no-op when `sound` was never enabled. */
369
+ setSoundMuted(muted: boolean): void;
335
370
  on<K extends keyof ZineEventMap>(event: K, listener: (payload: ZineEventMap[K]) => void): () => void;
336
371
  destroy(): void;
337
372
  /** The URL that opens this book at `page` (defaults to the current one). */
@@ -83,6 +83,16 @@
83
83
  "default": "edge",
84
84
  "description": "How a tap/click turns pages: near an edge, by page half, or off."
85
85
  },
86
+ "fit": {
87
+ "type": "string",
88
+ "enum": ["contain", "fill"],
89
+ "default": "contain",
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
+ },
92
+ "strings": {
93
+ "type": "object",
94
+ "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."
95
+ },
86
96
  "clickZoneSize": {
87
97
  "type": "number",
88
98
  "minimum": 0,
@@ -109,6 +119,23 @@
109
119
  }
110
120
  ]
111
121
  },
122
+ "sound": {
123
+ "default": false,
124
+ "description": "Play a short sound on each page turn. Off by default (opt-in). true uses the bundled clip at default volume; an object overrides it (url, volume, muted to start silent, persist to remember the reader's mute choice). Playback is best-effort and never throws; a mute control appears in the toolbar and right-click menu whenever sound is enabled (audible or muted).",
125
+ "oneOf": [
126
+ { "type": "boolean" },
127
+ {
128
+ "type": "object",
129
+ "additionalProperties": false,
130
+ "properties": {
131
+ "url": { "type": "string", "description": "A clip URL that overrides the bundled default (fetched at runtime)." },
132
+ "volume": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.5, "description": "Playback volume, 0..1." },
133
+ "muted": { "type": "boolean", "default": false, "description": "Offer the sound but start it silent, so the reader enables it via the mute control." },
134
+ "persist": { "type": "boolean", "default": false, "description": "Remember the reader's mute choice across visits via localStorage, instead of resetting each load." }
135
+ }
136
+ }
137
+ ]
138
+ },
112
139
  "clickFlipDelay": {
113
140
  "type": "number",
114
141
  "minimum": 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zinejs/core",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Framework-agnostic flipbook engine with CSS and WebGL2 renderers and no dependencies.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -43,11 +43,19 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
+ "scripts": {
47
+ "build": "vite build && UMD=1 vite build && tsc --emitDeclarationOnly -p tsconfig.build.json",
48
+ "embed-clip": "node scripts/embed-flip-clip.mjs",
49
+ "typecheck": "tsc --noEmit -p tsconfig.json",
50
+ "test": "pnpm typecheck && vitest run",
51
+ "size": "pnpm build && size-limit",
52
+ "prepublishOnly": "pnpm build"
53
+ },
46
54
  "size-limit": [
47
55
  {
48
56
  "name": "core (ESM, gzip, excl. lazy renderer chunks)",
49
57
  "path": "dist/index.js",
50
- "limit": "15 kB",
58
+ "limit": "17 kB",
51
59
  "gzip": true
52
60
  },
53
61
  {
@@ -86,11 +94,5 @@
86
94
  "limit": "3 kB",
87
95
  "gzip": true
88
96
  }
89
- ],
90
- "scripts": {
91
- "build": "vite build && UMD=1 vite build && tsc --emitDeclarationOnly -p tsconfig.build.json",
92
- "typecheck": "tsc --noEmit -p tsconfig.json",
93
- "test": "pnpm typecheck && vitest run",
94
- "size": "pnpm build && size-limit"
95
- }
96
- }
97
+ ]
98
+ }