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.
- package/dist/postext/book.d.ts +35 -0
- package/dist/postext/book.d.ts.map +1 -0
- package/dist/postext/book.js +96 -0
- package/dist/postext/book.js.map +1 -0
- package/dist/postext/constants.d.ts +36 -0
- package/dist/postext/constants.d.ts.map +1 -0
- package/dist/postext/constants.js +13 -0
- package/dist/postext/constants.js.map +1 -0
- package/dist/postext/pageVideos.d.ts +26 -0
- package/dist/postext/pageVideos.d.ts.map +1 -0
- package/dist/postext/pageVideos.js +276 -0
- package/dist/postext/pageVideos.js.map +1 -0
- package/dist/postext/painting.d.ts +30 -0
- package/dist/postext/painting.d.ts.map +1 -0
- package/dist/postext/painting.js +167 -0
- package/dist/postext/painting.js.map +1 -0
- package/dist/postext/sharp.d.ts +19 -0
- package/dist/postext/sharp.d.ts.map +1 -0
- package/dist/postext/sharp.js +121 -0
- package/dist/postext/sharp.js.map +1 -0
- package/dist/postext/state.d.ts +29 -0
- package/dist/postext/state.d.ts.map +1 -0
- package/dist/postext/state.js +2 -0
- package/dist/postext/state.js.map +1 -0
- package/dist/postext/sweep.d.ts +24 -0
- package/dist/postext/sweep.d.ts.map +1 -0
- package/dist/postext/sweep.js +37 -0
- package/dist/postext/sweep.js.map +1 -0
- package/dist/postext/types.d.ts +77 -0
- package/dist/postext/types.d.ts.map +1 -0
- package/dist/postext/types.js +2 -0
- package/dist/postext/types.js.map +1 -0
- package/dist/postext.d.ts +6 -124
- package/dist/postext.d.ts.map +1 -1
- package/dist/postext.js +87 -732
- package/dist/postext.js.map +1 -1
- package/dist/viewer/constants.d.ts +30 -0
- package/dist/viewer/constants.d.ts.map +1 -0
- package/dist/viewer/constants.js +49 -0
- package/dist/viewer/constants.js.map +1 -0
- package/dist/viewer/input.d.ts +15 -0
- package/dist/viewer/input.d.ts.map +1 -0
- package/dist/viewer/input.js +142 -0
- package/dist/viewer/input.js.map +1 -0
- package/dist/viewer/layout.d.ts +24 -0
- package/dist/viewer/layout.d.ts.map +1 -0
- package/dist/viewer/layout.js +183 -0
- package/dist/viewer/layout.js.map +1 -0
- package/dist/viewer/lens.d.ts +42 -0
- package/dist/viewer/lens.d.ts.map +1 -0
- package/dist/viewer/lens.js +166 -0
- package/dist/viewer/lens.js.map +1 -0
- package/dist/viewer/pointer.d.ts +37 -0
- package/dist/viewer/pointer.d.ts.map +1 -0
- package/dist/viewer/pointer.js +250 -0
- package/dist/viewer/pointer.js.map +1 -0
- package/dist/viewer/state.d.ts +70 -0
- package/dist/viewer/state.d.ts.map +1 -0
- package/dist/viewer/state.js +2 -0
- package/dist/viewer/state.js.map +1 -0
- package/dist/viewer/turning.d.ts +34 -0
- package/dist/viewer/turning.d.ts.map +1 -0
- package/dist/viewer/turning.js +156 -0
- package/dist/viewer/turning.js.map +1 -0
- package/dist/viewer/types.d.ts +241 -0
- package/dist/viewer/types.d.ts.map +1 -0
- package/dist/viewer/types.js +2 -0
- package/dist/viewer/types.js.map +1 -0
- package/dist/viewer.d.ts +3 -242
- package/dist/viewer.d.ts.map +1 -1
- package/dist/viewer.js +104 -976
- package/dist/viewer.js.map +1 -1
- package/package.json +3 -3
package/dist/postext.js
CHANGED
|
@@ -1,142 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
31
|
+
ds.decorated = [];
|
|
160
32
|
/** Page → the slot width (device px) it was painted for. */
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
40
|
+
ds.sweep = [];
|
|
41
|
+
ds.sweepFrame = 0;
|
|
170
42
|
/** The run of turns since the book last came to rest. */
|
|
171
|
-
|
|
43
|
+
ds.run = null;
|
|
172
44
|
/** Leaves are in the air (sent to a spread that has not landed yet). */
|
|
173
|
-
|
|
45
|
+
ds.turning = false;
|
|
174
46
|
let resizeTimer = 0;
|
|
175
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|