postext-folio 0.52.5 → 0.52.7

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 (73) hide show
  1. package/dist/postext/book.d.ts +35 -0
  2. package/dist/postext/book.d.ts.map +1 -0
  3. package/dist/postext/book.js +96 -0
  4. package/dist/postext/book.js.map +1 -0
  5. package/dist/postext/constants.d.ts +36 -0
  6. package/dist/postext/constants.d.ts.map +1 -0
  7. package/dist/postext/constants.js +13 -0
  8. package/dist/postext/constants.js.map +1 -0
  9. package/dist/postext/pageVideos.d.ts +26 -0
  10. package/dist/postext/pageVideos.d.ts.map +1 -0
  11. package/dist/postext/pageVideos.js +276 -0
  12. package/dist/postext/pageVideos.js.map +1 -0
  13. package/dist/postext/painting.d.ts +30 -0
  14. package/dist/postext/painting.d.ts.map +1 -0
  15. package/dist/postext/painting.js +167 -0
  16. package/dist/postext/painting.js.map +1 -0
  17. package/dist/postext/sharp.d.ts +19 -0
  18. package/dist/postext/sharp.d.ts.map +1 -0
  19. package/dist/postext/sharp.js +121 -0
  20. package/dist/postext/sharp.js.map +1 -0
  21. package/dist/postext/state.d.ts +29 -0
  22. package/dist/postext/state.d.ts.map +1 -0
  23. package/dist/postext/state.js +2 -0
  24. package/dist/postext/state.js.map +1 -0
  25. package/dist/postext/sweep.d.ts +24 -0
  26. package/dist/postext/sweep.d.ts.map +1 -0
  27. package/dist/postext/sweep.js +37 -0
  28. package/dist/postext/sweep.js.map +1 -0
  29. package/dist/postext/types.d.ts +77 -0
  30. package/dist/postext/types.d.ts.map +1 -0
  31. package/dist/postext/types.js +2 -0
  32. package/dist/postext/types.js.map +1 -0
  33. package/dist/postext.d.ts +6 -124
  34. package/dist/postext.d.ts.map +1 -1
  35. package/dist/postext.js +87 -732
  36. package/dist/postext.js.map +1 -1
  37. package/dist/viewer/constants.d.ts +30 -0
  38. package/dist/viewer/constants.d.ts.map +1 -0
  39. package/dist/viewer/constants.js +49 -0
  40. package/dist/viewer/constants.js.map +1 -0
  41. package/dist/viewer/input.d.ts +15 -0
  42. package/dist/viewer/input.d.ts.map +1 -0
  43. package/dist/viewer/input.js +142 -0
  44. package/dist/viewer/input.js.map +1 -0
  45. package/dist/viewer/layout.d.ts +24 -0
  46. package/dist/viewer/layout.d.ts.map +1 -0
  47. package/dist/viewer/layout.js +183 -0
  48. package/dist/viewer/layout.js.map +1 -0
  49. package/dist/viewer/lens.d.ts +42 -0
  50. package/dist/viewer/lens.d.ts.map +1 -0
  51. package/dist/viewer/lens.js +166 -0
  52. package/dist/viewer/lens.js.map +1 -0
  53. package/dist/viewer/pointer.d.ts +37 -0
  54. package/dist/viewer/pointer.d.ts.map +1 -0
  55. package/dist/viewer/pointer.js +250 -0
  56. package/dist/viewer/pointer.js.map +1 -0
  57. package/dist/viewer/state.d.ts +70 -0
  58. package/dist/viewer/state.d.ts.map +1 -0
  59. package/dist/viewer/state.js +2 -0
  60. package/dist/viewer/state.js.map +1 -0
  61. package/dist/viewer/turning.d.ts +34 -0
  62. package/dist/viewer/turning.d.ts.map +1 -0
  63. package/dist/viewer/turning.js +156 -0
  64. package/dist/viewer/turning.js.map +1 -0
  65. package/dist/viewer/types.d.ts +241 -0
  66. package/dist/viewer/types.d.ts.map +1 -0
  67. package/dist/viewer/types.js +2 -0
  68. package/dist/viewer/types.js.map +1 -0
  69. package/dist/viewer.d.ts +3 -242
  70. package/dist/viewer.d.ts.map +1 -1
  71. package/dist/viewer.js +104 -976
  72. package/dist/viewer.js.map +1 -1
  73. package/package.json +3 -3
package/dist/viewer.js CHANGED
@@ -1,55 +1,12 @@
1
- import { resolveFolioConfig } from "postext";
2
- import { canFlip, PageFlipper } from "./pageFlip.js";
3
- import { spreadOfPage, spreadsOf } from "./spreads.js";
4
- import { injectStyles } from "./styles.js";
5
- const DEFAULT_LABELS = {
6
- region: "Book",
7
- prev: "Previous pages",
8
- next: "Next pages",
9
- count: (pages, total) => pages.length > 1 ? `Pages ${pages[0] + 1}–${pages[pages.length - 1] + 1} of ${total}` : `Page ${(pages[0] ?? 0) + 1} of ${total}`,
10
- };
11
- /** A press on a page that moves less than this (px) is a click: it turns
12
- * the page (the recto forward, the verso back). */
13
- const CLICK_SLOP = 6;
14
- /** A horizontal swipe longer than this (px) turns the spread. */
15
- const SWIPE = 40;
16
- /** Below this width (px) `mode: 'auto'` shows one page at a time. */
17
- export const SINGLE_BELOW = 560;
18
- /** Space kept round the book (CSS px): beside it for the ‹ › buttons, and
19
- * above and below it for the page count (and a lifted leaf's corner). */
20
- const GAP = 12;
21
- const MIN_MARGIN_Y = 20;
22
- /** The magnifying glass's range, and the default's aim: the page shown at
23
- * this many CSS px a millimetre. */
24
- const ZOOM_MIN = 1.5;
25
- const ZOOM_MAX = 10;
26
- const LENS_PX_PER_MM = 5.5;
27
- const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
28
- const CHEVRON = (dir) => `<svg aria-hidden="true" viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${dir === "left" ? "m15 18-6-6 6-6" : "m9 18 6-6-6-6"}"/></svg>`;
29
- const sourceOf = (page) => (page && typeof page === "object" && "src" in page ? page.src : (page ?? null));
30
- const altOf = (page) => (page && typeof page === "object" && "src" in page ? page.alt : undefined);
31
- const paperOf = (page) => page && typeof page === "object" && "src" in page ? page.paper : undefined;
32
- /** The natural width / height of a page source, when it is known. */
33
- function aspectOf(src) {
34
- if (!src || typeof src === "string")
35
- return null;
36
- const w = src instanceof HTMLCanvasElement ? src.width : src.naturalWidth;
37
- const h = src instanceof HTMLCanvasElement ? src.height : src.naturalHeight;
38
- return w > 0 && h > 0 ? w / h : null;
39
- }
40
- /** A CSS colour as 0 … 1 channels. */
41
- function rgbOf(color) {
42
- const ctx = document.createElement("canvas").getContext("2d");
43
- if (!ctx)
44
- return null;
45
- // Over white: a transparent page is white paper.
46
- ctx.fillStyle = "#fff";
47
- ctx.fillRect(0, 0, 1, 1);
48
- ctx.fillStyle = color;
49
- ctx.fillRect(0, 0, 1, 1);
50
- const d = ctx.getImageData(0, 0, 1, 1).data;
51
- return [d[0] / 255, d[1] / 255, d[2] / 255];
52
- }
1
+ import { spreadOfPage } from './spreads.js';
2
+ import { injectStyles } from './styles.js';
3
+ import { DEFAULT_LABELS, paperOf, sourceOf, SINGLE_BELOW, clamp, ZOOM_MIN, ZOOM_MAX } from './viewer/constants.js';
4
+ import { createFolioLayout } from './viewer/layout.js';
5
+ import { createFolioTurning } from './viewer/turning.js';
6
+ import { createFolioInput } from './viewer/input.js';
7
+ import { createFolioLens } from './viewer/lens.js';
8
+ import { createFolioPointer } from './viewer/pointer.js';
9
+ export { SINGLE_BELOW } from './viewer/constants.js';
53
10
  /**
54
11
  * A book on the page: the pages in spreads by the recto rule, turned with
55
12
  * the ‹ › buttons, ←/→, a swipe, a click on a page, or taken by hand and
@@ -61,21 +18,23 @@ function rgbOf(color) {
61
18
  * simply change.
62
19
  */
63
20
  export function createFolio(container, options) {
21
+ const vs = {};
22
+ const vlate = {};
64
23
  injectStyles(container);
65
- let labels = { ...DEFAULT_LABELS, ...options.labels };
66
- let pages = options.pages;
67
- let firstPageRecto = options.firstPageRecto ?? true;
68
- let binding = options.binding ?? "left";
69
- let mode = options.mode ?? "auto";
70
- let paper = options.paper ?? "#fff";
71
- let appearance = options.appearance ?? {};
72
- let papersKey = JSON.stringify(pages.map(paperOf));
24
+ vs.labels = { ...DEFAULT_LABELS, ...options.labels };
25
+ vs.pages = options.pages;
26
+ vs.firstPageRecto = options.firstPageRecto ?? true;
27
+ vs.binding = options.binding ?? "left";
28
+ vs.mode = options.mode ?? "auto";
29
+ vs.paper = options.paper ?? "#fff";
30
+ vs.appearance = options.appearance ?? {};
31
+ let papersKey = JSON.stringify(vs.pages.map(paperOf));
73
32
  /** The appearance the flipper was last given, as text. */
74
- let appearanceKey = "";
33
+ vs.appearanceKey = "";
75
34
  const animate = options.animate ?? true;
76
35
  const controls = options.controls ?? true;
77
36
  const showCount = options.showCount ?? true;
78
- let interaction = options.interaction ?? "hand";
37
+ vs.interaction = options.interaction ?? "hand";
79
38
  const root = document.createElement("div");
80
39
  root.className = "postext-folio";
81
40
  root.tabIndex = 0;
@@ -103,888 +62,57 @@ export function createFolio(container, options) {
103
62
  if (!controls)
104
63
  prevBtn.hidden = nextBtn.hidden = true;
105
64
  container.append(root);
106
- let single = false;
107
- let spreads = [];
108
- let current = 0;
109
- let shown = 0;
110
- let flipper = null;
111
- let flipCanvas = null;
112
- let holding = null;
113
- let press = null;
114
- let swipe = null;
115
- let disposed = false;
116
- let slot = { width: 0, height: 0, deviceWidth: 0, deviceHeight: 0 };
117
- let pageVideos = [];
65
+ vs.single = false;
66
+ vs.spreads = [];
67
+ vs.current = 0;
68
+ vs.shown = 0;
69
+ vs.flipper = null;
70
+ vs.flipCanvas = null;
71
+ vs.holding = null;
72
+ vs.press = null;
73
+ vs.swipe = null;
74
+ vs.disposed = false;
75
+ vs.slot = { width: 0, height: 0, deviceWidth: 0, deviceHeight: 0 };
76
+ vs.pageVideos = [];
118
77
  /** Where a press on a page began, on the page (a click there may act on
119
78
  * it rather than turn it). */
120
- let pressPoint = null;
121
- const rtl = () => binding === "right";
79
+ vs.pressPoint = null;
80
+ const rtl = () => vs.binding === "right";
122
81
  const pagesOf = (s) => (s ?? [null, null]).filter((i) => i !== null);
123
82
  /** One page at a time on a WebGL book: the spine runs along the page's
124
83
  * inner edge, the leaf turns over it and out of the box. */
125
- const singleGl = () => single && !!flipper;
84
+ const singleGl = () => vs.single && !!vs.flipper;
126
85
  // In single mode every sheet carries one page: its back is blank paper
127
86
  // (the page after it lies under it, not on its back).
128
- const bookOf = () => spreads.map((s, k) => (single ? [k === 0 ? null : "", sourceOf(pages[s[1]])] : s.map((i) => (i === null ? null : sourceOf(pages[i])))));
129
- /** The flipper's appearance: the leaves' own papers (a leaf takes its
130
- * recto's, else its verso's) and the leaves outside the pages given. */
131
- function flipAppearance() {
132
- const extra = appearance.extraPages ?? { before: 0, after: 0 };
133
- const leafPapers = spreads.slice(0, -1).map((s, k) => single ? paperOf(pages[s[1]]) : (paperOf(pages[s[1] ?? -1]) ?? paperOf(pages[spreads[k + 1]?.[0] ?? -1])));
134
- return {
135
- folio: appearance.folio,
136
- pageWidthMm: appearance.pageWidthMm,
137
- extraLeaves: single ? { before: extra.before, after: extra.after } : { before: Math.ceil(extra.before / 2), after: Math.ceil(extra.after / 2) },
138
- leafPapers,
139
- singlePage: single,
140
- textureBaseUrl: appearance.textureBaseUrl,
141
- coverLeaves: coverLeavesOf(),
142
- ...(appearance.spineImage ? { spineImage: appearance.spineImage } : {}),
143
- };
144
- }
145
- /** The leaves that are the covers: the first, when the book opens on its
146
- * first page alone; the last, when the last page is a verso alone. */
147
- function coverLeavesOf() {
148
- const c = appearance.covers;
149
- if (!c || spreads.length < 2)
150
- return undefined;
151
- const last = spreads[spreads.length - 1];
152
- return {
153
- ...(c.front && (single || spreads[0][0] === null) ? { front: 0 } : {}),
154
- ...(c.back && !single && last[1] === null ? { back: spreads.length - 2 } : {}),
155
- };
156
- }
157
- function aspect() {
158
- if (options.aspect)
159
- return options.aspect;
160
- for (const page of pages) {
161
- const ar = aspectOf(sourceOf(page));
162
- if (ar)
163
- return ar;
164
- }
165
- return 1 / Math.SQRT2;
166
- }
167
- /**
168
- * Fits the book into the box, as large as it goes: the ‹ › buttons and
169
- * the count sit in the margins round it. The spread lies on the device
170
- * pixel grid, its page slots a whole number of device pixels wide, so a
171
- * page painted at that size is shown 1:1 in the DOM and on the canvas
172
- * alike. The WebGL canvas covers the box, centred on the spread (the
173
- * camera maps the spread onto it 1:1), with room for a lifted leaf.
174
- */
175
- function fit() {
176
- const dpr = window.devicePixelRatio || 1;
177
- const box = root.getBoundingClientRect();
178
- const boxW = Math.floor(box.width * dpr);
179
- const boxH = Math.floor(box.height * dpr);
180
- const ar = aspect();
181
- const gap = Math.round(GAP * dpr);
182
- // The widest page up to `max` device px whose width and height are
183
- // even: the spread's centre and the spine (the camera's centre in one
184
- // or the other mode) then lie on the pixel grid.
185
- const fitWidth = (max) => {
186
- let w = Math.max(2, Math.floor(max));
187
- while (w > 2 && (w % 2 || Math.round(w / ar) % 2))
188
- w--;
189
- return w;
190
- };
191
- let deviceWidth;
192
- let left;
193
- let top;
194
- let across;
195
- if (single) {
196
- // One page across the box, the buttons and the count in a bar below.
197
- const bar = controls ? Math.round((prevBtn.offsetWidth + 2 * GAP) * dpr) : gap;
198
- const availW = Math.max(0, boxW - 2 * gap);
199
- const availH = Math.max(0, boxH - gap - bar);
200
- deviceWidth = fitWidth(Math.min(availW, availH > 0 ? availH * ar : Infinity));
201
- const deviceHeight = Math.round(deviceWidth / ar);
202
- const pageLeft = gap + Math.floor((availW - deviceWidth) / 2);
203
- top = gap + Math.max(0, Math.floor((availH - deviceHeight) / 2));
204
- if (flipper) {
205
- // The WebGL book is a spread whose other page lies off the box: the
206
- // spine on the page's inner edge (its left, or its right when the
207
- // book is bound on the right).
208
- across = 2;
209
- left = rtl() ? pageLeft : pageLeft - deviceWidth;
210
- }
211
- else {
212
- across = 1;
213
- left = pageLeft;
214
- }
215
- }
216
- else {
217
- across = 2;
218
- const marginX = Math.round((controls ? prevBtn.offsetWidth + 2 * GAP : GAP) * dpr);
219
- const marginY = Math.round(Math.max(MIN_MARGIN_Y, showCount ? count.offsetHeight + 2 * GAP : GAP) * dpr);
220
- const availW = Math.max(0, boxW - 2 * marginX);
221
- const availH = Math.max(0, boxH - 2 * marginY);
222
- // A tilted book is foreshortened: its pages may be taller than the
223
- // box before their image is.
224
- const tall = flipper ? 1 / Math.max(0.7, Math.cos((resolveFolioConfig(appearance.folio).tilt * Math.PI) / 180)) : 1;
225
- deviceWidth = fitWidth(Math.min(availW / 2, availH > 0 ? availH * tall * ar : Infinity));
226
- left = Math.floor((boxW - 2 * deviceWidth) / 2);
227
- // Centred even when the tilted book's spread is taller than the box
228
- // (it overflows both edges): the camera centres the book on it.
229
- top = Math.floor((boxH - Math.round(deviceWidth / ar)) / 2);
230
- }
231
- const deviceHeight = Math.max(1, Math.round(deviceWidth / ar));
232
- Object.assign(spreadEl.style, {
233
- left: `${left / dpr}px`,
234
- top: `${top / dpr}px`,
235
- width: `${(across * deviceWidth) / dpr}px`,
236
- height: `${deviceHeight / dpr}px`,
237
- });
238
- if (flipCanvas) {
239
- // Centred on the spine (the camera maps the spread onto the canvas
240
- // 1:1 about its centre) and reaching every edge of the box; what
241
- // falls outside the box is clipped.
242
- const cx = left + deviceWidth;
243
- const cy = top + deviceHeight / 2;
244
- const hw = Math.max(cx, boxW - cx);
245
- const hh = Math.max(cy, boxH - cy);
246
- Object.assign(flipCanvas.style, {
247
- left: `${(cx - hw) / dpr}px`,
248
- top: `${(cy - hh) / dpr}px`,
249
- width: `${(2 * hw) / dpr}px`,
250
- height: `${(2 * hh) / dpr}px`,
251
- });
252
- }
253
- const next = { width: deviceWidth / dpr, height: deviceHeight / dpr, deviceWidth, deviceHeight };
254
- if (next.deviceWidth !== slot.deviceWidth || next.deviceHeight !== slot.deviceHeight) {
255
- slot = next;
256
- options.onLayout?.(slot);
257
- scheduleDetail(300);
258
- }
259
- placeDomVideo();
260
- flipper?.redraw();
261
- }
262
- function pageEl(i, slot) {
263
- const el = document.createElement("div");
264
- el.className = "postext-folio-page";
265
- if (!single || singleGl())
266
- el.classList.add(slot === 0 ? "is-verso" : "is-recto");
267
- const src = i === null ? null : sourceOf(pages[i]);
268
- if (src === null) {
269
- el.classList.add("is-empty");
270
- el.setAttribute("aria-hidden", "true");
271
- }
272
- else if (src === "") {
273
- el.classList.add("is-blank");
274
- el.setAttribute("aria-hidden", "true");
275
- }
276
- else {
277
- const alt = altOf(pages[i]) ?? "";
278
- if (typeof src === "string") {
279
- const img = document.createElement("img");
280
- img.src = src;
281
- img.alt = alt;
282
- img.draggable = false;
283
- img.decoding = "async";
284
- el.append(img);
285
- }
286
- else {
287
- // The page's own element (a canvas, or a decoded image), shown as is.
288
- src.classList.add("postext-folio-surface");
289
- if (src instanceof HTMLCanvasElement) {
290
- src.setAttribute("role", "img");
291
- if (alt)
292
- src.setAttribute("aria-label", alt);
293
- }
294
- else {
295
- src.alt = alt;
296
- src.draggable = false;
297
- }
298
- el.append(src);
299
- }
300
- }
301
- return el;
302
- }
303
- /** Shows the page videos: on the WebGL book, drawn by the leaves that
304
- * carry their pages; on the DOM spread, laid over the pages. */
305
- function applyVideo() {
306
- flipper?.setVideos(pageVideos.map((v) => ({ src: sourceOf(pages[v.page]), element: v.element, frame: v })));
307
- placeDomVideo();
308
- }
309
- /** Takes a video element laid over a DOM page off it. */
310
- const unplace = (v) => {
311
- if (!v.element.classList.contains("postext-folio-video"))
312
- return;
313
- v.element.classList.remove("postext-folio-video");
314
- v.element.remove();
315
- };
316
- /** The DOM spread (no WebGL): each video element over its page, mapped
317
- * onto the picture's place by a transform (it may lie turned). */
318
- function placeDomVideo() {
319
- if (flipper) {
320
- for (const v of pageVideos)
321
- unplace(v);
322
- return;
323
- }
324
- const els = [...spreadEl.children];
325
- const s = spreads[shown] ?? [null, null];
326
- const slots = single && !singleGl() ? [s[0] ?? s[1]] : s;
327
- for (const v of pageVideos) {
328
- const at = slots.indexOf(v.page);
329
- const el = at >= 0 ? els[at] : undefined;
330
- if (!el) {
331
- v.element.remove();
332
- continue;
333
- }
334
- const w = el.clientWidth;
335
- const h = el.clientHeight;
336
- const unit = 100;
337
- v.element.classList.add("postext-folio-video");
338
- Object.assign(v.element.style, {
339
- width: `${unit}px`,
340
- height: `${unit}px`,
341
- transform: `matrix(${(v.across.x * w) / unit}, ${(v.across.y * h) / unit}, ${(v.down.x * w) / unit}, ${(v.down.y * h) / unit}, ${v.origin.x * w}, ${v.origin.y * h})`,
342
- });
343
- if (v.element.parentElement !== el)
344
- el.append(v.element);
345
- }
346
- }
347
- function render() {
348
- const s = spreads[shown] ?? [null, null];
349
- const slots = single && !singleGl() ? [s[0] ?? s[1]] : s;
350
- spreadEl.replaceChildren(...slots.map((i, side) => pageEl(i, side)));
351
- spreadEl.classList.toggle("is-solo", single && !singleGl());
352
- spreadEl.classList.toggle("is-by-hand", !!flipper);
353
- // The canvas draws the book, still or turning; the DOM pages stay as
354
- // the textures' sources and the pages' text alternatives.
355
- spreadEl.classList.toggle("is-gl", !!flipper);
356
- const on = pagesOf(spreads[current]);
357
- count.textContent = on.length ? labels.count(on, pages.length) : "";
358
- prevBtn.disabled = current <= 0;
359
- nextBtn.disabled = current >= spreads.length - 1;
360
- placeDomVideo();
361
- // The settled spread, drawn at rest.
362
- flipper?.clear();
363
- scheduleDetail(120);
364
- }
365
- function settle(index) {
366
- shown = index;
367
- render();
368
- options.onChange?.({ spread: index, pages: pagesOf(spreads[index]) });
369
- }
370
- function go(index) {
371
- const to = Math.max(0, Math.min(spreads.length - 1, index));
372
- if (to === current && to === shown)
373
- return;
374
- current = to;
375
- options.onTarget?.({ spread: to, pages: pagesOf(spreads[to]) });
376
- if (flipper) {
377
- flipper.go(to);
378
- // The count and buttons follow the hand at once.
379
- count.textContent = labels.count(pagesOf(spreads[to]), pages.length);
380
- prevBtn.disabled = to <= 0;
381
- nextBtn.disabled = to >= spreads.length - 1;
382
- }
383
- else
384
- settle(to);
385
- }
386
- /** Opens spread `index` at once, no leaf turning. */
387
- function jump(index) {
388
- const to = Math.max(0, Math.min(spreads.length - 1, index));
389
- if (to === current && to === shown)
390
- return;
391
- current = to;
392
- options.onTarget?.({ spread: to, pages: pagesOf(spreads[to]) });
393
- flipper?.setBook(bookOf(), to);
394
- settle(to);
395
- }
396
- function setUpFlipper() {
397
- flipper?.dispose();
398
- flipper = null;
399
- flipCanvas?.remove();
400
- flipCanvas = null;
401
- if (!animate || spreads.length < 2 || !canFlip())
402
- return;
403
- flipCanvas = document.createElement("canvas");
404
- flipCanvas.className = "postext-folio-flip";
405
- flipCanvas.setAttribute("aria-hidden", "true");
406
- root.append(flipCanvas);
407
- const initial = flipAppearance();
408
- appearanceKey = JSON.stringify(initial);
409
- flipper = new PageFlipper(flipCanvas, spreadEl, bookOf(), shown, settle, (i) => {
410
- current = i;
411
- options.onTarget?.({ spread: i, pages: pagesOf(spreads[i]) });
412
- count.textContent = labels.count(pagesOf(spreads[i]), pages.length);
413
- }, binding,
414
- // One page at a time the spine is the box's edge: the leaf cannot be
415
- // carried past halfway, so a shorter pull turns it.
416
- { persistent: true, turnAt: single ? 0.2 : 0.5, appearance: initial });
417
- const rgb = rgbOf(paper);
418
- if (rgb)
419
- flipper.setPaper(...rgb);
420
- if (pageVideos.length)
421
- applyVideo();
422
- // A new book: its glass and paintings start afresh.
423
- details = new Map();
424
- detailAsked = new Map();
425
- if (lensAt)
426
- flipper.setLens(lensOf(lensAt));
427
- }
428
- /** Lays the book out again: spreads, mode, flipper. */
429
- function rebuild(atPage) {
430
- single = mode === "single" || (mode === "auto" && root.clientWidth > 0 && root.clientWidth < SINGLE_BELOW);
431
- spreads = single ? pages.map((_, i) => [null, i]) : spreadsOf(pages.length, firstPageRecto);
432
- current = shown = spreadOfPage(spreads, atPage);
433
- root.dir = rtl() ? "rtl" : "ltr";
434
- root.style.setProperty("--postext-folio-paper", paper);
435
- root.classList.toggle("is-single", single);
436
- prevBtn.innerHTML = CHEVRON(rtl() ? "right" : "left");
437
- nextBtn.innerHTML = CHEVRON(rtl() ? "left" : "right");
438
- setUpFlipper();
439
- fit();
440
- render();
441
- }
442
- function applyLabels() {
443
- root.setAttribute("aria-label", labels.region);
444
- prevBtn.setAttribute("aria-label", labels.prev);
445
- nextBtn.setAttribute("aria-label", labels.next);
446
- }
447
- // ── Input ──
448
- prevBtn.addEventListener("click", () => go(current - 1));
449
- nextBtn.addEventListener("click", () => go(current + 1));
450
- root.addEventListener("keydown", (event) => {
451
- if (event.target !== root)
452
- return;
453
- if (interaction === "magnify" && flipper && (event.key === "+" || event.key === "=" || event.key === "-" || event.key === "Escape")) {
454
- if (event.key === "Escape") {
455
- if (!lensAt)
456
- return;
457
- hideLens();
458
- }
459
- else {
460
- raiseLens();
461
- zoomBy(event.key === "-" ? 1 / 1.25 : 1.25);
462
- }
463
- event.preventDefault();
464
- return;
465
- }
466
- const forward = rtl() ? "ArrowLeft" : "ArrowRight";
467
- const back = rtl() ? "ArrowRight" : "ArrowLeft";
468
- if (event.key === back || event.key === "PageUp")
469
- go(current - 1);
470
- else if (event.key === forward || event.key === "PageDown")
471
- go(current + 1);
472
- else if (event.key === "Home")
473
- go(0);
474
- else if (event.key === "End")
475
- go(spreads.length - 1);
476
- else
477
- return;
478
- event.preventDefault();
87
+ const bookOf = () => vs.spreads.map((s, k) => (vs.single ? [k === 0 ? null : "", sourceOf(vs.pages[s[1]])] : s.map((i) => (i === null ? null : sourceOf(vs.pages[i])))));
88
+ const { flipAppearance, aspect, fit, pageEl } = createFolioLayout({
89
+ options, controls, showCount, root, prevBtn, spreadEl, count, rtl, singleGl, vs, vlate
479
90
  });
480
- // Right-drag orbits the view round the book; the view stays where it is
481
- // left until `resetView()` (the host's button) eases it back.
482
- root.addEventListener("contextmenu", (event) => {
483
- if (flipper)
484
- event.preventDefault();
91
+ const { applyVideo, unplace, placeDomVideo, render, go, jump, rebuild, applyLabels } = createFolioTurning({
92
+ options, animate, root, prevBtn, nextBtn, spreadEl, count, rtl, pagesOf, singleGl, bookOf,
93
+ flipAppearance, fit, pageEl, vs, vlate
485
94
  });
486
- root.addEventListener("pointerdown", (event) => {
487
- // In orbit mode the left button (one finger, a pen) orbits too.
488
- const orbits = event.button === 2 || (event.button === 0 && interaction === "orbit");
489
- if (!orbits || !flipper || holding)
490
- return;
491
- if (event.target.closest(".postext-folio-nav"))
492
- return;
493
- event.preventDefault();
494
- const mask = event.button === 2 ? 2 : 1;
495
- const id = event.pointerId;
496
- root.classList.add("is-orbiting");
497
- let x = event.clientX;
498
- let y = event.clientY;
499
- const move = (e) => {
500
- if (e.pointerId !== id)
501
- return;
502
- if ((e.buttons & mask) === 0)
503
- return end();
504
- flipper?.orbitBy(e.clientX - x, e.clientY - y);
505
- x = e.clientX;
506
- y = e.clientY;
507
- };
508
- const up = (e) => {
509
- if (e.pointerId === id)
510
- end();
511
- };
512
- const end = () => {
513
- root.classList.remove("is-orbiting");
514
- window.removeEventListener("pointermove", move);
515
- window.removeEventListener("pointerup", up);
516
- window.removeEventListener("pointercancel", up);
517
- window.removeEventListener("blur", end);
518
- };
519
- window.addEventListener("pointermove", move);
520
- window.addEventListener("pointerup", up);
521
- window.addEventListener("pointercancel", up);
522
- window.addEventListener("blur", end);
95
+ Object.assign(vlate, { placeDomVideo });
96
+ createFolioInput({
97
+ options, root, prevBtn, nextBtn, rtl, go, vs, vlate
523
98
  });
524
- // Select mode: a text cursor over the pages as the book is seen.
525
- root.addEventListener("pointermove", (event) => {
526
- if (event.buttons)
527
- return;
528
- const point = holding ? null : aimedPageAt(event);
529
- if (interaction === "select" || interaction === "magnify")
530
- root.classList.toggle("is-over-page", !!point);
531
- if (options.isPageAction)
532
- root.classList.toggle("is-over-action", !!point && options.isPageAction(point));
533
- // Hand mode: the hand shows only where a page can be taken, its
534
- // outer half (#494).
535
- root.classList.toggle("is-over-grip", interaction === "hand" && !holding && !!flipper && flipper.hit(event) !== 0);
99
+ const { zoomNow, lensOf, heldAt, raiseLens, hideLens, zoomBy, scheduleDetail, releaseLens } = createFolioLens({
100
+ options, root, spreadEl, rtl, pagesOf, aspect, go, vs, vlate
536
101
  });
537
- // Orbit, select and magnify modes: a click on something a click acts on
538
- // (a video) acts on it, as in hand mode. The press is taken before the
539
- // host hears it in select and magnify modes (no caret, no link
540
- // followed); in orbit mode a drag from it still turns the view.
541
- let actionPress = null;
542
- let swallowClick = false;
543
- root.addEventListener("pointerdown", (event) => {
544
- actionPress = null;
545
- if (interaction === "hand" || event.button !== 0 || !options.onPageClick || !options.isPageAction)
546
- return;
547
- if (event.target.closest(".postext-folio-nav"))
548
- return;
549
- const point = aimedPageAt(event);
550
- if (!point || !options.isPageAction(point))
551
- return;
552
- actionPress = { x: event.clientX, y: event.clientY, id: event.pointerId, point };
553
- if (leftToHost())
554
- event.stopPropagation();
555
- }, true);
556
- root.addEventListener("pointermove", (event) => {
557
- if (actionPress && leftToHost() && event.pointerId === actionPress.id)
558
- event.stopPropagation();
559
- }, true);
560
- root.addEventListener("pointerup", (event) => {
561
- const press = actionPress;
562
- if (!press || event.pointerId !== press.id)
563
- return;
564
- actionPress = null;
565
- if (leftToHost()) {
566
- event.stopPropagation();
567
- swallowClick = true;
568
- }
569
- if (Math.hypot(event.clientX - press.x, event.clientY - press.y) < CLICK_SLOP)
570
- options.onPageClick?.(press.point);
571
- }, true);
572
- root.addEventListener("click", (event) => {
573
- if (!swallowClick)
574
- return;
575
- swallowClick = false;
576
- event.stopPropagation();
577
- }, true);
578
- root.addEventListener("pointerleave", (event) => {
579
- root.classList.remove("is-over-action", "is-over-grip", "is-over-page");
580
- if (event.pointerType === "mouse")
581
- hideLens();
102
+ Object.assign(vlate, { lensOf, raiseLens, hideLens, zoomBy, scheduleDetail });
103
+ const { onPage, leftToHost, aimedPageAt, pointOnScreen, applyInteraction } = createFolioPointer({
104
+ options, root, spreadEl, rtl, singleGl, go, heldAt, scheduleDetail, releaseLens, vs
582
105
  });
583
- // ── The magnifying glass (#527) ──
584
- /** Where the glass is held (client px), when it is. */
585
- let lensAt = null;
586
- let lensZoom = options.magnifier?.zoom ? clamp(options.magnifier.zoom, ZOOM_MIN, ZOOM_MAX) : null;
587
- /** Page source → its sharper painting, and the width each was asked
588
- * for at. */
589
- let details = new Map();
590
- let detailAsked = new Map();
591
- let detailTimer = 0;
592
- function zoomNow() {
593
- if (lensZoom)
594
- return lensZoom;
595
- const perMm = slot.width / (appearance.pageWidthMm ?? 150);
596
- return perMm > 0 ? clamp(LENS_PX_PER_MM / perMm, 2, 8) : 3;
597
- }
598
- function lensOf(at) {
599
- const box = root.getBoundingClientRect();
600
- const diameter = options.magnifier?.diameter ?? clamp(0.42 * Math.min(box.width, box.height), 180, 520);
601
- const rim = clamp(diameter * 0.045, 8, 18);
602
- return { x: at.x, y: at.y, radius: diameter / 2 - rim, rim, zoom: zoomNow() };
603
- }
604
- /** Where the glass is held for a pointer: under a mouse or a pen, but
605
- * a finger holds it above itself, where it does not hide it. */
606
- function heldAt(event) {
607
- if (event.pointerType !== "touch")
608
- return { x: event.clientX, y: event.clientY };
609
- const lens = lensOf({ x: event.clientX, y: event.clientY });
610
- return { x: lens.x, y: lens.y - (lens.radius + lens.rim + 28) };
611
- }
612
- function placeLens(event) {
613
- if (!flipper)
614
- return;
615
- const lens = lensOf(heldAt(event));
616
- lensAt = { x: lens.x, y: lens.y };
617
- root.classList.add("is-magnifying");
618
- flipper.setLens(lens);
619
- }
620
- /** Without a pointer to hold it (the keyboard, a host), the glass comes
621
- * up over the spread's centre. */
622
- function raiseLens() {
623
- if (lensAt)
624
- return;
625
- const r = spreadEl.getBoundingClientRect();
626
- lensAt = { x: r.left + r.width / 2, y: r.top + r.height / 2 };
627
- root.classList.add("is-magnifying");
628
- }
629
- function hideLens() {
630
- if (!lensAt)
631
- return;
632
- lensAt = null;
633
- root.classList.remove("is-magnifying");
634
- flipper?.setLens(null);
635
- }
636
- function zoomBy(factor) {
637
- lensZoom = clamp(zoomNow() * factor, ZOOM_MIN, ZOOM_MAX);
638
- if (lensAt)
639
- flipper?.setLens(lensOf(lensAt));
640
- scheduleDetail(250);
641
- }
642
- root.addEventListener("pointermove", (event) => {
643
- if (interaction !== "magnify" || holding)
644
- return;
645
- if (event.pointerType === "touch" && !event.buttons)
646
- return;
647
- placeLens(event);
648
- });
649
- root.addEventListener("pointerdown", (event) => {
650
- if (interaction !== "magnify" || event.button !== 0)
651
- return;
652
- if (event.target.closest(".postext-folio-nav"))
653
- return;
654
- placeLens(event);
655
- });
656
- for (const type of ["pointerup", "pointercancel"]) {
657
- root.addEventListener(type, (event) => {
658
- if (event.pointerType === "touch")
659
- hideLens();
660
- });
661
- }
662
- root.addEventListener("wheel", (event) => {
663
- if (interaction !== "magnify" || !flipper)
664
- return;
665
- event.preventDefault();
666
- const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
667
- zoomBy(Math.exp(-delta * 0.0015));
668
- }, { passive: false });
669
- function scheduleDetail(delay) {
670
- if (interaction !== "magnify" || !options.detail)
671
- return;
672
- clearTimeout(detailTimer);
673
- detailTimer = window.setTimeout(askDetail, delay);
674
- }
675
- /** Asks the host for the open pages painted sharp enough for the glass
676
- * (a texel for each screen pixel at its centre, within the largest
677
- * texture the book shows), and lets go of the others' paintings. */
678
- function askDetail() {
679
- const host = options.detail;
680
- const flip = flipper;
681
- if (interaction !== "magnify" || !host || !flip || !slot.deviceWidth || disposed)
682
- return;
683
- const cap = flip.maxTextureSize;
684
- const want = Math.max(slot.deviceWidth, Math.min(cap, Math.floor(cap * aspect()), Math.ceil((slot.deviceWidth * zoomNow()) / 256) * 256));
685
- const on = new Map();
686
- for (const i of new Set([...pagesOf(spreads[shown]), ...pagesOf(spreads[current])])) {
687
- const src = sourceOf(pages[i]);
688
- if (src)
689
- on.set(src, i);
690
- }
691
- for (const src of [...details.keys()])
692
- if (!on.has(src))
693
- details.delete(src);
694
- for (const src of [...detailAsked.keys()])
695
- if (!on.has(src))
696
- detailAsked.delete(src);
697
- flip.setDetail(details);
698
- for (const [src, i] of on) {
699
- if ((detailAsked.get(src) ?? 0) >= want)
700
- continue;
701
- detailAsked.set(src, want);
702
- void Promise.resolve(host.paint(i, want)).then((painting) => {
703
- if (!painting || flipper !== flip || interaction !== "magnify" || sourceOf(pages[i]) !== src)
704
- return;
705
- details.set(src, painting);
706
- flip.setDetail(details);
707
- }, () => detailAsked.delete(src));
708
- }
709
- }
710
- /** Puts the glass away and lets go of its paintings. */
711
- function releaseLens() {
712
- hideLens();
713
- clearTimeout(detailTimer);
714
- if (!details.size && !detailAsked.size)
715
- return;
716
- details = new Map();
717
- detailAsked = new Map();
718
- flipper?.setDetail(details);
719
- options.detail?.release?.();
720
- }
721
- root.addEventListener("pointerdown", (event) => {
722
- // Only the hand takes pages, swipes and clicks them over.
723
- if (interaction !== "hand")
724
- return;
725
- if (onPage(event))
726
- return;
727
- if (event.pointerType !== "mouse")
728
- swipe = { x: event.clientX, y: event.clientY };
729
- });
730
- root.addEventListener("pointerup", (event) => {
731
- const start = swipe;
732
- swipe = null;
733
- if (!start)
734
- return;
735
- const dx = event.clientX - start.x;
736
- if (Math.abs(dx) > SWIPE && Math.abs(dx) > Math.abs(event.clientY - start.y))
737
- go(current + ((dx < 0) !== rtl() ? 1 : -1));
738
- });
739
- /** A press on a page: taken by hand (the WebGL book, hit in 3D, since
740
- * the tilted book does not lie over the DOM spread), or remembered as a
741
- * possible click. True when the stage's swipe should not hear it. */
742
- function onPage(event) {
743
- if (event.button !== 0 || holding)
744
- return false;
745
- const target = event.target;
746
- if (target.closest(".postext-folio-nav"))
747
- return false;
748
- if (flipper ? !flipper.hit(event) : !target.closest(".postext-folio-spread"))
749
- return false;
750
- press = { x: event.clientX, y: event.clientY };
751
- // Where it was pressed, read before the hand lifts the leaf.
752
- pressPoint = options.onPageClick ? pageAt(event) : null;
753
- if (singleGl()) {
754
- decide(event);
755
- return true;
756
- }
757
- if (!flipper?.grab(event))
758
- return false;
759
- event.preventDefault();
760
- hold(event, press);
761
- return true;
762
- }
763
- /**
764
- * One page at a time, the page is taken only once the pointer shows
765
- * which way it goes: towards the spine it is lifted and dragged over
766
- * (from where it was pressed), away from it the book goes back a page (a
767
- * swipe); a press that does not move is a tap and turns forward.
768
- */
769
- function decide(down) {
770
- down.stopPropagation();
771
- swipe = null;
772
- const id = down.pointerId;
773
- const start = { x: down.clientX, y: down.clientY };
774
- const toSpine = rtl() ? 1 : -1;
775
- const done = () => {
776
- window.removeEventListener("pointermove", onMove);
777
- window.removeEventListener("pointerup", onUp, true);
778
- window.removeEventListener("pointercancel", onCancel);
779
- };
780
- const onMove = (e) => {
781
- if (e.pointerId !== id)
782
- return;
783
- const dx = e.clientX - start.x;
784
- const dy = e.clientY - start.y;
785
- if (Math.hypot(dx, dy) < CLICK_SLOP || Math.abs(dy) > Math.abs(dx))
786
- return;
787
- if (Math.sign(dx) !== toSpine)
788
- return; // a swipe back: settled on release
789
- done();
790
- if (!flipper?.grab(down))
791
- return;
792
- hold(down, start);
793
- flipper.drag(e);
794
- };
795
- const onUp = (e) => {
796
- if (e.pointerId !== id)
797
- return;
798
- done();
799
- press = null;
800
- const dx = e.clientX - start.x;
801
- const dy = e.clientY - start.y;
802
- if (Math.hypot(dx, dy) < CLICK_SLOP) {
803
- if (!pageClick())
804
- go(current + 1);
805
- }
806
- else if (Math.abs(dx) > SWIPE && Math.abs(dx) > Math.abs(dy) && Math.sign(dx) === -toSpine)
807
- go(current - 1);
808
- e.stopPropagation();
809
- };
810
- const onCancel = (e) => {
811
- if (e.pointerId === id)
812
- done();
813
- };
814
- window.addEventListener("pointermove", onMove);
815
- window.addEventListener("pointerup", onUp, true);
816
- window.addEventListener("pointercancel", onCancel);
817
- }
818
- /** Holds the page taken at `event` until the pointer lets go. */
819
- function hold(event, start) {
820
- // The hold follows the pointer anywhere: the window hears it even if
821
- // the capture is lost, and a move with the button up means the release
822
- // was missed.
823
- const id = event.pointerId;
824
- const onMove = (e) => {
825
- if (e.pointerId !== id)
826
- return;
827
- if ((e.buttons & 1) === 0)
828
- end(false);
829
- else
830
- flipper?.drag(e);
831
- };
832
- const onUp = (e) => {
833
- if (e.pointerId === id)
834
- end(Math.hypot(e.clientX - start.x, e.clientY - start.y) < CLICK_SLOP);
835
- };
836
- const onCancel = (e) => {
837
- if (e.pointerId === id)
838
- end(false);
839
- };
840
- const onBlur = () => end(false);
841
- const end = (click) => {
842
- window.removeEventListener("pointermove", onMove);
843
- window.removeEventListener("pointerup", onUp, true);
844
- window.removeEventListener("pointercancel", onCancel);
845
- window.removeEventListener("blur", onBlur);
846
- spreadEl.classList.remove("is-held");
847
- holding = null;
848
- press = null;
849
- // A click that acts on the page (a video) lets the leaf fall back.
850
- flipper?.release(click && !pageClick());
851
- };
852
- window.addEventListener("pointermove", onMove);
853
- // Captured, so the stage's swipe does not hear the hold's release.
854
- window.addEventListener("pointerup", onUp, true);
855
- window.addEventListener("pointercancel", onCancel);
856
- window.addEventListener("blur", onBlur);
857
- holding = end;
858
- swipe = null;
859
- spreadEl.classList.add("is-held");
860
- try {
861
- spreadEl.setPointerCapture(id);
862
- }
863
- catch {
864
- // The window listeners carry the hold without it.
865
- }
866
- }
867
- root.addEventListener("pointerup", (event) => {
868
- if (holding || singleGl() || interaction !== "hand")
869
- return;
870
- const start = press;
871
- press = null;
872
- const click = !!start && Math.hypot(event.clientX - start.x, event.clientY - start.y) < CLICK_SLOP;
873
- if (!click)
874
- return;
875
- if (pageClick()) {
876
- swipe = null;
877
- return;
878
- }
879
- // A click on the recto turns forward, on the verso back (pages already
880
- // in the air: it joins them).
881
- if (flipper) {
882
- const side = flipper.hit(event);
883
- if (!side)
884
- return;
885
- swipe = null;
886
- return go(current + (single ? 1 : side));
887
- }
888
- if (!event.target.closest(".postext-folio-page:not(.is-empty)"))
889
- return;
890
- const rect = spreadEl.getBoundingClientRect();
891
- swipe = null;
892
- if (single)
893
- go(current + 1);
894
- else
895
- go(current + ((event.clientX > rect.left + rect.width / 2) !== rtl() ? 1 : -1));
896
- });
897
- /** A click at the point pressed: the host's to act on (true), or the
898
- * page's to turn. */
899
- function pageClick() {
900
- const point = pressPoint;
901
- pressPoint = null;
902
- return !!point && !!options.onPageClick?.(point);
903
- }
904
- /** The page index in slot `side` (0 the verso, 1 the recto) of the
905
- * spread on show. */
906
- const pageInSlot = (side) => {
907
- const s = spreads[shown];
908
- if (!s)
909
- return null;
910
- return single && !singleGl() ? (s[0] ?? s[1]) : s[side];
911
- };
912
- function pageAt(event) {
913
- if (flipper) {
914
- const hit = flipper.pagePoint(event);
915
- if (!hit)
916
- return null;
917
- const page = pageInSlot(hit.side);
918
- return page === null || page === undefined ? null : { page, x: hit.x, y: hit.y };
919
- }
920
- // The DOM spread: its pages in slot order (`render`), whatever side
921
- // the layout puts them on (a right-bound book runs right to left, so
922
- // its verso stands on the right); each is found by its own box.
923
- const els = [...spreadEl.children];
924
- for (let k = 0; k < els.length; k++) {
925
- const r = els[k].getBoundingClientRect();
926
- if (event.clientX < r.left || event.clientX > r.right || event.clientY < r.top || event.clientY > r.bottom)
927
- continue;
928
- const side = (els.length === 1 ? 1 : k);
929
- const page = els.length === 1 ? pageInSlot(0) : pageInSlot(side);
930
- if (page === null || page === undefined)
931
- return null;
932
- return { page, x: (event.clientX - r.left) / r.width, y: (event.clientY - r.top) / r.height };
933
- }
934
- return null;
935
- }
936
- /** The pointer is the host's to select with (select mode, and under the
937
- * glass in magnify mode, #543). */
938
- function leftToHost() {
939
- return interaction === "select" || interaction === "magnify";
940
- }
941
- /** The page point a pointer aims at: under it, or in magnify mode under
942
- * the glass's centre (#543). */
943
- function aimedPageAt(event) {
944
- if (interaction !== "magnify")
945
- return pageAt(event);
946
- const at = heldAt(event);
947
- return pageAt({ clientX: at.x, clientY: at.y });
948
- }
949
- function pointOnScreen(point) {
950
- const s = spreads[shown];
951
- if (!s)
952
- return null;
953
- const found = pageInSlot(1) === point.page ? 1 : pageInSlot(0) === point.page ? 0 : -1;
954
- if (found < 0)
955
- return null;
956
- const side = found;
957
- if (flipper)
958
- return flipper.screenPoint(side, point.x, point.y);
959
- const els = [...spreadEl.children];
960
- const el = els.length === 1 ? els[0] : els[side];
961
- if (!el)
962
- return null;
963
- const r = el.getBoundingClientRect();
964
- return { x: r.left + point.x * r.width, y: r.top + point.y * r.height };
965
- }
966
- function applyInteraction() {
967
- root.classList.toggle("is-orbit", interaction === "orbit");
968
- root.classList.toggle("is-select", interaction === "select");
969
- root.classList.toggle("is-magnify", interaction === "magnify");
970
- if (!leftToHost())
971
- root.classList.remove("is-over-page");
972
- if (interaction !== "hand")
973
- root.classList.remove("is-over-grip");
974
- if (interaction === "magnify")
975
- scheduleDetail(0);
976
- else
977
- releaseLens();
978
- }
106
+ Object.assign(vlate, { onPage, leftToHost, aimedPageAt });
979
107
  let lastWidth = -1;
980
108
  const observer = new ResizeObserver(() => {
981
- if (disposed)
109
+ if (vs.disposed)
982
110
  return;
983
111
  const w = root.clientWidth;
984
- const wasSingle = single;
985
- const nowSingle = mode === "single" || (mode === "auto" && w > 0 && w < SINGLE_BELOW);
112
+ const wasSingle = vs.single;
113
+ const nowSingle = vs.mode === "single" || (vs.mode === "auto" && w > 0 && w < SINGLE_BELOW);
986
114
  if (lastWidth >= 0 && nowSingle !== wasSingle)
987
- rebuild(pagesOf(spreads[shown])[0] ?? 0);
115
+ rebuild(pagesOf(vs.spreads[vs.shown])[0] ?? 0);
988
116
  else
989
117
  fit();
990
118
  lastWidth = w;
@@ -995,123 +123,123 @@ export function createFolio(container, options) {
995
123
  rebuild(options.at ?? 0);
996
124
  return {
997
125
  element: root,
998
- goToPage: (index, opts) => (opts?.instant ? jump : go)(spreadOfPage(spreads, index)),
126
+ goToPage: (index, opts) => (opts?.instant ? jump : go)(spreadOfPage(vs.spreads, index)),
999
127
  goToSpread: (index, opts) => (opts?.instant ? jump : go)(index),
1000
- next: () => go(current + 1),
1001
- prev: () => go(current - 1),
128
+ next: () => go(vs.current + 1),
129
+ prev: () => go(vs.current - 1),
1002
130
  setLabels(next) {
1003
- labels = { ...labels, ...next };
131
+ vs.labels = { ...vs.labels, ...next };
1004
132
  applyLabels();
1005
133
  render();
1006
134
  },
1007
135
  get pageSize() {
1008
- return slot;
136
+ return vs.slot;
1009
137
  },
1010
138
  resetView() {
1011
- flipper?.resetOrbit();
139
+ vs.flipper?.resetOrbit();
1012
140
  },
1013
141
  getView() {
1014
- return flipper?.view() ?? null;
142
+ return vs.flipper?.view() ?? null;
1015
143
  },
1016
144
  setInteraction(next) {
1017
- if (next === interaction)
145
+ if (next === vs.interaction)
1018
146
  return;
1019
- interaction = next;
147
+ vs.interaction = next;
1020
148
  applyInteraction();
1021
149
  },
1022
150
  setMagnification(zoom) {
1023
- lensZoom = zoom ? clamp(zoom, ZOOM_MIN, ZOOM_MAX) : null;
1024
- if (lensAt)
1025
- flipper?.setLens(lensOf(lensAt));
151
+ vs.lensZoom = zoom ? clamp(zoom, ZOOM_MIN, ZOOM_MAX) : null;
152
+ if (vs.lensAt)
153
+ vs.flipper?.setLens(lensOf(vs.lensAt));
1026
154
  scheduleDetail(250);
1027
155
  },
1028
156
  getMagnifier() {
1029
- return { zoom: zoomNow(), held: lensAt !== null };
157
+ return { zoom: zoomNow(), held: vs.lensAt !== null };
1030
158
  },
1031
159
  holdMagnifier(held) {
1032
160
  if (!held) {
1033
161
  hideLens();
1034
162
  return;
1035
163
  }
1036
- if (interaction !== "magnify" || !flipper)
164
+ if (vs.interaction !== "magnify" || !vs.flipper)
1037
165
  return;
1038
166
  raiseLens();
1039
- if (lensAt)
1040
- flipper.setLens(lensOf(lensAt));
167
+ if (vs.lensAt)
168
+ vs.flipper.setLens(lensOf(vs.lensAt));
1041
169
  scheduleDetail(250);
1042
170
  },
1043
171
  pageAt: aimedPageAt,
1044
172
  pointOnScreen,
1045
173
  refreshPage(src) {
1046
- flipper?.touch(src);
174
+ vs.flipper?.touch(src);
1047
175
  },
1048
176
  setPageVideo(video) {
1049
177
  this.setPageVideos(video ? [video] : []);
1050
178
  },
1051
179
  setPageVideos(videos) {
1052
- for (const v of pageVideos)
180
+ for (const v of vs.pageVideos)
1053
181
  if (!videos.some((n) => n.element === v.element))
1054
182
  unplace(v);
1055
- pageVideos = videos.slice();
183
+ vs.pageVideos = videos.slice();
1056
184
  applyVideo();
1057
185
  },
1058
186
  setAppearance(next) {
1059
- appearance = { ...appearance, ...next };
187
+ vs.appearance = { ...vs.appearance, ...next };
1060
188
  const flip = flipAppearance();
1061
189
  const key = JSON.stringify(flip);
1062
- if (key === appearanceKey)
190
+ if (key === vs.appearanceKey)
1063
191
  return;
1064
- appearanceKey = key;
1065
- flipper?.setAppearance(flip);
192
+ vs.appearanceKey = key;
193
+ vs.flipper?.setAppearance(flip);
1066
194
  fit();
1067
195
  },
1068
196
  get state() {
1069
- return { spread: current, pages: pagesOf(spreads[current]) };
197
+ return { spread: vs.current, pages: pagesOf(vs.spreads[vs.current]) };
1070
198
  },
1071
199
  setPages(next, opts = {}) {
1072
- const atPage = opts.at ?? pagesOf(spreads[shown])[0] ?? 0;
1073
- const relayout = next.length !== pages.length ||
1074
- (opts.firstPageRecto ?? firstPageRecto) !== firstPageRecto ||
1075
- (opts.binding ?? binding) !== binding ||
1076
- (opts.mode ?? mode) !== mode ||
1077
- (opts.paper ?? paper) !== paper;
1078
- pages = next;
1079
- firstPageRecto = opts.firstPageRecto ?? firstPageRecto;
1080
- binding = opts.binding ?? binding;
1081
- mode = opts.mode ?? mode;
1082
- paper = opts.paper ?? paper;
200
+ const atPage = opts.at ?? pagesOf(vs.spreads[vs.shown])[0] ?? 0;
201
+ const relayout = next.length !== vs.pages.length ||
202
+ (opts.firstPageRecto ?? vs.firstPageRecto) !== vs.firstPageRecto ||
203
+ (opts.binding ?? vs.binding) !== vs.binding ||
204
+ (opts.mode ?? vs.mode) !== vs.mode ||
205
+ (opts.paper ?? vs.paper) !== vs.paper;
206
+ vs.pages = next;
207
+ vs.firstPageRecto = opts.firstPageRecto ?? vs.firstPageRecto;
208
+ vs.binding = opts.binding ?? vs.binding;
209
+ vs.mode = opts.mode ?? vs.mode;
210
+ vs.paper = opts.paper ?? vs.paper;
1083
211
  if (relayout)
1084
212
  return rebuild(atPage);
1085
213
  // Same spreads: the leaves take their new pages where they lie.
1086
- flipper?.setBook(bookOf(), opts.at !== undefined ? spreadOfPage(spreads, opts.at) : undefined);
214
+ vs.flipper?.setBook(bookOf(), opts.at !== undefined ? spreadOfPage(vs.spreads, opts.at) : undefined);
1087
215
  // The glass's paintings follow pages that came as other sources (a
1088
216
  // page decorated for the first time, #543).
1089
217
  scheduleDetail(0);
1090
218
  // Pages that came with another paper (a relayout moved a plate run).
1091
- const papers = JSON.stringify(pages.map(paperOf));
1092
- if (flipper && papers !== papersKey) {
219
+ const papers = JSON.stringify(vs.pages.map(paperOf));
220
+ if (vs.flipper && papers !== papersKey) {
1093
221
  const flip = flipAppearance();
1094
- appearanceKey = JSON.stringify(flip);
1095
- flipper.setAppearance(flip);
222
+ vs.appearanceKey = JSON.stringify(flip);
223
+ vs.flipper.setAppearance(flip);
1096
224
  }
1097
225
  papersKey = papers;
1098
226
  if (opts.at !== undefined)
1099
- current = shown = spreadOfPage(spreads, opts.at);
227
+ vs.current = vs.shown = spreadOfPage(vs.spreads, opts.at);
1100
228
  // The videos' pages may be painted on other canvases now.
1101
- if (pageVideos.length)
1102
- flipper?.setVideos(pageVideos.map((v) => ({ src: sourceOf(pages[v.page]), element: v.element, frame: v })));
229
+ if (vs.pageVideos.length)
230
+ vs.flipper?.setVideos(vs.pageVideos.map((v) => ({ src: sourceOf(vs.pages[v.page]), element: v.element, frame: v })));
1103
231
  fit();
1104
232
  render();
1105
233
  },
1106
234
  dispose() {
1107
- disposed = true;
1108
- clearTimeout(detailTimer);
1109
- for (const v of pageVideos)
235
+ vs.disposed = true;
236
+ clearTimeout(vs.detailTimer);
237
+ for (const v of vs.pageVideos)
1110
238
  v.element.remove();
1111
- pageVideos = [];
239
+ vs.pageVideos = [];
1112
240
  observer.disconnect();
1113
- holding?.(false);
1114
- flipper?.dispose();
241
+ vs.holding?.(false);
242
+ vs.flipper?.dispose();
1115
243
  root.remove();
1116
244
  },
1117
245
  };