@getnarro/atlas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/check/index.d.ts +82 -0
  4. package/dist/check/index.js +150 -0
  5. package/dist/check/index.js.map +1 -0
  6. package/dist/chunk-5SRKWTOU.js +33 -0
  7. package/dist/chunk-5SRKWTOU.js.map +1 -0
  8. package/dist/chunk-BDF2X5LC.js +28 -0
  9. package/dist/chunk-BDF2X5LC.js.map +1 -0
  10. package/dist/chunk-RHTCDWZD.js +142 -0
  11. package/dist/chunk-RHTCDWZD.js.map +1 -0
  12. package/dist/chunk-RWYOYDBD.js +75 -0
  13. package/dist/chunk-RWYOYDBD.js.map +1 -0
  14. package/dist/chunk-SD4Y4EBG.js +69 -0
  15. package/dist/chunk-SD4Y4EBG.js.map +1 -0
  16. package/dist/chunk-UTCNNNPT.js +89 -0
  17. package/dist/chunk-UTCNNNPT.js.map +1 -0
  18. package/dist/embed/narro-atlas.js +12 -0
  19. package/dist/image/index.d.ts +132 -0
  20. package/dist/image/index.js +117 -0
  21. package/dist/image/index.js.map +1 -0
  22. package/dist/index.d.ts +33 -0
  23. package/dist/index.js +12 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lod-DBuZ0MG3.d.ts +86 -0
  26. package/dist/map/index.d.ts +222 -0
  27. package/dist/map/index.js +184 -0
  28. package/dist/map/index.js.map +1 -0
  29. package/dist/path-BIxJH8g2.d.ts +101 -0
  30. package/dist/provider-CjL3i-0A.d.ts +54 -0
  31. package/dist/react/index.d.ts +183 -0
  32. package/dist/react/index.js +291 -0
  33. package/dist/react/index.js.map +1 -0
  34. package/dist/replay/index.d.ts +162 -0
  35. package/dist/replay/index.js +138 -0
  36. package/dist/replay/index.js.map +1 -0
  37. package/dist/tag-BP51cBza.d.ts +24 -0
  38. package/dist/timeline-B_dNydQA.d.ts +194 -0
  39. package/dist/video/index.d.ts +101 -0
  40. package/dist/video/index.js +91 -0
  41. package/dist/video/index.js.map +1 -0
  42. package/dist/view-BzztOpV6.d.ts +83 -0
  43. package/dist/web/embed.d.ts +59 -0
  44. package/dist/web/embed.js +192 -0
  45. package/dist/web/embed.js.map +1 -0
  46. package/dist/web/index.d.ts +89 -0
  47. package/dist/web/index.js +4 -0
  48. package/dist/web/index.js.map +1 -0
  49. package/package.json +119 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/camera/flight.ts","../src/camera/path.ts"],"names":[],"mappings":"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","file":"chunk-RHTCDWZD.js","sourcesContent":["/**\n * Smooth and efficient zooming and panning — van Wijk & Nuij, InfoVis 2003.\n *\n * This is the whole reason this package exists, so it is worth saying what it fixes.\n *\n * Tween `cx`, `cy` and `w` as three independent curves — which is what every\n * presentation tool in this field does, `TransformSlide` included — and consider what a\n * viewer's eye actually tracks. Apparent velocity is world velocity divided by the\n * current view width. Fly 5000 world units while zooming from width 4000 to width 200,\n * and the camera starts at about one screen per second and ends at *fifty*, because the\n * denominator collapsed while the numerator did not. The picture accelerates by more\n * than an order of magnitude while the numbers say it is slowing down. The eye tries to\n * track features that keep getting faster, the vestibular system gets no matching\n * signal, and that mismatch is the mechanical cause of the motion-sickness complaint\n * that tops every review of every product in this category. No easing curve fixes it;\n * easing only moves the peak around.\n *\n * Van Wijk and Nuij solve it properly: treat `(cx, cy, log w)` as a hyperbolic space and\n * take the geodesic between the two views. Two things fall out, and a presentation tool\n * wants both.\n *\n * - **Apparent velocity is constant** for the whole flight, by construction rather than\n * by tuning. The lurch is gone.\n * - **The path knows how long it should take.** {@link Flight.length} is the distance\n * through that space, so a far, deep flight *asks* for more time than a small nudge.\n * The author does not have to guess, and {@link Flight.suggestedDuration} is what\n * `narro check` compares an author's override against.\n *\n * The characteristic \"pull back, travel, drop in\" arc that people actually like about\n * Prezi is emergent here, not authored. `rho` is how pronounced it is.\n *\n * It is closed-form in `t`, which matters more here than anywhere else: the video\n * renderer advances a frame counter and calls a render function, so frame 37 must not\n * depend on frames 0-36 having been drawn. A spring would have to be integrated. This\n * does not.\n *\n * @packageDocumentation\n */\n\nimport { normalizeView, type View } from \"./view\";\n\n/**\n * How much the camera pulls back on the way — van Wijk's ρ.\n *\n * √2 is d3's default and the value this is tested against. The paper's own user study\n * lands on ≈1.42 for \"fastest without discomfort\", which is the same number to within\n * the width of the preference. Higher arcs out further and reads as more dramatic;\n * lower flies flatter and reads as more direct. Below about 1 the geometry degenerates.\n */\nexport const DEFAULT_RHO = Math.SQRT2;\n\n/** Below this, two views are the same place and the general solution divides by zero. */\nconst EPSILON_SQUARED = 1e-12;\n\nfunction cosh(x: number): number {\n const e = Math.exp(x);\n return (e + 1 / e) / 2;\n}\n\nfunction sinh(x: number): number {\n const e = Math.exp(x);\n return (e - 1 / e) / 2;\n}\n\nfunction tanh(x: number): number {\n const e = Math.exp(2 * x);\n return (e - 1) / (e + 1);\n}\n\n/** A camera move from one view to another, as a pure function of `t` in `[0, 1]`. */\nexport interface Flight {\n readonly from: View;\n readonly to: View;\n /** The ρ this flight was built with. */\n readonly rho: number;\n /**\n * The length of the path through (x, y, log w) space.\n *\n * This is the number that makes a duration checkable: it is \"how far the picture\n * travels\", counted in a unit where zooming and panning are commensurate. Zero for a\n * flight that goes nowhere.\n */\n readonly length: number;\n /**\n * How long this flight wants to take, in seconds, at a comfortable apparent speed.\n *\n * Derived from {@link length}, so it scales with the move rather than being a constant\n * somebody picked. Use it as the default and let authors override it — then check the\n * override against it.\n */\n readonly suggestedDuration: number;\n /** The view at `t`, clamped to `[0, 1]`. `at(0)` is `from` and `at(1)` is `to`. */\n at(t: number): View;\n}\n\n/**\n * Seconds per unit of path length.\n *\n * Calibrated so a flight that pans one screen width at a constant zoom takes about a\n * second, which is the speed a room reads without anybody feeling hurried or stranded.\n */\nconst SECONDS_PER_UNIT = 0.9;\n\n/** The shortest flight worth animating, in seconds. */\nexport const MIN_FLIGHT_SECONDS = 0.2;\n\n/** The longest a single flight should ever be asked to take, in seconds. */\nexport const MAX_FLIGHT_SECONDS = 4;\n\n/**\n * Build the camera move between two views.\n *\n * @param from - where the camera starts\n * @param to - where it ends\n * @param rho - how far it pulls back on the way; see {@link DEFAULT_RHO}\n */\nexport function flight(from: View, to: View, rho: number = DEFAULT_RHO): Flight {\n const u0 = normalizeView(from);\n const u1 = normalizeView(to);\n const safeRho = rho > 0 ? rho : DEFAULT_RHO;\n\n const dx = u1.cx - u0.cx;\n const dy = u1.cy - u0.cy;\n const d2 = dx * dx + dy * dy;\n\n const rho2 = safeRho * safeRho;\n const rho4 = rho2 * rho2;\n\n let length: number;\n let at: (t: number) => View;\n\n if (d2 < EPSILON_SQUARED) {\n // Straight up or straight down: same place, different zoom. The general solution's\n // denominator is the distance travelled, so it has to be special-cased rather than\n // approximated — and the answer is simply exponential in the zoom ratio.\n const ratio = Math.log(u1.w / u0.w) / safeRho;\n length = Math.abs(ratio);\n at = (t: number) => ({\n cx: u0.cx + t * dx,\n cy: u0.cy + t * dy,\n w: u0.w * Math.exp(safeRho * t * ratio),\n });\n } else {\n const d1 = Math.sqrt(d2);\n const b0 = (u1.w * u1.w - u0.w * u0.w + rho4 * d2) / (2 * u0.w * rho2 * d1);\n const b1 = (u1.w * u1.w - u0.w * u0.w - rho4 * d2) / (2 * u1.w * rho2 * d1);\n const r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0);\n const r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);\n const S = (r1 - r0) / safeRho;\n const coshr0 = cosh(r0);\n const sinhr0 = sinh(r0);\n\n length = Math.abs(S);\n at = (t: number) => {\n const s = t * S;\n const u = (u0.w / (rho2 * d1)) * (coshr0 * tanh(safeRho * s + r0) - sinhr0);\n return {\n cx: u0.cx + u * dx,\n cy: u0.cy + u * dy,\n w: (u0.w * coshr0) / cosh(safeRho * s + r0),\n };\n };\n }\n\n const suggested = Math.min(\n MAX_FLIGHT_SECONDS,\n Math.max(MIN_FLIGHT_SECONDS, length * SECONDS_PER_UNIT),\n );\n\n return {\n from: u0,\n to: u1,\n rho: safeRho,\n length,\n suggestedDuration: suggested,\n at(t: number): View {\n if (!Number.isFinite(t) || t <= 0) return u0;\n if (t >= 1) return u1;\n const view = at(t);\n // A degenerate input (a zero-width view, coincident points at the same zoom) can\n // fall out as NaN. Hold the start rather than handing a NaN to a CSS transform,\n // which paints nothing at all and looks like a crash.\n return isFiniteTriple(view) ? view : u0;\n },\n };\n}\n\nfunction isFiniteTriple(view: View): boolean {\n return Number.isFinite(view.cx) && Number.isFinite(view.cy) && Number.isFinite(view.w);\n}\n\n/**\n * The perceived speed of a flight at `t`, in the units the flight is smooth in.\n *\n * Panning and zooming are different sensations, and the whole trick of the algorithm is\n * that ρ is the exchange rate between them. Writing `a = ρs + r₀`, the path's pan\n * component works out to `sech(a)/ρ` and its zoom component to `ρ·tanh(a)`, so\n *\n * ```text\n * (ρ · pan)² + (zoom / ρ)² = sech²(a) + tanh²(a) = 1\n * ```\n *\n * — identically, everywhere along the path. That identity *is* the guarantee: a viewer\n * experiences one steady rate of change, however the flight trades travelling for\n * zooming along the way. Measured here numerically, which makes it a property a test can\n * assert rather than a claim in a comment.\n *\n * Returns speed per unit of `t`, so it is `length` for a well-formed flight.\n */\nexport function perceivedSpeedAt(f: Flight, t: number, h = 1e-5): number {\n const lo = Math.max(0, t - h);\n const hi = Math.min(1, t + h);\n const dt = hi - lo;\n if (dt === 0) return 0;\n\n const a = f.at(lo);\n const b = f.at(hi);\n const here = f.at(t);\n if (here.w <= 0) return 0;\n\n const pan = Math.hypot(b.cx - a.cx, b.cy - a.cy) / dt / here.w;\n const zoom = Math.log(b.w / a.w) / dt;\n\n return Math.hypot(f.rho * pan, zoom / f.rho);\n}\n\n/**\n * How much bigger one view is than the other, as a factor ≥ 1.\n *\n * The number the motion-sickness rule is written in: a jump of more than about 8× in one\n * move is the thing audiences report as disorienting, whatever interpolation gets it there.\n */\nexport function zoomRatio(a: View, b: View): number {\n const wa = Math.max(a.w, Number.EPSILON);\n const wb = Math.max(b.w, Number.EPSILON);\n return wa > wb ? wa / wb : wb / wa;\n}\n","/**\n * The camera path: an ordered list of named framings, as data.\n *\n * A waypoint names a place, never an index. `TransformConfig.positions` is a\n * `Map<string, SlidePosition>` keyed by slide index, and that is the second thing this\n * package fixes: an index renumbers itself the moment somebody inserts a waypoint above\n * it, and it cannot be a URL fragment, an export filename, or the thing a checklist item\n * binds to. A name can be all four.\n *\n * @packageDocumentation\n */\n\nimport { DEFAULT_RHO } from \"./flight\";\nimport type { View } from \"./view\";\n\n/**\n * Where a waypoint looks.\n *\n * Either a framing in world units, or a **reference** the scene provider resolves — a\n * CSS selector over a replay, a node id on a plane. A reference is the authorable form:\n * `\"#publish-button\"` survives a re-record that moves the button, where the coordinates\n * it resolved to last week do not.\n */\nexport type WaypointTarget = View | string;\n\n/** One named framing on the path. */\nexport interface Waypoint {\n /**\n * A stable name, unique within the path.\n *\n * It is the URL fragment, the export filename and the binding a checklist item uses,\n * so it outlives reordering in a way an index cannot.\n */\n id: string;\n /** Where to look: a `{ cx, cy, w }` framing, or a reference for the provider. */\n at: WaypointTarget;\n /**\n * How far the camera pulls back on the way *in* to this waypoint — van Wijk's ρ.\n *\n * @defaultValue {@link DEFAULT_RHO}\n */\n arc?: number;\n /**\n * How long the flight *in* to this waypoint takes, in **seconds**.\n *\n * Omit it and the flight uses its own `suggestedDuration`, which is derived from how\n * far the picture actually travels. Overriding it is what `narro check` measures\n * against that suggestion — an override far below it is the lurch, and far above it is\n * the sixteen-minute camera move.\n */\n duration?: number;\n /** How long to rest here before flying on, in seconds. Only autoplay and video use it. */\n hold?: number;\n /** Human-readable label, for a checklist, a progress rail or an export filename. */\n label?: string;\n /** A chapter in a `.narrocast`, for the replay provider. Ignored by other providers. */\n chapter?: string;\n}\n\n/** A camera path over one scene. */\nexport interface AtlasPath {\n waypoints: Waypoint[];\n /** ρ for any waypoint that does not set its own. */\n arc?: number;\n /** `hold` for any waypoint that does not set its own. Seconds. */\n hold?: number;\n}\n\n/** A waypoint whose target has been resolved to a concrete framing. */\nexport interface ResolvedWaypoint extends Omit<Waypoint, \"at\"> {\n at: View;\n /** The reference this came from, when it was authored as one. */\n ref?: string;\n /** Its position on the path. */\n index: number;\n}\n\n/** Why a waypoint could not be resolved. */\nexport interface UnresolvedWaypoint {\n id: string;\n index: number;\n ref: string;\n}\n\nexport interface ResolveResult {\n resolved: ResolvedWaypoint[];\n unresolved: UnresolvedWaypoint[];\n}\n\n/** Is this target a concrete framing rather than a reference for the provider? */\nexport function isView(target: WaypointTarget): target is View {\n return typeof target === \"object\" && target !== null && \"w\" in target;\n}\n\n/**\n * Turn every waypoint's target into a framing, using `resolve` for the references.\n *\n * Unresolved waypoints are returned rather than thrown: the checker wants to report all\n * of them at once, and a host wants to skip the broken one and still fly the rest.\n */\nexport function resolvePath(path: AtlasPath, resolve: (ref: string) => View | null): ResolveResult {\n const resolved: ResolvedWaypoint[] = [];\n const unresolved: UnresolvedWaypoint[] = [];\n\n path.waypoints.forEach((waypoint, index) => {\n if (isView(waypoint.at)) {\n resolved.push({ ...waypoint, at: waypoint.at, index });\n return;\n }\n const view = resolve(waypoint.at);\n if (view) {\n resolved.push({ ...waypoint, at: view, ref: waypoint.at, index });\n } else {\n unresolved.push({ id: waypoint.id, index, ref: waypoint.at });\n }\n });\n\n return { resolved, unresolved };\n}\n\n/** The ρ a waypoint flies in with, falling back to the path's and then the default. */\nexport function arcFor(path: AtlasPath, waypoint: Pick<Waypoint, \"arc\">): number {\n return waypoint.arc ?? path.arc ?? DEFAULT_RHO;\n}\n\n/** The rest a waypoint takes, falling back to the path's and then to none. */\nexport function holdFor(path: AtlasPath, waypoint: Pick<Waypoint, \"hold\">): number {\n return Math.max(0, waypoint.hold ?? path.hold ?? 0);\n}\n\n/** Find a waypoint by name. */\nexport function waypointIndex(path: AtlasPath, id: string): number {\n return path.waypoints.findIndex((w) => w.id === id);\n}\n\n/** Names used more than once, in the order they first repeat. */\nexport function duplicateIds(path: AtlasPath): string[] {\n const seen = new Set<string>();\n const dupes = new Set<string>();\n for (const w of path.waypoints) {\n if (seen.has(w.id)) dupes.add(w.id);\n seen.add(w.id);\n }\n return [...dupes];\n}\n"]}
@@ -0,0 +1,75 @@
1
+ // src/camera/view.ts
2
+ var MIN_VIEW_WIDTH = 1e-6;
3
+ function isFiniteView(view) {
4
+ return Number.isFinite(view.cx) && Number.isFinite(view.cy) && Number.isFinite(view.w) && view.w > MIN_VIEW_WIDTH;
5
+ }
6
+ function normalizeView(view) {
7
+ return { cx: view.cx, cy: view.cy, w: Math.max(view.w, MIN_VIEW_WIDTH) };
8
+ }
9
+ function viewToRect(view, viewport) {
10
+ const height = viewport.width === 0 ? view.w : view.w * viewport.height / viewport.width;
11
+ return {
12
+ x: view.cx - view.w / 2,
13
+ y: view.cy - height / 2,
14
+ width: view.w,
15
+ height
16
+ };
17
+ }
18
+ function rectToView(rect, viewport, padding = 0) {
19
+ const cx = rect.x + rect.width / 2;
20
+ const cy = rect.y + rect.height / 2;
21
+ const aspect = viewport.height === 0 ? 1 : viewport.width / viewport.height;
22
+ const widthNeededForHeight = rect.height * aspect;
23
+ const w = Math.max(rect.width, widthNeededForHeight) * (1 + padding * 2);
24
+ return normalizeView({ cx, cy, w });
25
+ }
26
+ function rectsIntersect(a, b) {
27
+ return a.x <= b.x + b.width && b.x <= a.x + a.width && a.y <= b.y + b.height && b.y <= a.y + a.height;
28
+ }
29
+ function growRect(rect, margin) {
30
+ const dx = rect.width * margin;
31
+ const dy = rect.height * margin;
32
+ return {
33
+ x: rect.x - dx,
34
+ y: rect.y - dy,
35
+ width: rect.width + dx * 2,
36
+ height: rect.height + dy * 2
37
+ };
38
+ }
39
+ function unionRects(rects) {
40
+ if (rects.length === 0) return null;
41
+ let minX = Number.POSITIVE_INFINITY;
42
+ let minY = Number.POSITIVE_INFINITY;
43
+ let maxX = Number.NEGATIVE_INFINITY;
44
+ let maxY = Number.NEGATIVE_INFINITY;
45
+ for (const r of rects) {
46
+ minX = Math.min(minX, r.x);
47
+ minY = Math.min(minY, r.y);
48
+ maxX = Math.max(maxX, r.x + r.width);
49
+ maxY = Math.max(maxY, r.y + r.height);
50
+ }
51
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
52
+ }
53
+ function rectWithin(rect, bounds, slack = 0) {
54
+ const room = growRect(bounds, slack);
55
+ return rect.x >= room.x && rect.y >= room.y && rect.x + rect.width <= room.x + room.width && rect.y + rect.height <= room.y + room.height;
56
+ }
57
+ function viewToTransform(view, viewport) {
58
+ const safe = normalizeView(view);
59
+ const scale = viewport.width / safe.w;
60
+ return {
61
+ scale,
62
+ x: viewport.width / 2 - scale * safe.cx,
63
+ y: viewport.height / 2 - scale * safe.cy
64
+ };
65
+ }
66
+ function transformToCss(transform) {
67
+ return `translate3d(${transform.x}px, ${transform.y}px, 0) scale(${transform.scale})`;
68
+ }
69
+ function viewToCss(view, viewport) {
70
+ return transformToCss(viewToTransform(view, viewport));
71
+ }
72
+
73
+ export { MIN_VIEW_WIDTH, growRect, isFiniteView, normalizeView, rectToView, rectWithin, rectsIntersect, transformToCss, unionRects, viewToCss, viewToRect, viewToTransform };
74
+ //# sourceMappingURL=chunk-RWYOYDBD.js.map
75
+ //# sourceMappingURL=chunk-RWYOYDBD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/camera/view.ts"],"names":[],"mappings":"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","file":"chunk-RWYOYDBD.js","sourcesContent":["/**\n * The geometry the camera works in.\n *\n * A **view** is `{ cx, cy, w }` — a centre and a width, both in world units. Not a\n * scale. A scale is a ratio against an implicit viewport, so the same waypoint frames\n * different content in a 16:9 deck and a 9:16 video; a width is a fact about the world\n * and survives the render target, the aspect ratio and a re-layout of the scene. It is\n * also exactly the triple van Wijk's interpolation is defined over, which is the other\n * reason to use it.\n *\n * @packageDocumentation\n */\n\n/** A framing of the world: where the camera looks, and how much world fits across it. */\nexport interface View {\n /** Centre of the frame, in world units. */\n cx: number;\n /** How much of the world's width the frame holds, in world units. Always > 0. */\n cy: number;\n /** Width of the frame in world units. Always > 0 — a zero-width view has no scale. */\n w: number;\n}\n\n/** An axis-aligned rectangle in world units. */\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** The size of whatever the camera is painting onto, in device or canvas units. */\nexport interface Viewport {\n width: number;\n height: number;\n}\n\n/** The smallest view width worth distinguishing. Below it, a view has effectively no size. */\nexport const MIN_VIEW_WIDTH = 1e-6;\n\n/** Is this a view that can actually be rendered? */\nexport function isFiniteView(view: View): boolean {\n return (\n Number.isFinite(view.cx) &&\n Number.isFinite(view.cy) &&\n Number.isFinite(view.w) &&\n view.w > MIN_VIEW_WIDTH\n );\n}\n\n/** A view, with a width clamped to something renderable. */\nexport function normalizeView(view: View): View {\n return { cx: view.cx, cy: view.cy, w: Math.max(view.w, MIN_VIEW_WIDTH) };\n}\n\n/**\n * The world rectangle a view covers, given the aspect ratio it is painted at.\n *\n * Height is derived, never stored: it is the one part of the framing that belongs to the\n * render target rather than to the author.\n */\nexport function viewToRect(view: View, viewport: Viewport): Rect {\n const height = viewport.width === 0 ? view.w : (view.w * viewport.height) / viewport.width;\n return {\n x: view.cx - view.w / 2,\n y: view.cy - height / 2,\n width: view.w,\n height,\n };\n}\n\n/** The view that frames a rectangle, with `padding` as a fraction of the rectangle's size. */\nexport function rectToView(rect: Rect, viewport: Viewport, padding = 0): View {\n const cx = rect.x + rect.width / 2;\n const cy = rect.y + rect.height / 2;\n const aspect = viewport.height === 0 ? 1 : viewport.width / viewport.height;\n\n // Fit the whole rectangle: widen when the rectangle is taller than the viewport shape.\n const widthNeededForHeight = rect.height * aspect;\n const w = Math.max(rect.width, widthNeededForHeight) * (1 + padding * 2);\n\n return normalizeView({ cx, cy, w });\n}\n\n/** Do two rectangles overlap at all? Touching edges count as overlapping. */\nexport function rectsIntersect(a: Rect, b: Rect): boolean {\n return (\n a.x <= b.x + b.width && b.x <= a.x + a.width && a.y <= b.y + b.height && b.y <= a.y + a.height\n );\n}\n\n/** A rectangle grown by a fraction of its own size on every side. */\nexport function growRect(rect: Rect, margin: number): Rect {\n const dx = rect.width * margin;\n const dy = rect.height * margin;\n return {\n x: rect.x - dx,\n y: rect.y - dy,\n width: rect.width + dx * 2,\n height: rect.height + dy * 2,\n };\n}\n\n/** The smallest rectangle containing all of them, or null for none. */\nexport function unionRects(rects: readonly Rect[]): Rect | null {\n if (rects.length === 0) return null;\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n for (const r of rects) {\n minX = Math.min(minX, r.x);\n minY = Math.min(minY, r.y);\n maxX = Math.max(maxX, r.x + r.width);\n maxY = Math.max(maxY, r.y + r.height);\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n}\n\n/** Is the rectangle inside the bounds, allowing `slack` of the bounds' size outside? */\nexport function rectWithin(rect: Rect, bounds: Rect, slack = 0): boolean {\n const room = growRect(bounds, slack);\n return (\n rect.x >= room.x &&\n rect.y >= room.y &&\n rect.x + rect.width <= room.x + room.width &&\n rect.y + rect.height <= room.y + room.height\n );\n}\n\n/**\n * The camera transform, as the three numbers a CSS matrix needs.\n *\n * One transform, on one element, per frame — never one per node. That is what keeps the\n * cost of a scene independent of how much is in it: one composited layer, one style\n * recalculation, no layout and no paint, whether the plane holds ten nodes or a thousand.\n */\nexport interface CameraTransform {\n /** Uniform scale: world units → viewport units. */\n scale: number;\n /** Viewport-unit offset, applied before the scale, with `transform-origin: 0 0`. */\n x: number;\n y: number;\n}\n\n/** The transform that puts `view` on screen in `viewport`, for `transform-origin: 0 0`. */\nexport function viewToTransform(view: View, viewport: Viewport): CameraTransform {\n const safe = normalizeView(view);\n const scale = viewport.width / safe.w;\n return {\n scale,\n x: viewport.width / 2 - scale * safe.cx,\n y: viewport.height / 2 - scale * safe.cy,\n };\n}\n\n/**\n * The transform as a CSS string.\n *\n * `translate3d` and not `translate`: the z component is what asks the compositor for a\n * layer, and a layer is the whole point — the alternative is a paint per frame.\n */\nexport function transformToCss(transform: CameraTransform): string {\n return `translate3d(${transform.x}px, ${transform.y}px, 0) scale(${transform.scale})`;\n}\n\n/** Shorthand for the common case. */\nexport function viewToCss(view: View, viewport: Viewport): string {\n return transformToCss(viewToTransform(view, viewport));\n}\n"]}
@@ -0,0 +1,69 @@
1
+ import { holdFor, flight, arcFor } from './chunk-RHTCDWZD.js';
2
+
3
+ // src/camera/timeline.ts
4
+ function timeline(path, waypoints, options = {}) {
5
+ const style = options.style ?? "fly";
6
+ const segments = [];
7
+ let clock = 0;
8
+ waypoints.forEach((waypoint, i) => {
9
+ const hold = options.defaultHold !== void 0 ? waypoint.hold ?? options.defaultHold : holdFor(path, waypoint);
10
+ if (hold > 0 || i === 0) {
11
+ segments.push({ kind: "hold", start: clock, end: clock + hold, waypoint });
12
+ clock += hold;
13
+ }
14
+ const next = waypoints[i + 1];
15
+ if (!next) return;
16
+ const f = flight(waypoint.at, next.at, arcFor(path, next));
17
+ const duration2 = style === "cut" ? 0 : next.duration ?? f.suggestedDuration;
18
+ segments.push({
19
+ kind: "flight",
20
+ start: clock,
21
+ end: clock + duration2,
22
+ from: waypoint,
23
+ to: next,
24
+ flight: f
25
+ });
26
+ clock += duration2;
27
+ });
28
+ const duration = clock;
29
+ const last = waypoints[waypoints.length - 1];
30
+ function segmentAt(seconds) {
31
+ if (segments.length === 0) return null;
32
+ const t = Number.isFinite(seconds) ? seconds : 0;
33
+ if (t <= 0) return segments[0];
34
+ for (let i = segments.length - 1; i >= 0; i--) {
35
+ if (t >= segments[i].start) return segments[i];
36
+ }
37
+ return segments[0];
38
+ }
39
+ return {
40
+ segments,
41
+ duration,
42
+ segmentAt,
43
+ at(seconds) {
44
+ const segment = segmentAt(seconds);
45
+ if (!segment) return { cx: 0, cy: 0, w: 1 };
46
+ if (segment.kind === "hold") return segment.waypoint.at;
47
+ const span = segment.end - segment.start;
48
+ if (span <= 0) return segment.to.at;
49
+ const t = (seconds - segment.start) / span;
50
+ return segment.flight.at(t);
51
+ },
52
+ waypointAt(seconds) {
53
+ const segment = segmentAt(seconds);
54
+ if (!segment) return null;
55
+ return segment.kind === "hold" ? segment.waypoint : segment.to;
56
+ },
57
+ arrivalOf(id) {
58
+ for (const segment of segments) {
59
+ if (segment.kind === "hold" && segment.waypoint.id === id) return segment.start;
60
+ if (segment.kind === "flight" && segment.to.id === id) return segment.end;
61
+ }
62
+ return last?.id === id ? duration : null;
63
+ }
64
+ };
65
+ }
66
+
67
+ export { timeline };
68
+ //# sourceMappingURL=chunk-SD4Y4EBG.js.map
69
+ //# sourceMappingURL=chunk-SD4Y4EBG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/camera/timeline.ts"],"names":["duration"],"mappings":"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","file":"chunk-SD4Y4EBG.js","sourcesContent":["/**\n * A path, plus time.\n *\n * The deck does not need this — a presenter is the clock, and the host only ever\n * interpolates one flight at a time. Video and autoplay do: they need the whole path\n * laid out on a timeline so the camera can be a pure function of a moment, with no\n * memory of the moments before it.\n *\n * That purity is the requirement the video renderer imposes and the reason the camera is\n * built from a closed-form flight rather than a spring. `at(12.5)` must give the same\n * view whether or not second 12.4 was ever drawn, because the renderer advances a frame\n * counter and may render frames out of order, in parallel, or not at all.\n *\n * @packageDocumentation\n */\n\nimport { type Flight, flight } from \"./flight\";\nimport { type AtlasPath, arcFor, holdFor, type ResolvedWaypoint } from \"./path\";\nimport type { View } from \"./view\";\n\n/** How a timeline gets from one waypoint to the next. */\nexport type MotionStyle =\n /** Fly, as authored. */\n | \"fly\"\n /**\n * Cut instantly, keeping the holds.\n *\n * What `prefers-reduced-motion: reduce` gets. A zooming canvas is the worst offender\n * in this whole category for motion sensitivity, and honouring the preference by\n * *cutting* — rather than by hiding the content, or by ignoring it — keeps every\n * waypoint reachable and loses nothing but the swoop.\n */\n | \"cut\";\n\n/** One stretch of the timeline: either resting at a waypoint, or travelling between two. */\nexport type Segment =\n | {\n kind: \"hold\";\n /** Seconds from the start of the path. */\n start: number;\n end: number;\n waypoint: ResolvedWaypoint;\n }\n | {\n kind: \"flight\";\n start: number;\n end: number;\n from: ResolvedWaypoint;\n to: ResolvedWaypoint;\n flight: Flight;\n };\n\nexport interface Timeline {\n readonly segments: readonly Segment[];\n /** Total length in seconds. */\n readonly duration: number;\n /** The view at a moment. Pure: no state, no history, safe to call out of order. */\n at(seconds: number): View;\n /** Which segment a moment falls in. */\n segmentAt(seconds: number): Segment | null;\n /** The waypoint a moment belongs to — the one being held, or the one being flown to. */\n waypointAt(seconds: number): ResolvedWaypoint | null;\n /** When the camera arrives at a waypoint, in seconds. */\n arrivalOf(id: string): number | null;\n}\n\nexport interface TimelineOptions {\n /** @defaultValue `\"fly\"` */\n style?: MotionStyle;\n /**\n * A hold given to every waypoint that does not ask for one, in seconds.\n *\n * A video with no holds is a camera that never stops, which is unwatchable; the deck\n * host passes 0 because a presenter provides the pauses.\n *\n * @defaultValue the path's `hold`, else 0\n */\n defaultHold?: number;\n}\n\n/**\n * Lay a resolved path out on a timeline.\n *\n * Empty paths and single-waypoint paths are legal and produce a timeline that holds\n * still, which is what a host wants while a scene is still loading.\n */\nexport function timeline(\n path: AtlasPath,\n waypoints: readonly ResolvedWaypoint[],\n options: TimelineOptions = {},\n): Timeline {\n const style = options.style ?? \"fly\";\n const segments: Segment[] = [];\n let clock = 0;\n\n waypoints.forEach((waypoint, i) => {\n const hold =\n options.defaultHold !== undefined\n ? (waypoint.hold ?? options.defaultHold)\n : holdFor(path, waypoint);\n\n if (hold > 0 || i === 0) {\n segments.push({ kind: \"hold\", start: clock, end: clock + hold, waypoint });\n clock += hold;\n }\n\n const next = waypoints[i + 1];\n if (!next) return;\n\n const f = flight(waypoint.at, next.at, arcFor(path, next));\n const duration = style === \"cut\" ? 0 : (next.duration ?? f.suggestedDuration);\n segments.push({\n kind: \"flight\",\n start: clock,\n end: clock + duration,\n from: waypoint,\n to: next,\n flight: f,\n });\n clock += duration;\n });\n\n const duration = clock;\n const last = waypoints[waypoints.length - 1];\n\n function segmentAt(seconds: number): Segment | null {\n if (segments.length === 0) return null;\n const t = Number.isFinite(seconds) ? seconds : 0;\n if (t <= 0) return segments[0];\n // A hold and the flight that follows it share an instant; the later segment wins, so\n // a zero-length hold does not swallow the flight out of it.\n for (let i = segments.length - 1; i >= 0; i--) {\n if (t >= segments[i].start) return segments[i];\n }\n return segments[0];\n }\n\n return {\n segments,\n duration,\n segmentAt,\n at(seconds: number): View {\n const segment = segmentAt(seconds);\n if (!segment) return { cx: 0, cy: 0, w: 1 };\n if (segment.kind === \"hold\") return segment.waypoint.at;\n\n const span = segment.end - segment.start;\n if (span <= 0) return segment.to.at;\n const t = (seconds - segment.start) / span;\n return segment.flight.at(t);\n },\n waypointAt(seconds: number): ResolvedWaypoint | null {\n const segment = segmentAt(seconds);\n if (!segment) return null;\n return segment.kind === \"hold\" ? segment.waypoint : segment.to;\n },\n arrivalOf(id: string): number | null {\n for (const segment of segments) {\n if (segment.kind === \"hold\" && segment.waypoint.id === id) return segment.start;\n if (segment.kind === \"flight\" && segment.to.id === id) return segment.end;\n }\n return last?.id === id ? duration : null;\n },\n };\n}\n"]}
@@ -0,0 +1,89 @@
1
+ import { rectToView, unionRects } from './chunk-RWYOYDBD.js';
2
+
3
+ // src/web/dom.ts
4
+ function offsetRectWithin(element, root) {
5
+ let x = 0;
6
+ let y = 0;
7
+ let node = element;
8
+ while (node && node !== root) {
9
+ x += node.offsetLeft;
10
+ y += node.offsetTop;
11
+ node = node.offsetParent;
12
+ }
13
+ return { x, y, width: element.offsetWidth, height: element.offsetHeight };
14
+ }
15
+ function domProvider(options) {
16
+ const { root, padding = 0.15, fallback } = options;
17
+ return {
18
+ name: "dom",
19
+ bounds() {
20
+ if (!root) return fallback ?? null;
21
+ const own = { x: 0, y: 0, width: root.offsetWidth, height: root.offsetHeight };
22
+ if (own.width > 0 && own.height > 0) return own;
23
+ const marked = [...root.querySelectorAll("[data-atlas-id]")];
24
+ return unionRects(marked.map((node) => offsetRectWithin(node, root))) ?? fallback ?? null;
25
+ },
26
+ resolve(ref, viewport) {
27
+ if (!root) return null;
28
+ const selector = ref.startsWith("#") || ref.startsWith(".") || ref.startsWith("[") ? ref : `#${ref}, [data-atlas-id="${ref}"]`;
29
+ let element = null;
30
+ try {
31
+ element = root.querySelector(selector);
32
+ } catch {
33
+ return null;
34
+ }
35
+ if (!element) return null;
36
+ const rect = offsetRectWithin(element, root);
37
+ if (rect.width <= 0 && rect.height <= 0) return null;
38
+ return rectToView(rect, viewport, padding);
39
+ },
40
+ ready() {
41
+ return true;
42
+ }
43
+ };
44
+ }
45
+
46
+ // src/web/scroll.ts
47
+ function scrollProgress(geometry) {
48
+ const travel = geometry.height - geometry.viewportHeight;
49
+ if (!(travel > 0)) return 0;
50
+ const scrolled = -geometry.top;
51
+ return Math.min(1, Math.max(0, scrolled / travel));
52
+ }
53
+ function scrollerHeight(waypointCount, viewportHeight, perWaypoint = 1) {
54
+ const steps = Math.max(0, waypointCount - 1);
55
+ return viewportHeight * (1 + steps * perWaypoint);
56
+ }
57
+ function progressToSeconds(timeline, progress) {
58
+ const clamped = Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 0));
59
+ const flights = timeline.segments.filter((segment) => segment.kind === "flight");
60
+ const flying = flights.reduce((total, segment) => total + (segment.end - segment.start), 0);
61
+ if (flying <= 0) return 0;
62
+ let remaining = clamped * flying;
63
+ for (const segment of flights) {
64
+ const span = segment.end - segment.start;
65
+ if (remaining <= span) return segment.start + remaining;
66
+ remaining -= span;
67
+ }
68
+ return timeline.duration;
69
+ }
70
+ function snapToWaypoint(timeline, seconds) {
71
+ let best = null;
72
+ let bestDistance = Number.POSITIVE_INFINITY;
73
+ for (const segment of timeline.segments) {
74
+ const arrival = segment.kind === "hold" ? segment.start : segment.end;
75
+ const distance = Math.abs(arrival - seconds);
76
+ if (distance < bestDistance) {
77
+ bestDistance = distance;
78
+ best = arrival;
79
+ }
80
+ }
81
+ return best ?? 0;
82
+ }
83
+
84
+ // src/web/tag.ts
85
+ var TAG = "narro-atlas";
86
+
87
+ export { TAG, domProvider, offsetRectWithin, progressToSeconds, scrollProgress, scrollerHeight, snapToWaypoint };
88
+ //# sourceMappingURL=chunk-UTCNNNPT.js.map
89
+ //# sourceMappingURL=chunk-UTCNNNPT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web/dom.ts","../src/web/scroll.ts","../src/web/tag.ts"],"names":[],"mappings":"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","file":"chunk-UTCNNNPT.js","sourcesContent":["/**\n * A scene that is just the page's own markup.\n *\n * The web embed's default, and the reason it needs no build step: whatever a page puts\n * inside `<narro-atlas>` *is* the scene. A waypoint that says `at: \"#pricing\"` resolves\n * against the element with that id, exactly as a reader would expect from a fragment\n * link, and the same markup is what shows with JavaScript off.\n *\n * Measured with `offsetLeft`/`offsetTop` rather than `getBoundingClientRect`, because the\n * camera's own transform is on an ancestor: a rect would include it and every waypoint\n * would resolve to a different place depending on where the camera happened to be.\n *\n * @packageDocumentation\n */\n\nimport { type Rect, rectToView, unionRects, type View, type Viewport } from \"../camera/view\";\nimport type { SceneProvider } from \"../scene/provider\";\n\nexport interface DomProviderOptions {\n /** The element whose coordinate system the world is. */\n root: HTMLElement | null;\n /** Padding when framing a matched element, as a fraction. @defaultValue 0.15 */\n padding?: number;\n /** The world grid, used when the root has not been laid out yet. */\n fallback?: Rect;\n}\n\n/** An element's box in the root's coordinates, following the offset chain. */\nexport function offsetRectWithin(element: HTMLElement, root: HTMLElement): Rect {\n let x = 0;\n let y = 0;\n let node: HTMLElement | null = element;\n\n while (node && node !== root) {\n x += node.offsetLeft;\n y += node.offsetTop;\n node = node.offsetParent as HTMLElement | null;\n }\n\n return { x, y, width: element.offsetWidth, height: element.offsetHeight };\n}\n\nexport function domProvider(options: DomProviderOptions): SceneProvider {\n const { root, padding = 0.15, fallback } = options;\n\n return {\n name: \"dom\",\n\n bounds(): Rect | null {\n if (!root) return fallback ?? null;\n const own = { x: 0, y: 0, width: root.offsetWidth, height: root.offsetHeight };\n if (own.width > 0 && own.height > 0) return own;\n // Before layout, the marked-up nodes are the only thing that knows the extent.\n const marked = [...root.querySelectorAll<HTMLElement>(\"[data-atlas-id]\")];\n return unionRects(marked.map((node) => offsetRectWithin(node, root))) ?? fallback ?? null;\n },\n\n resolve(ref: string, viewport: Viewport): View | null {\n if (!root) return null;\n\n const selector =\n ref.startsWith(\"#\") || ref.startsWith(\".\") || ref.startsWith(\"[\")\n ? ref\n : `#${ref}, [data-atlas-id=\"${ref}\"]`;\n\n let element: HTMLElement | null = null;\n try {\n element = root.querySelector<HTMLElement>(selector);\n } catch {\n // An invalid selector is an authoring mistake, reported by the checker. At\n // runtime it must not take the page down.\n return null;\n }\n if (!element) return null;\n\n const rect = offsetRectWithin(element, root);\n if (rect.width <= 0 && rect.height <= 0) return null;\n return rectToView(rect, viewport, padding);\n },\n\n ready(): boolean {\n // The page's own markup. If it is in the document, it is ready.\n return true;\n },\n };\n}\n","/**\n * Scroll as a clock.\n *\n * The third clock, after a presenter's keypress and a renderer's frame counter, and the\n * one a web page wants. It costs almost nothing to add because the camera is a pure\n * function of a moment: a deck asks for the moment a flight has reached, a video asks for\n * `frame / fps`, and a page asks how far it has been scrolled. Same camera, same path,\n * same checker.\n *\n * Pure arithmetic here, so the part that is fiddly — what \"how far through\" means for an\n * element that is taller than the window and sticky inside it — is pinned by tests rather\n * than discovered on a phone.\n *\n * @packageDocumentation\n */\n\nimport type { Timeline } from \"../camera/timeline\";\n\n/** What the page can tell us, without this module touching the DOM. */\nexport interface ScrollGeometry {\n /** The scroller's top edge relative to the viewport — `getBoundingClientRect().top`. */\n top: number;\n /** Its full height, which is what gives the camera room to travel. */\n height: number;\n /** The window's height. */\n viewportHeight: number;\n}\n\n/**\n * How far through the camera path a scroll position is, in `[0, 1]`.\n *\n * The scroller is taller than the window and holds a window-height panel stuck to the top\n * of it; the camera travels while that panel is stuck. So the travel distance is\n * `height - viewportHeight`, and progress is how much of it has gone by. A scroller no\n * taller than the window has nowhere to travel and sits at the start, which is the\n * honest answer rather than a division by zero.\n */\nexport function scrollProgress(geometry: ScrollGeometry): number {\n const travel = geometry.height - geometry.viewportHeight;\n if (!(travel > 0)) return 0;\n const scrolled = -geometry.top;\n return Math.min(1, Math.max(0, scrolled / travel));\n}\n\n/** The height a scroller needs to give every waypoint `perWaypoint` screens of travel. */\nexport function scrollerHeight(\n waypointCount: number,\n viewportHeight: number,\n perWaypoint = 1,\n): number {\n const steps = Math.max(0, waypointCount - 1);\n return viewportHeight * (1 + steps * perWaypoint);\n}\n\n/**\n * The moment on the timeline a scroll progress corresponds to.\n *\n * Holds are dropped from the mapping deliberately: a hold is time spent not moving, and a\n * reader who keeps scrolling through one has the distinct experience of a broken page.\n * Scroll drives the *flights*; the rests belong to autoplay and video.\n */\nexport function progressToSeconds(timeline: Timeline, progress: number): number {\n const clamped = Math.min(1, Math.max(0, Number.isFinite(progress) ? progress : 0));\n const flights = timeline.segments.filter((segment) => segment.kind === \"flight\");\n const flying = flights.reduce((total, segment) => total + (segment.end - segment.start), 0);\n if (flying <= 0) return 0;\n\n let remaining = clamped * flying;\n for (const segment of flights) {\n const span = segment.end - segment.start;\n if (remaining <= span) return segment.start + remaining;\n remaining -= span;\n }\n return timeline.duration;\n}\n\n/**\n * The nearest waypoint's arrival time — what a reduced-motion reader gets.\n *\n * Scrolling is the reader's own doing, so the motion is theirs to control; what they have\n * asked not to have is the camera sliding under them while they do it. Snapping to the\n * waypoint they are nearest keeps every framing reachable by scrolling and removes the\n * travel between, which is the same trade the deck host makes by cutting.\n */\nexport function snapToWaypoint(timeline: Timeline, seconds: number): number {\n let best: number | null = null;\n let bestDistance = Number.POSITIVE_INFINITY;\n\n for (const segment of timeline.segments) {\n // Where the camera is at rest: the start of a hold, and the end of a flight.\n const arrival = segment.kind === \"hold\" ? segment.start : segment.end;\n const distance = Math.abs(arrival - seconds);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = arrival;\n }\n }\n\n return best ?? 0;\n}\n","/**\n * The element's name and modes, split out from the element itself.\n *\n * `class NarroAtlasElement extends HTMLElement` is evaluated the moment its module is\n * imported, and `HTMLElement` does not exist in Node — so a module that re-exports the\n * class cannot be imported on a server at all. That would make\n * `import { scrollProgress } from \"@getnarro/atlas/web\"` throw during SSR, over\n * arithmetic that has nothing to do with the DOM.\n *\n * So the constants live here, `@getnarro/atlas/web` stays importable anywhere, and the\n * element is reached through `@getnarro/atlas/embed`, which is a browser entry by\n * construction.\n *\n * @packageDocumentation\n */\n\nexport const TAG = \"narro-atlas\";\n\n/** How the page drives the camera. */\nexport type AtlasMode =\n /** The camera follows the page's scroll. The default, and what a page usually wants. */\n | \"scroll\"\n /** Click, or arrow keys while focused, to step between waypoints. */\n | \"steps\";\n"]}
@@ -0,0 +1,12 @@
1
+ function g(n){return{cx:n.cx,cy:n.cy,w:Math.max(n.w,1e-6)}}function R(n,t,i=0){let r=n.x+n.width/2,e=n.y+n.height/2,o=t.height===0?1:t.width/t.height,s=n.height*o,h=Math.max(n.width,s)*(1+i*2);return g({cx:r,cy:e,w:h})}function N(n){if(n.length===0)return null;let t=Number.POSITIVE_INFINITY,i=Number.POSITIVE_INFINITY,r=Number.NEGATIVE_INFINITY,e=Number.NEGATIVE_INFINITY;for(let o of n)t=Math.min(t,o.x),i=Math.min(i,o.y),r=Math.max(r,o.x+o.width),e=Math.max(e,o.y+o.height);return{x:t,y:i,width:r-t,height:e-i}}function S(n,t){let i=g(n),r=t.width/i.w;return{scale:r,x:t.width/2-r*i.cx,y:t.height/2-r*i.cy}}function I(n){return`translate3d(${n.x}px, ${n.y}px, 0) scale(${n.scale})`}var y=Math.SQRT2,q=1e-12;function W(n){let t=Math.exp(n);return(t+1/t)/2}function Y(n){let t=Math.exp(n);return(t-1/t)/2}function U(n){let t=Math.exp(2*n);return(t-1)/(t+1)}var j=.9,K=.2,X=4;function H(n,t,i=y){let r=g(n),e=g(t),o=i>0?i:y,s=e.cx-r.cx,h=e.cy-r.cy,d=s*s+h*h,l=o*o,a=l*l,c,m;if(d<q){let u=Math.log(e.w/r.w)/o;c=Math.abs(u),m=p=>({cx:r.cx+p*s,cy:r.cy+p*h,w:r.w*Math.exp(o*p*u)})}else{let u=Math.sqrt(d),p=(e.w*e.w-r.w*r.w+a*d)/(2*r.w*l*u),v=(e.w*e.w-r.w*r.w-a*d)/(2*e.w*l*u),w=Math.log(Math.sqrt(p*p+1)-p),M=(Math.log(Math.sqrt(v*v+1)-v)-w)/o,E=W(w),G=Y(w);c=Math.abs(M),m=$=>{let V=$*M,A=r.w/(l*u)*(E*U(o*V+w)-G);return{cx:r.cx+A*s,cy:r.cy+A*h,w:r.w*E/W(o*V+w)}}}let x=Math.min(X,Math.max(K,c*j));return{from:r,to:e,rho:o,length:c,suggestedDuration:x,at(u){if(!Number.isFinite(u)||u<=0)return r;if(u>=1)return e;let p=m(u);return J(p)?p:r}}}function J(n){return Number.isFinite(n.cx)&&Number.isFinite(n.cy)&&Number.isFinite(n.w)}function Q(n){return typeof n=="object"&&n!==null&&"w"in n}function P(n,t){let i=[],r=[];return n.waypoints.forEach((e,o)=>{if(Q(e.at)){i.push({...e,at:e.at,index:o});return}let s=t(e.at);s?i.push({...e,at:s,ref:e.at,index:o}):r.push({id:e.id,index:o,ref:e.at})}),{resolved:i,unresolved:r}}function F(n,t){return t.arc??n.arc??y}function k(n,t){return Math.max(0,t.hold??n.hold??0)}function L(n,t,i={}){let r=i.style??"fly",e=[],o=0;t.forEach((l,a)=>{let c=i.defaultHold!==void 0?l.hold??i.defaultHold:k(n,l);(c>0||a===0)&&(e.push({kind:"hold",start:o,end:o+c,waypoint:l}),o+=c);let m=t[a+1];if(!m)return;let x=H(l.at,m.at,F(n,m)),u=r==="cut"?0:m.duration??x.suggestedDuration;e.push({kind:"flight",start:o,end:o+u,from:l,to:m,flight:x}),o+=u});let s=o,h=t[t.length-1];function d(l){if(e.length===0)return null;let a=Number.isFinite(l)?l:0;if(a<=0)return e[0];for(let c=e.length-1;c>=0;c--)if(a>=e[c].start)return e[c];return e[0]}return{segments:e,duration:s,segmentAt:d,at(l){let a=d(l);if(!a)return{cx:0,cy:0,w:1};if(a.kind==="hold")return a.waypoint.at;let c=a.end-a.start;if(c<=0)return a.to.at;let m=(l-a.start)/c;return a.flight.at(m)},waypointAt(l){let a=d(l);return a?a.kind==="hold"?a.waypoint:a.to:null},arrivalOf(l){for(let a of e){if(a.kind==="hold"&&a.waypoint.id===l)return a.start;if(a.kind==="flight"&&a.to.id===l)return a.end}return h?.id===l?s:null}}}function C(n,t){let i=0,r=0,e=n;for(;e&&e!==t;)i+=e.offsetLeft,r+=e.offsetTop,e=e.offsetParent;return{x:i,y:r,width:n.offsetWidth,height:n.offsetHeight}}function O(n){let{root:t,padding:i=.15,fallback:r}=n;return{name:"dom",bounds(){if(!t)return r??null;let e={x:0,y:0,width:t.offsetWidth,height:t.offsetHeight};if(e.width>0&&e.height>0)return e;let o=[...t.querySelectorAll("[data-atlas-id]")];return N(o.map(s=>C(s,t)))??r??null},resolve(e,o){if(!t)return null;let s=e.startsWith("#")||e.startsWith(".")||e.startsWith("[")?e:`#${e}, [data-atlas-id="${e}"]`,h=null;try{h=t.querySelector(s)}catch{return null}if(!h)return null;let d=C(h,t);return d.width<=0&&d.height<=0?null:R(d,o,i)},ready(){return!0}}}function _(n){let t=n.height-n.viewportHeight;if(!(t>0))return 0;let i=-n.top;return Math.min(1,Math.max(0,i/t))}function D(n,t){let i=Math.min(1,Math.max(0,Number.isFinite(t)?t:0)),r=n.segments.filter(s=>s.kind==="flight"),e=r.reduce((s,h)=>s+(h.end-h.start),0);if(e<=0)return 0;let o=i*e;for(let s of r){let h=s.end-s.start;if(o<=h)return s.start+o;o-=h}return n.duration}function z(n,t){let i=null,r=Number.POSITIVE_INFINITY;for(let e of n.segments){let o=e.kind==="hold"?e.start:e.end,s=Math.abs(o-t);s<r&&(r=s,i=o)}return i??0}var f="narro-atlas";var B=`
2
+ :host{display:block;position:relative}
3
+ :host([hidden]){display:none}
4
+ .frame{position:sticky;top:0;height:100vh;overflow:hidden}
5
+ .camera{position:absolute;inset:0;transform-origin:0 0;will-change:auto}
6
+ .rail{position:absolute;right:1rem;bottom:1rem;display:flex;gap:.4rem;z-index:2}
7
+ .dot{width:.55rem;height:.55rem;border-radius:50%;border:0;padding:0;cursor:pointer;
8
+ background:currentColor;opacity:.3}
9
+ .dot[aria-current="true"]{opacity:.9}
10
+ .dot:focus-visible{outline:2px solid currentColor;outline-offset:3px}
11
+ @media (prefers-reduced-motion: reduce){.camera{transition:none}}
12
+ `;function Z(n){let t=/^(\d+)\s*[x×]\s*(\d+)$/.exec((n??"").trim());return t?{width:Number(t[1]),height:Number(t[2])}:{width:1920,height:1080}}var b=class extends HTMLElement{static observedAttributes=["path","mode","world","hold"];shadow=null;frame=null;camera=null;rail=null;line=null;path=null;index=0;raf=0;generation=0;onScroll=()=>this.schedule();onResize=()=>this.schedule();connectedCallback(){this.shadow||this.mount(),this.load(),window.addEventListener("scroll",this.onScroll,{passive:!0}),window.addEventListener("resize",this.onResize,{passive:!0})}disconnectedCallback(){window.removeEventListener("scroll",this.onScroll),window.removeEventListener("resize",this.onResize),cancelAnimationFrame(this.raf)}attributeChangedCallback(t){this.shadow&&(t==="path"?this.load():this.schedule())}mount(){this.shadow=this.attachShadow({mode:"open"});let t=document.createElement("style");t.textContent=B,this.frame=document.createElement("div"),this.frame.className="frame",this.camera=document.createElement("div"),this.camera.className="camera",this.camera.appendChild(document.createElement("slot")),this.rail=document.createElement("div"),this.rail.className="rail",this.frame.append(this.camera,this.rail),this.shadow.append(t,this.frame),this.addEventListener("keydown",i=>this.onKeyDown(i))}async load(){let t=++this.generation,i=this.getAttribute("path"),r=null;if(i)try{r=await(await fetch(i)).json()}catch(e){console.error(`[${f}] could not load the camera path from ${i}`,e);return}else{let e=this.querySelector('script[type="application/json"]');if(e?.textContent)try{r=JSON.parse(e.textContent)}catch(o){console.error(`[${f}] the inline camera path is not valid JSON`,o);return}}t===this.generation&&r?.waypoints?.length&&(this.path=r,this.build(),this.drawRail(),this.schedule())}build(){if(!this.path)return;let t=Z(this.getAttribute("world")),i=O({root:this,fallback:{x:0,y:0,...t}}),{resolved:r}=P(this.path,e=>i.resolve(e,t));this.line=L(this.path,r,{defaultHold:0})}drawRail(){!this.rail||!this.line||(this.rail.textContent="",this.mode()==="steps"&&this.line.segments.filter(t=>t.kind==="hold").forEach((t,i)=>{let r=document.createElement("button");r.className="dot",r.type="button";let e=t.kind==="hold"?t.waypoint.label??t.waypoint.id:"";r.setAttribute("aria-label",e),r.addEventListener("click",()=>this.goTo(i)),this.rail?.appendChild(r)}))}mode(){return this.getAttribute("mode")==="steps"?"steps":"scroll"}reduced(){return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches??!1}schedule(){cancelAnimationFrame(this.raf),this.raf=requestAnimationFrame(()=>this.draw())}goTo(t){if(!this.line)return;let i=this.line.segments.filter(r=>r.kind==="hold");this.index=Math.min(Math.max(t,0),Math.max(0,i.length-1)),this.schedule()}onKeyDown(t){this.mode()==="steps"&&(t.key==="ArrowRight"?(t.preventDefault(),this.goTo(this.index+1)):t.key==="ArrowLeft"&&(t.preventDefault(),this.goTo(this.index-1)))}draw(){if(!this.line||!this.camera||!this.frame)return;let t={width:this.frame.offsetWidth||this.offsetWidth||1,height:this.frame.offsetHeight||this.offsetHeight||1},i;if(this.mode()==="steps"){let o=this.line.segments.filter(h=>h.kind==="hold"),s=o[Math.min(this.index,o.length-1)];i=s?.kind==="hold"?s.start:0}else{let o=this.getBoundingClientRect(),s=_({top:o.top,height:o.height,viewportHeight:window.innerHeight});i=D(this.line,s),this.reduced()&&(i=z(this.line,i))}let r=this.line.at(i);this.camera.style.transform=I(S(r,t));let e=this.line.waypointAt(i);e&&(this.setAttribute("data-waypoint",e.id),(this.rail?.querySelectorAll(".dot")??[]).forEach((s,h)=>{s.setAttribute("aria-current",String(h===this.index))}))}};function T(){typeof customElements>"u"||customElements.get(f)||customElements.define(f,b)}T();export{b as NarroAtlasElement,f as TAG,T as defineAtlasElement};
@@ -0,0 +1,132 @@
1
+ import { R as Rect, V as View, c as Viewport } from '../view-BzztOpV6.js';
2
+ import { S as SceneProvider } from '../provider-CjL3i-0A.js';
3
+
4
+ /**
5
+ * A gigapixel image as a scene, without loading a gigapixel image.
6
+ *
7
+ * The problem this solves is the one the checker's `bitmap-over-zoomed` rule can only
8
+ * complain about: a camera that flies close to a raster needs pixels the raster does not
9
+ * have, and the fix — ship a bigger file — stops being possible somewhere around the
10
+ * point where the file is bigger than the deck. A pyramid inverts it. The image is cut
11
+ * into tiles at every power-of-two scale in advance, and the camera fetches only the tiles
12
+ * for the level and the region it is actually looking at. A 40000×30000 scan costs the
13
+ * same handful of tiles at every zoom, all the way in.
14
+ *
15
+ * The layout is Deep Zoom's (DZI), because it is the one every tiler already emits —
16
+ * `vips dzsave`, `libvips`, OpenSeadragon's own tools — and because reusing a convention
17
+ * beats inventing one nobody can produce files for.
18
+ *
19
+ * Everything here is arithmetic: which level, which tiles, and where each one sits in
20
+ * world units. No fetching, no DOM, no React — so the part that is easy to get subtly
21
+ * wrong is the part a test can pin down exactly.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+
26
+ interface Pyramid {
27
+ /** Full-resolution size, in image pixels. The world is measured in these. */
28
+ width: number;
29
+ height: number;
30
+ /** Tile edge in pixels. 254 and 256 are both common; DZI's default is 254 with overlap 1. */
31
+ tileSize: number;
32
+ /**
33
+ * Pixels each tile repeats on every side, so neighbours have no seam.
34
+ *
35
+ * DZI's own default is 1. It shifts where a tile sits and how big it is, which is the
36
+ * detail that makes a naive implementation draw a visible grid.
37
+ *
38
+ * @defaultValue 0
39
+ */
40
+ overlap?: number;
41
+ /** `https://…/image_files/{level}/{col}_{row}.jpg` — DZI's own layout. */
42
+ url: (level: number, col: number, row: number) => string;
43
+ }
44
+ /**
45
+ * The deepest level, where the image is at full resolution.
46
+ *
47
+ * DZI counts up from a level 0 that is the whole image in a single pixel, doubling each
48
+ * step, so the deepest level is `ceil(log2(max(width, height)))`.
49
+ */
50
+ declare function maxLevel(pyramid: Pick<Pyramid, "width" | "height">): number;
51
+ /** How much the image is scaled down at a level. 1 at the deepest. */
52
+ declare function levelScale(pyramid: Pick<Pyramid, "width" | "height">, level: number): number;
53
+ /** The image's size in pixels at a level. */
54
+ declare function levelSize(pyramid: Pick<Pyramid, "width" | "height">, level: number): {
55
+ width: number;
56
+ height: number;
57
+ };
58
+ /**
59
+ * The level whose pixels are closest to screen pixels for this framing.
60
+ *
61
+ * One below would be visibly soft; one above costs four times the tiles for detail the
62
+ * screen cannot show. Rounded up rather than to nearest, deliberately: between two levels
63
+ * the sharper one is the right call, because softness is the thing a viewer notices.
64
+ */
65
+ declare function levelFor(pyramid: Pyramid, view: View, viewport: Viewport): number;
66
+ /** One tile: where to fetch it, and where it goes. */
67
+ interface Tile {
68
+ /** Stable identity, for keying a list and tracking loads. */
69
+ key: string;
70
+ level: number;
71
+ col: number;
72
+ row: number;
73
+ url: string;
74
+ /** Where it sits, in full-resolution image pixels — the world's own units. */
75
+ rect: Rect;
76
+ }
77
+ /** How many tiles across and down a level is. */
78
+ declare function tileCounts(pyramid: Pyramid, level: number): {
79
+ cols: number;
80
+ rows: number;
81
+ };
82
+ /**
83
+ * The tiles a framing needs, in world units.
84
+ *
85
+ * `margin` fetches a ring beyond the frame, as a fraction of it, so a camera arriving
86
+ * somewhere does not arrive on empty squares.
87
+ */
88
+ declare function tilesFor(pyramid: Pyramid, view: View, viewport: Viewport, margin?: number): Tile[];
89
+ /** The whole image, as a rectangle in world units. */
90
+ declare function pyramidBounds(pyramid: Pick<Pyramid, "width" | "height">): Rect;
91
+
92
+ /**
93
+ * A gigapixel image as a scene.
94
+ *
95
+ * The provider that has the most to say about `ready`. A plane is ready immediately, a
96
+ * replay is ready when the host says so, a map asks the library — this one has to know
97
+ * which specific tiles a framing needs and whether each has arrived, because the renderer
98
+ * will otherwise encode a frame of half-drawn squares and nobody will notice until the
99
+ * video is finished.
100
+ *
101
+ * @packageDocumentation
102
+ */
103
+
104
+ /** A named region of the image a waypoint can fly to by name. */
105
+ type NamedRegion = Rect;
106
+ interface ImageProviderOptions {
107
+ pyramid: Pyramid;
108
+ /**
109
+ * Regions a waypoint may name — `at: "the-signature"`.
110
+ *
111
+ * The same reasoning as the map's named places: a name survives someone re-cropping the
112
+ * region, and the four numbers it stood for do not.
113
+ */
114
+ regions?: Record<string, NamedRegion>;
115
+ /** Padding when framing a named region, as a fraction. @defaultValue 0.1 */
116
+ padding?: number;
117
+ /** How much beyond the frame to fetch, as a fraction of it. @defaultValue 0.25 */
118
+ margin?: number;
119
+ }
120
+ interface ImageScene extends SceneProvider {
121
+ /** The tiles a framing needs. What the renderer draws. */
122
+ tiles(view: View, viewport: Viewport): Tile[];
123
+ /** Tell the scene a tile has arrived. The readiness gate is built from these. */
124
+ noteLoaded(key: string): void;
125
+ /** Tell the scene a tile will never arrive, so a 404 cannot stall a render forever. */
126
+ noteFailed(key: string): void;
127
+ /** How many tiles are known to have arrived. For diagnostics and tests. */
128
+ loadedCount(): number;
129
+ }
130
+ declare function imageProvider(options: ImageProviderOptions): ImageScene;
131
+
132
+ export { type ImageProviderOptions, type ImageScene, type NamedRegion, type Pyramid, type Tile, imageProvider, levelFor, levelScale, levelSize, maxLevel, pyramidBounds, tileCounts, tilesFor };
@@ -0,0 +1,117 @@
1
+ import { viewToRect, rectToView } from '../chunk-RWYOYDBD.js';
2
+
3
+ // src/image/pyramid.ts
4
+ function maxLevel(pyramid) {
5
+ const longest = Math.max(pyramid.width, pyramid.height, 1);
6
+ return Math.ceil(Math.log2(longest));
7
+ }
8
+ function levelScale(pyramid, level) {
9
+ return 2 ** (level - maxLevel(pyramid));
10
+ }
11
+ function levelSize(pyramid, level) {
12
+ const scale = levelScale(pyramid, level);
13
+ return {
14
+ width: Math.max(1, Math.ceil(pyramid.width * scale)),
15
+ height: Math.max(1, Math.ceil(pyramid.height * scale))
16
+ };
17
+ }
18
+ function levelFor(pyramid, view, viewport) {
19
+ const deepest = maxLevel(pyramid);
20
+ if (view.w <= 0) return deepest;
21
+ const screenScale = viewport.width / view.w;
22
+ const level = deepest + Math.log2(Math.max(screenScale, Number.EPSILON));
23
+ return Math.min(deepest, Math.max(0, Math.ceil(level)));
24
+ }
25
+ function tileCounts(pyramid, level) {
26
+ const size = levelSize(pyramid, level);
27
+ return {
28
+ cols: Math.ceil(size.width / pyramid.tileSize),
29
+ rows: Math.ceil(size.height / pyramid.tileSize)
30
+ };
31
+ }
32
+ function tilesFor(pyramid, view, viewport, margin = 0.25) {
33
+ const level = levelFor(pyramid, view, viewport);
34
+ const scale = levelScale(pyramid, level);
35
+ const overlap = pyramid.overlap ?? 0;
36
+ const { cols, rows } = tileCounts(pyramid, level);
37
+ const size = levelSize(pyramid, level);
38
+ const frame = viewToRect(view, viewport);
39
+ const grow = { x: frame.width * margin, y: frame.height * margin };
40
+ const left = (frame.x - grow.x) * scale;
41
+ const top = (frame.y - grow.y) * scale;
42
+ const right = (frame.x + frame.width + grow.x) * scale;
43
+ const bottom = (frame.y + frame.height + grow.y) * scale;
44
+ const firstCol = Math.max(0, Math.floor(left / pyramid.tileSize));
45
+ const lastCol = Math.min(cols - 1, Math.floor(right / pyramid.tileSize));
46
+ const firstRow = Math.max(0, Math.floor(top / pyramid.tileSize));
47
+ const lastRow = Math.min(rows - 1, Math.floor(bottom / pyramid.tileSize));
48
+ const tiles = [];
49
+ for (let row = firstRow; row <= lastRow; row++) {
50
+ for (let col = firstCol; col <= lastCol; col++) {
51
+ const x = col * pyramid.tileSize - (col > 0 ? overlap : 0);
52
+ const y = row * pyramid.tileSize - (row > 0 ? overlap : 0);
53
+ const width = Math.min(
54
+ pyramid.tileSize + (col > 0 ? overlap : 0) + (col < cols - 1 ? overlap : 0),
55
+ size.width - x
56
+ );
57
+ const height = Math.min(
58
+ pyramid.tileSize + (row > 0 ? overlap : 0) + (row < rows - 1 ? overlap : 0),
59
+ size.height - y
60
+ );
61
+ if (width <= 0 || height <= 0) continue;
62
+ tiles.push({
63
+ key: `${level}/${col}_${row}`,
64
+ level,
65
+ col,
66
+ row,
67
+ url: pyramid.url(level, col, row),
68
+ // Back into full-resolution pixels, which is what the camera works in.
69
+ rect: { x: x / scale, y: y / scale, width: width / scale, height: height / scale }
70
+ });
71
+ }
72
+ }
73
+ return tiles;
74
+ }
75
+ function pyramidBounds(pyramid) {
76
+ return { x: 0, y: 0, width: pyramid.width, height: pyramid.height };
77
+ }
78
+
79
+ // src/image/provider.ts
80
+ function imageProvider(options) {
81
+ const { pyramid, regions = {}, padding = 0.1, margin = 0.25 } = options;
82
+ const settled = /* @__PURE__ */ new Set();
83
+ const loaded = /* @__PURE__ */ new Set();
84
+ let lastViewport = null;
85
+ return {
86
+ name: "image",
87
+ bounds() {
88
+ return pyramidBounds(pyramid);
89
+ },
90
+ resolve(ref, viewport) {
91
+ const region = regions[ref] ?? regions[ref.replace(/^#/, "")];
92
+ return region ? rectToView(region, viewport, padding) : null;
93
+ },
94
+ ready(view) {
95
+ const viewport = lastViewport ?? { width: 1920, height: 1080 };
96
+ return tilesFor(pyramid, view, viewport, margin).every((tile) => settled.has(tile.key));
97
+ },
98
+ tiles(view, viewport) {
99
+ lastViewport = viewport;
100
+ return tilesFor(pyramid, view, viewport, margin);
101
+ },
102
+ noteLoaded(key) {
103
+ loaded.add(key);
104
+ settled.add(key);
105
+ },
106
+ noteFailed(key) {
107
+ settled.add(key);
108
+ },
109
+ loadedCount() {
110
+ return loaded.size;
111
+ }
112
+ };
113
+ }
114
+
115
+ export { imageProvider, levelFor, levelScale, levelSize, maxLevel, pyramidBounds, tileCounts, tilesFor };
116
+ //# sourceMappingURL=index.js.map
117
+ //# sourceMappingURL=index.js.map