postext-folio 0.6.1 → 0.7.1

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/paper.d.ts CHANGED
@@ -37,8 +37,15 @@ export interface PaperSpec {
37
37
  * through about twice the light its opacity loses (opacity is a contrast
38
38
  * over a black backing; diffuse transmittance runs 15–20 % for 80 g/m²
39
39
  * offset, a third and more for bible paper).
40
+ *
41
+ * That bending length is the stock's, in millimetres, so across a sheet
42
+ * wider than a book's page (`pageWidthMm`: a tabloid, a Berliner, a
43
+ * broadsheet) it is a smaller share of the page: such a sheet rolls
44
+ * tighter for its width, flutters more and swings back more slowly, as a
45
+ * longer pendulum does. A broadsheet of newsprint flops where a book page
46
+ * of the same paper curls.
40
47
  */
41
- export declare function paperSpec(paper: ResolvedPaper): PaperSpec;
48
+ export declare function paperSpec(paper: ResolvedPaper, pageWidthMm?: number): PaperSpec;
42
49
  /** The paper of a page: the book's, with the page's own stock over it (a
43
50
  * page that names a stock takes that stock's defaults, not the book's). */
44
51
  export declare function pagePaper(book: FolioConfig | undefined, own: FolioPaperConfig | undefined): ResolvedPaper;
@@ -1 +1 @@
1
- {"version":3,"file":"paper.d.ts","sourceRoot":"","sources":["../src/paper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEhH,MAAM,MAAM,aAAa,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAEzD,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB;6EACyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;CACf;AAwBD;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAoBzD;AAED;4EAC4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,GAAG,EAAE,gBAAgB,GAAG,SAAS,GAAG,aAAa,CAIzG;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAM9D"}
1
+ {"version":3,"file":"paper.d.ts","sourceRoot":"","sources":["../src/paper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEhH,MAAM,MAAM,aAAa,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAEzD,8DAA8D;AAC9D,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,aAAa,CAAC;IACrB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB;6EACyE;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;CACf;AAkCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAqB/E;AAED;4EAC4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,EAAE,GAAG,EAAE,gBAAgB,GAAG,SAAS,GAAG,aAAa,CAIzG;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAM9D"}
package/dist/paper.js CHANGED
@@ -12,8 +12,17 @@ const OPACITY_K = {
12
12
  cardStock: 0.03,
13
13
  board: 0.03,
14
14
  };
15
+ /** How much more of the reverse's print shows than the sheet's opacity
16
+ * alone lets through: on newsprint the coldset ink is not dried on the
17
+ * surface but soaks into the open groundwood sheet (strike-through), so
18
+ * the other side reads through more than its 90 % opacity suggests. */
19
+ const STRIKE_THROUGH = {
20
+ newsprint: 1.7,
21
+ };
15
22
  /** The reference leaf: 90 g/m² × 1.25 cm³/g. */
16
23
  const REF_CALIPER_UM = 112.5;
24
+ /** Pages up to this wide (mm) curl as their stock alone says. */
25
+ const LARGE_PAGE_MM = 230;
17
26
  const smooth = (a, b, x) => {
18
27
  const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
19
28
  return t * t * (3 - 2 * t);
@@ -28,22 +37,30 @@ const smooth = (a, b, x) => {
28
37
  * through about twice the light its opacity loses (opacity is a contrast
29
38
  * over a black backing; diffuse transmittance runs 15–20 % for 80 g/m²
30
39
  * offset, a third and more for bible paper).
40
+ *
41
+ * That bending length is the stock's, in millimetres, so across a sheet
42
+ * wider than a book's page (`pageWidthMm`: a tabloid, a Berliner, a
43
+ * broadsheet) it is a smaller share of the page: such a sheet rolls
44
+ * tighter for its width, flutters more and swings back more slowly, as a
45
+ * longer pendulum does. A broadsheet of newsprint flops where a book page
46
+ * of the same paper curls.
31
47
  */
32
- export function paperSpec(paper) {
48
+ export function paperSpec(paper, pageWidthMm) {
33
49
  const caliperUm = paper.grammage * paper.bulk;
34
50
  const ratio = Math.pow(caliperUm / REF_CALIPER_UM, 2 / 3);
51
+ const size = pageWidthMm && pageWidthMm > LARGE_PAGE_MM ? LARGE_PAGE_MM / pageWidthMm : 1;
35
52
  const opacity = 1 - Math.exp(-OPACITY_K[paper.type] * paper.grammage);
36
53
  const finish = paper.finish;
37
54
  return {
38
55
  paper,
39
56
  caliperMm: caliperUm / 1000,
40
- roll: Math.min(6, Math.max(0.45, ratio)),
57
+ roll: Math.min(6, Math.max(0.45, ratio)) * size,
41
58
  rigidity: smooth(1.3, 4, ratio),
42
59
  opacity,
43
- showThrough: paper.showThrough ? Math.min(0.4, (1 - opacity) * 1.25) : 0,
60
+ showThrough: paper.showThrough ? Math.min(0.4, (1 - opacity) * 1.25 * (STRIKE_THROUGH[paper.type] ?? 1)) : 0,
44
61
  transmission: paper.showThrough ? Math.min(0.5, (1 - opacity) * 2) : 0,
45
62
  follow: 0.35 / (1 + 0.35 * Math.max(0, Math.log(ratio))),
46
- spring: 0.0085 * Math.min(1.6, Math.max(0.75, Math.sqrt(ratio))),
63
+ spring: 0.0085 * Math.min(1.6, Math.max(0.75, Math.sqrt(ratio))) * Math.sqrt(size),
47
64
  roughness: finish === "gloss" ? 0.32 : finish === "silk" ? 0.5 : finish === "matte" ? 0.7 : 0.88,
48
65
  clearcoat: finish === "gloss" ? 1 : finish === "silk" ? 0.45 : finish === "matte" ? 0.12 : 0,
49
66
  clearcoatRoughness: finish === "gloss" ? 0.06 : finish === "silk" ? 0.28 : 0.6,
package/dist/paper.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"paper.js","sourceRoot":"","sources":["../src/paper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"paper.js","sourceRoot":"","sources":["../src/paper.ts"],"names":[],"mappings":"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"}
package/dist/postext.d.ts CHANGED
@@ -22,11 +22,17 @@ export interface FolioDocumentOptions extends Omit<FolioOptions, "pages" | "firs
22
22
  /**
23
23
  * Videos printed on the pages play there (#477): a click on a video's
24
24
  * poster plays it on the page (in every pointer mode), a click on it
25
- * again pauses it, and it goes on playing while its leaf turns. A video
26
- * with `player.autoplay` starts by itself the first time its spread is
27
- * shown (muted until the reader has interacted with the page). Starting another
28
- * stops the one playing; it stops too when the book comes to rest on a
29
- * spread that does not show it. Self-hosted videos only (a file, or its
25
+ * again pauses it, and it goes on playing while its leaf turns. Several
26
+ * may play at once (#507, up to eight drawn): starting a video stops the
27
+ * other exclusive ones (`player.exclusive`, the default) and holds those
28
+ * playing alongside (`exclusive: false`) until it pauses or ends; one
29
+ * playing alongside pauses only the exclusive ones. A video with
30
+ * `player.autoplay` starts by itself, muted: an exclusive one the first
31
+ * time its spread is shown (with its sound once the reader has
32
+ * interacted with the page), one playing alongside each time its page
33
+ * comes into view, unless the reader asks for less motion
34
+ * (`prefers-reduced-motion`). A video stops when the book comes to rest
35
+ * on a spread that does not show it. Self-hosted videos only (a file, or its
30
36
  * address: MP4, WebM or an HLS stream); a YouTube or Vimeo poster turns
31
37
  * the page as any other. The video's server must allow cross-origin
32
38
  * reads (CORS) for WebGL to draw it. Default true.
@@ -61,7 +67,7 @@ export interface FolioDocumentViewer extends FolioViewer {
61
67
  /** Plays or pauses the video printed at a point of a page, as a click
62
68
  * there does; false when no playable video lies there. */
63
69
  toggleVideoAt(point: FolioPagePoint): boolean;
64
- /** Stops the video playing on a page (its poster shows again). */
70
+ /** Stops the videos playing on the pages (their posters show again). */
65
71
  stopVideo(): void;
66
72
  }
67
73
  /** Whether the document's first page is a recto: page 1, or a chapter
@@ -79,7 +85,10 @@ export declare function trimmedSize(page: VDTPage, doc: VDTDocument): {
79
85
  * book), with the host's own appearance laid over them. The pages round
80
86
  * it are the host's word when it gives them (`extraPages`: a host laying
81
87
  * a book out a chapter at a time knows the chapters after this one, which
82
- * the document only knows when it was given the book's page count). */
88
+ * the document only knows when it was given the book's page count). A
89
+ * newspaper trim (`page.sizePreset` broadsheet, berliner, tabloid or
90
+ * compact) shows folded newsprint unless the settings name a stock or a
91
+ * binding (#506). */
83
92
  export declare function appearanceOf(doc: VDTDocument, own: FolioAppearance | undefined): FolioAppearance;
84
93
  /**
85
94
  * Which paintings a new layout can keep: new page index → old page index,
@@ -1 +1 @@
1
- {"version":3,"file":"postext.d.ts","sourceRoot":"","sources":["../src/postext.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"postext.d.ts","sourceRoot":"","sources":["../src/postext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,EAAe,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,cAAc,EAAuC,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAG5J,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,YAAY,EAAE,OAAO,GAAG,gBAAgB,GAAG,SAAS,GAAG,IAAI,GAAG,QAAQ,CAAC;IACxH,gDAAgD;IAChD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;qBAEiB;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,mDAAmD;IACnD,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,wBAAwB,KAAK,OAAO,CAAC;IACrE;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;4EACwE;IACxE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAClD;2EACuE;IACvE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;CACpH;AAED,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD;;;;;2DAKuD;IACvD,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,eAAe,CAAA;KAAE,GAAG,IAAI,CAAC;IAChH;4EACwE;IACxE,UAAU,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC3C;+DAC2D;IAC3D,aAAa,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC;IAC9C,wEAAwE;IACxE,SAAS,IAAI,IAAI,CAAC;CACnB;AAED;+CAC+C;AAC/C,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED;;yDAEyD;AACzD,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAG9F;AAQD;;;;;;;;sBAQsB;AACtB,wBAAgB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,eAAe,GAAG,SAAS,GAAG,eAAe,CAmBhG;AAiBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,OAAO,EAAE,EAC1B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EACzB,IAAI,EAAE,SAAS,OAAO,EAAE,EACxB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GACvB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBrB;AAKD;;2BAE2B;AAC3B,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAerI;AA8BD;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,oBAAyB,GAAG,mBAAmB,CAgmBzI"}
package/dist/postext.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { renderPageToCanvas } from "postext";
2
- import { resolveFolioConfig } from "postext";
2
+ import { folioForTrim, resolveFolioConfig } from "postext";
3
3
  import { BLOCK_PAGES } from "./pageFlip";
4
4
  import { createFolio } from "./viewer";
5
- import { attachVideoSource, isHlsVideo, pageVideoSpots, spotContains } from "./videos";
5
+ import { attachVideoSource, autoKey, autoplayPlan, isHlsVideo, makeWay, pageVideoSpots, spotContains } from "./videos";
6
6
  /** Whether the document's first page is a recto: page 1, or a chapter
7
7
  * continued after an even number of pages. */
8
8
  export function firstPageIsRecto(doc) {
@@ -25,12 +25,15 @@ function paperOf(doc) {
25
25
  * book), with the host's own appearance laid over them. The pages round
26
26
  * it are the host's word when it gives them (`extraPages`: a host laying
27
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). */
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). */
29
32
  export function appearanceOf(doc, own) {
30
33
  const page = doc.pages[0];
31
34
  const before = Math.max(0, own?.extraPages?.before ?? doc.pageIndexOffset ?? 0);
32
35
  const after = Math.max(0, own?.extraPages?.after ?? (doc.bookPageCount ?? 0) - before - doc.pages.length);
33
- const folio = own && "folio" in own ? own.folio : doc.config.folio;
36
+ const folio = folioForTrim(own && "folio" in own ? own.folio : doc.config.folio, doc.config.page.sizePreset);
34
37
  // The document's own covers: the book's first page (a recto) and its
35
38
  // last, when that is a verso (an even page number). A chapter from the
36
39
  // middle of the book has neither: its first and last leaves are paper
@@ -38,11 +41,11 @@ export function appearanceOf(doc, own) {
38
41
  const ownCovers = resolveFolioConfig(folio).binding.cover === "pages";
39
42
  const total = before + doc.pages.length;
40
43
  return {
41
- folio: doc.config.folio,
42
44
  covers: { front: ownCovers && before === 0, back: ownCovers && after === 0 && total % 2 === 0 },
43
45
  // Page sizes are in device pixels at the page's dpi.
44
46
  ...(page ? { pageWidthMm: (trimmedSize(page, doc).width * 25.4) / (doc.config.page.dpi || 300) } : {}),
45
47
  ...own,
48
+ folio,
46
49
  extraPages: { before, after },
47
50
  };
48
51
  }
@@ -124,6 +127,9 @@ export function sweepFor(run, settled, target, count) {
124
127
  }
125
128
  /** Painting time per frame while leaves are in the air. */
126
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;
127
133
  /** How long the book must stay as it is before a video set to play on its
128
134
  * own starts (a host's layouts settling, a run of turns). */
129
135
  const AUTOPLAY_SETTLE_MS = 700;
@@ -326,14 +332,18 @@ export function createFolioFromDocument(container, doc, options = {}) {
326
332
  // The leaf a hand would lift next shows the neighbours' pages.
327
333
  return [...pages, pages[0] - 2, pages[0] - 1, pages[pages.length - 1] + 1, pages[pages.length - 1] + 2];
328
334
  };
329
- // ── Videos on the pages (#477) ──
335
+ // ── Videos on the pages (#477, #507) ──
330
336
  /** The video spots of each page of the current document, as found. */
331
337
  let spots = new Map();
332
- /** The video playing (or paused) on a page; `autoMuted` while it plays
333
- * muted because it started on its own. */
334
- let playing = null;
335
- /** The videos that have started on their own once (`player.autoplay`). */
338
+ /** The videos playing (or paused) on the pages, in the order they
339
+ * started. */
340
+ let players = [];
341
+ /** The videos that have started on their own once (`player.autoplay`
342
+ * with `exclusive`). */
336
343
  const autoplayed = new Set();
344
+ /** The videos playing alongside that have started on their own since
345
+ * their page last came into view (`page|key`). */
346
+ const autoStarted = new Set();
337
347
  const spotsOf = (i) => {
338
348
  let found = spots.get(i);
339
349
  if (!found) {
@@ -351,57 +361,118 @@ export function createFolioFromDocument(container, doc, options = {}) {
351
361
  return (v.fileId ? options.videoUrl?.(v.fileId) : undefined) ?? v.link;
352
362
  };
353
363
  const spotAt = (point) => options.videos === false ? undefined : spotsOf(point.page).find((s) => urlOf(s) && spotContains(s, point.x, point.y));
354
- const report = (state) => {
355
- if (playing)
356
- options.onVideo?.({ resourceId: playing.spot.resourceId, page: playing.page, state });
364
+ const playerOf = (page, spot) => players.find((p) => p.page === page && p.spot.key === spot.key);
365
+ const onShow = (p) => viewer.state.pages.includes(p.page);
366
+ const report = (p, state) => {
367
+ options.onVideo?.({ resourceId: p.spot.resourceId, page: p.page, state });
357
368
  };
358
- /** Shows the playing video on its page (once it has a frame to show). */
359
- function showVideo() {
360
- if (!playing)
361
- return;
362
- playing.shown = true;
363
- viewer.setPageVideo({ page: playing.page, element: playing.element, origin: playing.spot.origin, across: playing.spot.across, down: playing.spot.down, ...(playing.spot.crop ? { crop: playing.spot.crop } : {}) });
369
+ /** Shows the videos that have a frame to show on their pages. */
370
+ function showVideos() {
371
+ viewer.setPageVideos(players
372
+ .filter((p) => p.shown)
373
+ .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 } : {}) })));
364
374
  }
365
- function stopVideo(state = "stopped") {
366
- const p = playing;
367
- if (!p)
375
+ /** Stops a video: its poster shows again, and the videos it held go on. */
376
+ function stopPlayer(p, state = "stopped") {
377
+ if (!players.includes(p))
368
378
  return;
369
- report(state);
370
- playing = null;
379
+ players = players.filter((q) => q !== p);
380
+ report(p, state);
371
381
  p.element.pause();
372
382
  p.release?.();
373
383
  if (p.shown)
374
- viewer.setPageVideo(null);
384
+ showVideos();
385
+ resumeHeld(p);
386
+ }
387
+ function stopVideos() {
388
+ for (const p of [...players])
389
+ stopPlayer(p);
390
+ }
391
+ /** The videos playing alongside that `by` held go on (on show only),
392
+ * and those set to play on their own that have not started yet start. */
393
+ function resumeHeld(by) {
394
+ if (by.alongside)
395
+ return;
396
+ for (const q of players) {
397
+ if (q.heldBy !== by)
398
+ continue;
399
+ q.heldBy = null;
400
+ if (onShow(q))
401
+ void q.element.play().catch(() => { });
402
+ }
403
+ if (!turning)
404
+ autoplayOn(viewer.state.pages, { exclusive: false });
405
+ }
406
+ /** A video starts (or goes on): the others make way for it (see
407
+ * `makeWay`). */
408
+ function makeWayFor(p) {
409
+ p.heldBy = null;
410
+ // Those waiting for another exclusive video wait for this one now.
411
+ if (!p.alongside)
412
+ for (const q of players)
413
+ if (q.heldBy)
414
+ q.heldBy = p;
415
+ const list = [...players.filter((q) => q !== p), p].map((q) => ({ q, alongside: q.alongside, paused: q.element.paused }));
416
+ const way = makeWay(list[list.length - 1], list);
417
+ for (const { q } of way.hold) {
418
+ q.heldBy = p;
419
+ q.element.pause();
420
+ }
421
+ for (const { q } of way.pause)
422
+ q.element.pause();
423
+ for (const { q } of way.stop)
424
+ stopPlayer(q);
375
425
  }
426
+ /** Plays the video at `spot` on `page`, from its start; `auto` when it
427
+ * starts on its own. */
376
428
  function startVideo(page, spot, auto = false) {
377
- stopVideo();
378
429
  const url = urlOf(spot);
379
430
  if (!url)
380
431
  return;
432
+ const player = spot.video.player;
433
+ const alongside = player.exclusive === false;
381
434
  const element = document.createElement("video");
382
435
  element.playsInline = true;
383
436
  element.preload = "auto";
384
437
  // WebGL draws the picture only when its server allows the read.
385
438
  if (!/^(blob|data):/i.test(url))
386
439
  element.crossOrigin = "anonymous";
387
- const player = spot.video.player;
440
+ const range = { start: spot.video.start ?? 0, end: spot.video.end };
441
+ // A loop from the file's start loops by itself (seamlessly); one from
442
+ // later on is sent back by hand.
443
+ element.loop = player.loop && !range.start;
388
444
  // A video that starts on its own has its sound once the reader has
389
445
  // clicked or typed on the page (the browser allows it then); before
390
446
  // that it starts muted, as browsers require, and a click unmutes it.
447
+ // One playing alongside always starts muted on its own: a page of
448
+ // living pictures is a quiet one.
391
449
  const activated = navigator.userActivation?.hasBeenActive ?? false;
392
- element.muted = player.muted || (auto && !activated);
393
- const entry = { page, spot, element, shown: false, autoMuted: !player.muted && element.muted };
394
- playing = entry;
395
- const live = () => playing === entry;
396
- const range = { start: spot.video.start ?? 0, end: spot.video.end };
397
- element.addEventListener("error", () => live() && stopVideo("error"));
450
+ element.muted = player.muted || (auto && (alongside || !activated));
451
+ const entry = { page, spot, element, shown: false, autoMuted: !player.muted && element.muted, alongside, heldBy: null };
452
+ // The others make way at once (an exclusive one, still loading, holds
453
+ // the videos playing alongside from now).
454
+ players = [...players, entry];
455
+ makeWayFor(entry);
456
+ const live = () => players.includes(entry);
457
+ element.addEventListener("error", () => live() && stopPlayer(entry, "error"));
398
458
  element.addEventListener("loadedmetadata", () => {
399
459
  if (range.start)
400
460
  element.currentTime = range.start;
401
461
  }, { once: true });
402
- element.addEventListener("loadeddata", () => live() && showVideo(), { once: true });
403
- element.addEventListener("playing", () => live() && report("playing"));
404
- element.addEventListener("pause", () => live() && !element.ended && report("paused"));
462
+ element.addEventListener("loadeddata", () => {
463
+ if (!live())
464
+ return;
465
+ entry.shown = true;
466
+ showVideos();
467
+ }, { once: true });
468
+ element.addEventListener("play", () => live() && makeWayFor(entry));
469
+ element.addEventListener("playing", () => live() && report(entry, "playing"));
470
+ element.addEventListener("pause", () => {
471
+ if (!live() || element.ended)
472
+ return;
473
+ report(entry, "paused");
474
+ resumeHeld(entry);
475
+ });
405
476
  const atEnd = () => {
406
477
  if (!live())
407
478
  return;
@@ -410,7 +481,7 @@ export function createFolioFromDocument(container, doc, options = {}) {
410
481
  void element.play().catch(() => { });
411
482
  }
412
483
  else
413
- stopVideo();
484
+ stopPlayer(entry);
414
485
  };
415
486
  element.addEventListener("ended", atEnd);
416
487
  element.addEventListener("timeupdate", () => {
@@ -424,6 +495,9 @@ export function createFolioFromDocument(container, doc, options = {}) {
424
495
  if (!live())
425
496
  return release();
426
497
  entry.release = release;
498
+ // Held back already (an exclusive video started meanwhile).
499
+ if (entry.heldBy)
500
+ return;
427
501
  // The click's gesture lets it play with sound; a browser that still
428
502
  // refuses gets it muted.
429
503
  element.play().catch((err) => {
@@ -435,8 +509,8 @@ export function createFolioFromDocument(container, doc, options = {}) {
435
509
  return element.play();
436
510
  }
437
511
  throw err;
438
- }).catch(() => live() && stopVideo("error"));
439
- }, () => live() && stopVideo("error"));
512
+ }).catch(() => live() && stopPlayer(entry, "error"));
513
+ }, () => live() && stopPlayer(entry, "error"));
440
514
  }
441
515
  /** A click at a point of a page: plays, pauses or resumes the video
442
516
  * printed there. */
@@ -444,18 +518,16 @@ export function createFolioFromDocument(container, doc, options = {}) {
444
518
  const spot = spotAt(point);
445
519
  if (!spot)
446
520
  return false;
447
- if (playing && playing.page === point.page && playing.spot.key === spot.key)
448
- togglePlaying();
521
+ const p = playerOf(point.page, spot);
522
+ if (p)
523
+ togglePlaying(p);
449
524
  else
450
525
  startVideo(point.page, spot);
451
526
  return true;
452
527
  }
453
- /** A click (or Space) on the video playing: one that started on its own
528
+ /** A click (or Space) on a video playing: one that started on its own
454
529
  * is unmuted first; otherwise it pauses or resumes. */
455
- function togglePlaying() {
456
- const p = playing;
457
- if (!p)
458
- return;
530
+ function togglePlaying(p) {
459
531
  if (p.autoMuted && !p.element.paused) {
460
532
  p.autoMuted = false;
461
533
  p.element.muted = false;
@@ -477,42 +549,51 @@ export function createFolioFromDocument(container, doc, options = {}) {
477
549
  autoplayOn(viewer.state.pages);
478
550
  }, AUTOPLAY_SETTLE_MS);
479
551
  };
480
- /** The first time a spread shows a video set to play on its own
481
- * (`player.autoplay`), it starts, muted; never again in this viewer. */
482
- function autoplayOn(pages) {
483
- if (options.videos === false || playing)
552
+ /**
553
+ * The videos on show set to play on their own (`player.autoplay`) start,
554
+ * muted. One playing alongside the others (`exclusive: false`) starts
555
+ * each time its page comes into view (not while an exclusive video
556
+ * plays there, nor when the reader asks for less motion); an exclusive
557
+ * one, the first time its spread is shown and never again in this
558
+ * viewer, when no other exclusive video is on.
559
+ */
560
+ function autoplayOn(pages, { exclusive = true } = {}) {
561
+ if (options.videos === false)
484
562
  return;
485
- for (const page of pages) {
486
- for (const spot of spotsOf(page)) {
487
- if (!spot.video.player.autoplay || !urlOf(spot) || autoplayed.has(spot.key))
488
- continue;
563
+ const state = {
564
+ players: players.map((p) => ({ page: p.page, key: p.spot.key, alongside: p.alongside, paused: p.element.paused, shown: p.shown })),
565
+ autoplayed,
566
+ autoStarted,
567
+ };
568
+ for (const { page, spot } of autoplayPlan(pages, spotsOf, (s) => !!urlOf(s), state, { exclusive, reducedMotion: reducedMotion() })) {
569
+ if (spot.video.player.exclusive === false)
570
+ autoStarted.add(autoKey(page, spot.key));
571
+ else
489
572
  autoplayed.add(spot.key);
490
- startVideo(page, spot, true);
491
- return;
492
- }
573
+ startVideo(page, spot, true);
493
574
  }
494
575
  }
495
- /** A new layout: the playing video follows its resource to where the
496
- * layout puts it, or stops when it is gone. */
497
- function followVideo() {
576
+ /** A new layout: each video follows its resource to where the layout
577
+ * puts it, or stops when it is gone (or, at rest, off the pages on
578
+ * show). */
579
+ function followVideos() {
498
580
  spots = new Map();
499
- const p = playing;
500
- if (!p)
501
- return;
502
- const near = [p.page, ...viewer.state.pages];
503
- for (let i = 0; i < current.pages.length; i++)
504
- near.push(i);
505
- for (const i of near) {
506
- const spot = spotsOf(i).find((s) => s.key === p.spot.key && urlOf(s) === urlOf(p.spot));
507
- if (!spot)
581
+ let moved = false;
582
+ for (const p of [...players]) {
583
+ const near = [p.page, ...viewer.state.pages];
584
+ for (let i = 0; i < current.pages.length; i++)
585
+ near.push(i);
586
+ const at = near.find((i) => spotsOf(i).some((s) => s.key === p.spot.key && urlOf(s) === urlOf(p.spot)));
587
+ if (at === undefined || (!turning && !viewer.state.pages.includes(at))) {
588
+ stopPlayer(p);
508
589
  continue;
509
- p.page = i;
510
- p.spot = spot;
511
- if (p.shown)
512
- showVideo();
513
- return;
590
+ }
591
+ p.page = at;
592
+ p.spot = spotsOf(at).find((s) => s.key === p.spot.key && urlOf(s) === urlOf(p.spot));
593
+ moved ||= p.shown;
514
594
  }
515
- stopVideo();
595
+ if (moved)
596
+ showVideos();
516
597
  }
517
598
  const first = doc.pages[0];
518
599
  const viewer = createFolio(container, {
@@ -538,9 +619,13 @@ export function createFolioFromDocument(container, doc, options = {}) {
538
619
  isPageAction: (point) => !!spotAt(point) || !!options.isPageAction?.(point),
539
620
  onChange: (state) => {
540
621
  // A video whose page has turned away stops; one set to play on its
541
- // own starts the first time its spread is shown.
542
- if (playing && !state.pages.includes(playing.page))
543
- stopVideo();
622
+ // own starts when its spread is shown (see `autoplayOn`).
623
+ for (const p of [...players])
624
+ if (!state.pages.includes(p.page))
625
+ stopPlayer(p);
626
+ for (const key of [...autoStarted])
627
+ if (!state.pages.some((i) => key.startsWith(autoKey(i, ""))))
628
+ autoStarted.delete(key);
544
629
  scheduleAutoplay();
545
630
  focus = state.pages;
546
631
  settled = state.pages;
@@ -570,7 +655,7 @@ export function createFolioFromDocument(container, doc, options = {}) {
570
655
  // Debugging (`__postextFolioPreserve`, as for the renderer): the videos.
571
656
  if (globalThis.__postextFolioPreserve) {
572
657
  globalThis.__postextFolioVideos = {
573
- playing: () => playing,
658
+ playing: () => players,
574
659
  autoplayed,
575
660
  spots: (i) => spotsOf(i),
576
661
  state: () => viewer.state,
@@ -582,8 +667,10 @@ export function createFolioFromDocument(container, doc, options = {}) {
582
667
  if (event.target !== viewer.element || (event.key !== " " && event.key !== "Enter") || options.videos === false)
583
668
  return;
584
669
  const on = viewer.state.pages;
585
- if (playing && on.includes(playing.page))
586
- togglePlaying();
670
+ const shown = players.filter((p) => on.includes(p.page));
671
+ const p = shown.find((x) => !x.alongside) ?? shown[0];
672
+ if (p)
673
+ togglePlaying(p);
587
674
  else {
588
675
  const page = on.find((i) => spotsOf(i).some((s) => urlOf(s)));
589
676
  if (page === undefined)
@@ -601,8 +688,9 @@ export function createFolioFromDocument(container, doc, options = {}) {
601
688
  setPages: viewer.setPages,
602
689
  refreshPage: viewer.refreshPage,
603
690
  setPageVideo: viewer.setPageVideo,
691
+ setPageVideos: viewer.setPageVideos,
604
692
  toggleVideoAt,
605
- stopVideo: () => stopVideo(),
693
+ stopVideo: stopVideos,
606
694
  setLabels: viewer.setLabels,
607
695
  resetView: viewer.resetView,
608
696
  getView: viewer.getView,
@@ -679,7 +767,7 @@ export function createFolioFromDocument(container, doc, options = {}) {
679
767
  run = null;
680
768
  }
681
769
  refresh(around(viewer.state));
682
- followVideo();
770
+ followVideos();
683
771
  scheduleAutoplay();
684
772
  },
685
773
  redecorate(pages) {
@@ -698,7 +786,7 @@ export function createFolioFromDocument(container, doc, options = {}) {
698
786
  },
699
787
  dispose() {
700
788
  clearTimeout(autoTimer);
701
- stopVideo();
789
+ stopVideos();
702
790
  cancelIdle(idle);
703
791
  cancelAnimationFrame(sweepFrame);
704
792
  clearTimeout(resizeTimer);