@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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/check/index.d.ts +82 -0
- package/dist/check/index.js +150 -0
- package/dist/check/index.js.map +1 -0
- package/dist/chunk-5SRKWTOU.js +33 -0
- package/dist/chunk-5SRKWTOU.js.map +1 -0
- package/dist/chunk-BDF2X5LC.js +28 -0
- package/dist/chunk-BDF2X5LC.js.map +1 -0
- package/dist/chunk-RHTCDWZD.js +142 -0
- package/dist/chunk-RHTCDWZD.js.map +1 -0
- package/dist/chunk-RWYOYDBD.js +75 -0
- package/dist/chunk-RWYOYDBD.js.map +1 -0
- package/dist/chunk-SD4Y4EBG.js +69 -0
- package/dist/chunk-SD4Y4EBG.js.map +1 -0
- package/dist/chunk-UTCNNNPT.js +89 -0
- package/dist/chunk-UTCNNNPT.js.map +1 -0
- package/dist/embed/narro-atlas.js +12 -0
- package/dist/image/index.d.ts +132 -0
- package/dist/image/index.js +117 -0
- package/dist/image/index.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lod-DBuZ0MG3.d.ts +86 -0
- package/dist/map/index.d.ts +222 -0
- package/dist/map/index.js +184 -0
- package/dist/map/index.js.map +1 -0
- package/dist/path-BIxJH8g2.d.ts +101 -0
- package/dist/provider-CjL3i-0A.d.ts +54 -0
- package/dist/react/index.d.ts +183 -0
- package/dist/react/index.js +291 -0
- package/dist/react/index.js.map +1 -0
- package/dist/replay/index.d.ts +162 -0
- package/dist/replay/index.js +138 -0
- package/dist/replay/index.js.map +1 -0
- package/dist/tag-BP51cBza.d.ts +24 -0
- package/dist/timeline-B_dNydQA.d.ts +194 -0
- package/dist/video/index.d.ts +101 -0
- package/dist/video/index.js +91 -0
- package/dist/video/index.js.map +1 -0
- package/dist/view-BzztOpV6.d.ts +83 -0
- package/dist/web/embed.d.ts +59 -0
- package/dist/web/embed.js +192 -0
- package/dist/web/embed.js.map +1 -0
- package/dist/web/index.d.ts +89 -0
- package/dist/web/index.js +4 -0
- package/dist/web/index.js.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { A as AtlasPath } from '../path-BIxJH8g2.js';
|
|
3
|
+
import { b as MotionStyle, T as Timeline } from '../timeline-B_dNydQA.js';
|
|
4
|
+
import { c as Viewport, V as View } from '../view-BzztOpV6.js';
|
|
5
|
+
import { P as PlacedNode } from '../lod-DBuZ0MG3.js';
|
|
6
|
+
import { S as SceneProvider } from '../provider-CjL3i-0A.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The `@getnarro/video` adapter.
|
|
10
|
+
*
|
|
11
|
+
* A separate entry point because `@getnarro/video` is an **optional** peer: a deck that
|
|
12
|
+
* puts a zooming canvas on a slide should not have to install a video framework to do it.
|
|
13
|
+
* Only this subpath imports one.
|
|
14
|
+
*
|
|
15
|
+
* Two things differ from the deck host, and both follow from the render target rather
|
|
16
|
+
* than from taste.
|
|
17
|
+
*
|
|
18
|
+
* **The clock is the frame counter.** No `requestAnimationFrame` anywhere — the view is
|
|
19
|
+
* `timeline.at(frame / fps)`, so a frame rendered on its own, out of order, or in another
|
|
20
|
+
* process gives bit-identical output.
|
|
21
|
+
*
|
|
22
|
+
* **`will-change` stays off.** In the deck it is switched on during a flight to keep the
|
|
23
|
+
* frames cheap, at the cost of blur while moving. Here every frame is a deliverable and
|
|
24
|
+
* nothing is interactive, so the trade inverts: pay the re-raster, keep the sharpness.
|
|
25
|
+
*
|
|
26
|
+
* @packageDocumentation
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
interface AtlasSceneProps {
|
|
30
|
+
path: AtlasPath;
|
|
31
|
+
provider?: SceneProvider;
|
|
32
|
+
nodes?: readonly PlacedNode[];
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
/** The world grid. Defaults to the composition's own pixel size. */
|
|
35
|
+
viewport?: Viewport;
|
|
36
|
+
/**
|
|
37
|
+
* A rest at every waypoint that does not ask for one, in seconds.
|
|
38
|
+
*
|
|
39
|
+
* A video with no holds is a camera that never stops, which is unwatchable. The deck
|
|
40
|
+
* host has no equivalent because a presenter supplies the pauses.
|
|
41
|
+
*
|
|
42
|
+
* @defaultValue 1
|
|
43
|
+
*/
|
|
44
|
+
hold?: number;
|
|
45
|
+
/** @defaultValue `"fly"` */
|
|
46
|
+
style?: MotionStyle;
|
|
47
|
+
className?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A zooming canvas as a video scene.
|
|
51
|
+
*
|
|
52
|
+
* ```tsx
|
|
53
|
+
* <AtlasScene path={path} nodes={nodes}>
|
|
54
|
+
* <AtlasNode id="ingest" x={0} y={0} width={400} height={300}>…</AtlasNode>
|
|
55
|
+
* </AtlasScene>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @behaviour Fills its container absolutely and owns the whole frame, so it cannot share
|
|
59
|
+
* a scene with ordinary content. Its children are positioned in world units on a plane the
|
|
60
|
+
* camera moves over — `AtlasNode` is what places them — and anything outside the frame is
|
|
61
|
+
* unmounted rather than merely hidden.
|
|
62
|
+
*/
|
|
63
|
+
declare function AtlasScene({ path, provider, nodes, children, viewport, hold, style, className, }: AtlasSceneProps): React.ReactElement;
|
|
64
|
+
/**
|
|
65
|
+
* How many frames a path needs, for a composition's `durationInFrames`.
|
|
66
|
+
*
|
|
67
|
+
* Derived from the flights' own suggested durations plus the holds, so a scene's length
|
|
68
|
+
* follows its camera path rather than being a constant somebody has to keep in step.
|
|
69
|
+
*/
|
|
70
|
+
declare function atlasDurationInFrames(path: AtlasPath, fps: number, options?: {
|
|
71
|
+
hold?: number;
|
|
72
|
+
viewport?: Viewport;
|
|
73
|
+
provider?: SceneProvider;
|
|
74
|
+
}): number;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The camera on a frame counter.
|
|
78
|
+
*
|
|
79
|
+
* No React, no `@getnarro/video`, no clock — just the arithmetic, so the interesting
|
|
80
|
+
* property can be tested without mounting anything.
|
|
81
|
+
*
|
|
82
|
+
* @packageDocumentation
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
/** Seconds at a frame. The renderer's whole notion of time. */
|
|
86
|
+
declare function frameToSeconds(frame: number, fps: number): number;
|
|
87
|
+
/** How many frames a timeline occupies, rounded up so the last waypoint is held. */
|
|
88
|
+
declare function timelineFrames(timeline: Timeline, fps: number): number;
|
|
89
|
+
/**
|
|
90
|
+
* The view at a frame.
|
|
91
|
+
*
|
|
92
|
+
* The one thing that matters here is what is *absent*: no accumulator, no previous
|
|
93
|
+
* frame, no `requestAnimationFrame`. The renderer advances a counter and calls a render
|
|
94
|
+
* function; it may render frames out of order or across several processes, and it caches
|
|
95
|
+
* and re-renders single frames on their own. A camera that integrated a spring would
|
|
96
|
+
* give a different answer depending on how it got to frame 372. This gives the same
|
|
97
|
+
* answer always, because the flight underneath it is closed-form in `t`.
|
|
98
|
+
*/
|
|
99
|
+
declare function cameraAtFrame(timeline: Timeline, frame: number, fps: number): View;
|
|
100
|
+
|
|
101
|
+
export { AtlasScene, type AtlasSceneProps, atlasDurationInFrames, cameraAtFrame, frameToSeconds, timelineFrames };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { planeProvider } from '../chunk-BDF2X5LC.js';
|
|
2
|
+
import { visibleNodes } from '../chunk-5SRKWTOU.js';
|
|
3
|
+
import { timeline } from '../chunk-SD4Y4EBG.js';
|
|
4
|
+
import { resolvePath } from '../chunk-RHTCDWZD.js';
|
|
5
|
+
import { transformToCss, viewToTransform } from '../chunk-RWYOYDBD.js';
|
|
6
|
+
import { useFrame, useVideo, holdFrame } from '@getnarro/video';
|
|
7
|
+
import { useMemo, useRef, useEffect } from 'react';
|
|
8
|
+
import { jsx } from 'react/jsx-runtime';
|
|
9
|
+
|
|
10
|
+
// src/video/frame.ts
|
|
11
|
+
function frameToSeconds(frame, fps) {
|
|
12
|
+
if (!Number.isFinite(frame) || !Number.isFinite(fps) || fps <= 0) return 0;
|
|
13
|
+
return frame / fps;
|
|
14
|
+
}
|
|
15
|
+
function timelineFrames(timeline2, fps) {
|
|
16
|
+
if (fps <= 0) return 0;
|
|
17
|
+
return Math.max(1, Math.ceil(timeline2.duration * fps));
|
|
18
|
+
}
|
|
19
|
+
function cameraAtFrame(timeline2, frame, fps) {
|
|
20
|
+
return timeline2.at(frameToSeconds(frame, fps));
|
|
21
|
+
}
|
|
22
|
+
function AtlasScene({
|
|
23
|
+
path,
|
|
24
|
+
provider,
|
|
25
|
+
nodes = [],
|
|
26
|
+
children,
|
|
27
|
+
viewport,
|
|
28
|
+
hold = 1,
|
|
29
|
+
style = "fly",
|
|
30
|
+
className
|
|
31
|
+
}) {
|
|
32
|
+
const frame = useFrame();
|
|
33
|
+
const video = useVideo();
|
|
34
|
+
const grid = viewport ?? { width: video.width, height: video.height };
|
|
35
|
+
const scene = useMemo(() => provider ?? planeProvider({ nodes }), [provider, nodes]);
|
|
36
|
+
const line = useMemo(() => {
|
|
37
|
+
const { resolved } = resolvePath(path, (ref) => scene.resolve(ref, grid));
|
|
38
|
+
return timeline(path, resolved, { style, defaultHold: hold });
|
|
39
|
+
}, [path, scene, grid, style, hold]);
|
|
40
|
+
const view = cameraAtFrame(line, frame, video.fps);
|
|
41
|
+
const release = useRef(null);
|
|
42
|
+
const ready = scene.ready(view);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (ready) {
|
|
45
|
+
release.current?.();
|
|
46
|
+
release.current = null;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
release.current ??= holdFrame(`atlas:${scene.name}`);
|
|
50
|
+
}, [ready, scene.name]);
|
|
51
|
+
useEffect(() => () => release.current?.(), []);
|
|
52
|
+
const mounted = useMemo(
|
|
53
|
+
() => new Set(visibleNodes(nodes, view, grid).map((n) => n.id)),
|
|
54
|
+
[nodes, view, grid]
|
|
55
|
+
);
|
|
56
|
+
const handled = scene.apply?.(view, grid) ?? false;
|
|
57
|
+
return /* @__PURE__ */ jsx(
|
|
58
|
+
"div",
|
|
59
|
+
{
|
|
60
|
+
className,
|
|
61
|
+
style: { position: "absolute", inset: 0, overflow: "hidden" },
|
|
62
|
+
"data-atlas-mounted": mounted.size,
|
|
63
|
+
children: /* @__PURE__ */ jsx(
|
|
64
|
+
"div",
|
|
65
|
+
{
|
|
66
|
+
"data-atlas-camera": "",
|
|
67
|
+
style: {
|
|
68
|
+
position: "absolute",
|
|
69
|
+
inset: 0,
|
|
70
|
+
transform: handled ? void 0 : transformToCss(viewToTransform(view, grid)),
|
|
71
|
+
transformOrigin: "0 0",
|
|
72
|
+
// Deliberately not `transform`. See the note at the top of this file.
|
|
73
|
+
willChange: "auto"
|
|
74
|
+
},
|
|
75
|
+
children
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
function atlasDurationInFrames(path, fps, options = {}) {
|
|
82
|
+
const grid = options.viewport ?? { width: 1920, height: 1080 };
|
|
83
|
+
const scene = options.provider ?? planeProvider();
|
|
84
|
+
const { resolved } = resolvePath(path, (ref) => scene.resolve(ref, grid));
|
|
85
|
+
const line = timeline(path, resolved, { defaultHold: options.hold ?? 1 });
|
|
86
|
+
return Math.max(1, Math.ceil(line.duration * fps));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { AtlasScene, atlasDurationInFrames, cameraAtFrame, frameToSeconds, timelineFrames };
|
|
90
|
+
//# sourceMappingURL=index.js.map
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/video/frame.ts","../../src/video/AtlasScene.tsx"],"names":["timeline"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The camera on a frame counter.\n *\n * No React, no `@getnarro/video`, no clock — just the arithmetic, so the interesting\n * property can be tested without mounting anything.\n *\n * @packageDocumentation\n */\n\nimport type { Timeline } from \"../camera/timeline\";\nimport type { View } from \"../camera/view\";\n\n/** Seconds at a frame. The renderer's whole notion of time. */\nexport function frameToSeconds(frame: number, fps: number): number {\n if (!Number.isFinite(frame) || !Number.isFinite(fps) || fps <= 0) return 0;\n return frame / fps;\n}\n\n/** How many frames a timeline occupies, rounded up so the last waypoint is held. */\nexport function timelineFrames(timeline: Timeline, fps: number): number {\n if (fps <= 0) return 0;\n return Math.max(1, Math.ceil(timeline.duration * fps));\n}\n\n/**\n * The view at a frame.\n *\n * The one thing that matters here is what is *absent*: no accumulator, no previous\n * frame, no `requestAnimationFrame`. The renderer advances a counter and calls a render\n * function; it may render frames out of order or across several processes, and it caches\n * and re-renders single frames on their own. A camera that integrated a spring would\n * give a different answer depending on how it got to frame 372. This gives the same\n * answer always, because the flight underneath it is closed-form in `t`.\n */\nexport function cameraAtFrame(timeline: Timeline, frame: number, fps: number): View {\n return timeline.at(frameToSeconds(frame, fps));\n}\n","/**\n * The `@getnarro/video` adapter.\n *\n * A separate entry point because `@getnarro/video` is an **optional** peer: a deck that\n * puts a zooming canvas on a slide should not have to install a video framework to do it.\n * Only this subpath imports one.\n *\n * Two things differ from the deck host, and both follow from the render target rather\n * than from taste.\n *\n * **The clock is the frame counter.** No `requestAnimationFrame` anywhere — the view is\n * `timeline.at(frame / fps)`, so a frame rendered on its own, out of order, or in another\n * process gives bit-identical output.\n *\n * **`will-change` stays off.** In the deck it is switched on during a flight to keep the\n * frames cheap, at the cost of blur while moving. Here every frame is a deliverable and\n * nothing is interactive, so the trade inverts: pay the re-raster, keep the sharpness.\n *\n * @packageDocumentation\n */\n\nimport { holdFrame, useFrame, useVideo } from \"@getnarro/video\";\nimport type React from \"react\";\nimport { useEffect, useMemo, useRef } from \"react\";\nimport { type AtlasPath, resolvePath } from \"../camera/path\";\nimport { type MotionStyle, timeline } from \"../camera/timeline\";\nimport { transformToCss, type Viewport, viewToTransform } from \"../camera/view\";\nimport { type PlacedNode, visibleNodes } from \"../scene/lod\";\nimport { planeProvider } from \"../scene/plane\";\nimport type { SceneProvider } from \"../scene/provider\";\nimport { cameraAtFrame } from \"./frame\";\n\nexport interface AtlasSceneProps {\n path: AtlasPath;\n provider?: SceneProvider;\n nodes?: readonly PlacedNode[];\n children?: React.ReactNode;\n /** The world grid. Defaults to the composition's own pixel size. */\n viewport?: Viewport;\n /**\n * A rest at 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 has no equivalent because a presenter supplies the pauses.\n *\n * @defaultValue 1\n */\n hold?: number;\n /** @defaultValue `\"fly\"` */\n style?: MotionStyle;\n className?: string;\n}\n\n/**\n * A zooming canvas as a video scene.\n *\n * ```tsx\n * <AtlasScene path={path} nodes={nodes}>\n * <AtlasNode id=\"ingest\" x={0} y={0} width={400} height={300}>…</AtlasNode>\n * </AtlasScene>\n * ```\n *\n * @behaviour Fills its container absolutely and owns the whole frame, so it cannot share\n * a scene with ordinary content. Its children are positioned in world units on a plane the\n * camera moves over — `AtlasNode` is what places them — and anything outside the frame is\n * unmounted rather than merely hidden.\n */\nexport function AtlasScene({\n path,\n provider,\n nodes = [],\n children,\n viewport,\n hold = 1,\n style = \"fly\",\n className,\n}: AtlasSceneProps): React.ReactElement {\n const frame = useFrame();\n const video = useVideo();\n const grid: Viewport = viewport ?? { width: video.width, height: video.height };\n\n const scene = useMemo(() => provider ?? planeProvider({ nodes }), [provider, nodes]);\n\n const line = useMemo(() => {\n const { resolved } = resolvePath(path, (ref) => scene.resolve(ref, grid));\n return timeline(path, resolved, { style, defaultHold: hold });\n }, [path, scene, grid, style, hold]);\n\n const view = cameraAtFrame(line, frame, video.fps);\n\n // The readiness gate. A plane is ready the instant it exists, but a replay has to\n // reach a chapter and a map has to fetch tiles — both over the network, both while the\n // renderer is advancing the counter as fast as it can. Without this, a rendered video\n // of a map is a video of empty tiles, and nobody finds out until it is encoded.\n const release = useRef<(() => void) | null>(null);\n const ready = scene.ready(view);\n useEffect(() => {\n if (ready) {\n release.current?.();\n release.current = null;\n return;\n }\n release.current ??= holdFrame(`atlas:${scene.name}`);\n }, [ready, scene.name]);\n useEffect(() => () => release.current?.(), []);\n\n const mounted = useMemo(\n () => new Set(visibleNodes(nodes, view, grid).map((n) => n.id)),\n [nodes, view, grid],\n );\n\n const handled = scene.apply?.(view, grid) ?? false;\n\n return (\n <div\n className={className}\n style={{ position: \"absolute\", inset: 0, overflow: \"hidden\" }}\n data-atlas-mounted={mounted.size}\n >\n <div\n data-atlas-camera=\"\"\n style={{\n position: \"absolute\",\n inset: 0,\n transform: handled ? undefined : transformToCss(viewToTransform(view, grid)),\n transformOrigin: \"0 0\",\n // Deliberately not `transform`. See the note at the top of this file.\n willChange: \"auto\",\n }}\n >\n {children}\n </div>\n </div>\n );\n}\n\n/**\n * How many frames a path needs, for a composition's `durationInFrames`.\n *\n * Derived from the flights' own suggested durations plus the holds, so a scene's length\n * follows its camera path rather than being a constant somebody has to keep in step.\n */\nexport function atlasDurationInFrames(\n path: AtlasPath,\n fps: number,\n options: { hold?: number; viewport?: Viewport; provider?: SceneProvider } = {},\n): number {\n const grid = options.viewport ?? { width: 1920, height: 1080 };\n const scene = options.provider ?? planeProvider();\n const { resolved } = resolvePath(path, (ref) => scene.resolve(ref, grid));\n const line = timeline(path, resolved, { defaultHold: options.hold ?? 1 });\n return Math.max(1, Math.ceil(line.duration * fps));\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The geometry the camera works in.
|
|
3
|
+
*
|
|
4
|
+
* A **view** is `{ cx, cy, w }` — a centre and a width, both in world units. Not a
|
|
5
|
+
* scale. A scale is a ratio against an implicit viewport, so the same waypoint frames
|
|
6
|
+
* different content in a 16:9 deck and a 9:16 video; a width is a fact about the world
|
|
7
|
+
* and survives the render target, the aspect ratio and a re-layout of the scene. It is
|
|
8
|
+
* also exactly the triple van Wijk's interpolation is defined over, which is the other
|
|
9
|
+
* reason to use it.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/** A framing of the world: where the camera looks, and how much world fits across it. */
|
|
14
|
+
interface View {
|
|
15
|
+
/** Centre of the frame, in world units. */
|
|
16
|
+
cx: number;
|
|
17
|
+
/** How much of the world's width the frame holds, in world units. Always > 0. */
|
|
18
|
+
cy: number;
|
|
19
|
+
/** Width of the frame in world units. Always > 0 — a zero-width view has no scale. */
|
|
20
|
+
w: number;
|
|
21
|
+
}
|
|
22
|
+
/** An axis-aligned rectangle in world units. */
|
|
23
|
+
interface Rect {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
}
|
|
29
|
+
/** The size of whatever the camera is painting onto, in device or canvas units. */
|
|
30
|
+
interface Viewport {
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
}
|
|
34
|
+
/** The smallest view width worth distinguishing. Below it, a view has effectively no size. */
|
|
35
|
+
declare const MIN_VIEW_WIDTH = 0.000001;
|
|
36
|
+
/** Is this a view that can actually be rendered? */
|
|
37
|
+
declare function isFiniteView(view: View): boolean;
|
|
38
|
+
/** A view, with a width clamped to something renderable. */
|
|
39
|
+
declare function normalizeView(view: View): View;
|
|
40
|
+
/**
|
|
41
|
+
* The world rectangle a view covers, given the aspect ratio it is painted at.
|
|
42
|
+
*
|
|
43
|
+
* Height is derived, never stored: it is the one part of the framing that belongs to the
|
|
44
|
+
* render target rather than to the author.
|
|
45
|
+
*/
|
|
46
|
+
declare function viewToRect(view: View, viewport: Viewport): Rect;
|
|
47
|
+
/** The view that frames a rectangle, with `padding` as a fraction of the rectangle's size. */
|
|
48
|
+
declare function rectToView(rect: Rect, viewport: Viewport, padding?: number): View;
|
|
49
|
+
/** Do two rectangles overlap at all? Touching edges count as overlapping. */
|
|
50
|
+
declare function rectsIntersect(a: Rect, b: Rect): boolean;
|
|
51
|
+
/** A rectangle grown by a fraction of its own size on every side. */
|
|
52
|
+
declare function growRect(rect: Rect, margin: number): Rect;
|
|
53
|
+
/** The smallest rectangle containing all of them, or null for none. */
|
|
54
|
+
declare function unionRects(rects: readonly Rect[]): Rect | null;
|
|
55
|
+
/** Is the rectangle inside the bounds, allowing `slack` of the bounds' size outside? */
|
|
56
|
+
declare function rectWithin(rect: Rect, bounds: Rect, slack?: number): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The camera transform, as the three numbers a CSS matrix needs.
|
|
59
|
+
*
|
|
60
|
+
* One transform, on one element, per frame — never one per node. That is what keeps the
|
|
61
|
+
* cost of a scene independent of how much is in it: one composited layer, one style
|
|
62
|
+
* recalculation, no layout and no paint, whether the plane holds ten nodes or a thousand.
|
|
63
|
+
*/
|
|
64
|
+
interface CameraTransform {
|
|
65
|
+
/** Uniform scale: world units → viewport units. */
|
|
66
|
+
scale: number;
|
|
67
|
+
/** Viewport-unit offset, applied before the scale, with `transform-origin: 0 0`. */
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
}
|
|
71
|
+
/** The transform that puts `view` on screen in `viewport`, for `transform-origin: 0 0`. */
|
|
72
|
+
declare function viewToTransform(view: View, viewport: Viewport): CameraTransform;
|
|
73
|
+
/**
|
|
74
|
+
* The transform as a CSS string.
|
|
75
|
+
*
|
|
76
|
+
* `translate3d` and not `translate`: the z component is what asks the compositor for a
|
|
77
|
+
* layer, and a layer is the whole point — the alternative is a paint per frame.
|
|
78
|
+
*/
|
|
79
|
+
declare function transformToCss(transform: CameraTransform): string;
|
|
80
|
+
/** Shorthand for the common case. */
|
|
81
|
+
declare function viewToCss(view: View, viewport: Viewport): string;
|
|
82
|
+
|
|
83
|
+
export { type CameraTransform as C, MIN_VIEW_WIDTH as M, type Rect as R, type View as V, rectToView as a, rectWithin as b, type Viewport as c, viewToRect as d, viewToTransform as e, growRect as g, isFiniteView as i, normalizeView as n, rectsIntersect as r, transformToCss as t, unionRects as u, viewToCss as v };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export { A as AtlasMode, T as TAG } from '../tag-BP51cBza.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<narro-atlas>` — a zooming canvas on an ordinary web page.
|
|
5
|
+
*
|
|
6
|
+
* The third host, and the one that costs least: the camera is already a pure function of
|
|
7
|
+
* a moment, so a page only has to say which moment. Scrolling says it.
|
|
8
|
+
*
|
|
9
|
+
* Built to the same rules as `<narro-terminal>` and `<narro-replay>`:
|
|
10
|
+
*
|
|
11
|
+
* - **No framework.** The camera is arithmetic and one CSS transform; shipping React to
|
|
12
|
+
* apply it would cost more than everything it moves.
|
|
13
|
+
* - **The light DOM is the scene *and* the fallback.** The shadow root slots the page's
|
|
14
|
+
* own markup, so with JavaScript off a reader sees the whole diagram, laid out and
|
|
15
|
+
* indexed, rather than an empty box. Nothing is duplicated and nothing is hidden.
|
|
16
|
+
* - **`prefers-reduced-motion` is honoured** by snapping to the nearest waypoint rather
|
|
17
|
+
* than by disabling the feature. Scrolling is the reader's own doing; what they asked
|
|
18
|
+
* not to have is the camera sliding under them while they do it.
|
|
19
|
+
*
|
|
20
|
+
* This module is **browser-only**: the class below extends `HTMLElement`, which is
|
|
21
|
+
* evaluated on import and does not exist in Node. Reach it through
|
|
22
|
+
* `@getnarro/atlas/embed`; `@getnarro/atlas/web` deliberately does not re-export it, so
|
|
23
|
+
* the scroll arithmetic stays importable during server rendering.
|
|
24
|
+
*
|
|
25
|
+
* @packageDocumentation
|
|
26
|
+
*/
|
|
27
|
+
declare class NarroAtlasElement extends HTMLElement {
|
|
28
|
+
static observedAttributes: string[];
|
|
29
|
+
private shadow;
|
|
30
|
+
private frame;
|
|
31
|
+
private camera;
|
|
32
|
+
private rail;
|
|
33
|
+
private line;
|
|
34
|
+
private path;
|
|
35
|
+
private index;
|
|
36
|
+
private raf;
|
|
37
|
+
/** Bumped on every load, so a slow fetch that lost the race cannot mount over a newer one. */
|
|
38
|
+
private generation;
|
|
39
|
+
private onScroll;
|
|
40
|
+
private onResize;
|
|
41
|
+
connectedCallback(): void;
|
|
42
|
+
disconnectedCallback(): void;
|
|
43
|
+
attributeChangedCallback(name: string): void;
|
|
44
|
+
private mount;
|
|
45
|
+
/** The camera path: an inline JSON script, or a URL in `path`. */
|
|
46
|
+
private load;
|
|
47
|
+
private build;
|
|
48
|
+
private drawRail;
|
|
49
|
+
private mode;
|
|
50
|
+
private reduced;
|
|
51
|
+
private schedule;
|
|
52
|
+
private goTo;
|
|
53
|
+
private onKeyDown;
|
|
54
|
+
private draw;
|
|
55
|
+
}
|
|
56
|
+
/** Register the element. Idempotent, so loading the script twice is harmless. */
|
|
57
|
+
declare function defineAtlasElement(): void;
|
|
58
|
+
|
|
59
|
+
export { NarroAtlasElement, defineAtlasElement };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { TAG, domProvider, scrollProgress, progressToSeconds, snapToWaypoint } from '../chunk-UTCNNNPT.js';
|
|
2
|
+
export { TAG } from '../chunk-UTCNNNPT.js';
|
|
3
|
+
import { timeline } from '../chunk-SD4Y4EBG.js';
|
|
4
|
+
import { resolvePath } from '../chunk-RHTCDWZD.js';
|
|
5
|
+
import { transformToCss, viewToTransform } from '../chunk-RWYOYDBD.js';
|
|
6
|
+
|
|
7
|
+
// src/web/element.ts
|
|
8
|
+
var STYLE = `
|
|
9
|
+
:host{display:block;position:relative}
|
|
10
|
+
:host([hidden]){display:none}
|
|
11
|
+
.frame{position:sticky;top:0;height:100vh;overflow:hidden}
|
|
12
|
+
.camera{position:absolute;inset:0;transform-origin:0 0;will-change:auto}
|
|
13
|
+
.rail{position:absolute;right:1rem;bottom:1rem;display:flex;gap:.4rem;z-index:2}
|
|
14
|
+
.dot{width:.55rem;height:.55rem;border-radius:50%;border:0;padding:0;cursor:pointer;
|
|
15
|
+
background:currentColor;opacity:.3}
|
|
16
|
+
.dot[aria-current="true"]{opacity:.9}
|
|
17
|
+
.dot:focus-visible{outline:2px solid currentColor;outline-offset:3px}
|
|
18
|
+
@media (prefers-reduced-motion: reduce){.camera{transition:none}}
|
|
19
|
+
`;
|
|
20
|
+
function parseWorld(value) {
|
|
21
|
+
const match = /^(\d+)\s*[x×]\s*(\d+)$/.exec((value ?? "").trim());
|
|
22
|
+
if (!match) return { width: 1920, height: 1080 };
|
|
23
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
24
|
+
}
|
|
25
|
+
var NarroAtlasElement = class extends HTMLElement {
|
|
26
|
+
static observedAttributes = ["path", "mode", "world", "hold"];
|
|
27
|
+
shadow = null;
|
|
28
|
+
frame = null;
|
|
29
|
+
camera = null;
|
|
30
|
+
rail = null;
|
|
31
|
+
line = null;
|
|
32
|
+
path = null;
|
|
33
|
+
index = 0;
|
|
34
|
+
raf = 0;
|
|
35
|
+
/** Bumped on every load, so a slow fetch that lost the race cannot mount over a newer one. */
|
|
36
|
+
generation = 0;
|
|
37
|
+
onScroll = () => this.schedule();
|
|
38
|
+
onResize = () => this.schedule();
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
if (!this.shadow) this.mount();
|
|
41
|
+
void this.load();
|
|
42
|
+
window.addEventListener("scroll", this.onScroll, { passive: true });
|
|
43
|
+
window.addEventListener("resize", this.onResize, { passive: true });
|
|
44
|
+
}
|
|
45
|
+
disconnectedCallback() {
|
|
46
|
+
window.removeEventListener("scroll", this.onScroll);
|
|
47
|
+
window.removeEventListener("resize", this.onResize);
|
|
48
|
+
cancelAnimationFrame(this.raf);
|
|
49
|
+
}
|
|
50
|
+
attributeChangedCallback(name) {
|
|
51
|
+
if (!this.shadow) return;
|
|
52
|
+
if (name === "path") void this.load();
|
|
53
|
+
else this.schedule();
|
|
54
|
+
}
|
|
55
|
+
mount() {
|
|
56
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
57
|
+
const style = document.createElement("style");
|
|
58
|
+
style.textContent = STYLE;
|
|
59
|
+
this.frame = document.createElement("div");
|
|
60
|
+
this.frame.className = "frame";
|
|
61
|
+
this.camera = document.createElement("div");
|
|
62
|
+
this.camera.className = "camera";
|
|
63
|
+
this.camera.appendChild(document.createElement("slot"));
|
|
64
|
+
this.rail = document.createElement("div");
|
|
65
|
+
this.rail.className = "rail";
|
|
66
|
+
this.frame.append(this.camera, this.rail);
|
|
67
|
+
this.shadow.append(style, this.frame);
|
|
68
|
+
this.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
69
|
+
}
|
|
70
|
+
/** The camera path: an inline JSON script, or a URL in `path`. */
|
|
71
|
+
async load() {
|
|
72
|
+
const generation = ++this.generation;
|
|
73
|
+
const src = this.getAttribute("path");
|
|
74
|
+
let path = null;
|
|
75
|
+
if (src) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(src);
|
|
78
|
+
path = await response.json();
|
|
79
|
+
} catch (cause) {
|
|
80
|
+
console.error(`[${TAG}] could not load the camera path from ${src}`, cause);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const inline = this.querySelector('script[type="application/json"]');
|
|
85
|
+
if (inline?.textContent) {
|
|
86
|
+
try {
|
|
87
|
+
path = JSON.parse(inline.textContent);
|
|
88
|
+
} catch (cause) {
|
|
89
|
+
console.error(`[${TAG}] the inline camera path is not valid JSON`, cause);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (generation !== this.generation) return;
|
|
95
|
+
if (!path?.waypoints?.length) return;
|
|
96
|
+
this.path = path;
|
|
97
|
+
this.build();
|
|
98
|
+
this.drawRail();
|
|
99
|
+
this.schedule();
|
|
100
|
+
}
|
|
101
|
+
build() {
|
|
102
|
+
if (!this.path) return;
|
|
103
|
+
const world = parseWorld(this.getAttribute("world"));
|
|
104
|
+
const provider = domProvider({ root: this, fallback: { x: 0, y: 0, ...world } });
|
|
105
|
+
const { resolved } = resolvePath(this.path, (ref) => provider.resolve(ref, world));
|
|
106
|
+
this.line = timeline(this.path, resolved, { defaultHold: 0 });
|
|
107
|
+
}
|
|
108
|
+
drawRail() {
|
|
109
|
+
if (!this.rail || !this.line) return;
|
|
110
|
+
this.rail.textContent = "";
|
|
111
|
+
if (this.mode() !== "steps") return;
|
|
112
|
+
this.line.segments.filter((segment) => segment.kind === "hold").forEach((segment, index) => {
|
|
113
|
+
const dot = document.createElement("button");
|
|
114
|
+
dot.className = "dot";
|
|
115
|
+
dot.type = "button";
|
|
116
|
+
const label = segment.kind === "hold" ? segment.waypoint.label ?? segment.waypoint.id : "";
|
|
117
|
+
dot.setAttribute("aria-label", label);
|
|
118
|
+
dot.addEventListener("click", () => this.goTo(index));
|
|
119
|
+
this.rail?.appendChild(dot);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
mode() {
|
|
123
|
+
return this.getAttribute("mode") === "steps" ? "steps" : "scroll";
|
|
124
|
+
}
|
|
125
|
+
reduced() {
|
|
126
|
+
return window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
127
|
+
}
|
|
128
|
+
schedule() {
|
|
129
|
+
cancelAnimationFrame(this.raf);
|
|
130
|
+
this.raf = requestAnimationFrame(() => this.draw());
|
|
131
|
+
}
|
|
132
|
+
goTo(index) {
|
|
133
|
+
if (!this.line) return;
|
|
134
|
+
const stops = this.line.segments.filter((segment) => segment.kind === "hold");
|
|
135
|
+
this.index = Math.min(Math.max(index, 0), Math.max(0, stops.length - 1));
|
|
136
|
+
this.schedule();
|
|
137
|
+
}
|
|
138
|
+
onKeyDown(event) {
|
|
139
|
+
if (this.mode() !== "steps") return;
|
|
140
|
+
if (event.key === "ArrowRight") {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
this.goTo(this.index + 1);
|
|
143
|
+
} else if (event.key === "ArrowLeft") {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
this.goTo(this.index - 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
draw() {
|
|
149
|
+
if (!this.line || !this.camera || !this.frame) return;
|
|
150
|
+
const surface = {
|
|
151
|
+
width: this.frame.offsetWidth || this.offsetWidth || 1,
|
|
152
|
+
height: this.frame.offsetHeight || this.offsetHeight || 1
|
|
153
|
+
};
|
|
154
|
+
let seconds;
|
|
155
|
+
if (this.mode() === "steps") {
|
|
156
|
+
const stops = this.line.segments.filter((segment) => segment.kind === "hold");
|
|
157
|
+
const stop = stops[Math.min(this.index, stops.length - 1)];
|
|
158
|
+
seconds = stop?.kind === "hold" ? stop.start : 0;
|
|
159
|
+
} else {
|
|
160
|
+
const rect = this.getBoundingClientRect();
|
|
161
|
+
const progress = scrollProgress({
|
|
162
|
+
top: rect.top,
|
|
163
|
+
height: rect.height,
|
|
164
|
+
viewportHeight: window.innerHeight
|
|
165
|
+
});
|
|
166
|
+
seconds = progressToSeconds(this.line, progress);
|
|
167
|
+
if (this.reduced()) seconds = snapToWaypoint(this.line, seconds);
|
|
168
|
+
}
|
|
169
|
+
const view = this.line.at(seconds);
|
|
170
|
+
this.camera.style.transform = transformToCss(viewToTransform(view, surface));
|
|
171
|
+
const current = this.line.waypointAt(seconds);
|
|
172
|
+
if (current) {
|
|
173
|
+
this.setAttribute("data-waypoint", current.id);
|
|
174
|
+
const dots = this.rail?.querySelectorAll(".dot") ?? [];
|
|
175
|
+
dots.forEach((dot, index) => {
|
|
176
|
+
dot.setAttribute("aria-current", String(index === this.index));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
function defineAtlasElement() {
|
|
182
|
+
if (typeof customElements === "undefined") return;
|
|
183
|
+
if (customElements.get(TAG)) return;
|
|
184
|
+
customElements.define(TAG, NarroAtlasElement);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/web/embed.ts
|
|
188
|
+
defineAtlasElement();
|
|
189
|
+
|
|
190
|
+
export { NarroAtlasElement, defineAtlasElement };
|
|
191
|
+
//# sourceMappingURL=embed.js.map
|
|
192
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/web/element.ts","../../src/web/embed.ts"],"names":[],"mappings":"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","file":"embed.js","sourcesContent":["/**\n * `<narro-atlas>` — a zooming canvas on an ordinary web page.\n *\n * The third host, and the one that costs least: the camera is already a pure function of\n * a moment, so a page only has to say which moment. Scrolling says it.\n *\n * Built to the same rules as `<narro-terminal>` and `<narro-replay>`:\n *\n * - **No framework.** The camera is arithmetic and one CSS transform; shipping React to\n * apply it would cost more than everything it moves.\n * - **The light DOM is the scene *and* the fallback.** The shadow root slots the page's\n * own markup, so with JavaScript off a reader sees the whole diagram, laid out and\n * indexed, rather than an empty box. Nothing is duplicated and nothing is hidden.\n * - **`prefers-reduced-motion` is honoured** by snapping to the nearest waypoint rather\n * than by disabling the feature. Scrolling is the reader's own doing; what they asked\n * not to have is the camera sliding under them while they do it.\n *\n * This module is **browser-only**: the class below extends `HTMLElement`, which is\n * evaluated on import and does not exist in Node. Reach it through\n * `@getnarro/atlas/embed`; `@getnarro/atlas/web` deliberately does not re-export it, so\n * the scroll arithmetic stays importable during server rendering.\n *\n * @packageDocumentation\n */\n\nimport { type AtlasPath, resolvePath } from \"../camera/path\";\nimport { type Timeline, timeline } from \"../camera/timeline\";\nimport { transformToCss, type Viewport, viewToTransform } from \"../camera/view\";\nimport { domProvider } from \"./dom\";\nimport { progressToSeconds, scrollProgress, snapToWaypoint } from \"./scroll\";\nimport { type AtlasMode, TAG } from \"./tag\";\n\nconst STYLE = `\n:host{display:block;position:relative}\n:host([hidden]){display:none}\n.frame{position:sticky;top:0;height:100vh;overflow:hidden}\n.camera{position:absolute;inset:0;transform-origin:0 0;will-change:auto}\n.rail{position:absolute;right:1rem;bottom:1rem;display:flex;gap:.4rem;z-index:2}\n.dot{width:.55rem;height:.55rem;border-radius:50%;border:0;padding:0;cursor:pointer;\n background:currentColor;opacity:.3}\n.dot[aria-current=\"true\"]{opacity:.9}\n.dot:focus-visible{outline:2px solid currentColor;outline-offset:3px}\n@media (prefers-reduced-motion: reduce){.camera{transition:none}}\n`;\n\nfunction parseWorld(value: string | null): Viewport {\n const match = /^(\\d+)\\s*[x×]\\s*(\\d+)$/.exec((value ?? \"\").trim());\n if (!match) return { width: 1920, height: 1080 };\n return { width: Number(match[1]), height: Number(match[2]) };\n}\n\nexport class NarroAtlasElement extends HTMLElement {\n static observedAttributes = [\"path\", \"mode\", \"world\", \"hold\"];\n\n private shadow: ShadowRoot | null = null;\n private frame: HTMLElement | null = null;\n private camera: HTMLElement | null = null;\n private rail: HTMLElement | null = null;\n private line: Timeline | null = null;\n private path: AtlasPath | null = null;\n private index = 0;\n private raf = 0;\n /** Bumped on every load, so a slow fetch that lost the race cannot mount over a newer one. */\n private generation = 0;\n private onScroll = () => this.schedule();\n private onResize = () => this.schedule();\n\n connectedCallback(): void {\n if (!this.shadow) this.mount();\n void this.load();\n window.addEventListener(\"scroll\", this.onScroll, { passive: true });\n window.addEventListener(\"resize\", this.onResize, { passive: true });\n }\n\n disconnectedCallback(): void {\n window.removeEventListener(\"scroll\", this.onScroll);\n window.removeEventListener(\"resize\", this.onResize);\n cancelAnimationFrame(this.raf);\n }\n\n attributeChangedCallback(name: string): void {\n if (!this.shadow) return;\n if (name === \"path\") void this.load();\n else this.schedule();\n }\n\n private mount(): void {\n this.shadow = this.attachShadow({ mode: \"open\" });\n const style = document.createElement(\"style\");\n style.textContent = STYLE;\n\n this.frame = document.createElement(\"div\");\n this.frame.className = \"frame\";\n\n this.camera = document.createElement(\"div\");\n this.camera.className = \"camera\";\n // A slot, not a copy: the page's markup stays in the light DOM, so it is what a\n // crawler indexes and what a reader without JavaScript sees.\n this.camera.appendChild(document.createElement(\"slot\"));\n\n this.rail = document.createElement(\"div\");\n this.rail.className = \"rail\";\n\n this.frame.append(this.camera, this.rail);\n this.shadow.append(style, this.frame);\n\n this.addEventListener(\"keydown\", (event) => this.onKeyDown(event));\n }\n\n /** The camera path: an inline JSON script, or a URL in `path`. */\n private async load(): Promise<void> {\n const generation = ++this.generation;\n const src = this.getAttribute(\"path\");\n\n let path: AtlasPath | null = null;\n if (src) {\n try {\n const response = await fetch(src);\n path = (await response.json()) as AtlasPath;\n } catch (cause) {\n console.error(`[${TAG}] could not load the camera path from ${src}`, cause);\n return;\n }\n } else {\n const inline = this.querySelector('script[type=\"application/json\"]');\n if (inline?.textContent) {\n try {\n path = JSON.parse(inline.textContent) as AtlasPath;\n } catch (cause) {\n console.error(`[${TAG}] the inline camera path is not valid JSON`, cause);\n return;\n }\n }\n }\n\n if (generation !== this.generation) return;\n if (!path?.waypoints?.length) return;\n\n this.path = path;\n this.build();\n this.drawRail();\n this.schedule();\n }\n\n private build(): void {\n if (!this.path) return;\n const world = parseWorld(this.getAttribute(\"world\"));\n const provider = domProvider({ root: this, fallback: { x: 0, y: 0, ...world } });\n const { resolved } = resolvePath(this.path, (ref) => provider.resolve(ref, world));\n this.line = timeline(this.path, resolved, { defaultHold: 0 });\n }\n\n private drawRail(): void {\n if (!this.rail || !this.line) return;\n this.rail.textContent = \"\";\n if (this.mode() !== \"steps\") return;\n\n this.line.segments\n .filter((segment) => segment.kind === \"hold\")\n .forEach((segment, index) => {\n const dot = document.createElement(\"button\");\n dot.className = \"dot\";\n dot.type = \"button\";\n const label =\n segment.kind === \"hold\" ? (segment.waypoint.label ?? segment.waypoint.id) : \"\";\n dot.setAttribute(\"aria-label\", label);\n dot.addEventListener(\"click\", () => this.goTo(index));\n this.rail?.appendChild(dot);\n });\n }\n\n private mode(): AtlasMode {\n return this.getAttribute(\"mode\") === \"steps\" ? \"steps\" : \"scroll\";\n }\n\n private reduced(): boolean {\n return window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches ?? false;\n }\n\n private schedule(): void {\n cancelAnimationFrame(this.raf);\n this.raf = requestAnimationFrame(() => this.draw());\n }\n\n private goTo(index: number): void {\n if (!this.line) return;\n const stops = this.line.segments.filter((segment) => segment.kind === \"hold\");\n this.index = Math.min(Math.max(index, 0), Math.max(0, stops.length - 1));\n this.schedule();\n }\n\n private onKeyDown(event: KeyboardEvent): void {\n if (this.mode() !== \"steps\") return;\n if (event.key === \"ArrowRight\") {\n event.preventDefault();\n this.goTo(this.index + 1);\n } else if (event.key === \"ArrowLeft\") {\n event.preventDefault();\n this.goTo(this.index - 1);\n }\n }\n\n private draw(): void {\n if (!this.line || !this.camera || !this.frame) return;\n\n const surface: Viewport = {\n width: this.frame.offsetWidth || this.offsetWidth || 1,\n height: this.frame.offsetHeight || this.offsetHeight || 1,\n };\n\n let seconds: number;\n if (this.mode() === \"steps\") {\n const stops = this.line.segments.filter((segment) => segment.kind === \"hold\");\n const stop = stops[Math.min(this.index, stops.length - 1)];\n seconds = stop?.kind === \"hold\" ? stop.start : 0;\n } else {\n const rect = this.getBoundingClientRect();\n const progress = scrollProgress({\n top: rect.top,\n height: rect.height,\n viewportHeight: window.innerHeight,\n });\n seconds = progressToSeconds(this.line, progress);\n if (this.reduced()) seconds = snapToWaypoint(this.line, seconds);\n }\n\n const view = this.line.at(seconds);\n this.camera.style.transform = transformToCss(viewToTransform(view, surface));\n\n const current = this.line.waypointAt(seconds);\n if (current) {\n this.setAttribute(\"data-waypoint\", current.id);\n const dots = this.rail?.querySelectorAll<HTMLElement>(\".dot\") ?? [];\n dots.forEach((dot, index) => {\n dot.setAttribute(\"aria-current\", String(index === this.index));\n });\n }\n }\n}\n\n/** Register the element. Idempotent, so loading the script twice is harmless. */\nexport function defineAtlasElement(): void {\n if (typeof customElements === \"undefined\") return;\n if (customElements.get(TAG)) return;\n customElements.define(TAG, NarroAtlasElement);\n}\n","/**\n * `<narro-atlas>`, registered. Load this and the element works.\n *\n * A browser entry by construction: it evaluates a class extending `HTMLElement`. The\n * portable half of the web host — the scroll arithmetic, the DOM scene — is\n * `@getnarro/atlas/web`, which stays importable on a server.\n *\n * @packageDocumentation\n */\n\nimport { defineAtlasElement } from \"./element.js\";\n\ndefineAtlasElement();\n\nexport { defineAtlasElement, NarroAtlasElement } from \"./element.js\";\nexport { type AtlasMode, TAG } from \"./tag.js\";\n"]}
|