@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 { V as View } from './view-BzztOpV6.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The camera path: an ordered list of named framings, as data.
|
|
5
|
+
*
|
|
6
|
+
* A waypoint names a place, never an index. `TransformConfig.positions` is a
|
|
7
|
+
* `Map<string, SlidePosition>` keyed by slide index, and that is the second thing this
|
|
8
|
+
* package fixes: an index renumbers itself the moment somebody inserts a waypoint above
|
|
9
|
+
* it, and it cannot be a URL fragment, an export filename, or the thing a checklist item
|
|
10
|
+
* binds to. A name can be all four.
|
|
11
|
+
*
|
|
12
|
+
* @packageDocumentation
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where a waypoint looks.
|
|
17
|
+
*
|
|
18
|
+
* Either a framing in world units, or a **reference** the scene provider resolves — a
|
|
19
|
+
* CSS selector over a replay, a node id on a plane. A reference is the authorable form:
|
|
20
|
+
* `"#publish-button"` survives a re-record that moves the button, where the coordinates
|
|
21
|
+
* it resolved to last week do not.
|
|
22
|
+
*/
|
|
23
|
+
type WaypointTarget = View | string;
|
|
24
|
+
/** One named framing on the path. */
|
|
25
|
+
interface Waypoint {
|
|
26
|
+
/**
|
|
27
|
+
* A stable name, unique within the path.
|
|
28
|
+
*
|
|
29
|
+
* It is the URL fragment, the export filename and the binding a checklist item uses,
|
|
30
|
+
* so it outlives reordering in a way an index cannot.
|
|
31
|
+
*/
|
|
32
|
+
id: string;
|
|
33
|
+
/** Where to look: a `{ cx, cy, w }` framing, or a reference for the provider. */
|
|
34
|
+
at: WaypointTarget;
|
|
35
|
+
/**
|
|
36
|
+
* How far the camera pulls back on the way *in* to this waypoint — van Wijk's ρ.
|
|
37
|
+
*
|
|
38
|
+
* @defaultValue {@link DEFAULT_RHO}
|
|
39
|
+
*/
|
|
40
|
+
arc?: number;
|
|
41
|
+
/**
|
|
42
|
+
* How long the flight *in* to this waypoint takes, in **seconds**.
|
|
43
|
+
*
|
|
44
|
+
* Omit it and the flight uses its own `suggestedDuration`, which is derived from how
|
|
45
|
+
* far the picture actually travels. Overriding it is what `narro check` measures
|
|
46
|
+
* against that suggestion — an override far below it is the lurch, and far above it is
|
|
47
|
+
* the sixteen-minute camera move.
|
|
48
|
+
*/
|
|
49
|
+
duration?: number;
|
|
50
|
+
/** How long to rest here before flying on, in seconds. Only autoplay and video use it. */
|
|
51
|
+
hold?: number;
|
|
52
|
+
/** Human-readable label, for a checklist, a progress rail or an export filename. */
|
|
53
|
+
label?: string;
|
|
54
|
+
/** A chapter in a `.narrocast`, for the replay provider. Ignored by other providers. */
|
|
55
|
+
chapter?: string;
|
|
56
|
+
}
|
|
57
|
+
/** A camera path over one scene. */
|
|
58
|
+
interface AtlasPath {
|
|
59
|
+
waypoints: Waypoint[];
|
|
60
|
+
/** ρ for any waypoint that does not set its own. */
|
|
61
|
+
arc?: number;
|
|
62
|
+
/** `hold` for any waypoint that does not set its own. Seconds. */
|
|
63
|
+
hold?: number;
|
|
64
|
+
}
|
|
65
|
+
/** A waypoint whose target has been resolved to a concrete framing. */
|
|
66
|
+
interface ResolvedWaypoint extends Omit<Waypoint, "at"> {
|
|
67
|
+
at: View;
|
|
68
|
+
/** The reference this came from, when it was authored as one. */
|
|
69
|
+
ref?: string;
|
|
70
|
+
/** Its position on the path. */
|
|
71
|
+
index: number;
|
|
72
|
+
}
|
|
73
|
+
/** Why a waypoint could not be resolved. */
|
|
74
|
+
interface UnresolvedWaypoint {
|
|
75
|
+
id: string;
|
|
76
|
+
index: number;
|
|
77
|
+
ref: string;
|
|
78
|
+
}
|
|
79
|
+
interface ResolveResult {
|
|
80
|
+
resolved: ResolvedWaypoint[];
|
|
81
|
+
unresolved: UnresolvedWaypoint[];
|
|
82
|
+
}
|
|
83
|
+
/** Is this target a concrete framing rather than a reference for the provider? */
|
|
84
|
+
declare function isView(target: WaypointTarget): target is View;
|
|
85
|
+
/**
|
|
86
|
+
* Turn every waypoint's target into a framing, using `resolve` for the references.
|
|
87
|
+
*
|
|
88
|
+
* Unresolved waypoints are returned rather than thrown: the checker wants to report all
|
|
89
|
+
* of them at once, and a host wants to skip the broken one and still fly the rest.
|
|
90
|
+
*/
|
|
91
|
+
declare function resolvePath(path: AtlasPath, resolve: (ref: string) => View | null): ResolveResult;
|
|
92
|
+
/** The ρ a waypoint flies in with, falling back to the path's and then the default. */
|
|
93
|
+
declare function arcFor(path: AtlasPath, waypoint: Pick<Waypoint, "arc">): number;
|
|
94
|
+
/** The rest a waypoint takes, falling back to the path's and then to none. */
|
|
95
|
+
declare function holdFor(path: AtlasPath, waypoint: Pick<Waypoint, "hold">): number;
|
|
96
|
+
/** Find a waypoint by name. */
|
|
97
|
+
declare function waypointIndex(path: AtlasPath, id: string): number;
|
|
98
|
+
/** Names used more than once, in the order they first repeat. */
|
|
99
|
+
declare function duplicateIds(path: AtlasPath): string[];
|
|
100
|
+
|
|
101
|
+
export { type AtlasPath as A, type ResolvedWaypoint as R, type UnresolvedWaypoint as U, type Waypoint as W, arcFor as a, type ResolveResult as b, type WaypointTarget as c, duplicateIds as d, holdFor as h, isView as i, resolvePath as r, waypointIndex as w };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { R as Rect, c as Viewport, V as View } from './view-BzztOpV6.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One interface, so a deck, a video and a web page render four different kinds of scene
|
|
5
|
+
* the same way.
|
|
6
|
+
*
|
|
7
|
+
* The member that earns the interface is {@link SceneProvider.ready}. A plane is ready
|
|
8
|
+
* the instant it mounts, but a map has to fetch tiles and a replay has to reach a
|
|
9
|
+
* chapter — both over the network, both asynchronously. The video renderer advances a
|
|
10
|
+
* frame counter as fast as it can, so without a gate it will happily render frame 12 of
|
|
11
|
+
* a map that has not drawn anything yet and encode a video of grey squares. That failure
|
|
12
|
+
* is the single most likely way a spatial canvas ships broken, so the check for it is in
|
|
13
|
+
* the type rather than in a provider's good intentions.
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** The providers this package knows about. A closed set, so `catalog.json` can carry it. */
|
|
19
|
+
declare const PROVIDER_NAMES: readonly ["plane", "replay", "map", "image", "dom"];
|
|
20
|
+
type ProviderName = (typeof PROVIDER_NAMES)[number];
|
|
21
|
+
interface SceneProvider {
|
|
22
|
+
/** Which kind of scene this is. */
|
|
23
|
+
readonly name: ProviderName;
|
|
24
|
+
/**
|
|
25
|
+
* The world rectangle the scene occupies, or null when it does not know yet.
|
|
26
|
+
*
|
|
27
|
+
* The checker uses it to catch a waypoint that flies to somewhere there is nothing.
|
|
28
|
+
*/
|
|
29
|
+
bounds(): Rect | null;
|
|
30
|
+
/**
|
|
31
|
+
* A waypoint reference — a CSS selector, a node id — resolved to a framing.
|
|
32
|
+
*
|
|
33
|
+
* Null when it matches nothing, which is a diagnostic rather than a crash: the host
|
|
34
|
+
* skips that waypoint and the checker reports it by name.
|
|
35
|
+
*/
|
|
36
|
+
resolve(ref: string, viewport: Viewport): View | null;
|
|
37
|
+
/**
|
|
38
|
+
* Has everything this view needs finished loading?
|
|
39
|
+
*
|
|
40
|
+
* The video host holds the frame until this is true; the deck host ignores it, because
|
|
41
|
+
* a presenter can see perfectly well that the tiles have not arrived.
|
|
42
|
+
*/
|
|
43
|
+
ready(view: View): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Put the view on screen itself.
|
|
46
|
+
*
|
|
47
|
+
* Return `true` when the provider has handled it — a map moves its own camera — and
|
|
48
|
+
* the host will not apply a CSS transform. Return `false`, or leave it undefined, and
|
|
49
|
+
* the host applies the one composited transform that is the default rendering model.
|
|
50
|
+
*/
|
|
51
|
+
apply?(view: View, viewport: Viewport): boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { PROVIDER_NAMES as P, type SceneProvider as S, type ProviderName as a };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { A as AtlasPath } from '../path-BIxJH8g2.js';
|
|
3
|
+
import { c as Viewport, V as View } from '../view-BzztOpV6.js';
|
|
4
|
+
import { P as PlacedNode } from '../lod-DBuZ0MG3.js';
|
|
5
|
+
import { S as SceneProvider } from '../provider-CjL3i-0A.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The deck host: one plane, one camera, one composited transform.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface AtlasContextValue {
|
|
14
|
+
/** Where the camera is this frame. */
|
|
15
|
+
view: View;
|
|
16
|
+
/** Is a flight in progress? */
|
|
17
|
+
flying: boolean;
|
|
18
|
+
/** Index of the waypoint the camera is at or heading for. */
|
|
19
|
+
index: number;
|
|
20
|
+
/** Its id, when there is one. */
|
|
21
|
+
waypointId: string | null;
|
|
22
|
+
total: number;
|
|
23
|
+
goTo: (indexOrId: number | string) => void;
|
|
24
|
+
next: () => void;
|
|
25
|
+
previous: () => void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Read the camera from inside a scene.
|
|
29
|
+
*
|
|
30
|
+
* A checklist beside the canvas ticking as the camera arrives is this hook plus the
|
|
31
|
+
* waypoint ids, and nothing else.
|
|
32
|
+
*/
|
|
33
|
+
declare function useAtlas(): AtlasContextValue;
|
|
34
|
+
interface AtlasProps {
|
|
35
|
+
/** The camera path. */
|
|
36
|
+
path: AtlasPath;
|
|
37
|
+
/** The scene. Defaults to a plane built from `nodes`. */
|
|
38
|
+
provider?: SceneProvider;
|
|
39
|
+
/** Nodes on the default plane. Ignored when `provider` is given. */
|
|
40
|
+
nodes?: readonly PlacedNode[];
|
|
41
|
+
/** What to render on the plane. Node ids come from `data-atlas-id`. */
|
|
42
|
+
children?: React.ReactNode;
|
|
43
|
+
/**
|
|
44
|
+
* The coordinate grid the world is measured in.
|
|
45
|
+
*
|
|
46
|
+
* This is the *world*, not the surface it is painted on: it fixes what `at: 0, 0, 4000`
|
|
47
|
+
* means, and it is what a reference resolves against. The actual pixel size of the
|
|
48
|
+
* canvas is measured, so a deck rendered at any size frames the same content.
|
|
49
|
+
*
|
|
50
|
+
* @defaultValue 1920x1080
|
|
51
|
+
*/
|
|
52
|
+
viewport?: Viewport;
|
|
53
|
+
/** Which waypoint to start at. @defaultValue 0 */
|
|
54
|
+
initialWaypoint?: number | string;
|
|
55
|
+
/**
|
|
56
|
+
* Take the arrow keys.
|
|
57
|
+
*
|
|
58
|
+
* Uses the same contract as `Slide`'s sub-slides: capture the key, and swallow it only
|
|
59
|
+
* while there is another waypoint to move to, so the deck advances normally at either
|
|
60
|
+
* end. Without that, the deck and the canvas both act on one press.
|
|
61
|
+
*
|
|
62
|
+
* @defaultValue true
|
|
63
|
+
*/
|
|
64
|
+
keyboard?: boolean;
|
|
65
|
+
/** Only listen while this is true — a deck passes its slide's `isActive`. @defaultValue true */
|
|
66
|
+
active?: boolean;
|
|
67
|
+
/** Called on arrival at each waypoint. */
|
|
68
|
+
onArrive?: (id: string, index: number) => void;
|
|
69
|
+
className?: string;
|
|
70
|
+
style?: React.CSSProperties;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A zooming canvas.
|
|
74
|
+
*
|
|
75
|
+
* ```tsx
|
|
76
|
+
* <Atlas path={{ waypoints: [
|
|
77
|
+
* { id: "whole", at: { cx: 0, cy: 0, w: 4000 } },
|
|
78
|
+
* { id: "ingest", at: "#ingest" },
|
|
79
|
+
* ] }} nodes={nodes}>
|
|
80
|
+
* <div data-atlas-id="ingest">…</div>
|
|
81
|
+
* </Atlas>
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @behaviour Fills its container and clips to it, so it wants a slide of its own. Its
|
|
85
|
+
* children are placed in world units on a plane the camera moves over rather than laid out
|
|
86
|
+
* by the slide — `AtlasNode` is what places them — and a node the camera cannot see, or
|
|
87
|
+
* that its level-of-detail band excludes, is unmounted rather than merely hidden.
|
|
88
|
+
*/
|
|
89
|
+
declare function Atlas({ path, provider, nodes, children, viewport, initialWaypoint, keyboard, active, onArrive, className, style, }: AtlasProps): React.ReactElement;
|
|
90
|
+
/**
|
|
91
|
+
* Is this node currently mounted?
|
|
92
|
+
*
|
|
93
|
+
* A node the scene never described — a backdrop, a caption, a progress rail — is always
|
|
94
|
+
* mounted. Culling is opt-in per node, so declaring `nodes` can never make content the
|
|
95
|
+
* author did not describe disappear.
|
|
96
|
+
*/
|
|
97
|
+
declare function useIsMounted(id: string | undefined): boolean;
|
|
98
|
+
interface AtlasNodeProps {
|
|
99
|
+
/** Must match a `PlacedNode.id`, or the node is never culled. */
|
|
100
|
+
id: string;
|
|
101
|
+
/** Where it sits, in world units. */
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
children?: React.ReactNode;
|
|
107
|
+
className?: string;
|
|
108
|
+
style?: React.CSSProperties;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A node on the plane.
|
|
112
|
+
*
|
|
113
|
+
* `contain: layout paint` is not decoration: it stops a node's own layout from escaping
|
|
114
|
+
* into the camera root, which is what keeps a frame at one style recalculation and zero
|
|
115
|
+
* layout however many nodes are mounted.
|
|
116
|
+
*
|
|
117
|
+
* @behaviour Positions itself absolutely at `x`/`y` in world units, so it only works
|
|
118
|
+
* inside an `Atlas` and ignores whatever layout surrounds it. It renders nothing at all
|
|
119
|
+
* while the camera cannot see it, or while its `lod` band excludes the current framing.
|
|
120
|
+
*/
|
|
121
|
+
declare function AtlasNode({ id, x, y, width, height, children, className, style, }: AtlasNodeProps): React.ReactElement | null;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The camera as a hook: a target waypoint goes in, a view comes out every frame.
|
|
125
|
+
*
|
|
126
|
+
* Driven by `requestAnimationFrame` rather than by a CSS transition or a spring library,
|
|
127
|
+
* for three reasons. The flight is not a cubic bézier, so a CSS transition cannot express
|
|
128
|
+
* it. The view is needed in JavaScript anyway, because culling and level-of-detail are
|
|
129
|
+
* decided from it. And the same `flight` has to serve the video host, where there is no
|
|
130
|
+
* rAF at all — so the interpolation lives in a pure function and only the clock differs.
|
|
131
|
+
*
|
|
132
|
+
* @packageDocumentation
|
|
133
|
+
*/
|
|
134
|
+
|
|
135
|
+
/** Does this viewer want less motion? Re-read live, because they can change it mid-deck. */
|
|
136
|
+
declare function usePrefersReducedMotion(): boolean;
|
|
137
|
+
interface CameraState {
|
|
138
|
+
/** Where the camera is right now. */
|
|
139
|
+
view: View;
|
|
140
|
+
/** Is a flight in progress? The `will-change` switch keys off this. */
|
|
141
|
+
flying: boolean;
|
|
142
|
+
}
|
|
143
|
+
interface UseCameraOptions {
|
|
144
|
+
/** Where the camera should end up. */
|
|
145
|
+
target: View;
|
|
146
|
+
/** How long to take, in seconds. Omit and the flight's own suggestion is used. */
|
|
147
|
+
duration?: number;
|
|
148
|
+
/** van Wijk's ρ. */
|
|
149
|
+
arc?: number;
|
|
150
|
+
/** Cut instead of flying — what a reduced-motion viewer gets. */
|
|
151
|
+
cut?: boolean;
|
|
152
|
+
/** Called once the camera settles. */
|
|
153
|
+
onArrive?: () => void;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Fly the camera to `target` whenever it changes.
|
|
157
|
+
*
|
|
158
|
+
* A target that changes mid-flight is honoured immediately: the new flight starts from
|
|
159
|
+
* wherever the camera actually is, not from the waypoint it was heading for. A presenter
|
|
160
|
+
* who presses `→` twice quickly gets one continuous move, not a jump.
|
|
161
|
+
*/
|
|
162
|
+
declare function useCamera({ target, duration, arc, cut, onArrive, }: UseCameraOptions): CameraState;
|
|
163
|
+
/**
|
|
164
|
+
* The `will-change` value for a camera root, and the reason it is not a constant.
|
|
165
|
+
*
|
|
166
|
+
* Chrome re-rasterises content when its transform scale changes — *unless* it carries
|
|
167
|
+
* `will-change: transform`, which is a promise to the compositor to apply the transform
|
|
168
|
+
* fast and skip the re-raster. That makes the property a straight trade:
|
|
169
|
+
*
|
|
170
|
+
* - always on → cheap frames, and permanently blurry after a zoom, because the texture
|
|
171
|
+
* is still rasterised at whatever scale the layer was created at. This is the blur
|
|
172
|
+
* reported against impress.js and against WebKit 3D transforms for a decade.
|
|
173
|
+
* - never set → sharp, and a re-raster of the whole subtree on every frame of a zoom.
|
|
174
|
+
*
|
|
175
|
+
* So switch it: on while flying, off on arrival. The blur then exists only during the
|
|
176
|
+
* move, when nobody is reading, and every resting frame — every frame anyone stops on,
|
|
177
|
+
* screenshots, or exports — is sharp.
|
|
178
|
+
*/
|
|
179
|
+
declare function cameraWillChange(flying: boolean): "transform" | "auto";
|
|
180
|
+
/** Step to a waypoint index, clamped, without wrapping. */
|
|
181
|
+
declare function clampIndex(index: number, length: number): number;
|
|
182
|
+
|
|
183
|
+
export { Atlas, type AtlasContextValue, AtlasNode, type AtlasNodeProps, type AtlasProps, type CameraState, type UseCameraOptions, cameraWillChange, clampIndex, useAtlas, useCamera, useIsMounted, usePrefersReducedMotion };
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { planeProvider } from '../chunk-BDF2X5LC.js';
|
|
2
|
+
import { visibleNodes } from '../chunk-5SRKWTOU.js';
|
|
3
|
+
import { flight, resolvePath, arcFor } from '../chunk-RHTCDWZD.js';
|
|
4
|
+
import { transformToCss, viewToTransform } from '../chunk-RWYOYDBD.js';
|
|
5
|
+
import { createContext, useState, useEffect, useRef, useMemo, useContext } from 'react';
|
|
6
|
+
import { jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
function usePrefersReducedMotion() {
|
|
9
|
+
const [reduced, setReduced] = useState(() => {
|
|
10
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
11
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
12
|
+
});
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
15
|
+
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
16
|
+
const onChange = (event) => setReduced(event.matches);
|
|
17
|
+
query.addEventListener("change", onChange);
|
|
18
|
+
return () => query.removeEventListener("change", onChange);
|
|
19
|
+
}, []);
|
|
20
|
+
return reduced;
|
|
21
|
+
}
|
|
22
|
+
function useCamera({
|
|
23
|
+
target,
|
|
24
|
+
duration,
|
|
25
|
+
arc,
|
|
26
|
+
cut = false,
|
|
27
|
+
onArrive
|
|
28
|
+
}) {
|
|
29
|
+
const [view, setView] = useState(target);
|
|
30
|
+
const [flying, setFlying] = useState(false);
|
|
31
|
+
const viewRef = useRef(view);
|
|
32
|
+
viewRef.current = view;
|
|
33
|
+
const onArriveRef = useRef(onArrive);
|
|
34
|
+
onArriveRef.current = onArrive;
|
|
35
|
+
const { cx, cy, w } = target;
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const to = { cx, cy, w };
|
|
38
|
+
const from = viewRef.current;
|
|
39
|
+
if (from.cx === to.cx && from.cy === to.cy && from.w === to.w) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (cut) {
|
|
43
|
+
setView(to);
|
|
44
|
+
setFlying(false);
|
|
45
|
+
onArriveRef.current?.();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const path = flight(from, to, arc);
|
|
49
|
+
const seconds = duration ?? path.suggestedDuration;
|
|
50
|
+
if (seconds <= 0) {
|
|
51
|
+
setView(to);
|
|
52
|
+
onArriveRef.current?.();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let raf = 0;
|
|
56
|
+
let start = null;
|
|
57
|
+
setFlying(true);
|
|
58
|
+
const step = (now) => {
|
|
59
|
+
if (start === null) start = now;
|
|
60
|
+
const t = Math.min(1, (now - start) / (seconds * 1e3));
|
|
61
|
+
setView(path.at(t));
|
|
62
|
+
if (t < 1) {
|
|
63
|
+
raf = requestAnimationFrame(step);
|
|
64
|
+
} else {
|
|
65
|
+
setFlying(false);
|
|
66
|
+
onArriveRef.current?.();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
raf = requestAnimationFrame(step);
|
|
70
|
+
return () => {
|
|
71
|
+
cancelAnimationFrame(raf);
|
|
72
|
+
setFlying(false);
|
|
73
|
+
};
|
|
74
|
+
}, [cx, cy, w, cut, duration, arc]);
|
|
75
|
+
return useMemo(() => ({ view, flying }), [view, flying]);
|
|
76
|
+
}
|
|
77
|
+
function cameraWillChange(flying) {
|
|
78
|
+
return flying ? "transform" : "auto";
|
|
79
|
+
}
|
|
80
|
+
function clampIndex(index, length) {
|
|
81
|
+
if (length <= 0) return 0;
|
|
82
|
+
return Math.min(Math.max(index, 0), length - 1);
|
|
83
|
+
}
|
|
84
|
+
var DEFAULT_VIEWPORT = { width: 1920, height: 1080 };
|
|
85
|
+
var AtlasContext = createContext(null);
|
|
86
|
+
function useAtlas() {
|
|
87
|
+
const value = useContext(AtlasContext);
|
|
88
|
+
if (!value) throw new Error("useAtlas must be used inside an <Atlas>");
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function Atlas({
|
|
92
|
+
path,
|
|
93
|
+
provider,
|
|
94
|
+
nodes = [],
|
|
95
|
+
children,
|
|
96
|
+
viewport = DEFAULT_VIEWPORT,
|
|
97
|
+
initialWaypoint = 0,
|
|
98
|
+
keyboard = true,
|
|
99
|
+
active = true,
|
|
100
|
+
onArrive,
|
|
101
|
+
className,
|
|
102
|
+
style
|
|
103
|
+
}) {
|
|
104
|
+
const scene = useMemo(() => provider ?? planeProvider({ nodes }), [provider, nodes]);
|
|
105
|
+
const { resolved } = useMemo(
|
|
106
|
+
() => resolvePath(path, (ref) => scene.resolve(ref, viewport)),
|
|
107
|
+
[path, scene, viewport]
|
|
108
|
+
);
|
|
109
|
+
const startIndex = useMemo(() => {
|
|
110
|
+
if (typeof initialWaypoint === "string") {
|
|
111
|
+
const found = resolved.findIndex((w) => w.id === initialWaypoint);
|
|
112
|
+
return found === -1 ? 0 : found;
|
|
113
|
+
}
|
|
114
|
+
return clampIndex(initialWaypoint, resolved.length);
|
|
115
|
+
}, [initialWaypoint, resolved]);
|
|
116
|
+
const rootRef = useRef(null);
|
|
117
|
+
const [painted, setPainted] = useState(null);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
const element = rootRef.current;
|
|
120
|
+
if (!element) return;
|
|
121
|
+
const measure = () => {
|
|
122
|
+
const width = element.offsetWidth;
|
|
123
|
+
const height = element.offsetHeight;
|
|
124
|
+
if (width <= 0 || height <= 0) return;
|
|
125
|
+
setPainted(
|
|
126
|
+
(previous) => previous && previous.width === width && previous.height === height ? previous : { width, height }
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
measure();
|
|
130
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
131
|
+
const observer = new ResizeObserver(measure);
|
|
132
|
+
observer.observe(element);
|
|
133
|
+
return () => observer.disconnect();
|
|
134
|
+
}, []);
|
|
135
|
+
const surface = painted ?? viewport;
|
|
136
|
+
const [index, setIndex] = useState(startIndex);
|
|
137
|
+
const safeIndex = clampIndex(index, resolved.length);
|
|
138
|
+
const current = resolved[safeIndex];
|
|
139
|
+
const reduced = usePrefersReducedMotion();
|
|
140
|
+
const target = current?.at ?? { cx: 0, cy: 0, w: viewport.width };
|
|
141
|
+
const { view, flying } = useCamera({
|
|
142
|
+
target,
|
|
143
|
+
duration: current?.duration,
|
|
144
|
+
arc: current ? arcFor(path, current) : void 0,
|
|
145
|
+
cut: reduced,
|
|
146
|
+
onArrive: () => {
|
|
147
|
+
if (current) onArrive?.(current.id, safeIndex);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!keyboard || !active || resolved.length === 0) return;
|
|
152
|
+
const onKeyDown = (event) => {
|
|
153
|
+
if (event.ctrlKey || event.metaKey) return;
|
|
154
|
+
const target2 = event.target;
|
|
155
|
+
if (target2 && (target2.tagName === "INPUT" || target2.tagName === "TEXTAREA" || target2.isContentEditable)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const forward = event.key === "ArrowRight" || event.key === " " || event.key === "Enter";
|
|
159
|
+
const back = event.key === "ArrowLeft";
|
|
160
|
+
if (forward && safeIndex < resolved.length - 1) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
event.stopPropagation();
|
|
163
|
+
setIndex(safeIndex + 1);
|
|
164
|
+
} else if (back && safeIndex > 0) {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
setIndex(safeIndex - 1);
|
|
168
|
+
} else if (event.key === "Home") {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
event.stopPropagation();
|
|
171
|
+
setIndex(0);
|
|
172
|
+
} else if (event.key === "End") {
|
|
173
|
+
event.preventDefault();
|
|
174
|
+
event.stopPropagation();
|
|
175
|
+
setIndex(resolved.length - 1);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
window.addEventListener("keydown", onKeyDown, { capture: true });
|
|
179
|
+
return () => window.removeEventListener("keydown", onKeyDown, { capture: true });
|
|
180
|
+
}, [keyboard, active, safeIndex, resolved.length]);
|
|
181
|
+
const keep = useMemo(() => {
|
|
182
|
+
const ids = /* @__PURE__ */ new Set();
|
|
183
|
+
for (const offset of [-1, 0, 1]) {
|
|
184
|
+
const w = resolved[safeIndex + offset];
|
|
185
|
+
if (w?.ref) ids.add(w.ref.startsWith("#") ? w.ref.slice(1) : w.ref);
|
|
186
|
+
}
|
|
187
|
+
return ids;
|
|
188
|
+
}, [resolved, safeIndex]);
|
|
189
|
+
const mounted = useMemo(
|
|
190
|
+
() => new Set(visibleNodes(nodes, view, viewport, { keep }).map((n) => n.id)),
|
|
191
|
+
[nodes, view, viewport, keep]
|
|
192
|
+
);
|
|
193
|
+
const handled = scene.apply?.(view, surface) ?? false;
|
|
194
|
+
const transform = handled ? void 0 : transformToCss(viewToTransform(view, surface));
|
|
195
|
+
const context = useMemo(
|
|
196
|
+
() => ({
|
|
197
|
+
view,
|
|
198
|
+
flying,
|
|
199
|
+
index: safeIndex,
|
|
200
|
+
waypointId: current?.id ?? null,
|
|
201
|
+
total: resolved.length,
|
|
202
|
+
goTo: (indexOrId) => {
|
|
203
|
+
const next = typeof indexOrId === "string" ? resolved.findIndex((w) => w.id === indexOrId) : indexOrId;
|
|
204
|
+
if (next >= 0) setIndex(clampIndex(next, resolved.length));
|
|
205
|
+
},
|
|
206
|
+
next: () => setIndex((i) => clampIndex(i + 1, resolved.length)),
|
|
207
|
+
previous: () => setIndex((i) => clampIndex(i - 1, resolved.length))
|
|
208
|
+
}),
|
|
209
|
+
[view, flying, safeIndex, current, resolved]
|
|
210
|
+
);
|
|
211
|
+
return /* @__PURE__ */ jsx(AtlasContext.Provider, { value: context, children: /* @__PURE__ */ jsx(
|
|
212
|
+
"div",
|
|
213
|
+
{
|
|
214
|
+
ref: rootRef,
|
|
215
|
+
className,
|
|
216
|
+
style: {
|
|
217
|
+
position: "relative",
|
|
218
|
+
width: "100%",
|
|
219
|
+
height: "100%",
|
|
220
|
+
overflow: "hidden",
|
|
221
|
+
...style
|
|
222
|
+
},
|
|
223
|
+
children: /* @__PURE__ */ jsx(
|
|
224
|
+
"div",
|
|
225
|
+
{
|
|
226
|
+
"data-atlas-camera": "",
|
|
227
|
+
style: {
|
|
228
|
+
position: "absolute",
|
|
229
|
+
inset: 0,
|
|
230
|
+
transform,
|
|
231
|
+
transformOrigin: "0 0",
|
|
232
|
+
// On while flying, off on arrival — see `cameraWillChange`. This one line is
|
|
233
|
+
// the difference between text that is sharp at rest and text that never is.
|
|
234
|
+
willChange: cameraWillChange(flying)
|
|
235
|
+
},
|
|
236
|
+
children: /* @__PURE__ */ jsx(MountedNodes, { mounted, nodes, children })
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
) });
|
|
241
|
+
}
|
|
242
|
+
var MountedContext = createContext(null);
|
|
243
|
+
function MountedNodes({
|
|
244
|
+
mounted,
|
|
245
|
+
nodes,
|
|
246
|
+
children
|
|
247
|
+
}) {
|
|
248
|
+
const declared = useMemo(() => new Set(nodes.map((n) => n.id)), [nodes]);
|
|
249
|
+
const value = useMemo(() => ({ mounted, declared }), [mounted, declared]);
|
|
250
|
+
return /* @__PURE__ */ jsx(MountedContext.Provider, { value, children });
|
|
251
|
+
}
|
|
252
|
+
function useIsMounted(id) {
|
|
253
|
+
const culling = useContext(MountedContext);
|
|
254
|
+
if (!culling || !id) return true;
|
|
255
|
+
if (!culling.declared.has(id)) return true;
|
|
256
|
+
return culling.mounted.has(id);
|
|
257
|
+
}
|
|
258
|
+
function AtlasNode({
|
|
259
|
+
id,
|
|
260
|
+
x,
|
|
261
|
+
y,
|
|
262
|
+
width,
|
|
263
|
+
height,
|
|
264
|
+
children,
|
|
265
|
+
className,
|
|
266
|
+
style
|
|
267
|
+
}) {
|
|
268
|
+
const mounted = useIsMounted(id);
|
|
269
|
+
if (!mounted) return null;
|
|
270
|
+
return /* @__PURE__ */ jsx(
|
|
271
|
+
"div",
|
|
272
|
+
{
|
|
273
|
+
"data-atlas-id": id,
|
|
274
|
+
className,
|
|
275
|
+
style: {
|
|
276
|
+
position: "absolute",
|
|
277
|
+
left: x,
|
|
278
|
+
top: y,
|
|
279
|
+
width,
|
|
280
|
+
height,
|
|
281
|
+
contain: "layout paint",
|
|
282
|
+
...style
|
|
283
|
+
},
|
|
284
|
+
children
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export { Atlas, AtlasNode, cameraWillChange, clampIndex, useAtlas, useCamera, useIsMounted, usePrefersReducedMotion };
|
|
290
|
+
//# sourceMappingURL=index.js.map
|
|
291
|
+
//# sourceMappingURL=index.js.map
|