@zinejs/core 0.9.2 → 1.0.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.
@@ -1,5 +1,6 @@
1
+ import { i as e, n as t, r as n, t as r } from "./types-DYqgosP1.js";
1
2
  //#region src/geometry/flipProgressToPose.ts
2
- function e(e) {
3
+ function i(e) {
3
4
  let t = e < 0 ? 0 : e > 1 ? 1 : e, n = Math.sin(t * Math.PI);
4
5
  return {
5
6
  angle: t * Math.PI,
@@ -9,11 +10,11 @@ function e(e) {
9
10
  }
10
11
  //#endregion
11
12
  //#region src/renderer/cssRenderer.ts
12
- var t = 180 / Math.PI;
13
- function n(e) {
13
+ var a = 180 / Math.PI;
14
+ function o(e) {
14
15
  return e.left === null == (e.right === null) ? 0 : e.right === null ? 1 : -1;
15
16
  }
16
- function r(e) {
17
+ function s(e) {
17
18
  let t = e === "forward" ? "to right" : "to left";
18
19
  return [
19
20
  `linear-gradient(${t}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,
@@ -21,7 +22,7 @@ function r(e) {
21
22
  `linear-gradient(${t}, transparent 66%, rgba(0,0,0,0.35))`
22
23
  ].join(",");
23
24
  }
24
- var i = class {
25
+ var c = class {
25
26
  #e = null;
26
27
  #t;
27
28
  #n;
@@ -42,28 +43,43 @@ var i = class {
42
43
  #_ = 0;
43
44
  mount(e) {
44
45
  let t = e.ownerDocument;
45
- return this.#e = e, this.#t = t.createElement("div"), this.#t.className = "zine-clip", this.#t.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden;", this.#n = t.createElement("div"), this.#n.className = "zine-viewport", this.#n.style.cssText = "position:absolute;inset:0;transform-origin:0 0;", this.#r = t.createElement("div"), this.#r.className = "zine-book", this.#r.style.cssText = "position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;", this.#i = this.#w(t, "zine-page-left", "left:0;"), this.#a = this.#w(t, "zine-page-right", "right:0;"), this.#o = t.createElement("div"), this.#o.className = "zine-leaf", this.#o.style.cssText = "position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;", this.#s = t.createElement("canvas"), this.#s.className = "zine-leaf-front", this.#s.style.cssText = "position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;", this.#c = t.createElement("canvas"), this.#c.className = "zine-leaf-back", this.#c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);", this.#l = t.createElement("div"), this.#l.className = "zine-leaf-shadow", this.#l.style.cssText = "position:absolute;inset:0;opacity:0;pointer-events:none;", this.#l.style.background = r("forward"), this.#o.append(this.#s, this.#c, this.#l), this.#r.append(this.#i, this.#a, this.#o), this.#n.append(this.#r), this.#t.append(this.#n), e.append(this.#t), Promise.resolve();
46
+ return this.#e = e, this.#t = t.createElement("div"), this.#t.className = "zine-clip", this.#t.style.cssText = "position:relative;width:100%;height:100%;overflow:hidden;", this.#n = t.createElement("div"), this.#n.className = "zine-viewport", this.#n.style.cssText = "position:absolute;inset:0;transform-origin:0 0;", this.#r = t.createElement("div"), this.#r.className = "zine-book", this.#r.style.cssText = "position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;", this.#i = this.#w(t, "zine-page-left", "left:0;"), this.#a = this.#w(t, "zine-page-right", "right:0;"), this.#o = t.createElement("div"), this.#o.className = "zine-leaf", this.#o.style.cssText = "position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;", this.#s = t.createElement("canvas"), this.#s.className = "zine-leaf-front", this.#s.style.cssText = "position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;", this.#c = t.createElement("canvas"), this.#c.className = "zine-leaf-back", this.#c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);", this.#l = t.createElement("div"), this.#l.className = "zine-leaf-shadow", this.#l.style.cssText = "position:absolute;inset:0;opacity:0;pointer-events:none;", this.#l.style.background = s("forward"), this.#o.append(this.#s, this.#c, this.#l), this.#r.append(this.#i, this.#a, this.#o), this.#n.append(this.#r), this.#t.append(this.#n), e.append(this.#t), Promise.resolve();
46
47
  }
47
48
  destroy() {
48
49
  this.#t?.remove(), this.#e = null;
49
50
  }
50
- renderSpread(e, t, r) {
51
- r?.fill ? (this.#i.style.width = "100%", this.#a.style.display = "none", this.#T(this.#i, t.right ?? t.left)) : (this.#i.style.width = "50%", this.#a.style.display = "", this.#T(this.#i, t.left), this.#T(this.#a, t.right)), this.#g = r?.fill ?? !1, r?.fit && (this.#m = r.fit), this.#_ = this.#g ? 0 : n(t), this.#y(t), this.#x(), this.#v(this.#_), this.#o.style.display = "none", this.#o.style.transform = "", this.#l.style.opacity = "0";
51
+ renderSpread(r, i, a) {
52
+ if (a?.fill) this.#i.style.width = "100%", this.#a.style.display = "none", this.#T(this.#i, i.right ?? i.left);
53
+ else {
54
+ this.#i.style.width = "50%", this.#a.style.display = "";
55
+ let r = e(i, !1);
56
+ this.#T(this.#i, i.left, t(r)), this.#T(this.#a, i.right, n(r));
57
+ }
58
+ this.#g = a?.fill ?? !1, a?.fit && (this.#m = a.fit), this.#_ = this.#g ? 0 : o(i), this.#y(i, this.#g), this.#x(), this.#v(this.#_), this.#o.style.display = "none", this.#o.style.transform = "", this.#l.style.opacity = "0";
52
59
  }
53
- beginFlip(e, t, i, a) {
54
- a?.fill ? (this.#i.style.width = "100%", this.#a.style.display = "none", this.#o.style.width = "100%", this.#o.style.left = "0", this.#o.style.transformOrigin = i === "forward" ? "left center" : "right center", this.#T(this.#i, t.right ?? t.left), this.#T(this.#s, e.right ?? e.left), this.#T(this.#c, t.right ?? t.left)) : (this.#i.style.width = "50%", this.#a.style.display = "", this.#o.style.width = "50%", i === "forward" ? (this.#T(this.#i, e.left), this.#T(this.#a, t.right), this.#T(this.#s, e.right), this.#T(this.#c, t.left), this.#o.style.left = "50%", this.#o.style.transformOrigin = "left center") : (this.#T(this.#a, e.right), this.#T(this.#i, t.left), this.#T(this.#s, e.left), this.#T(this.#c, t.right), this.#o.style.left = "0", this.#o.style.transformOrigin = "right center")), this.#u = a?.fill ? 0 : n(e), this.#d = a?.fill ? 0 : n(t), this.#g = a?.fill ?? !1, a?.fit && (this.#m = a.fit), this.#y(t), this.#x(), this.#v(this.#u), this.#o.style.display = "block", this.#o.style.transform = "rotateY(0deg)", this.#l.style.background = r(i), this.#l.style.opacity = "0";
60
+ beginFlip(r, i, a, c) {
61
+ if (c?.fill) this.#i.style.width = "100%", this.#a.style.display = "none", this.#o.style.width = "100%", this.#o.style.left = "0", this.#o.style.transformOrigin = a === "forward" ? "left center" : "right center", this.#T(this.#i, i.right ?? i.left), this.#T(this.#s, r.right ?? r.left), this.#T(this.#c, i.right ?? i.left);
62
+ else {
63
+ this.#i.style.width = "50%", this.#a.style.display = "", this.#o.style.width = "50%";
64
+ let o = e(r, !1), s = e(i, !1);
65
+ a === "forward" ? (this.#T(this.#i, r.left, t(o)), this.#T(this.#a, i.right, n(s)), this.#T(this.#s, r.right, n(o)), this.#T(this.#c, i.left, t(s)), this.#o.style.left = "50%", this.#o.style.transformOrigin = "left center") : (this.#T(this.#a, r.right, n(o)), this.#T(this.#i, i.left, t(s)), this.#T(this.#s, r.left, t(o)), this.#T(this.#c, i.right, n(s)), this.#o.style.left = "0", this.#o.style.transformOrigin = "right center");
66
+ }
67
+ this.#u = c?.fill ? 0 : o(r), this.#d = c?.fill ? 0 : o(i), this.#g = c?.fill ?? !1, c?.fit && (this.#m = c.fit), this.#y(i, this.#g), this.#x(), this.#v(this.#u), this.#o.style.display = "block", this.#o.style.transform = "rotateY(0deg)", this.#l.style.background = s(a), this.#l.style.opacity = "0";
55
68
  }
56
- setFlipProgress(n, r) {
57
- let i = e(n), a = i.angle * t;
58
- this.#v(this.#u + (this.#d - this.#u) * n), this.#o.style.display = "block", this.#o.style.transform = `rotateY(${r === "forward" ? -a : a}deg)`, this.#l.style.opacity = String(i.shadowAlpha);
69
+ setFlipProgress(e, t) {
70
+ let n = i(e), r = n.angle * a;
71
+ this.#v(this.#u + (this.#d - this.#u) * e), this.#o.style.display = "block", this.#o.style.transform = `rotateY(${t === "forward" ? -r : r}deg)`, this.#l.style.opacity = String(n.shadowAlpha);
59
72
  }
60
73
  #v(e) {
61
74
  let t = e * (this.#h / 4);
62
75
  this.#r.style.transform = t ? `translateX(${t}px)` : "";
63
76
  }
64
- #y(e) {
65
- let t = e.left ?? e.right;
66
- t && t.height && (this.#f = t.width / t.height, this.#p === 0 && (this.#p = this.#f));
77
+ #y(t, n) {
78
+ let r = t.left ?? t.right;
79
+ if (r && r.height) {
80
+ let i = r.width / r.height;
81
+ this.#f = i * (1 - e(t, n)), this.#p === 0 && (this.#p = i);
82
+ }
67
83
  }
68
84
  #b() {
69
85
  return this.#m === "fill" && this.#p > 0 ? this.#p : this.#f;
@@ -124,16 +140,17 @@ var i = class {
124
140
  let r = e.createElement("canvas");
125
141
  return r.className = t, r.style.cssText = `position:absolute;top:0;width:50%;height:100%;${n}`, r;
126
142
  }
127
- #T(e, t) {
128
- let n = e.getContext("2d");
143
+ #T(e, t, n = r) {
144
+ let i = e.getContext("2d");
129
145
  if (t === null) {
130
- n?.clearRect(0, 0, e.width, e.height);
146
+ i?.clearRect(0, 0, e.width, e.height);
131
147
  return;
132
148
  }
133
- e.width = t.width, e.height = t.height, n?.drawImage(t, 0, 0);
149
+ let a = n[0] * t.width, o = (n[1] - n[0]) * t.width;
150
+ e.width = Math.round(o), e.height = t.height, i?.drawImage(t, a, 0, o, t.height, 0, 0, e.width, t.height);
134
151
  }
135
152
  };
136
153
  //#endregion
137
- export { i as CssRenderer };
154
+ export { c as CssRenderer };
138
155
 
139
- //# sourceMappingURL=cssRenderer-CTaM4cAj.js.map
156
+ //# sourceMappingURL=cssRenderer-DAW29lU2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cssRenderer-DAW29lU2.js","names":["#container","#clip","#viewport","#book","#pageLeft","#makeCanvas","#pageRight","#leaf","#leafFront","#leafBack","#leafShadow","#paint","#fill","#fit","#contentShift","#trackAspect","#layoutBook","#applyShift","#fromShiftUnit","#toShiftUnit","#bookW","#pageAspect","#docAspect","#bookBox","#drawAspect","#contentBox"],"sources":["../src/geometry/flipProgressToPose.ts","../src/renderer/cssRenderer.ts"],"sourcesContent":["export interface FlipPose {\n /** Turning-page rotation about the spine, radians: 0 (flat/open) → π (fully turned). */\n angle: number;\n /** Mesh cylinder-wrap curl, 0..1 — flat at the ends, peaks edge-on at t=0.5. CSS renderer ignores it. */\n curl: number;\n /** Fold-shadow intensity, 0..1 — peaks edge-on at t=0.5. */\n shadowAlpha: number;\n}\n\n/**\n * Map a 0→1 flip progress to a renderer-neutral fold pose.\n *\n * Linear in `t` on purpose — easing belongs to the animator that produces `t`,\n * so seeking to a fixed progress (e.g. the 0/.25/.5/.75/1 visual-regression stops)\n * is deterministic. The CSS renderer consumes `angle` as a spine `rotateY`; the\n * WebGL2 renderer additionally bends its mesh by `curl`.\n */\nexport function flipProgressToPose(t: number): FlipPose {\n const clamped = t < 0 ? 0 : t > 1 ? 1 : t;\n const arc = Math.sin(clamped * Math.PI);\n return {\n angle: clamped * Math.PI,\n curl: arc,\n shadowAlpha: arc,\n };\n}\n","import { flipProgressToPose } from '../geometry/flipProgressToPose';\nimport type { Spread } from '../engine/spread';\nimport {\n FULL_SPAN,\n leftSpan,\n rightSpan,\n spineTrimOf,\n type FlipDirection,\n type LayoutMetrics,\n type PageContent,\n type PageSpan,\n type Renderer,\n type RenderOptions,\n type SpreadContent,\n} from './types';\n\nconst RAD_TO_DEG = 180 / Math.PI;\n\n/** How far to shift a spread to centre a lone page, in units of a quarter container width:\n * -1 = only a right page (cover), +1 = only a left page (back), 0 = a full spread. */\nfunction shiftUnit(content: SpreadContent): number {\n const lone = (content.left === null) !== (content.right === null);\n return lone ? (content.right !== null ? -1 : 1) : 0;\n}\n\n/**\n * Layered gradients painted over the turning leaf to fake paper depth on the flat\n * fold: a crease shadow at the spine, a soft curve sheen, and a free-edge shade.\n * The spine is the leaf's pivot edge — left for a forward flip, right for backward.\n */\nfunction foldOverlay(direction: FlipDirection): string {\n const dir = direction === 'forward' ? 'to right' : 'to left';\n return [\n `linear-gradient(${dir}, rgba(0,0,0,0.6), rgba(0,0,0,0.15) 40%, rgba(0,0,0,0) 60%)`,\n `linear-gradient(${dir}, transparent 40%, rgba(255,255,255,0.32) 66%, transparent 90%)`,\n `linear-gradient(${dir}, transparent 66%, rgba(0,0,0,0.35))`,\n ].join(',');\n}\n\n/**\n * Baseline renderer: a 2-panel CSS-3D fold. Runs on every browser, no GPU. Pages\n * are <canvas> elements; the turning leaf (front + back faces, backface-hidden)\n * rotates about the spine driven by flipProgressToPose(), with a gradient shadow\n * overlay for depth. `beginFlip` stages the surfaces; `setFlipProgress` animates.\n */\nexport class CssRenderer implements Renderer {\n #container: HTMLElement | null = null;\n #clip!: HTMLDivElement;\n #viewport!: HTMLDivElement;\n #book!: HTMLDivElement;\n #pageLeft!: HTMLCanvasElement;\n #pageRight!: HTMLCanvasElement;\n #leaf!: HTMLDivElement;\n #leafFront!: HTMLCanvasElement;\n #leafBack!: HTMLCanvasElement;\n #leafShadow!: HTMLDivElement;\n // Lone-page centering, interpolated across a flip (see the WebGL renderer).\n #fromShiftUnit = 0;\n #toShiftUnit = 0;\n // Book aspect fit: the spread is letterboxed to the page aspect (see the WebGL renderer).\n #pageAspect = 0;\n // First page's aspect, latched: the container is sized to this document-wide so off-size pages\n // do not resize it. `contain` letterboxes them within it; `fill` stretches them to it.\n #docAspect = 0;\n #fit: 'contain' | 'fill' = 'contain';\n #bookW = 0;\n #fill = false;\n #contentShift = 0; // shiftUnit of the current static spread (nonzero = lone page, centered)\n\n mount(container: HTMLElement): Promise<void> {\n const doc = container.ownerDocument;\n this.#container = container;\n\n // Fixed-size clip box; NOT transformed, so zoom can't scale the crop region.\n this.#clip = doc.createElement('div');\n this.#clip.className = 'zine-clip';\n this.#clip.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;';\n\n // Transform target for zoom/pan; lives inside the clip.\n this.#viewport = doc.createElement('div');\n this.#viewport.className = 'zine-viewport';\n this.#viewport.style.cssText = 'position:absolute;inset:0;transform-origin:0 0;';\n\n this.#book = doc.createElement('div');\n this.#book.className = 'zine-book';\n this.#book.style.cssText =\n 'position:absolute;inset:0;perspective:2000px;transform-style:preserve-3d;';\n\n this.#pageLeft = this.#makeCanvas(doc, 'zine-page-left', 'left:0;');\n this.#pageRight = this.#makeCanvas(doc, 'zine-page-right', 'right:0;');\n\n this.#leaf = doc.createElement('div');\n this.#leaf.className = 'zine-leaf';\n this.#leaf.style.cssText =\n 'position:absolute;top:0;width:50%;height:100%;transform-style:preserve-3d;display:none;';\n\n this.#leafFront = doc.createElement('canvas');\n this.#leafFront.className = 'zine-leaf-front';\n this.#leafFront.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;';\n\n this.#leafBack = doc.createElement('canvas');\n this.#leafBack.className = 'zine-leaf-back';\n this.#leafBack.style.cssText =\n 'position:absolute;inset:0;width:100%;height:100%;backface-visibility:hidden;transform:rotateY(180deg);';\n\n this.#leafShadow = doc.createElement('div');\n this.#leafShadow.className = 'zine-leaf-shadow';\n this.#leafShadow.style.cssText = 'position:absolute;inset:0;opacity:0;pointer-events:none;';\n this.#leafShadow.style.background = foldOverlay('forward'); // default; beginFlip sets per direction\n\n this.#leaf.append(this.#leafFront, this.#leafBack, this.#leafShadow);\n this.#book.append(this.#pageLeft, this.#pageRight, this.#leaf);\n this.#viewport.append(this.#book);\n this.#clip.append(this.#viewport);\n container.append(this.#clip);\n\n return Promise.resolve();\n }\n\n destroy(): void {\n this.#clip?.remove();\n this.#container = null;\n }\n\n renderSpread(_spread: Spread, content: SpreadContent, options?: RenderOptions): void {\n if (options?.fill) {\n // Single-page mode: the lone page fills the container; hide the other panel.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#paint(this.#pageLeft, content.right ?? content.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n const trim = spineTrimOf(content, false);\n this.#paint(this.#pageLeft, content.left, leftSpan(trim));\n this.#paint(this.#pageRight, content.right, rightSpan(trim));\n }\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#contentShift = this.#fill ? 0 : shiftUnit(content);\n this.#trackAspect(content, this.#fill);\n this.#layoutBook();\n this.#applyShift(this.#contentShift);\n // A fresh spread cancels any in-progress flip.\n this.#leaf.style.display = 'none';\n this.#leaf.style.transform = '';\n this.#leafShadow.style.opacity = '0';\n }\n\n beginFlip(\n from: SpreadContent,\n to: SpreadContent,\n direction: FlipDirection,\n options?: RenderOptions,\n ): void {\n if (options?.fill) {\n // Single-page: a full-width leaf turns about one edge, revealing `to` underneath.\n this.#pageLeft.style.width = '100%';\n this.#pageRight.style.display = 'none';\n this.#leaf.style.width = '100%';\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = direction === 'forward' ? 'left center' : 'right center';\n this.#paint(this.#pageLeft, to.right ?? to.left);\n this.#paint(this.#leafFront, from.right ?? from.left);\n this.#paint(this.#leafBack, to.right ?? to.left);\n } else {\n this.#pageLeft.style.width = '50%';\n this.#pageRight.style.display = '';\n this.#leaf.style.width = '50%';\n // Each page keeps the spine trim of its own spread (see the WebGL renderer).\n const fromTrim = spineTrimOf(from, false);\n const toTrim = spineTrimOf(to, false);\n if (direction === 'forward') {\n // Right page lifts and swings left about the spine. Left stays; right reveals `to`.\n this.#paint(this.#pageLeft, from.left, leftSpan(fromTrim));\n this.#paint(this.#pageRight, to.right, rightSpan(toTrim));\n this.#paint(this.#leafFront, from.right, rightSpan(fromTrim));\n this.#paint(this.#leafBack, to.left, leftSpan(toTrim));\n this.#leaf.style.left = '50%';\n this.#leaf.style.transformOrigin = 'left center';\n } else {\n // Left page swings right about its right edge. Right stays; left reveals `to`.\n this.#paint(this.#pageRight, from.right, rightSpan(fromTrim));\n this.#paint(this.#pageLeft, to.left, leftSpan(toTrim));\n this.#paint(this.#leafFront, from.left, leftSpan(fromTrim));\n this.#paint(this.#leafBack, to.right, rightSpan(toTrim));\n this.#leaf.style.left = '0';\n this.#leaf.style.transformOrigin = 'right center';\n }\n }\n this.#fromShiftUnit = options?.fill ? 0 : shiftUnit(from);\n this.#toShiftUnit = options?.fill ? 0 : shiftUnit(to);\n this.#fill = options?.fill ?? false;\n if (options?.fit) this.#fit = options.fit;\n this.#trackAspect(to, this.#fill);\n this.#layoutBook();\n this.#applyShift(this.#fromShiftUnit);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = 'rotateY(0deg)';\n this.#leafShadow.style.background = foldOverlay(direction);\n this.#leafShadow.style.opacity = '0';\n }\n\n setFlipProgress(t: number, direction: FlipDirection): void {\n const pose = flipProgressToPose(t);\n const deg = pose.angle * RAD_TO_DEG;\n this.#applyShift(this.#fromShiftUnit + (this.#toShiftUnit - this.#fromShiftUnit) * t);\n this.#leaf.style.display = 'block';\n this.#leaf.style.transform = `rotateY(${direction === 'forward' ? -deg : deg}deg)`;\n this.#leafShadow.style.opacity = String(pose.shadowAlpha);\n }\n\n #applyShift(unit: number): void {\n const px = unit * (this.#bookW / 4);\n this.#book.style.transform = px ? `translateX(${px}px)` : '';\n }\n\n /** Remember the current spread's page aspect (for drawing; narrowed by any spine trim) and latch\n * the first page's untrimmed aspect (for the container), as in the WebGL renderer. */\n #trackAspect(content: SpreadContent, fill: boolean): void {\n const p = content.left ?? content.right;\n if (p && p.height) {\n const raw = p.width / p.height;\n this.#pageAspect = raw * (1 - spineTrimOf(content, fill));\n if (this.#docAspect === 0) this.#docAspect = raw;\n }\n }\n\n /** Aspect the book is drawn at: the current page under `contain` (letterboxed within the stable\n * container), or the document aspect under `fill` (fills it, stretching off-size pages). */\n #drawAspect(): number {\n return this.#fit === 'fill' && this.#docAspect > 0 ? this.#docAspect : this.#pageAspect;\n }\n\n /** Letterbox the 2-page book to the page aspect, centered in the container, so pages\n * fill their halves without stretching; the surround is the container background. */\n #layoutBook(): void {\n const b = this.#bookBox();\n this.#bookW = b.width;\n const s = this.#book.style;\n s.inset = 'auto';\n s.left = `${b.x}px`;\n s.top = `${b.y}px`;\n s.width = `${b.width}px`;\n s.height = `${b.height}px`;\n }\n\n /** The fitted book rect in container px (letterbox aware) — a stable 2-page area. */\n #bookBox(): { x: number; y: number; width: number; height: number } {\n const cw = this.#container?.clientWidth ?? 0;\n const ch = this.#container?.clientHeight ?? 0;\n const aspect = this.#drawAspect();\n if (aspect <= 0 || cw <= 0 || ch <= 0) return { x: 0, y: 0, width: cw, height: ch };\n const ba = (this.#fill ? 1 : 2) * aspect;\n let bw = cw;\n let bh = ch;\n if (ba > cw / ch) bh = cw / ba;\n else bw = ch * ba;\n return { x: (cw - bw) / 2, y: (ch - bh) / 2, width: bw, height: bh };\n }\n\n /** Where the current spread is actually painted: the book, or its centered half for a lone\n * page (mirrors #applyShift). This is what the engine hit-tests against. */\n #contentBox(): { x: number; y: number; width: number; height: number } {\n const b = this.#bookBox();\n return this.#contentShift !== 0 ? { x: b.x + b.width / 4, y: b.y, width: b.width / 2, height: b.height } : b;\n }\n\n setViewTransform(scale: number, x: number, y: number): void {\n this.#viewport.style.transform = `translate(${x}px, ${y}px) scale(${scale})`;\n }\n\n measure(): LayoutMetrics {\n const el = this.#container;\n const w = el?.clientWidth ?? 0;\n const h = el?.clientHeight ?? 0;\n const known = this.#pageAspect > 0;\n const book = known ? this.#bookBox() : undefined;\n const content = known ? this.#contentBox() : undefined;\n // The shift lives on #book, inside the scaled #viewport, so unlike the WebGL2 renderer it\n // magnifies along with everything else and the whole box simply scales.\n const screenAt = content\n ? (scale: number): { x: number; y: number; width: number; height: number } => ({\n x: content.x * scale,\n y: content.y * scale,\n width: content.width * scale,\n height: content.height * scale,\n })\n : undefined;\n return {\n containerWidth: w,\n containerHeight: h,\n pageWidth: w / 2,\n pageHeight: h,\n book,\n content,\n screenAt,\n containerAspect: this.#docAspect > 0 ? (this.#fill ? 1 : 2) * this.#docAspect : undefined,\n };\n }\n\n #makeCanvas(doc: Document, className: string, extra: string): HTMLCanvasElement {\n const c = doc.createElement('canvas');\n c.className = className;\n c.style.cssText = `position:absolute;top:0;width:50%;height:100%;${extra}`;\n return c;\n }\n\n /** Paint the visible `span` of a page (the whole page unless a spine trim narrows it). */\n #paint(canvas: HTMLCanvasElement, content: PageContent | null, span: PageSpan = FULL_SPAN): void {\n const ctx = canvas.getContext('2d');\n if (content === null) {\n ctx?.clearRect(0, 0, canvas.width, canvas.height);\n return;\n }\n const sx = span[0] * content.width;\n const sw = (span[1] - span[0]) * content.width;\n canvas.width = Math.round(sw);\n canvas.height = content.height;\n ctx?.drawImage(content, sx, 0, sw, content.height, 0, 0, canvas.width, content.height);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { PageContent } from '../renderer/types';
2
+ /**
3
+ * Find a repeated strip across the spine. `left` and `right` are RGBA pixels of two strips of the
4
+ * same size (`width` columns, `height` rows): `left` ends at the left page's spine edge, `right`
5
+ * starts at the right page's. Returns the overlap in columns, or 0 when there is no clear repeat.
6
+ */
7
+ export declare function matchOverlap(left: Uint8ClampedArray, right: Uint8ClampedArray, width: number, height: number): number;
8
+ /**
9
+ * The print-gutter overlap between two facing pages, as a fraction of page width (0 = none found).
10
+ * Pages of different sizes are never matched: their rows would not line up.
11
+ */
12
+ export declare function detectOverlap(left: PageContent, right: PageContent): number;
@@ -0,0 +1,43 @@
1
+ function e(e) {
2
+ let t = new Float32Array(e.length / 4);
3
+ for (let n = 0; n < t.length; n++) t[n] = (e[n * 4] + e[n * 4 + 1] + e[n * 4 + 2]) / 3;
4
+ return t;
5
+ }
6
+ function t(t, n, r, i) {
7
+ let a = e(t), o = e(n), s = new Float64Array(r + 1).fill(Infinity), c = 0;
8
+ for (let e = 3; e <= r; e++) {
9
+ let t = 0, n = 0;
10
+ for (let s = 0; s < i; s++) {
11
+ let i = s * r;
12
+ for (let s = 0; s < e; s++) {
13
+ let c = a[i + r - e + s], l = o[i + s];
14
+ (c < 235 || l < 235) && (t += Math.abs(c - l), n++);
15
+ }
16
+ }
17
+ n < Math.max(64, .02 * e * i) || (s[e] = t / n, (c === 0 || s[e] < s[c]) && (c = e));
18
+ }
19
+ if (c === 0 || s[c] > 16) return 0;
20
+ let l = Math.max(s[c] * 2.5, s[c] + 6), u = Math.max(2, Math.round(c / 4)), d = [c - u, c + u].filter((e) => e >= 3 && e <= r);
21
+ return d.length === 0 || d.some((e) => s[e] < l) ? 0 : c;
22
+ }
23
+ function n(e, t, n) {
24
+ let r = (typeof OffscreenCanvas < "u" ? new OffscreenCanvas(n, 400) : Object.assign(document.createElement("canvas"), {
25
+ width: n,
26
+ height: 400
27
+ })).getContext("2d", { willReadFrequently: !0 });
28
+ return r === null ? null : (r.drawImage(e, t, 0, n, e.height, 0, 0, n, 400), r.getImageData(0, 0, n, 400).data);
29
+ }
30
+ function r(e, r) {
31
+ if (e.width !== r.width || e.height !== r.height || e.width === 0) return 0;
32
+ let i = Math.max(3, Math.ceil(e.width * .06));
33
+ try {
34
+ let a = n(e, e.width - i, i), o = n(r, 0, i);
35
+ return a === null || o === null ? 0 : t(a, o, i, 400) / e.width;
36
+ } catch {
37
+ return 0;
38
+ }
39
+ }
40
+ //#endregion
41
+ export { r as detectOverlap };
42
+
43
+ //# sourceMappingURL=gutter-C3H2CDT2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gutter-C3H2CDT2.js","names":[],"sources":["../src/engine/gutter.ts"],"sourcesContent":["import type { PageContent } from '../renderer/types';\n\n/**\n * Print-gutter duplicate detection (the `gutterOverlap: 'auto'` option).\n *\n * Print PDFs sometimes repeat a strip at the spine: the left page's last N columns show the same\n * artwork as the right page's first N, so nothing is lost in the binding. Shown flat on screen\n * the strip appears twice. We look for that exact repeat and report its width; the renderers then\n * hide half of it on each page so the spread joins seamlessly.\n *\n * Conservative by design: anything short of a sharp, near-exact match reports 0 and the spread\n * renders as-is.\n */\n\n/** Widest overlap searched, as a fraction of page width. Print allowances are a few percent. */\nconst MAX_OVERLAP = 0.06;\n/** Rows sampled per strip (the strip is scaled vertically to this; columns stay native). */\nconst ROWS = 400;\n/** Narrowest overlap considered, in columns. */\nconst MIN_COLUMNS = 3;\n/** Luma below this counts as ink; near-white margin pixels are ignored so blank edges can't match. */\nconst INK = 235;\n/** A match must be this close (mean absolute luma difference, 0..255) to count as a duplicate. */\nconst MAX_DIFF = 16;\n/** Widths either side of the best match must differ by at least this factor and margin, which\n * rejects uniform strips (solid colour matches at every width, so it has no sharp minimum). */\nconst CONTRAST_RATIO = 2.5;\nconst CONTRAST_MARGIN = 6;\n\nfunction luma(rgba: Uint8ClampedArray): Float32Array {\n const out = new Float32Array(rgba.length / 4);\n for (let i = 0; i < out.length; i++) out[i] = (rgba[i * 4]! + rgba[i * 4 + 1]! + rgba[i * 4 + 2]!) / 3;\n return out;\n}\n\n/**\n * Find a repeated strip across the spine. `left` and `right` are RGBA pixels of two strips of the\n * same size (`width` columns, `height` rows): `left` ends at the left page's spine edge, `right`\n * starts at the right page's. Returns the overlap in columns, or 0 when there is no clear repeat.\n */\nexport function matchOverlap(\n left: Uint8ClampedArray,\n right: Uint8ClampedArray,\n width: number,\n height: number,\n): number {\n const a = luma(left);\n const b = luma(right);\n const diff = new Float64Array(width + 1).fill(Infinity);\n let best = 0;\n for (let n = MIN_COLUMNS; n <= width; n++) {\n let sum = 0;\n let inked = 0;\n for (let y = 0; y < height; y++) {\n const row = y * width;\n for (let i = 0; i < n; i++) {\n const la = a[row + width - n + i]!;\n const lb = b[row + i]!;\n if (la < INK || lb < INK) {\n sum += Math.abs(la - lb);\n inked++;\n }\n }\n }\n // Too little ink to judge: the strip is mostly blank margin at this width.\n if (inked < Math.max(64, 0.02 * n * height)) continue;\n diff[n] = sum / inked;\n if (best === 0 || diff[n]! < diff[best]!) best = n;\n }\n if (best === 0 || diff[best]! > MAX_DIFF) return 0;\n\n const floor = Math.max(diff[best]! * CONTRAST_RATIO, diff[best]! + CONTRAST_MARGIN);\n const offset = Math.max(2, Math.round(best / 4));\n const neighbours = [best - offset, best + offset].filter((n) => n >= MIN_COLUMNS && n <= width);\n if (neighbours.length === 0 || neighbours.some((n) => diff[n]! < floor)) return 0;\n return best;\n}\n\nfunction stripPixels(page: PageContent, x: number, width: number): Uint8ClampedArray | null {\n const canvas =\n typeof OffscreenCanvas !== 'undefined'\n ? new OffscreenCanvas(width, ROWS)\n : Object.assign(document.createElement('canvas'), { width, height: ROWS });\n const ctx = canvas.getContext('2d', { willReadFrequently: true }) as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (ctx === null) return null;\n ctx.drawImage(page, x, 0, width, page.height, 0, 0, width, ROWS);\n return ctx.getImageData(0, 0, width, ROWS).data;\n}\n\n/**\n * The print-gutter overlap between two facing pages, as a fraction of page width (0 = none found).\n * Pages of different sizes are never matched: their rows would not line up.\n */\nexport function detectOverlap(left: PageContent, right: PageContent): number {\n if (left.width !== right.width || left.height !== right.height || left.width === 0) return 0;\n const width = Math.max(MIN_COLUMNS, Math.ceil(left.width * MAX_OVERLAP));\n try {\n const a = stripPixels(left, left.width - width, width);\n const b = stripPixels(right, 0, width);\n if (a === null || b === null) return 0;\n return matchOverlap(a, b, width, ROWS) / left.width;\n } catch {\n // An unreadable raster (e.g. a tainted cross-origin image) just skips detection.\n return 0;\n }\n}\n"],"mappings":"AA6BA,SAAS,EAAK,GAAuC;CACnD,IAAM,IAAM,IAAI,aAAa,EAAK,SAAS,CAAC;CAC5C,KAAK,IAAI,IAAI,GAAG,IAAI,EAAI,QAAQ,KAAK,EAAI,MAAM,EAAK,IAAI,KAAM,EAAK,IAAI,IAAI,KAAM,EAAK,IAAI,IAAI,MAAO;CACrG,OAAO;AACT;AAOA,SAAgB,EACd,GACA,GACA,GACA,GACQ;CACR,IAAM,IAAI,EAAK,CAAI,GACb,IAAI,EAAK,CAAK,GACd,IAAO,IAAI,aAAa,IAAQ,CAAC,CAAC,CAAC,KAAK,QAAQ,GAClD,IAAO;CACX,KAAK,IAAI,IAAI,GAAa,KAAK,GAAO,KAAK;EACzC,IAAI,IAAM,GACN,IAAQ;EACZ,KAAK,IAAI,IAAI,GAAG,IAAI,GAAQ,KAAK;GAC/B,IAAM,IAAM,IAAI;GAChB,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;IAC1B,IAAM,IAAK,EAAE,IAAM,IAAQ,IAAI,IACzB,IAAK,EAAE,IAAM;IACnB,CAAI,IAAK,OAAO,IAAK,SACnB,KAAO,KAAK,IAAI,IAAK,CAAE,GACvB;GAEJ;EACF;EAEI,IAAQ,KAAK,IAAI,IAAI,MAAO,IAAI,CAAM,MAC1C,EAAK,KAAK,IAAM,IACZ,MAAS,KAAK,EAAK,KAAM,EAAK,QAAQ,IAAO;CACnD;CACA,IAAI,MAAS,KAAK,EAAK,KAAS,IAAU,OAAO;CAEjD,IAAM,IAAQ,KAAK,IAAI,EAAK,KAAS,KAAgB,EAAK,KAAS,CAAe,GAC5E,IAAS,KAAK,IAAI,GAAG,KAAK,MAAM,IAAO,CAAC,CAAC,GACzC,IAAa,CAAC,IAAO,GAAQ,IAAO,CAAM,CAAC,CAAC,QAAQ,MAAM,KAAK,KAAe,KAAK,CAAK;CAE9F,OADI,EAAW,WAAW,KAAK,EAAW,MAAM,MAAM,EAAK,KAAM,CAAK,IAAU,IACzE;AACT;AAEA,SAAS,EAAY,GAAmB,GAAW,GAAyC;CAK1F,IAAM,KAHJ,OAAO,kBAAoB,MACvB,IAAI,gBAAgB,GAAO,GAAI,IAC/B,OAAO,OAAO,SAAS,cAAc,QAAQ,GAAG;EAAE;EAAO,QAAQ;CAAK,CAAC,EAAA,CAC1D,WAAW,MAAM,EAAE,oBAAoB,GAAK,CAAC;CAMhE,OAFI,MAAQ,OAAa,QACzB,EAAI,UAAU,GAAM,GAAG,GAAG,GAAO,EAAK,QAAQ,GAAG,GAAG,GAAO,GAAI,GACxD,EAAI,aAAa,GAAG,GAAG,GAAO,GAAI,CAAC,CAAC;AAC7C;AAMA,SAAgB,EAAc,GAAmB,GAA4B;CAC3E,IAAI,EAAK,UAAU,EAAM,SAAS,EAAK,WAAW,EAAM,UAAU,EAAK,UAAU,GAAG,OAAO;CAC3F,IAAM,IAAQ,KAAK,IAAI,GAAa,KAAK,KAAK,EAAK,QAAQ,GAAW,CAAC;CACvE,IAAI;EACF,IAAM,IAAI,EAAY,GAAM,EAAK,QAAQ,GAAO,CAAK,GAC/C,IAAI,EAAY,GAAO,GAAG,CAAK;EAErC,OADI,MAAM,QAAQ,MAAM,OAAa,IAC9B,EAAa,GAAG,GAAG,GAAO,GAAI,IAAI,EAAK;CAChD,QAAQ;EAEN,OAAO;CACT;AACF"}