postext-folio 0.52.6 → 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/postext.js CHANGED
@@ -1,142 +1,12 @@
1
- import { renderPageToCanvas } from "postext";
2
- import { folioForTrim, resolveFolioConfig } from "postext";
3
- import { BLOCK_PAGES } from "./pageFlip.js";
4
- import { createFolio } from "./viewer.js";
5
- import { attachVideoSource, autoKey, autoplayPlan, isHlsVideo, makeWay, pageVideoSpots, spotContains } from "./videos.js";
6
- /** Whether the document's first page is a recto: page 1, or a chapter
7
- * continued after an even number of pages. */
8
- export function firstPageIsRecto(doc) {
9
- return (doc.pageIndexOffset ?? 0) % 2 === 0;
10
- }
11
- /** A page's width and height as bound: the trim box, since a book is cut
12
- * (a sheet laid out with cut lines also carries the bleed, the slug and
13
- * the crop marks, none of which a bound page shows). */
14
- export function trimmedSize(page, doc) {
15
- const inset = Math.max(0, doc.trimOffset);
16
- return { width: page.width - 2 * inset, height: page.height - 2 * inset };
17
- }
18
- /** The page's colour, for blank pages: white when it has none. */
19
- function paperOf(doc) {
20
- const hex = doc.config.page.backgroundColor.hex;
21
- return hex && hex !== "transparent" ? hex : "#fff";
22
- }
23
- /** How the document's book is presented: its `folio` settings, its page
24
- * width, the book's pages before and after it (a chapter of a longer
25
- * book), with the host's own appearance laid over them. The pages round
26
- * it are the host's word when it gives them (`extraPages`: a host laying
27
- * a book out a chapter at a time knows the chapters after this one, which
28
- * the document only knows when it was given the book's page count). A
29
- * newspaper trim (`page.sizePreset` broadsheet, berliner, tabloid or
30
- * compact) shows folded newsprint unless the settings name a stock or a
31
- * binding (#506). */
32
- export function appearanceOf(doc, own) {
33
- const page = doc.pages[0];
34
- const before = Math.max(0, own?.extraPages?.before ?? doc.pageIndexOffset ?? 0);
35
- const after = Math.max(0, own?.extraPages?.after ?? (doc.bookPageCount ?? 0) - before - doc.pages.length);
36
- const folio = folioForTrim(own && "folio" in own ? own.folio : doc.config.folio, doc.config.page.sizePreset);
37
- // The document's own covers: the book's first page (a recto) and its
38
- // last, when that is a verso (an even page number). A chapter from the
39
- // middle of the book has neither: its first and last leaves are paper
40
- // (#449).
41
- const ownCovers = resolveFolioConfig(folio).binding.cover === "pages";
42
- const total = before + doc.pages.length;
43
- return {
44
- covers: { front: ownCovers && before === 0, back: ownCovers && after === 0 && total % 2 === 0 },
45
- // Page sizes are in device pixels at the page's dpi.
46
- ...(page ? { pageWidthMm: (trimmedSize(page, doc).width * 25.4) / (doc.config.page.dpi || 300) } : {}),
47
- ...own,
48
- folio,
49
- extraPages: { before, after },
50
- };
51
- }
52
- /** The paper a page is printed on, when a `:::paper` run sets one. */
53
- const pagePaperOf = (page) => page?.paper;
54
- /** What a page is painted from, as text: two pages with the same
55
- * signature under the same config paint the same. */
56
- const signatures = new WeakMap();
57
- function signature(of) {
58
- let sig = signatures.get(of);
59
- if (sig === undefined) {
60
- sig = JSON.stringify(of);
61
- signatures.set(of, sig);
62
- }
63
- return sig;
64
- }
65
- /**
66
- * Which paintings a new layout can keep: new page index → old page index,
67
- * for every page of `wanted` that is the same page as a painted one (the
68
- * same object, or one that reads the same). Each old painting goes to one
69
- * page at most.
70
- */
71
- export function carriedPaintings(before, painted, next, wanted) {
72
- const carried = new Map();
73
- const byPage = new Map();
74
- for (const k of painted)
75
- if (before[k])
76
- byPage.set(before[k], k);
77
- if (byPage.size === 0)
78
- return carried;
79
- let bySignature = null;
80
- for (const i of wanted) {
81
- const page = next[i];
82
- if (!page)
83
- continue;
84
- let k = byPage.get(page);
85
- if (k === undefined) {
86
- bySignature ??= new Map([...byPage].map(([p, j]) => [signature(p), j]));
87
- k = bySignature.get(signature(page));
88
- }
89
- if (k === undefined)
90
- continue;
91
- carried.set(i, k);
92
- byPage.delete(before[k]);
93
- bySignature = null;
94
- }
95
- return carried;
96
- }
97
- /** Pages a turn paints for its leaves, at most. */
98
- const MAX_SWEEP = 160;
99
- /**
100
- * The pages a turn to `target` sweeps past, in the order its leaves lift,
101
- * and the run it belongs to. A turn set at rest (`run` null) more than
102
- * `BLOCK_PAGES` away goes over as one block, as the renderer turns it: it
103
- * keeps only the pages on show (the block's top face), the target's being
104
- * painted with it. A target set while leaves are in the air carries the
105
- * run on leaf by leaf (the renderer lifts a block only with no leaf in
106
- * the air), so every page from where the run started (or its block
107
- * lands) to the target is swept, a run of fast clicks included.
108
- */
109
- export function sweepFor(run, settled, target, count) {
110
- if (!run) {
111
- const from = settled.length ? settled : [0];
112
- if (Math.abs((target[0] ?? 0) - (from[0] ?? 0)) > BLOCK_PAGES)
113
- return { run: { from, block: target }, pages: [...from] };
114
- run = { from, block: null };
115
- }
116
- const from = run.block ?? run.from;
117
- const lo = Math.max(0, Math.min(...from, ...target) - 1);
118
- const hi = Math.min(count - 1, Math.max(...from, ...target) + 1);
119
- const pages = [];
120
- for (let i = lo; i <= hi; i++)
121
- pages.push(i);
122
- if ((target[0] ?? 0) < (from[0] ?? 0))
123
- pages.reverse();
124
- // The block's top face stays until it lands.
125
- const swept = run.block ? [...run.from, ...pages] : pages;
126
- return { run, pages: swept.slice(0, MAX_SWEEP) };
127
- }
128
- /** Painting time per frame while leaves are in the air. */
129
- const SWEEP_BUDGET_MS = 8;
130
- /** The reader has asked for less motion: no video starts on its own to
131
- * play alongside the others. */
132
- const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
133
- /** How long the book must stay as it is before a video set to play on its
134
- * own starts (a host's layouts settling, a run of turns). */
135
- const AUTOPLAY_SETTLE_MS = 700;
136
- /** The most pixels a page is painted in for the magnifying glass. */
137
- const SHARP_MAX_PIXELS = 24e6;
138
- /** How long a resize settles before the pages are painted at the new size. */
139
- const RESIZE_SETTLE_MS = 120;
1
+ import { createFolio } from './viewer.js';
2
+ import { autoKey } from './videos.js';
3
+ import { pagePaperOf, trimmedSize, appearanceOf, firstPageIsRecto, paperOf, signature, carriedPaintings } from './postext/book.js';
4
+ import { cancelIdle, RESIZE_SETTLE_MS } from './postext/constants.js';
5
+ import { createDocumentPainting } from './postext/painting.js';
6
+ import { createDocumentVideos } from './postext/pageVideos.js';
7
+ import { createDocumentSharp } from './postext/sharp.js';
8
+ export { firstPageIsRecto, trimmedSize, appearanceOf, carriedPaintings } from './postext/book.js';
9
+ export { sweepFor } from './postext/sweep.js';
140
10
  /**
141
11
  * A postext document as a book: `createFolio` over its pages, painted with
142
12
  * `renderPageToCanvas` at exactly the device pixels of a page slot, so the
@@ -151,562 +21,48 @@ const RESIZE_SETTLE_MS = 120;
151
21
  * as for `renderPage`.
152
22
  */
153
23
  export function createFolioFromDocument(container, doc, options = {}) {
24
+ const ds = {};
25
+ const dlate = {};
154
26
  const reach = options.window ?? 3;
155
- let current = doc;
156
- let canvases = [];
27
+ ds.current = doc;
28
+ ds.canvases = [];
157
29
  /** A decorated page's canvas: its painting with the decorations over
158
30
  * it, kept (and drawn again in place) once a page has been decorated. */
159
- let decorated = [];
31
+ ds.decorated = [];
160
32
  /** Page → the slot width (device px) it was painted for. */
161
- let painted = new Map();
162
- let focus = [options.at ?? 0];
163
- let idle = 0;
33
+ ds.painted = new Map();
34
+ ds.focus = [options.at ?? 0];
35
+ ds.idle = 0;
164
36
  /** The pages on show when the book last came to rest. */
165
- let settled = [];
37
+ ds.settled = [];
166
38
  /** The pages a turn in progress sweeps past, in the order their leaves
167
39
  * lift (from the spread at rest towards the target). */
168
- let sweep = [];
169
- let sweepFrame = 0;
40
+ ds.sweep = [];
41
+ ds.sweepFrame = 0;
170
42
  /** The run of turns since the book last came to rest. */
171
- let run = null;
43
+ ds.run = null;
172
44
  /** Leaves are in the air (sent to a spread that has not landed yet). */
173
- let turning = false;
45
+ ds.turning = false;
174
46
  let resizeTimer = 0;
175
- let ready = false;
47
+ ds.ready = false;
176
48
  let hostAppearance = options.appearance;
177
49
  const slotWidth = () => viewer.pageSize.deviceWidth;
178
- const sources = () => current.pages.map((_, i) => {
179
- const canvas = canvases[i];
180
- const paper = pagePaperOf(current.pages[i]);
181
- if (painted.has(i) && canvas)
182
- return { src: decorated[i] ?? canvas, alt: options.alt?.(i) ?? `Page ${i + 1}`, ...(paper ? { paper } : {}) };
50
+ const sources = () => ds.current.pages.map((_, i) => {
51
+ const canvas = ds.canvases[i];
52
+ const paper = pagePaperOf(ds.current.pages[i]);
53
+ if (ds.painted.has(i) && canvas)
54
+ return { src: ds.decorated[i] ?? canvas, alt: options.alt?.(i) ?? `Page ${i + 1}`, ...(paper ? { paper } : {}) };
183
55
  return paper ? { src: "", paper } : "";
184
56
  });
185
- /** Pages within `reach` spreads of the ones on show. */
186
- function wanted() {
187
- const keep = new Set();
188
- const span = 2 * reach + 1;
189
- for (const p of focus)
190
- for (let i = p - span; i <= p + span; i++)
191
- if (i >= 0 && i < current.pages.length)
192
- keep.add(i);
193
- for (const i of sweep)
194
- keep.add(i);
195
- return keep;
196
- }
197
- /** Paints page `i` on a fresh canvas (the leaves cache their texture by
198
- * element) exactly `width` device pixels wide. */
199
- function paint(i, width) {
200
- const page = current.pages[i];
201
- if (!page)
202
- return;
203
- const canvas = document.createElement("canvas");
204
- // The page as bound: trimmed, without the slug and the crop marks.
205
- renderPageToCanvas(page, current, canvas, {
206
- trim: true,
207
- scale: width / trimmedSize(page, current).width,
208
- singleInk: options.singleInk,
209
- pageNegative: options.pageNegative,
210
- ...(options.printPreview ? { printPreview: options.printPreview } : {}),
211
- });
212
- const old = canvases[i];
213
- if (old)
214
- old.width = old.height = 0;
215
- canvases[i] = canvas;
216
- painted.set(i, width);
217
- // A new painting: decorated afresh, on a canvas of its own (its
218
- // texture follows the element).
219
- free(decorated[i]);
220
- decorated[i] = null;
221
- decorate(i);
222
- }
223
- const free = (canvas) => {
224
- if (canvas)
225
- canvas.width = canvas.height = 0;
226
- };
227
- /** Draws page `i`'s decorations over its painting. True when the page
228
- * shows another canvas than before (it was not decorated). */
229
- function decorate(i) {
230
- const draw = options.decorate;
231
- const base = canvases[i];
232
- const page = current.pages[i];
233
- if (!draw || !base || !page)
234
- return false;
235
- let target = decorated[i];
236
- const fresh = !target;
237
- target ??= document.createElement("canvas");
238
- if (target.width !== base.width || target.height !== base.height) {
239
- target.width = base.width;
240
- target.height = base.height;
241
- }
242
- const ctx = target.getContext("2d");
243
- if (!ctx)
244
- return false;
245
- ctx.setTransform(1, 0, 0, 1, 0, 0);
246
- ctx.globalCompositeOperation = "source-over";
247
- ctx.globalAlpha = 1;
248
- ctx.drawImage(base, 0, 0);
249
- // The page's own pixels: the canvas shows the trimmed page at its size.
250
- const inset = Math.max(0, current.trimOffset);
251
- const scale = base.width / trimmedSize(page, current).width;
252
- ctx.setTransform(scale, 0, 0, scale, -inset * scale, -inset * scale);
253
- ctx.save();
254
- const drew = draw(i, ctx);
255
- ctx.restore();
256
- if (!drew && fresh)
257
- return false;
258
- decorated[i] = target;
259
- return fresh;
260
- }
261
- /** Paints what the open spread needs now and lets go of the far pages;
262
- * the rest of the window fills in when the browser is idle. */
263
- function refresh(urgent) {
264
- if (!ready)
265
- return;
266
- const width = slotWidth();
267
- const keep = wanted();
268
- let changed = false;
269
- for (const i of urgent) {
270
- if (i < 0 || i >= current.pages.length || painted.get(i) === width)
271
- continue;
272
- paint(i, width);
273
- changed = true;
274
- }
275
- for (const i of [...painted.keys()]) {
276
- if (keep.has(i))
277
- continue;
278
- const canvas = canvases[i];
279
- if (canvas)
280
- canvas.width = canvas.height = 0;
281
- canvases[i] = null;
282
- free(decorated[i]);
283
- decorated[i] = null;
284
- painted.delete(i);
285
- changed = true;
286
- }
287
- if (changed)
288
- viewer.setPages(sources());
289
- schedule();
290
- }
291
- /** A turn across several spreads: every page its leaves carry is
292
- * painted, a frame at a time, in the order the leaves lift, so no page
293
- * goes over blank. */
294
- function startSweep(target) {
295
- const next = sweepFor(run, settled, target, current.pages.length);
296
- run = next.run;
297
- sweep = next.pages;
298
- cancelAnimationFrame(sweepFrame);
299
- sweepFrame = requestAnimationFrame(paintSweep);
300
- }
301
- function paintSweep() {
302
- sweepFrame = 0;
303
- const width = slotWidth();
304
- const start = performance.now();
305
- let changed = false;
306
- for (const i of sweep) {
307
- if (painted.get(i) === width)
308
- continue;
309
- paint(i, width);
310
- changed = true;
311
- if (performance.now() - start > SWEEP_BUDGET_MS)
312
- break;
313
- }
314
- if (!changed)
315
- return;
316
- viewer.setPages(sources());
317
- sweepFrame = requestAnimationFrame(paintSweep);
318
- }
319
- function schedule() {
320
- cancelIdle(idle);
321
- idle = onIdle(() => {
322
- const width = slotWidth();
323
- const distance = (i) => Math.min(...focus.map((f) => Math.abs(i - f)));
324
- const next = [...wanted()].filter((i) => painted.get(i) !== width).sort((a, b) => distance(a) - distance(b)).slice(0, 4);
325
- if (!next.length)
326
- return;
327
- for (const i of next)
328
- paint(i, width);
329
- viewer.setPages(sources());
330
- schedule();
331
- });
332
- }
333
- const around = (state) => {
334
- const pages = state.pages.length ? state.pages : [0];
335
- // The leaf a hand would lift next shows the neighbours' pages.
336
- return [...pages, pages[0] - 2, pages[0] - 1, pages[pages.length - 1] + 1, pages[pages.length - 1] + 2];
337
- };
338
- // ── Videos on the pages (#477, #507) ──
339
- /** The video spots of each page of the current document, as found. */
340
- let spots = new Map();
341
- /** The videos playing (or paused) on the pages, in the order they
342
- * started. */
343
- let players = [];
344
- /** The videos that have started on their own once (`player.autoplay`
345
- * with `exclusive`). */
346
- const autoplayed = new Set();
347
- /** The videos playing alongside that have started on their own since
348
- * their page last came into view (`page|key`). */
349
- const autoStarted = new Set();
350
- const spotsOf = (i) => {
351
- let found = spots.get(i);
352
- if (!found) {
353
- const page = current.pages[i];
354
- found = page ? pageVideoSpots(page, current) : [];
355
- spots.set(i, found);
356
- }
357
- return found;
358
- };
359
- /** Where a self-hosted video plays from: its file, else its address. */
360
- const urlOf = (spot) => {
361
- const v = spot.video;
362
- if (v.source !== "file")
363
- return undefined;
364
- return (v.fileId ? options.videoUrl?.(v.fileId) : undefined) ?? v.link;
365
- };
366
- const spotAt = (point) => options.videos === false ? undefined : spotsOf(point.page).find((s) => urlOf(s) && spotContains(s, point.x, point.y));
367
- const playerOf = (page, spot) => players.find((p) => p.page === page && p.spot.key === spot.key);
368
- const onShow = (p) => viewer.state.pages.includes(p.page);
369
- const report = (p, state) => {
370
- options.onVideo?.({ resourceId: p.spot.resourceId, page: p.page, state });
371
- };
372
- /** Shows the videos that have a frame to show on their pages. */
373
- function showVideos() {
374
- viewer.setPageVideos(players
375
- .filter((p) => p.shown)
376
- .map((p) => ({ page: p.page, element: p.element, origin: p.spot.origin, across: p.spot.across, down: p.spot.down, ...(p.spot.crop ? { crop: p.spot.crop } : {}) })));
377
- }
378
- /** Stops a video: its poster shows again, and the videos it held go on. */
379
- function stopPlayer(p, state = "stopped") {
380
- if (!players.includes(p))
381
- return;
382
- players = players.filter((q) => q !== p);
383
- report(p, state);
384
- p.element.pause();
385
- p.release?.();
386
- if (p.shown)
387
- showVideos();
388
- resumeHeld(p);
389
- }
390
- function stopVideos() {
391
- for (const p of [...players])
392
- stopPlayer(p);
393
- }
394
- /** The videos playing alongside that `by` held go on (on show only),
395
- * and those set to play on their own that have not started yet start. */
396
- function resumeHeld(by) {
397
- if (by.alongside)
398
- return;
399
- for (const q of players) {
400
- if (q.heldBy !== by)
401
- continue;
402
- q.heldBy = null;
403
- if (onShow(q))
404
- void q.element.play().catch(() => { });
405
- }
406
- if (!turning)
407
- autoplayOn(viewer.state.pages, { exclusive: false });
408
- }
409
- /** A video starts (or goes on): the others make way for it (see
410
- * `makeWay`). */
411
- function makeWayFor(p) {
412
- p.heldBy = null;
413
- // Those waiting for another exclusive video wait for this one now.
414
- if (!p.alongside)
415
- for (const q of players)
416
- if (q.heldBy)
417
- q.heldBy = p;
418
- const list = [...players.filter((q) => q !== p), p].map((q) => ({ q, alongside: q.alongside, paused: q.element.paused }));
419
- const way = makeWay(list[list.length - 1], list);
420
- for (const { q } of way.hold) {
421
- q.heldBy = p;
422
- q.element.pause();
423
- }
424
- for (const { q } of way.pause)
425
- q.element.pause();
426
- for (const { q } of way.stop)
427
- stopPlayer(q);
428
- }
429
- /** Plays the video at `spot` on `page`, from its start; `auto` when it
430
- * starts on its own. */
431
- function startVideo(page, spot, auto = false) {
432
- const url = urlOf(spot);
433
- if (!url)
434
- return;
435
- const player = spot.video.player;
436
- const alongside = player.exclusive === false;
437
- const element = document.createElement("video");
438
- element.playsInline = true;
439
- element.preload = "auto";
440
- // WebGL draws the picture only when its server allows the read.
441
- if (!/^(blob|data):/i.test(url))
442
- element.crossOrigin = "anonymous";
443
- const range = { start: spot.video.start ?? 0, end: spot.video.end };
444
- // A loop from the file's start loops by itself (seamlessly); one from
445
- // later on is sent back by hand.
446
- element.loop = player.loop && !range.start;
447
- // A video that starts on its own has its sound once the reader has
448
- // clicked or typed on the page (the browser allows it then); before
449
- // that it starts muted, as browsers require, and a click unmutes it.
450
- // One playing alongside always starts muted on its own: a page of
451
- // living pictures is a quiet one.
452
- const activated = navigator.userActivation?.hasBeenActive ?? false;
453
- element.muted = player.muted || (auto && (alongside || !activated));
454
- const entry = { page, spot, element, shown: false, autoMuted: !player.muted && element.muted, alongside, heldBy: null };
455
- // The others make way at once (an exclusive one, still loading, holds
456
- // the videos playing alongside from now).
457
- players = [...players, entry];
458
- makeWayFor(entry);
459
- const live = () => players.includes(entry);
460
- element.addEventListener("error", () => live() && stopPlayer(entry, "error"));
461
- element.addEventListener("loadedmetadata", () => {
462
- if (range.start)
463
- element.currentTime = range.start;
464
- }, { once: true });
465
- element.addEventListener("loadeddata", () => {
466
- if (!live())
467
- return;
468
- entry.shown = true;
469
- showVideos();
470
- }, { once: true });
471
- element.addEventListener("play", () => live() && makeWayFor(entry));
472
- element.addEventListener("playing", () => live() && report(entry, "playing"));
473
- element.addEventListener("pause", () => {
474
- if (!live() || element.ended)
475
- return;
476
- report(entry, "paused");
477
- resumeHeld(entry);
478
- });
479
- const atEnd = () => {
480
- if (!live())
481
- return;
482
- if (player.loop) {
483
- element.currentTime = range.start;
484
- void element.play().catch(() => { });
485
- }
486
- else
487
- stopPlayer(entry);
488
- };
489
- element.addEventListener("ended", atEnd);
490
- element.addEventListener("timeupdate", () => {
491
- if (range.end && element.currentTime >= range.end)
492
- atEnd();
493
- });
494
- // The picture's height on screen: an HLS stream needs no larger variant.
495
- const size = viewer.pageSize;
496
- const maxHeight = Math.max(Math.hypot(spot.down.x * size.deviceWidth, spot.down.y * size.deviceHeight), Math.hypot(spot.across.x * size.deviceWidth, spot.across.y * size.deviceHeight));
497
- void attachVideoSource(element, url, { hls: isHlsVideo(spot.video, url), maxHeight, corsTag: true }).then((release) => {
498
- if (!live())
499
- return release();
500
- entry.release = release;
501
- // Held back already (an exclusive video started meanwhile).
502
- if (entry.heldBy)
503
- return;
504
- // The click's gesture lets it play with sound; a browser that still
505
- // refuses gets it muted.
506
- element.play().catch((err) => {
507
- if (!live())
508
- return;
509
- if (err?.name === "NotAllowedError" && !element.muted) {
510
- element.muted = true;
511
- entry.autoMuted = !player.muted;
512
- return element.play();
513
- }
514
- throw err;
515
- }).catch(() => live() && stopPlayer(entry, "error"));
516
- }, () => live() && stopPlayer(entry, "error"));
517
- }
518
- /** A click at a point of a page: plays, pauses or resumes the video
519
- * printed there. */
520
- function toggleVideoAt(point) {
521
- const spot = spotAt(point);
522
- if (!spot)
523
- return false;
524
- const p = playerOf(point.page, spot);
525
- if (p)
526
- togglePlaying(p);
527
- else
528
- startVideo(point.page, spot);
529
- return true;
530
- }
531
- /** A click (or Space) on a video playing: one that started on its own
532
- * is unmuted first; otherwise it pauses or resumes. */
533
- function togglePlaying(p) {
534
- if (p.autoMuted && !p.element.paused) {
535
- p.autoMuted = false;
536
- p.element.muted = false;
537
- return;
538
- }
539
- if (p.element.paused)
540
- void p.element.play().catch(() => { });
541
- else
542
- p.element.pause();
543
- }
544
- /** Autoplay waits for the book to settle: a host lays a book out in
545
- * steps (a chapter, then the whole book), and only the spread the reader
546
- * is left looking at counts as shown. */
547
- let autoTimer = 0;
548
- const scheduleAutoplay = () => {
549
- clearTimeout(autoTimer);
550
- autoTimer = window.setTimeout(() => {
551
- if (!turning)
552
- autoplayOn(viewer.state.pages);
553
- }, AUTOPLAY_SETTLE_MS);
554
- };
555
- /**
556
- * The videos on show set to play on their own (`player.autoplay`) start,
557
- * muted. One playing alongside the others (`exclusive: false`) starts
558
- * each time its page comes into view (not while an exclusive video
559
- * plays there, nor when the reader asks for less motion); an exclusive
560
- * one, the first time its spread is shown and never again in this
561
- * viewer, when no other exclusive video is on.
562
- */
563
- function autoplayOn(pages, { exclusive = true } = {}) {
564
- if (options.videos === false)
565
- return;
566
- const state = {
567
- players: players.map((p) => ({ page: p.page, key: p.spot.key, alongside: p.alongside, paused: p.element.paused, shown: p.shown })),
568
- autoplayed,
569
- autoStarted,
570
- };
571
- for (const { page, spot } of autoplayPlan(pages, spotsOf, (s) => !!urlOf(s), state, { exclusive, reducedMotion: reducedMotion() })) {
572
- if (spot.video.player.exclusive === false)
573
- autoStarted.add(autoKey(page, spot.key));
574
- else
575
- autoplayed.add(spot.key);
576
- startVideo(page, spot, true);
577
- }
578
- }
579
- /** A new layout: each video follows its resource to where the layout
580
- * puts it, or stops when it is gone (or, at rest, off the pages on
581
- * show). */
582
- function followVideos() {
583
- spots = new Map();
584
- let moved = false;
585
- for (const p of [...players]) {
586
- const near = [p.page, ...viewer.state.pages];
587
- for (let i = 0; i < current.pages.length; i++)
588
- near.push(i);
589
- const at = near.find((i) => spotsOf(i).some((s) => s.key === p.spot.key && urlOf(s) === urlOf(p.spot)));
590
- if (at === undefined || (!turning && !viewer.state.pages.includes(at))) {
591
- stopPlayer(p);
592
- continue;
593
- }
594
- p.page = at;
595
- p.spot = spotsOf(at).find((s) => s.key === p.spot.key && urlOf(s) === urlOf(p.spot));
596
- moved ||= p.shown;
597
- }
598
- if (moved)
599
- showVideos();
600
- }
601
- // ── Sharper paintings for the magnifying glass (#527) ──
602
- /** Page → its painting for the glass (with its decorations), the page
603
- * as painted without them while it has some (to draw them again from,
604
- * #543), the width it was painted at and the layout it was painted
605
- * from. */
606
- const sharp = new Map();
607
- let sharpChain = Promise.resolve();
608
- /** Pages whose sharp paintings wait to be decorated again (once a
609
- * frame: a drag selects on every move). */
610
- const sharpDirty = new Set();
611
- let sharpFrame = 0;
612
- /** Draws page `i`'s decorations over its painting for the glass, from
613
- * the bare painting (kept while the page has decorations). True when
614
- * it drew some. */
615
- function decorateSharp(i, entry) {
616
- const draw = options.decorate;
617
- const page = entry.doc.pages[i];
618
- const ctx = entry.canvas.getContext("2d");
619
- if (!draw || !page || !ctx)
620
- return false;
621
- if (entry.bare) {
622
- ctx.setTransform(1, 0, 0, 1, 0, 0);
623
- ctx.globalCompositeOperation = "source-over";
624
- ctx.globalAlpha = 1;
625
- ctx.drawImage(entry.bare, 0, 0);
626
- }
627
- else {
628
- const bare = document.createElement("canvas");
629
- bare.width = entry.canvas.width;
630
- bare.height = entry.canvas.height;
631
- bare.getContext("2d")?.drawImage(entry.canvas, 0, 0);
632
- entry.bare = bare;
633
- }
634
- const inset = Math.max(0, entry.doc.trimOffset);
635
- const scale = entry.canvas.width / trimmedSize(page, entry.doc).width;
636
- ctx.setTransform(scale, 0, 0, scale, -inset * scale, -inset * scale);
637
- ctx.save();
638
- const drew = draw(i, ctx);
639
- ctx.restore();
640
- ctx.setTransform(1, 0, 0, 1, 0, 0);
641
- // No decorations: the painting is bare again.
642
- if (!drew) {
643
- free(entry.bare);
644
- entry.bare = null;
645
- }
646
- return drew;
647
- }
648
- /** The glass's paintings of these pages decorated again (the selection
649
- * moved), and shown again. */
650
- function redecorateSharp() {
651
- sharpFrame = 0;
652
- for (const i of sharpDirty) {
653
- const entry = sharp.get(i);
654
- if (!entry || entry.doc !== current)
655
- continue;
656
- decorateSharp(i, entry);
657
- viewer.refreshPage(entry.canvas);
658
- }
659
- sharpDirty.clear();
660
- }
661
- /** Page `i` painted `width` px wide for the glass, within 1.5 times the
662
- * page's own resolution and 24 megapixels; one page at a time, each in
663
- * a task of its own (a newspaper page takes a while to paint). */
664
- function paintSharp(i, width) {
665
- const done = sharpChain.then(() => new Promise((resolve) => {
666
- setTimeout(() => {
667
- const page = current.pages[i];
668
- if (!page)
669
- return resolve(null);
670
- const size = trimmedSize(page, current);
671
- const w = Math.max(1, Math.min(width, Math.round(size.width * 1.5), Math.floor(Math.sqrt((SHARP_MAX_PIXELS * size.width) / size.height))));
672
- const had = sharp.get(i);
673
- if (had && had.doc === current && had.width >= w)
674
- return resolve(had.canvas);
675
- const canvas = document.createElement("canvas");
676
- const scale = w / size.width;
677
- renderPageToCanvas(page, current, canvas, { trim: true, scale, singleInk: options.singleInk, pageNegative: options.pageNegative, ...(options.printPreview ? { printPreview: options.printPreview } : {}) });
678
- const entry = { canvas, bare: null, width: w, doc: current };
679
- // The decorations too (a selection), over the page as painted.
680
- decorateSharp(i, entry);
681
- if (had)
682
- freeSharp(had);
683
- sharp.delete(i);
684
- sharp.set(i, entry);
685
- // A few pages kept: the spread on show and the one before.
686
- for (const [k, v] of sharp) {
687
- if (sharp.size <= 4)
688
- break;
689
- freeSharp(v);
690
- sharp.delete(k);
691
- }
692
- resolve(canvas);
693
- }, 0);
694
- }));
695
- sharpChain = done.catch(() => undefined);
696
- return done;
697
- }
698
- function freeSharp(entry) {
699
- free(entry.canvas);
700
- free(entry.bare);
701
- }
702
- function releaseSharp() {
703
- for (const v of sharp.values())
704
- freeSharp(v);
705
- sharp.clear();
706
- sharpDirty.clear();
707
- cancelAnimationFrame(sharpFrame);
708
- sharpFrame = 0;
709
- }
57
+ const { wanted, paint, free, decorate, refresh, startSweep, around } = createDocumentPainting({
58
+ options, reach, slotWidth, sources, ds, dlate
59
+ });
60
+ const { autoplayed, autoStarted, spotsOf, urlOf, spotAt, stopPlayer, stopVideos, startVideo, toggleVideoAt, togglePlaying, scheduleAutoplay, followVideos } = createDocumentVideos({
61
+ options, ds, dlate
62
+ });
63
+ const { sharp, sharpDirty, redecorateSharp, paintSharp, releaseSharp } = createDocumentSharp({
64
+ options, free, ds, dlate
65
+ });
710
66
  const first = doc.pages[0];
711
67
  const viewer = createFolio(container, {
712
68
  ...options,
@@ -721,8 +77,8 @@ export function createFolioFromDocument(container, doc, options = {}) {
721
77
  binding: doc.binding === "right" ? "right" : "left",
722
78
  paper: options.paper ?? paperOf(doc),
723
79
  onTarget: (state) => {
724
- focus = state.pages;
725
- turning = true;
80
+ ds.focus = state.pages;
81
+ ds.turning = true;
726
82
  // The sweep first: the pages it keeps are not let go by the refresh.
727
83
  startSweep(state.pages);
728
84
  refresh(around(state));
@@ -733,46 +89,47 @@ export function createFolioFromDocument(container, doc, options = {}) {
733
89
  onChange: (state) => {
734
90
  // A video whose page has turned away stops; one set to play on its
735
91
  // own starts when its spread is shown (see `autoplayOn`).
736
- for (const p of [...players])
92
+ for (const p of [...ds.players])
737
93
  if (!state.pages.includes(p.page))
738
94
  stopPlayer(p);
739
95
  for (const key of [...autoStarted])
740
96
  if (!state.pages.some((i) => key.startsWith(autoKey(i, ""))))
741
97
  autoStarted.delete(key);
742
98
  scheduleAutoplay();
743
- focus = state.pages;
744
- settled = state.pages;
745
- turning = false;
99
+ ds.focus = state.pages;
100
+ ds.settled = state.pages;
101
+ ds.turning = false;
746
102
  // At rest: the pages the turn carried past are let go.
747
- sweep = [];
748
- run = null;
749
- cancelAnimationFrame(sweepFrame);
103
+ ds.sweep = [];
104
+ ds.run = null;
105
+ cancelAnimationFrame(ds.sweepFrame);
750
106
  refresh([]);
751
107
  options.onChange?.(state);
752
108
  },
753
109
  onLayout: (size) => {
754
110
  options.onLayout?.(size);
755
- if (!ready)
111
+ if (!ds.ready)
756
112
  return;
757
113
  // Painted again once the box stops changing (a dragged divider).
758
114
  clearTimeout(resizeTimer);
759
115
  resizeTimer = window.setTimeout(() => refresh(around(viewer.state)), RESIZE_SETTLE_MS);
760
116
  },
761
117
  });
118
+ Object.assign(dlate, { viewer });
762
119
  // The open spread, painted at the slot's size before the first frame.
763
- ready = true;
764
- focus = viewer.state.pages;
765
- settled = viewer.state.pages;
120
+ ds.ready = true;
121
+ ds.focus = viewer.state.pages;
122
+ ds.settled = viewer.state.pages;
766
123
  refresh(around(viewer.state));
767
124
  scheduleAutoplay();
768
125
  // Debugging (`__postextFolioPreserve`, as for the renderer): the videos.
769
126
  if (globalThis.__postextFolioPreserve) {
770
127
  globalThis.__postextFolioVideos = {
771
- playing: () => players,
128
+ playing: () => ds.players,
772
129
  autoplayed,
773
130
  spots: (i) => spotsOf(i),
774
131
  state: () => viewer.state,
775
- turning: () => turning,
132
+ turning: () => ds.turning,
776
133
  };
777
134
  }
778
135
  // Space plays or pauses the first video on the spread on show.
@@ -780,7 +137,7 @@ export function createFolioFromDocument(container, doc, options = {}) {
780
137
  if (event.target !== viewer.element || (event.key !== " " && event.key !== "Enter") || options.videos === false)
781
138
  return;
782
139
  const on = viewer.state.pages;
783
- const shown = players.filter((p) => on.includes(p.page));
140
+ const shown = ds.players.filter((p) => on.includes(p.page));
784
141
  const p = shown.find((x) => !x.alongside) ?? shown[0];
785
142
  if (p)
786
143
  togglePlaying(p);
@@ -815,7 +172,7 @@ export function createFolioFromDocument(container, doc, options = {}) {
815
172
  pointOnScreen: viewer.pointOnScreen,
816
173
  setAppearance(next) {
817
174
  hostAppearance = { ...hostAppearance, ...next };
818
- viewer.setAppearance(appearanceOf(current, hostAppearance));
175
+ viewer.setAppearance(appearanceOf(ds.current, hostAppearance));
819
176
  },
820
177
  get pageSize() {
821
178
  return viewer.pageSize;
@@ -827,15 +184,15 @@ export function createFolioFromDocument(container, doc, options = {}) {
827
184
  if (opts.appearance)
828
185
  hostAppearance = { ...hostAppearance, ...opts.appearance };
829
186
  const at = Math.max(0, Math.min(next.pages.length - 1, opts.at ?? viewer.state.pages[0] ?? 0));
830
- const before = current;
831
- const oldCanvases = canvases;
832
- const oldDecorated = decorated;
833
- const oldPainted = painted;
834
- current = next;
835
- canvases = [];
836
- decorated = [];
837
- painted = new Map();
838
- focus = [at];
187
+ const before = ds.current;
188
+ const oldCanvases = ds.canvases;
189
+ const oldDecorated = ds.decorated;
190
+ const oldPainted = ds.painted;
191
+ ds.current = next;
192
+ ds.canvases = [];
193
+ ds.decorated = [];
194
+ ds.painted = new Map();
195
+ ds.focus = [at];
839
196
  // A page that reads the same keeps its painting (and its texture):
840
197
  // a relayout elsewhere in the book (the plan settling, an edit in
841
198
  // another chapter) leaves the open spread as it is.
@@ -845,12 +202,12 @@ export function createFolioFromDocument(container, doc, options = {}) {
845
202
  const canvas = oldCanvases[k];
846
203
  if (!canvas)
847
204
  continue;
848
- canvases[i] = canvas;
849
- painted.set(i, oldPainted.get(k));
205
+ ds.canvases[i] = canvas;
206
+ ds.painted.set(i, oldPainted.get(k));
850
207
  oldCanvases[k] = null;
851
208
  // Its decorated canvas too (drawn again below, for the new
852
209
  // document's selection).
853
- decorated[i] = oldDecorated[k] ?? null;
210
+ ds.decorated[i] = oldDecorated[k] ?? null;
854
211
  oldDecorated[k] = null;
855
212
  }
856
213
  }
@@ -858,14 +215,14 @@ export function createFolioFromDocument(container, doc, options = {}) {
858
215
  // blank; the old paintings no page took are let go.
859
216
  const width = slotWidth();
860
217
  for (let i = at - 2; i <= at + 3; i++)
861
- if (i >= 0 && i < next.pages.length && painted.get(i) !== width)
218
+ if (i >= 0 && i < next.pages.length && ds.painted.get(i) !== width)
862
219
  paint(i, width);
863
220
  for (const canvas of oldCanvases)
864
221
  if (canvas)
865
222
  canvas.width = canvas.height = 0;
866
223
  for (const canvas of oldDecorated)
867
224
  free(canvas);
868
- for (const i of painted.keys())
225
+ for (const i of ds.painted.keys())
869
226
  decorate(i);
870
227
  viewer.setAppearance(appearanceOf(next, hostAppearance));
871
228
  viewer.setPages(sources(), {
@@ -874,13 +231,13 @@ export function createFolioFromDocument(container, doc, options = {}) {
874
231
  paper: options.paper ?? paperOf(next),
875
232
  at,
876
233
  });
877
- focus = viewer.state.pages;
234
+ ds.focus = viewer.state.pages;
878
235
  // A relayout while leaves are in the air (the chapter the book was
879
236
  // sent to becoming the active one) keeps the pages they carry.
880
- if (!turning) {
881
- settled = viewer.state.pages;
882
- sweep = [];
883
- run = null;
237
+ if (!ds.turning) {
238
+ ds.settled = viewer.state.pages;
239
+ ds.sweep = [];
240
+ ds.run = null;
884
241
  }
885
242
  refresh(around(viewer.state));
886
243
  followVideos();
@@ -889,10 +246,10 @@ export function createFolioFromDocument(container, doc, options = {}) {
889
246
  redecorate(pages) {
890
247
  const list = pages ? [...pages] : null;
891
248
  let swapped = false;
892
- for (const i of list ?? [...painted.keys()]) {
893
- if (!painted.has(i))
249
+ for (const i of list ?? [...ds.painted.keys()]) {
250
+ if (!ds.painted.has(i))
894
251
  continue;
895
- const had = decorated[i];
252
+ const had = ds.decorated[i];
896
253
  if (decorate(i))
897
254
  swapped = true;
898
255
  else if (had)
@@ -904,28 +261,26 @@ export function createFolioFromDocument(container, doc, options = {}) {
904
261
  for (const i of list ?? [...sharp.keys()])
905
262
  if (sharp.has(i))
906
263
  sharpDirty.add(i);
907
- if (sharpDirty.size && !sharpFrame)
908
- sharpFrame = requestAnimationFrame(redecorateSharp);
264
+ if (sharpDirty.size && !ds.sharpFrame)
265
+ ds.sharpFrame = requestAnimationFrame(redecorateSharp);
909
266
  },
910
267
  dispose() {
911
- clearTimeout(autoTimer);
268
+ clearTimeout(ds.autoTimer);
912
269
  stopVideos();
913
- cancelIdle(idle);
914
- cancelAnimationFrame(sweepFrame);
270
+ cancelIdle(ds.idle);
271
+ cancelAnimationFrame(ds.sweepFrame);
915
272
  clearTimeout(resizeTimer);
916
273
  viewer.dispose();
917
274
  releaseSharp();
918
- for (const canvas of canvases)
275
+ for (const canvas of ds.canvases)
919
276
  if (canvas)
920
277
  canvas.width = canvas.height = 0;
921
- for (const canvas of decorated)
278
+ for (const canvas of ds.decorated)
922
279
  free(canvas);
923
- canvases = [];
924
- decorated = [];
925
- painted.clear();
280
+ ds.canvases = [];
281
+ ds.decorated = [];
282
+ ds.painted.clear();
926
283
  },
927
284
  };
928
285
  }
929
- const onIdle = (cb) => typeof requestIdleCallback === "function" ? requestIdleCallback(cb, { timeout: 500 }) : setTimeout(cb, 50);
930
- const cancelIdle = (id) => (typeof cancelIdleCallback === "function" ? cancelIdleCallback(id) : clearTimeout(id));
931
286
  //# sourceMappingURL=postext.js.map