postext-folio 0.5.3 → 0.6.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/videos.js ADDED
@@ -0,0 +1,173 @@
1
+ import { flowToPage, isHlsMimeType, resourceBlockToPage } from "postext";
2
+ /** The video resources printed on a page, in the order they are painted:
3
+ * each picture's place in fractions of the page as bound (trimmed), with
4
+ * the turns the page and the block give it. A mirrored (right-to-left)
5
+ * page sets its pictures unmirrored, as the renderers do. */
6
+ export function pageVideoSpots(page, doc) {
7
+ const out = [];
8
+ const blocks = [...page.columns.flatMap((c) => c.blocks), ...(page.floats ?? [])];
9
+ const inset = Math.max(0, doc.trimOffset);
10
+ const tw = page.width - 2 * inset;
11
+ const th = page.height - 2 * inset;
12
+ if (!(tw > 0 && th > 0))
13
+ return out;
14
+ const seen = new Map();
15
+ for (const block of blocks) {
16
+ const rb = block.resourceBlock;
17
+ if (!rb || rb.kind !== "video" || !rb.video)
18
+ continue;
19
+ const bx = (rb.rotation ? 0 : block.bbox.x) + rb.bodyRect.x;
20
+ const by = (rb.rotation ? 0 : block.bbox.y) + rb.bodyRect.y;
21
+ const { width: bw, height: bh } = rb.bodyRect;
22
+ if (!(bw > 0 && bh > 0))
23
+ continue;
24
+ // The body's corners: in the block's frame, the page's flow frame,
25
+ // then on the sheet.
26
+ const onSheet = (x, y) => {
27
+ const f = resourceBlockToPage(rb, x, y);
28
+ const p = flowToPage(page, f.x, f.y);
29
+ return { x: (p.x - inset) / tw, y: (p.y - inset) / th };
30
+ };
31
+ let origin = onSheet(bx, by);
32
+ const right = onSheet(bx + bw, by);
33
+ const below = onSheet(bx, by + bh);
34
+ let across = { x: right.x - origin.x, y: right.y - origin.y };
35
+ let down = { x: below.x - origin.x, y: below.y - origin.y };
36
+ // A mirrored flow lays the box out mirrored but turns the picture back
37
+ // in it: the axis that runs across the sheet is reversed again.
38
+ if (page.flow?.writingMode === "horizontal-tb") {
39
+ if (Math.abs(across.x) >= Math.abs(down.x)) {
40
+ origin = { x: origin.x + across.x, y: origin.y + across.y };
41
+ across = { x: -across.x, y: -across.y };
42
+ }
43
+ else {
44
+ origin = { x: origin.x + down.x, y: origin.y + down.y };
45
+ down = { x: -down.x, y: -down.y };
46
+ }
47
+ }
48
+ const id = rb.resource.id;
49
+ const n = seen.get(id) ?? 0;
50
+ seen.set(id, n + 1);
51
+ out.push({
52
+ key: `${id}#${n}`,
53
+ resourceId: id,
54
+ video: rb.video,
55
+ origin,
56
+ across,
57
+ down,
58
+ heightPx: bh,
59
+ ...(rb.bodySource ? { crop: { ...rb.bodySource } } : {}),
60
+ });
61
+ }
62
+ return out;
63
+ }
64
+ /** Whether a point of the page (fractions from its top left corner) lies on
65
+ * a spot's picture. */
66
+ export function spotContains(spot, x, y) {
67
+ const px = x - spot.origin.x;
68
+ const py = y - spot.origin.y;
69
+ const s = (px * spot.across.x + py * spot.across.y) / (spot.across.x ** 2 + spot.across.y ** 2);
70
+ const t = (px * spot.down.x + py * spot.down.y) / (spot.down.x ** 2 + spot.down.y ** 2);
71
+ return s >= 0 && s <= 1 && t >= 0 && t <= 1;
72
+ }
73
+ /** The variant to cap a stream at: the tallest no taller than `maxHeight`
74
+ * (with a quarter of slack), else the shortest there is. */
75
+ export function hlsLevelCap(levels, maxHeight) {
76
+ let cap = -1;
77
+ let lowest = -1;
78
+ levels.forEach((l, i) => {
79
+ if (lowest < 0 || l.height < levels[lowest].height)
80
+ lowest = i;
81
+ if (l.height <= maxHeight * 1.25 && (cap < 0 || l.height > levels[cap].height))
82
+ cap = i;
83
+ });
84
+ return cap >= 0 ? cap : lowest;
85
+ }
86
+ /**
87
+ * Gives a `<video>` its source: a file's address as it is, or an HLS
88
+ * stream (`hls`, or an `.m3u8` address). A stream plays through hls.js
89
+ * (loaded on demand) wherever Media Source Extensions exist, its quality
90
+ * capped at the variant nearest `maxHeight` px tall, so a picture a few
91
+ * hundred pixels tall never pulls a 4K variant; the browser's own HLS
92
+ * (Safari on iOS) is the fallback, or the first choice with `native:
93
+ * 'prefer'` (a player shown at its size, which picks its own variant).
94
+ * A fatal network or media error is retried once each. `lazy` loads no
95
+ * segment before the first play. Resolves to a function that lets the
96
+ * source go; rejects when the stream cannot play.
97
+ */
98
+ export async function attachVideoSource(element, url, options = {}) {
99
+ const hls = options.hls ?? /\.m3u8(?:$|[?#])/i.test(url);
100
+ const nativeHls = hls && !!element.canPlayType("application/vnd.apple.mpegurl");
101
+ const plain = () => {
102
+ element.src = url;
103
+ return () => {
104
+ element.removeAttribute("src");
105
+ element.load();
106
+ };
107
+ };
108
+ if (!hls || (nativeHls && options.native === "prefer"))
109
+ return plain();
110
+ let Hls = null;
111
+ try {
112
+ Hls = (await import("hls.js")).default;
113
+ }
114
+ catch {
115
+ Hls = null;
116
+ }
117
+ if (!Hls || !Hls.isSupported()) {
118
+ if (nativeHls)
119
+ return plain();
120
+ throw new Error("HLS is not supported");
121
+ }
122
+ const H = Hls;
123
+ const player = new H({
124
+ capLevelToPlayerSize: false,
125
+ startLevel: -1,
126
+ autoStartLoad: !options.lazy,
127
+ // `corsTag`: every playlist and segment is asked for with a query of its
128
+ // own, so the browser's cache never hands back a copy fetched without
129
+ // CORS by another player of the same stream (a plain <video> sends no
130
+ // Origin, and a server that answers it without `Vary: Origin` gets that
131
+ // copy reused for this cross-origin read, which then fails: playback
132
+ // stops at the first such segment).
133
+ ...(options.corsTag ? { xhrSetup: (xhr, u) => xhr.open("GET", corsTagged(u), true) } : {}),
134
+ });
135
+ if (options.lazy)
136
+ element.addEventListener("play", () => player.startLoad(), { once: true });
137
+ const maxHeight = options.maxHeight;
138
+ if (maxHeight) {
139
+ player.on(H.Events.MANIFEST_PARSED, (_e, data) => {
140
+ const cap = hlsLevelCap(data.levels ?? [], maxHeight);
141
+ if (cap >= 0)
142
+ player.autoLevelCapping = cap;
143
+ });
144
+ }
145
+ const retried = new Set();
146
+ player.on(H.Events.ERROR, (_e, data) => {
147
+ if (!data.fatal || !data.type || retried.has(data.type))
148
+ return;
149
+ retried.add(data.type);
150
+ if (data.type === H.ErrorTypes.NETWORK_ERROR)
151
+ player.startLoad();
152
+ else if (data.type === H.ErrorTypes.MEDIA_ERROR)
153
+ player.recoverMediaError();
154
+ });
155
+ player.loadSource(url);
156
+ player.attachMedia(element);
157
+ return () => player.destroy();
158
+ }
159
+ /** An address with the query that keeps its cache entry apart (see
160
+ * `corsTag`). */
161
+ export function corsTagged(url) {
162
+ if (/^(blob|data):/i.test(url) || /[?&]pt-cors(=|&|$)/.test(url))
163
+ return url;
164
+ const at = url.indexOf("#");
165
+ const base = at < 0 ? url : url.slice(0, at);
166
+ const hash = at < 0 ? "" : url.slice(at);
167
+ return `${base}${base.includes("?") ? "&" : "?"}pt-cors=1${hash}`;
168
+ }
169
+ /** Whether a video plays from an HLS address. */
170
+ export function isHlsVideo(video, url) {
171
+ return isHlsMimeType(video.mimeType) || /\.m3u8(?:$|[?#])/i.test(url);
172
+ }
173
+ //# sourceMappingURL=videos.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"videos.js","sourceRoot":"","sources":["../src/videos.ts"],"names":[],"mappings":"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"}
package/dist/viewer.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type FolioConfig, type FolioPaperConfig } from "postext";
2
- import { type PageSource } from "./pageFlip";
2
+ import { type PageSource, type PageVideoFrame } from "./pageFlip";
3
3
  /** A page of the book: its source, or its source with a text alternative
4
4
  * and the paper it is printed on when that is not the book's (a plate
5
5
  * section on gloss in a matte book). */
@@ -53,6 +53,12 @@ export interface FolioPagePoint {
53
53
  x: number;
54
54
  y: number;
55
55
  }
56
+ /** A video drawn on a page of the book (#477): the element playing it and
57
+ * where its picture lies on the page (see `PageVideoFrame`). */
58
+ export interface FolioPageVideo extends PageVideoFrame {
59
+ page: number;
60
+ element: HTMLVideoElement;
61
+ }
56
62
  export interface FolioLabels {
57
63
  /** The viewer's accessible name. */
58
64
  region: string;
@@ -99,6 +105,14 @@ export interface FolioOptions {
99
105
  /** Told as soon as the book is sent to another spread (a button, a key,
100
106
  * a page let go past halfway), before its leaves have landed. */
101
107
  onTarget?: (state: FolioState) => void;
108
+ /** A click on a page, before it turns the page (hand mode) or reaches
109
+ * the host (select mode): return true when the click did something else,
110
+ * such as playing a video printed there. In orbit and select modes it is
111
+ * asked only where `isPageAction` says a click acts. */
112
+ onPageClick?: (point: FolioPagePoint) => boolean;
113
+ /** Whether a click at this point of a page acts on it (`onPageClick`):
114
+ * the pointer shows a hand there. */
115
+ isPageAction?: (point: FolioPagePoint) => boolean;
102
116
  /** Told when the page slots change size: a host painting pages for the
103
117
  * viewer paints them at `deviceWidth` × `deviceHeight` pixels, so they
104
118
  * are shown 1:1 both at rest and on a turning leaf. */
@@ -154,6 +168,9 @@ export interface FolioViewer {
154
168
  setInteraction(mode: FolioInteraction): void;
155
169
  /** A page's canvas was drawn again in place: shows it again. */
156
170
  refreshPage(src: PageSource): void;
171
+ /** Plays a video on a page (#477): drawn over the page's painting, on
172
+ * the leaf that carries it as it lies or turns; `null` takes it off. */
173
+ setPageVideo(video: FolioPageVideo | null): void;
157
174
  /** The page under a pointer and where on it, as the book is seen (the
158
175
  * tilted, orbited 3D book included); null off the open pages. */
159
176
  pageAt(event: {
@@ -1 +1 @@
1
- {"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../src/viewer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"viewer.d.ts","sourceRoot":"","sources":["../src/viewer.ts"],"names":[],"mappings":"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"}
package/dist/viewer.js CHANGED
@@ -108,6 +108,10 @@ export function createFolio(container, options) {
108
108
  let swipe = null;
109
109
  let disposed = false;
110
110
  let slot = { width: 0, height: 0, deviceWidth: 0, deviceHeight: 0 };
111
+ let pageVideo = null;
112
+ /** Where a press on a page began, on the page (a click there may act on
113
+ * it rather than turn it). */
114
+ let pressPoint = null;
111
115
  const rtl = () => binding === "right";
112
116
  const pagesOf = (s) => (s ?? [null, null]).filter((i) => i !== null);
113
117
  /** One page at a time on a WebGL book: the spine runs along the page's
@@ -245,6 +249,7 @@ export function createFolio(container, options) {
245
249
  slot = next;
246
250
  options.onLayout?.(slot);
247
251
  }
252
+ placeDomVideo();
248
253
  flipper?.redraw();
249
254
  }
250
255
  function pageEl(i, slot) {
@@ -288,6 +293,45 @@ export function createFolio(container, options) {
288
293
  }
289
294
  return el;
290
295
  }
296
+ /** Shows the page video: on the WebGL book, drawn by the leaf that
297
+ * carries its page; on the DOM spread, laid over the page. */
298
+ function applyVideo() {
299
+ const v = pageVideo;
300
+ flipper?.setVideo(v ? { src: sourceOf(pages[v.page]), element: v.element, frame: v } : null);
301
+ placeDomVideo();
302
+ }
303
+ /** The DOM spread (no WebGL): the video element over its page, mapped
304
+ * onto the picture's place by a transform (it may lie turned). */
305
+ function placeDomVideo() {
306
+ const v = pageVideo;
307
+ if (!v || flipper) {
308
+ if (v?.element.classList.contains("postext-folio-video")) {
309
+ v.element.classList.remove("postext-folio-video");
310
+ v.element.remove();
311
+ }
312
+ return;
313
+ }
314
+ const els = [...spreadEl.children];
315
+ const s = spreads[shown] ?? [null, null];
316
+ const slots = single && !singleGl() ? [s[0] ?? s[1]] : s;
317
+ const at = slots.indexOf(v.page);
318
+ const el = at >= 0 ? els[at] : undefined;
319
+ if (!el) {
320
+ v.element.remove();
321
+ return;
322
+ }
323
+ const w = el.clientWidth;
324
+ const h = el.clientHeight;
325
+ const unit = 100;
326
+ v.element.classList.add("postext-folio-video");
327
+ Object.assign(v.element.style, {
328
+ width: `${unit}px`,
329
+ height: `${unit}px`,
330
+ transform: `matrix(${(v.across.x * w) / unit}, ${(v.across.y * h) / unit}, ${(v.down.x * w) / unit}, ${(v.down.y * h) / unit}, ${v.origin.x * w}, ${v.origin.y * h})`,
331
+ });
332
+ if (v.element.parentElement !== el)
333
+ el.append(v.element);
334
+ }
291
335
  function render() {
292
336
  const s = spreads[shown] ?? [null, null];
293
337
  const slots = single && !singleGl() ? [s[0] ?? s[1]] : s;
@@ -301,6 +345,7 @@ export function createFolio(container, options) {
301
345
  count.textContent = on.length ? labels.count(on, pages.length) : "";
302
346
  prevBtn.disabled = current <= 0;
303
347
  nextBtn.disabled = current >= spreads.length - 1;
348
+ placeDomVideo();
304
349
  // The settled spread, drawn at rest.
305
350
  flipper?.clear();
306
351
  }
@@ -359,6 +404,8 @@ export function createFolio(container, options) {
359
404
  const rgb = rgbOf(paper);
360
405
  if (rgb)
361
406
  flipper.setPaper(...rgb);
407
+ if (pageVideo)
408
+ applyVideo();
362
409
  }
363
410
  /** Lays the book out again: spreads, mode, flipper. */
364
411
  function rebuild(atPage) {
@@ -445,10 +492,59 @@ export function createFolio(container, options) {
445
492
  });
446
493
  // Select mode: a text cursor over the pages as the book is seen.
447
494
  root.addEventListener("pointermove", (event) => {
448
- if (interaction !== "select" || event.buttons)
495
+ if (event.buttons)
449
496
  return;
450
- root.classList.toggle("is-over-page", !!pageAt(event));
497
+ const point = holding ? null : pageAt(event);
498
+ if (interaction === "select")
499
+ root.classList.toggle("is-over-page", !!point);
500
+ if (options.isPageAction)
501
+ root.classList.toggle("is-over-action", !!point && options.isPageAction(point));
502
+ // Hand mode: the hand shows only where a page can be taken, its
503
+ // outer half (#494).
504
+ root.classList.toggle("is-over-grip", interaction === "hand" && !holding && !!flipper && flipper.hit(event) !== 0);
451
505
  });
506
+ // Orbit and select modes: a click on something a click acts on (a video)
507
+ // acts on it, as in hand mode. The press is taken before the host hears
508
+ // it in select mode (no caret, no link followed); in orbit mode a drag
509
+ // from it still turns the view.
510
+ let actionPress = null;
511
+ let swallowClick = false;
512
+ root.addEventListener("pointerdown", (event) => {
513
+ actionPress = null;
514
+ if (interaction === "hand" || event.button !== 0 || !options.onPageClick || !options.isPageAction)
515
+ return;
516
+ if (event.target.closest(".postext-folio-nav"))
517
+ return;
518
+ const point = pageAt(event);
519
+ if (!point || !options.isPageAction(point))
520
+ return;
521
+ actionPress = { x: event.clientX, y: event.clientY, id: event.pointerId, point };
522
+ if (interaction === "select")
523
+ event.stopPropagation();
524
+ }, true);
525
+ root.addEventListener("pointermove", (event) => {
526
+ if (actionPress && interaction === "select" && event.pointerId === actionPress.id)
527
+ event.stopPropagation();
528
+ }, true);
529
+ root.addEventListener("pointerup", (event) => {
530
+ const press = actionPress;
531
+ if (!press || event.pointerId !== press.id)
532
+ return;
533
+ actionPress = null;
534
+ if (interaction === "select") {
535
+ event.stopPropagation();
536
+ swallowClick = true;
537
+ }
538
+ if (Math.hypot(event.clientX - press.x, event.clientY - press.y) < CLICK_SLOP)
539
+ options.onPageClick?.(press.point);
540
+ }, true);
541
+ root.addEventListener("click", (event) => {
542
+ if (!swallowClick)
543
+ return;
544
+ swallowClick = false;
545
+ event.stopPropagation();
546
+ }, true);
547
+ root.addEventListener("pointerleave", () => root.classList.remove("is-over-action", "is-over-grip"));
452
548
  root.addEventListener("pointerdown", (event) => {
453
549
  // Only the hand takes pages, swipes and clicks them over.
454
550
  if (interaction !== "hand")
@@ -479,6 +575,8 @@ export function createFolio(container, options) {
479
575
  if (flipper ? !flipper.hit(event) : !target.closest(".postext-folio-spread"))
480
576
  return false;
481
577
  press = { x: event.clientX, y: event.clientY };
578
+ // Where it was pressed, read before the hand lifts the leaf.
579
+ pressPoint = options.onPageClick ? pageAt(event) : null;
482
580
  if (singleGl()) {
483
581
  decide(event);
484
582
  return true;
@@ -528,8 +626,10 @@ export function createFolio(container, options) {
528
626
  press = null;
529
627
  const dx = e.clientX - start.x;
530
628
  const dy = e.clientY - start.y;
531
- if (Math.hypot(dx, dy) < CLICK_SLOP)
532
- go(current + 1);
629
+ if (Math.hypot(dx, dy) < CLICK_SLOP) {
630
+ if (!pageClick())
631
+ go(current + 1);
632
+ }
533
633
  else if (Math.abs(dx) > SWIPE && Math.abs(dx) > Math.abs(dy) && Math.sign(dx) === -toSpine)
534
634
  go(current - 1);
535
635
  e.stopPropagation();
@@ -573,7 +673,8 @@ export function createFolio(container, options) {
573
673
  spreadEl.classList.remove("is-held");
574
674
  holding = null;
575
675
  press = null;
576
- flipper?.release(click);
676
+ // A click that acts on the page (a video) lets the leaf fall back.
677
+ flipper?.release(click && !pageClick());
577
678
  };
578
679
  window.addEventListener("pointermove", onMove);
579
680
  // Captured, so the stage's swipe does not hear the hold's release.
@@ -598,6 +699,10 @@ export function createFolio(container, options) {
598
699
  const click = !!start && Math.hypot(event.clientX - start.x, event.clientY - start.y) < CLICK_SLOP;
599
700
  if (!click)
600
701
  return;
702
+ if (pageClick()) {
703
+ swipe = null;
704
+ return;
705
+ }
601
706
  // A click on the recto turns forward, on the verso back (pages already
602
707
  // in the air: it joins them).
603
708
  if (flipper) {
@@ -616,6 +721,13 @@ export function createFolio(container, options) {
616
721
  else
617
722
  go(current + ((event.clientX > rect.left + rect.width / 2) !== rtl() ? 1 : -1));
618
723
  });
724
+ /** A click at the point pressed: the host's to act on (true), or the
725
+ * page's to turn. */
726
+ function pageClick() {
727
+ const point = pressPoint;
728
+ pressPoint = null;
729
+ return !!point && !!options.onPageClick?.(point);
730
+ }
619
731
  /** The page index in slot `side` (0 the verso, 1 the recto) of the
620
732
  * spread on show. */
621
733
  const pageInSlot = (side) => {
@@ -670,6 +782,8 @@ export function createFolio(container, options) {
670
782
  root.classList.toggle("is-select", interaction === "select");
671
783
  if (interaction !== "select")
672
784
  root.classList.remove("is-over-page");
785
+ if (interaction !== "hand")
786
+ root.classList.remove("is-over-grip");
673
787
  }
674
788
  let lastWidth = -1;
675
789
  const observer = new ResizeObserver(() => {
@@ -719,6 +833,14 @@ export function createFolio(container, options) {
719
833
  refreshPage(src) {
720
834
  flipper?.touch(src);
721
835
  },
836
+ setPageVideo(video) {
837
+ if (pageVideo && pageVideo.element !== video?.element && pageVideo.element.classList.contains("postext-folio-video")) {
838
+ pageVideo.element.classList.remove("postext-folio-video");
839
+ pageVideo.element.remove();
840
+ }
841
+ pageVideo = video;
842
+ applyVideo();
843
+ },
722
844
  setAppearance(next) {
723
845
  appearance = { ...appearance, ...next };
724
846
  const flip = flipAppearance();
@@ -758,11 +880,16 @@ export function createFolio(container, options) {
758
880
  papersKey = papers;
759
881
  if (opts.at !== undefined)
760
882
  current = shown = spreadOfPage(spreads, opts.at);
883
+ // The video's page may be painted on another canvas now.
884
+ if (pageVideo)
885
+ flipper?.setVideo({ src: sourceOf(pages[pageVideo.page]), element: pageVideo.element, frame: pageVideo });
761
886
  fit();
762
887
  render();
763
888
  },
764
889
  dispose() {
765
890
  disposed = true;
891
+ pageVideo?.element.remove();
892
+ pageVideo = null;
766
893
  observer.disconnect();
767
894
  holding?.(false);
768
895
  flipper?.dispose();