@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/image/pyramid.ts","../../src/image/provider.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * A gigapixel image as a scene, without loading a gigapixel image.\n *\n * The problem this solves is the one the checker's `bitmap-over-zoomed` rule can only\n * complain about: a camera that flies close to a raster needs pixels the raster does not\n * have, and the fix — ship a bigger file — stops being possible somewhere around the\n * point where the file is bigger than the deck. A pyramid inverts it. The image is cut\n * into tiles at every power-of-two scale in advance, and the camera fetches only the tiles\n * for the level and the region it is actually looking at. A 40000×30000 scan costs the\n * same handful of tiles at every zoom, all the way in.\n *\n * The layout is Deep Zoom's (DZI), because it is the one every tiler already emits —\n * `vips dzsave`, `libvips`, OpenSeadragon's own tools — and because reusing a convention\n * beats inventing one nobody can produce files for.\n *\n * Everything here is arithmetic: which level, which tiles, and where each one sits in\n * world units. No fetching, no DOM, no React — so the part that is easy to get subtly\n * wrong is the part a test can pin down exactly.\n *\n * @packageDocumentation\n */\n\nimport type { Rect, View, Viewport } from \"../camera/view\";\nimport { viewToRect } from \"../camera/view\";\n\nexport interface Pyramid {\n /** Full-resolution size, in image pixels. The world is measured in these. */\n width: number;\n height: number;\n /** Tile edge in pixels. 254 and 256 are both common; DZI's default is 254 with overlap 1. */\n tileSize: number;\n /**\n * Pixels each tile repeats on every side, so neighbours have no seam.\n *\n * DZI's own default is 1. It shifts where a tile sits and how big it is, which is the\n * detail that makes a naive implementation draw a visible grid.\n *\n * @defaultValue 0\n */\n overlap?: number;\n /** `https://…/image_files/{level}/{col}_{row}.jpg` — DZI's own layout. */\n url: (level: number, col: number, row: number) => string;\n}\n\n/**\n * The deepest level, where the image is at full resolution.\n *\n * DZI counts up from a level 0 that is the whole image in a single pixel, doubling each\n * step, so the deepest level is `ceil(log2(max(width, height)))`.\n */\nexport function maxLevel(pyramid: Pick<Pyramid, \"width\" | \"height\">): number {\n const longest = Math.max(pyramid.width, pyramid.height, 1);\n return Math.ceil(Math.log2(longest));\n}\n\n/** How much the image is scaled down at a level. 1 at the deepest. */\nexport function levelScale(pyramid: Pick<Pyramid, \"width\" | \"height\">, level: number): number {\n return 2 ** (level - maxLevel(pyramid));\n}\n\n/** The image's size in pixels at a level. */\nexport function levelSize(\n pyramid: Pick<Pyramid, \"width\" | \"height\">,\n level: number,\n): { width: number; height: number } {\n const scale = levelScale(pyramid, level);\n return {\n width: Math.max(1, Math.ceil(pyramid.width * scale)),\n height: Math.max(1, Math.ceil(pyramid.height * scale)),\n };\n}\n\n/**\n * The level whose pixels are closest to screen pixels for this framing.\n *\n * One below would be visibly soft; one above costs four times the tiles for detail the\n * screen cannot show. Rounded up rather than to nearest, deliberately: between two levels\n * the sharper one is the right call, because softness is the thing a viewer notices.\n */\nexport function levelFor(pyramid: Pyramid, view: View, viewport: Viewport): number {\n const deepest = maxLevel(pyramid);\n if (view.w <= 0) return deepest;\n\n // Screen pixels per image pixel.\n const screenScale = viewport.width / view.w;\n const level = deepest + Math.log2(Math.max(screenScale, Number.EPSILON));\n\n return Math.min(deepest, Math.max(0, Math.ceil(level)));\n}\n\n/** One tile: where to fetch it, and where it goes. */\nexport interface Tile {\n /** Stable identity, for keying a list and tracking loads. */\n key: string;\n level: number;\n col: number;\n row: number;\n url: string;\n /** Where it sits, in full-resolution image pixels — the world's own units. */\n rect: Rect;\n}\n\n/** How many tiles across and down a level is. */\nexport function tileCounts(pyramid: Pyramid, level: number): { cols: number; rows: number } {\n const size = levelSize(pyramid, level);\n return {\n cols: Math.ceil(size.width / pyramid.tileSize),\n rows: Math.ceil(size.height / pyramid.tileSize),\n };\n}\n\n/**\n * The tiles a framing needs, in world units.\n *\n * `margin` fetches a ring beyond the frame, as a fraction of it, so a camera arriving\n * somewhere does not arrive on empty squares.\n */\nexport function tilesFor(pyramid: Pyramid, view: View, viewport: Viewport, margin = 0.25): Tile[] {\n const level = levelFor(pyramid, view, viewport);\n const scale = levelScale(pyramid, level);\n const overlap = pyramid.overlap ?? 0;\n const { cols, rows } = tileCounts(pyramid, level);\n const size = levelSize(pyramid, level);\n\n // The visible region, in this level's pixels.\n const frame = viewToRect(view, viewport);\n const grow = { x: frame.width * margin, y: frame.height * margin };\n const left = (frame.x - grow.x) * scale;\n const top = (frame.y - grow.y) * scale;\n const right = (frame.x + frame.width + grow.x) * scale;\n const bottom = (frame.y + frame.height + grow.y) * scale;\n\n const firstCol = Math.max(0, Math.floor(left / pyramid.tileSize));\n const lastCol = Math.min(cols - 1, Math.floor(right / pyramid.tileSize));\n const firstRow = Math.max(0, Math.floor(top / pyramid.tileSize));\n const lastRow = Math.min(rows - 1, Math.floor(bottom / pyramid.tileSize));\n\n const tiles: Tile[] = [];\n for (let row = firstRow; row <= lastRow; row++) {\n for (let col = firstCol; col <= lastCol; col++) {\n // Overlap only exists on the sides that have a neighbour, which is why an edge tile\n // is a different size from a middle one and why this cannot be a single formula.\n const x = col * pyramid.tileSize - (col > 0 ? overlap : 0);\n const y = row * pyramid.tileSize - (row > 0 ? overlap : 0);\n const width = Math.min(\n pyramid.tileSize + (col > 0 ? overlap : 0) + (col < cols - 1 ? overlap : 0),\n size.width - x,\n );\n const height = Math.min(\n pyramid.tileSize + (row > 0 ? overlap : 0) + (row < rows - 1 ? overlap : 0),\n size.height - y,\n );\n\n if (width <= 0 || height <= 0) continue;\n\n tiles.push({\n key: `${level}/${col}_${row}`,\n level,\n col,\n row,\n url: pyramid.url(level, col, row),\n // Back into full-resolution pixels, which is what the camera works in.\n rect: { x: x / scale, y: y / scale, width: width / scale, height: height / scale },\n });\n }\n }\n\n return tiles;\n}\n\n/** The whole image, as a rectangle in world units. */\nexport function pyramidBounds(pyramid: Pick<Pyramid, \"width\" | \"height\">): Rect {\n return { x: 0, y: 0, width: pyramid.width, height: pyramid.height };\n}\n","/**\n * A gigapixel image as a scene.\n *\n * The provider that has the most to say about `ready`. A plane is ready immediately, a\n * replay is ready when the host says so, a map asks the library — this one has to know\n * which specific tiles a framing needs and whether each has arrived, because the renderer\n * will otherwise encode a frame of half-drawn squares and nobody will notice until the\n * video is finished.\n *\n * @packageDocumentation\n */\n\nimport type { Rect, View, Viewport } from \"../camera/view\";\nimport { rectToView } from \"../camera/view\";\nimport type { SceneProvider } from \"../scene/provider\";\nimport { type Pyramid, pyramidBounds, type Tile, tilesFor } from \"./pyramid\";\n\n/** A named region of the image a waypoint can fly to by name. */\nexport type NamedRegion = Rect;\n\nexport interface ImageProviderOptions {\n pyramid: Pyramid;\n /**\n * Regions a waypoint may name — `at: \"the-signature\"`.\n *\n * The same reasoning as the map's named places: a name survives someone re-cropping the\n * region, and the four numbers it stood for do not.\n */\n regions?: Record<string, NamedRegion>;\n /** Padding when framing a named region, as a fraction. @defaultValue 0.1 */\n padding?: number;\n /** How much beyond the frame to fetch, as a fraction of it. @defaultValue 0.25 */\n margin?: number;\n}\n\nexport interface ImageScene extends SceneProvider {\n /** The tiles a framing needs. What the renderer draws. */\n tiles(view: View, viewport: Viewport): Tile[];\n /** Tell the scene a tile has arrived. The readiness gate is built from these. */\n noteLoaded(key: string): void;\n /** Tell the scene a tile will never arrive, so a 404 cannot stall a render forever. */\n noteFailed(key: string): void;\n /** How many tiles are known to have arrived. For diagnostics and tests. */\n loadedCount(): number;\n}\n\nexport function imageProvider(options: ImageProviderOptions): ImageScene {\n const { pyramid, regions = {}, padding = 0.1, margin = 0.25 } = options;\n\n // A tile that arrived and a tile that never will are both \"not still waiting\", and the\n // gate has to treat them the same or one missing file stalls the whole render.\n const settled = new Set<string>();\n const loaded = new Set<string>();\n\n /**\n * The viewport the renderer last drew with.\n *\n * `ready` is handed a view but not a viewport — the interface is the same for every\n * provider, and a plane has no use for one — but which tiles a framing needs depends on\n * both. This is always set before `ready` matters, because a frame asks for its tiles\n * before it asks whether they arrived. Per provider, not per module: two images on one\n * page would otherwise answer each other's question.\n */\n let lastViewport: Viewport | null = null;\n\n return {\n name: \"image\",\n\n bounds(): Rect {\n return pyramidBounds(pyramid);\n },\n\n resolve(ref: string, viewport: Viewport): View | null {\n const region = regions[ref] ?? regions[ref.replace(/^#/, \"\")];\n return region ? rectToView(region, viewport, padding) : null;\n },\n\n ready(view: View): boolean {\n // Deliberately not \"the whole pyramid is loaded\": that is never true, and waiting\n // for it would stall every render forever. Only the tiles this framing draws.\n const viewport = lastViewport ?? { width: 1920, height: 1080 };\n return tilesFor(pyramid, view, viewport, margin).every((tile) => settled.has(tile.key));\n },\n\n tiles(view: View, viewport: Viewport): Tile[] {\n lastViewport = viewport;\n return tilesFor(pyramid, view, viewport, margin);\n },\n\n noteLoaded(key: string): void {\n loaded.add(key);\n settled.add(key);\n },\n\n noteFailed(key: string): void {\n settled.add(key);\n },\n\n loadedCount(): number {\n return loaded.size;\n },\n };\n}\n"]}
@@ -0,0 +1,33 @@
1
+ export { D as DEFAULT_RHO, F as Flight, M as MAX_FLIGHT_SECONDS, a as MIN_FLIGHT_SECONDS, b as MotionStyle, S as Segment, T as Timeline, c as TimelineOptions, f as flight, p as perceivedSpeedAt, t as timeline, z as zoomRatio } from './timeline-B_dNydQA.js';
2
+ export { A as AtlasPath, b as ResolveResult, R as ResolvedWaypoint, U as UnresolvedWaypoint, W as Waypoint, c as WaypointTarget, a as arcFor, d as duplicateIds, h as holdFor, i as isView, r as resolvePath, w as waypointIndex } from './path-BIxJH8g2.js';
3
+ import { R as Rect } from './view-BzztOpV6.js';
4
+ export { C as CameraTransform, M as MIN_VIEW_WIDTH, V as View, c as Viewport, g as growRect, i as isFiniteView, n as normalizeView, a as rectToView, b as rectWithin, r as rectsIntersect, t as transformToCss, u as unionRects, v as viewToCss, d as viewToRect, e as viewToTransform } from './view-BzztOpV6.js';
5
+ import { P as PlacedNode } from './lod-DBuZ0MG3.js';
6
+ export { D as DEFAULT_CULL_MARGIN, L as LodRange, V as VisibilityOptions, i as isLegible, a as isNear, l as legibleBand, v as visibleNodes } from './lod-DBuZ0MG3.js';
7
+ import { S as SceneProvider } from './provider-CjL3i-0A.js';
8
+ export { P as PROVIDER_NAMES, a as ProviderName } from './provider-CjL3i-0A.js';
9
+
10
+ /**
11
+ * The plane: content placed on a coordinate grid, in world units.
12
+ *
13
+ * The default scene, and the one a deck uses. It is ready the moment it exists — there
14
+ * is nothing to fetch — which is why it is also the provider every test uses as a stand-in.
15
+ *
16
+ * @packageDocumentation
17
+ */
18
+
19
+ interface PlaneOptions {
20
+ nodes?: readonly PlacedNode[];
21
+ /**
22
+ * The world rectangle the scene occupies.
23
+ *
24
+ * Defaults to whatever the nodes cover. Set it when the plane is a fixed backdrop — a
25
+ * diagram, an image — that waypoints should be checked against even where no node sits.
26
+ */
27
+ bounds?: Rect;
28
+ /** Padding around a node when a waypoint frames it, as a fraction of its size. */
29
+ padding?: number;
30
+ }
31
+ declare function planeProvider(options?: PlaneOptions): SceneProvider;
32
+
33
+ export { PlacedNode, type PlaneOptions, Rect, SceneProvider, planeProvider };
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { planeProvider } from './chunk-BDF2X5LC.js';
2
+ export { DEFAULT_CULL_MARGIN, isLegible, isNear, legibleBand, visibleNodes } from './chunk-5SRKWTOU.js';
3
+ export { timeline } from './chunk-SD4Y4EBG.js';
4
+ export { DEFAULT_RHO, MAX_FLIGHT_SECONDS, MIN_FLIGHT_SECONDS, arcFor, duplicateIds, flight, holdFor, isView, perceivedSpeedAt, resolvePath, waypointIndex, zoomRatio } from './chunk-RHTCDWZD.js';
5
+ export { MIN_VIEW_WIDTH, growRect, isFiniteView, normalizeView, rectToView, rectWithin, rectsIntersect, transformToCss, unionRects, viewToCss, viewToRect, viewToTransform } from './chunk-RWYOYDBD.js';
6
+
7
+ // src/scene/provider.ts
8
+ var PROVIDER_NAMES = ["plane", "replay", "map", "image", "dom"];
9
+
10
+ export { PROVIDER_NAMES };
11
+ //# sourceMappingURL=index.js.map
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/scene/provider.ts"],"names":[],"mappings":";;;;;;;AAkBO,IAAM,iBAAiB,CAAC,OAAA,EAAS,QAAA,EAAU,KAAA,EAAO,SAAS,KAAK","file":"index.js","sourcesContent":["/**\n * One interface, so a deck, a video and a web page render four different kinds of scene\n * the same way.\n *\n * The member that earns the interface is {@link SceneProvider.ready}. A plane is ready\n * the instant it mounts, but a map has to fetch tiles and a replay has to reach a\n * chapter — both over the network, both asynchronously. The video renderer advances a\n * frame counter as fast as it can, so without a gate it will happily render frame 12 of\n * a map that has not drawn anything yet and encode a video of grey squares. That failure\n * is the single most likely way a spatial canvas ships broken, so the check for it is in\n * the type rather than in a provider's good intentions.\n *\n * @packageDocumentation\n */\n\nimport type { Rect, View, Viewport } from \"../camera/view\";\n\n/** The providers this package knows about. A closed set, so `catalog.json` can carry it. */\nexport const PROVIDER_NAMES = [\"plane\", \"replay\", \"map\", \"image\", \"dom\"] as const;\n\nexport type ProviderName = (typeof PROVIDER_NAMES)[number];\n\nexport interface SceneProvider {\n /** Which kind of scene this is. */\n readonly name: ProviderName;\n\n /**\n * The world rectangle the scene occupies, or null when it does not know yet.\n *\n * The checker uses it to catch a waypoint that flies to somewhere there is nothing.\n */\n bounds(): Rect | null;\n\n /**\n * A waypoint reference — a CSS selector, a node id — resolved to a framing.\n *\n * Null when it matches nothing, which is a diagnostic rather than a crash: the host\n * skips that waypoint and the checker reports it by name.\n */\n resolve(ref: string, viewport: Viewport): View | null;\n\n /**\n * Has everything this view needs finished loading?\n *\n * The video host holds the frame until this is true; the deck host ignores it, because\n * a presenter can see perfectly well that the tiles have not arrived.\n */\n ready(view: View): boolean;\n\n /**\n * Put the view on screen itself.\n *\n * Return `true` when the provider has handled it — a map moves its own camera — and\n * the host will not apply a CSS transform. Return `false`, or leave it undefined, and\n * the host applies the one composited transform that is the default rendering model.\n */\n apply?(view: View, viewport: Viewport): boolean;\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import { R as Rect, V as View, c as Viewport } from './view-BzztOpV6.js';
2
+
3
+ /**
4
+ * Culling and level of detail — what makes a big canvas both readable and cheap.
5
+ *
6
+ * These are the same mechanism seen from two sides, which is why they live in one file.
7
+ *
8
+ * **Culling** is the cost story. impress.js keeps every step mounted forever, and its own
9
+ * issue tracker carries the consequence: a deck with large images exhausts memory, and
10
+ * the proposed fix was to limit visibility to the previous, current and next step.
11
+ * `TransformSlide` has the same bug today. Mounting only what the camera can see is what
12
+ * makes the cost of a scene independent of how much is in it.
13
+ *
14
+ * **Level of detail** is the content story, and it is the more interesting half. A node
15
+ * declares the range of view widths in which it is worth reading, so the same place on
16
+ * the plane can be a label at a wide framing and a full table up close. That is what
17
+ * makes "a UI with several points that can be checked" work as one scene rather than as
18
+ * a slideshow of crops — and it happens to also be what keeps the frame budget, since
19
+ * the detailed version is never mounted while it would be illegible anyway.
20
+ *
21
+ * @packageDocumentation
22
+ */
23
+
24
+ /**
25
+ * The band of view widths in which a node is worth showing.
26
+ *
27
+ * Both bounds are **view widths in world units**, not zoom factors — the same unit a
28
+ * waypoint's `w` is in, so an author reads one number off a waypoint and writes it here.
29
+ * Remember the direction: a *wider* view is zoomed *out*.
30
+ *
31
+ * - `maxWidth: 1000` — appears once the camera is closer than 1000 units wide. Detail.
32
+ * - `minWidth: 2000` — disappears once the camera is closer than 2000. An overview label.
33
+ * - both — a band, for something legible only at a middle distance.
34
+ */
35
+ interface LodRange {
36
+ /** Hide while the view is wider than this. */
37
+ maxWidth?: number;
38
+ /** Hide while the view is narrower than this. */
39
+ minWidth?: number;
40
+ }
41
+ /** Something placed on the plane. */
42
+ interface PlacedNode {
43
+ id: string;
44
+ /** Where it sits, in world units. */
45
+ rect: Rect;
46
+ /** When it is worth reading. Always visible if omitted. */
47
+ lod?: LodRange;
48
+ }
49
+ /**
50
+ * How much scene to keep mounted beyond the edge of the frame, as a fraction of the
51
+ * frame's own size.
52
+ *
53
+ * Half a screen on each side: enough that a node is already mounted and laid out before
54
+ * it scrolls into view, so arriving at a waypoint never costs a layout, and not so much
55
+ * that a wide framing mounts the whole plane.
56
+ */
57
+ declare const DEFAULT_CULL_MARGIN = 0.5;
58
+ /** Is the node inside the band of view widths where it is worth reading? */
59
+ declare function isLegible(node: PlacedNode, view: View): boolean;
60
+ /** Does the node fall inside the frame, plus the margin kept mounted around it? */
61
+ declare function isNear(node: PlacedNode, view: View, viewport: Viewport, margin?: number): boolean;
62
+ interface VisibilityOptions {
63
+ margin?: number;
64
+ /**
65
+ * Nodes to keep mounted whatever the camera is doing.
66
+ *
67
+ * The host passes the waypoints either side of the current one: a node that is about
68
+ * to be flown to should already be in the document, or the arrival costs a mount, a
69
+ * layout and a paint in the one frame where somebody is looking straight at it.
70
+ */
71
+ keep?: ReadonlySet<string>;
72
+ }
73
+ /** Which nodes should be in the document for this framing. */
74
+ declare function visibleNodes(nodes: readonly PlacedNode[], view: View, viewport: Viewport, options?: VisibilityOptions): PlacedNode[];
75
+ /**
76
+ * The widest and narrowest view a node is ever legible at, or null if it always is.
77
+ *
78
+ * The checker uses it to find a node whose band no waypoint ever enters — content that
79
+ * is in the scene, costs bytes, and is never once readable.
80
+ */
81
+ declare function legibleBand(node: PlacedNode): {
82
+ min: number;
83
+ max: number;
84
+ } | null;
85
+
86
+ export { DEFAULT_CULL_MARGIN as D, type LodRange as L, type PlacedNode as P, type VisibilityOptions as V, isNear as a, isLegible as i, legibleBand as l, visibleNodes as v };
@@ -0,0 +1,222 @@
1
+ import { c as Viewport, V as View } from '../view-BzztOpV6.js';
2
+ import { S as SceneProvider } from '../provider-CjL3i-0A.js';
3
+
4
+ /**
5
+ * Geography, as a coordinate space the camera already understands.
6
+ *
7
+ * A map looks like it needs its own camera — `flyTo` takes a centre and a zoom, not a
8
+ * rectangle — and building one would have meant a second interpolator, a second set of
9
+ * checker rules, and two ways for a waypoint to mean something. It does not need one.
10
+ *
11
+ * Web Mercator *is* a plane. Projecting the world onto the unit square makes longitude
12
+ * and latitude into ordinary x and y, and a zoom level into a view width: at zoom `z` the
13
+ * whole world is `512·2^z` pixels across, so a viewport `vw` pixels wide shows
14
+ * `vw / (512·2^z)` of it. That is a `View`. The camera, the checker, the timeline and the
15
+ * video host all work on a map with no changes at all, and the provider's whole job
16
+ * reduces to converting on the way out.
17
+ *
18
+ * It is also the same space MapLibre's own `flyTo` interpolates in — it implements van
19
+ * Wijk & Nuij too, which is where this package got the algorithm from in the first place.
20
+ * So driving `jumpTo` from this camera is not fighting the library; it is doing what the
21
+ * library does, on a clock the renderer controls.
22
+ *
23
+ * @packageDocumentation
24
+ */
25
+
26
+ /**
27
+ * The latitude where Web Mercator stops.
28
+ *
29
+ * `atan(sinh(π))` in degrees. Beyond it the projection runs to infinity, so every map
30
+ * library clamps here and so does this.
31
+ */
32
+ declare const MAX_LATITUDE = 85.0511287798066;
33
+ /** MapLibre's tile size. The world is this many pixels across at zoom 0. */
34
+ declare const TILE_SIZE = 512;
35
+ /** A place on the earth, the way an author names one. */
36
+ interface LngLat {
37
+ lng: number;
38
+ lat: number;
39
+ }
40
+ /** A framing of the earth: where to look, and how close. */
41
+ interface MapView extends LngLat {
42
+ /** Map zoom level, as MapLibre and Mapbox count it. */
43
+ zoom: number;
44
+ }
45
+ /** A rectangle of the earth. */
46
+ interface LngLatBounds {
47
+ west: number;
48
+ south: number;
49
+ east: number;
50
+ north: number;
51
+ }
52
+ /** Longitude → the unit square's x. */
53
+ declare function lngToX(lng: number): number;
54
+ /** Latitude → the unit square's y. Clamped at the poles, where Mercator gives up. */
55
+ declare function latToY(lat: number): number;
56
+ /** The unit square's x → longitude. */
57
+ declare function xToLng(x: number): number;
58
+ /** The unit square's y → latitude. */
59
+ declare function yToLat(y: number): number;
60
+ /**
61
+ * How much of the world a view holds, at a zoom level.
62
+ *
63
+ * The whole conversion, in one line: the world is `TILE_SIZE·2^zoom` pixels across, so a
64
+ * viewport that many pixels wide would show all of it.
65
+ */
66
+ declare function zoomToWidth(zoom: number, viewport: Viewport): number;
67
+ /** The zoom level at which a view holds `w` of the world. */
68
+ declare function widthToZoom(w: number, viewport: Viewport): number;
69
+ /** A map framing → the camera's own `{ cx, cy, w }`. */
70
+ declare function mapViewToView(map: MapView, viewport: Viewport): View;
71
+ /** The camera's `{ cx, cy, w }` → a map framing the library can be told to hold. */
72
+ declare function viewToMapView(view: View, viewport: Viewport): MapView;
73
+ /**
74
+ * The framing that holds a rectangle of the earth.
75
+ *
76
+ * `padding` is a fraction of the box, so `0.1` leaves a tenth of it as margin — the
77
+ * difference between a country filling the frame edge to edge and sitting in it.
78
+ */
79
+ declare function boundsToView(bounds: LngLatBounds, viewport: Viewport, padding?: number): View;
80
+ /** The smallest box containing every position. Null for none. */
81
+ declare function boundsOf(positions: readonly LngLat[]): LngLatBounds | null;
82
+
83
+ /**
84
+ * A camera over a real map.
85
+ *
86
+ * The provider is thin on purpose. Because Mercator is already a plane (see
87
+ * `./mercator.ts`), there is no second camera and no second interpolator here — the
88
+ * `View` the shared camera produces is converted to a centre and a zoom, and the map is
89
+ * *told* where to be. Every frame. That is the whole of `apply`.
90
+ *
91
+ * Telling rather than asking is the important part, and it is what makes a map work under
92
+ * the video renderer at all. `flyTo` animates on the wall clock: it decides where the
93
+ * camera is from `Date.now()`, which is exactly the thing a renderer advancing a frame
94
+ * counter cannot use — Remotion's own map documentation says the two models are
95
+ * incompatible, and Mapbox's cinematic-route write-up gave up composing `flyTo` with
96
+ * other camera control and wrote a custom interpolator instead. `jumpTo` per frame, from
97
+ * a camera that is a pure function of time, has neither problem.
98
+ *
99
+ * `maplibre-gl` is an optional peer dependency and this module never imports it. The map
100
+ * arrives as an object with the three methods used below, so the package does not grow by
101
+ * a megabyte for decks that have no map, and so this file is testable with a fake.
102
+ *
103
+ * @packageDocumentation
104
+ */
105
+
106
+ /**
107
+ * What this needs from a map. Structural, so `maplibre-gl` stays optional and a test can
108
+ * hand it a fake.
109
+ *
110
+ * Satisfied by a MapLibre or Mapbox `Map` as-is.
111
+ */
112
+ interface MapLike {
113
+ jumpTo(options: {
114
+ center: [number, number];
115
+ zoom: number;
116
+ }): unknown;
117
+ /** False while any tile in view is still in flight. */
118
+ areTilesLoaded?(): boolean;
119
+ /** False until the style JSON has been parsed and its sources registered. */
120
+ isStyleLoaded?(): boolean;
121
+ }
122
+ /** A named place a waypoint can fly to by name. */
123
+ type NamedPlace = MapView | LngLatBounds;
124
+ interface MapProviderOptions {
125
+ /**
126
+ * The map to drive. Omit it for a build-time or server render, where there is no map
127
+ * and nothing to move.
128
+ */
129
+ map?: MapLike | null;
130
+ /**
131
+ * Places a waypoint may name — `at: "reykjavik"`.
132
+ *
133
+ * A closed set the author writes once, which is what keeps coordinates out of the
134
+ * camera path: `at: "reykjavik"` survives someone deciding the city should be framed a
135
+ * little wider, and `at: -21.9, 64.1, 0.004` does not.
136
+ */
137
+ places?: Record<string, NamedPlace>;
138
+ /** Padding when a named place is a box rather than a point. @defaultValue 0.1 */
139
+ padding?: number;
140
+ /**
141
+ * Treat the map as ready even while tiles are loading.
142
+ *
143
+ * The deck host ignores readiness anyway; this is for a still export where waiting
144
+ * forever on a map that will never finish is worse than a partly-drawn frame.
145
+ *
146
+ * @defaultValue false
147
+ */
148
+ assumeReady?: boolean;
149
+ }
150
+ /**
151
+ * A scene backed by a map.
152
+ *
153
+ * The world is the Web Mercator unit square, so a waypoint's `at` is either a named place
154
+ * or — rarely, and legibly only for someone who knows the projection — a raw
155
+ * `{ cx, cy, w }`. Prefer names, and prefer {@link mapViewToView} when building one from
156
+ * a longitude, a latitude and a zoom.
157
+ */
158
+ declare function mapProvider(options?: MapProviderOptions): SceneProvider;
159
+
160
+ /**
161
+ * A route, and a camera that travels it.
162
+ *
163
+ * The second thing the brief asked a map for, after showing places: showing a *route*.
164
+ * Two pieces, both pure, both testable without a map on screen — where the traveller is
165
+ * at fraction `t`, and how much of the line to draw behind them.
166
+ *
167
+ * Distances are measured in projected space rather than on the sphere. That is the right
168
+ * choice here, not a shortcut: the camera and the drawn line both live in Mercator, so a
169
+ * traveller placed by projected distance moves at a *visually* even speed across the
170
+ * frame. Great-circle distance would be even on the globe and visibly accelerate as the
171
+ * route ran towards a pole, which is the wrong answer for something being watched rather
172
+ * than navigated.
173
+ *
174
+ * @packageDocumentation
175
+ */
176
+
177
+ /** A route: an ordered list of positions, as a GeoJSON LineString's coordinates would be. */
178
+ type Route = readonly LngLat[];
179
+ /** Read `[lng, lat]` pairs — the shape a GeoJSON LineString actually carries. */
180
+ declare function routeFromPositions(positions: readonly (readonly [number, number])[]): Route;
181
+ /** How long the route is, in units of the projected world's width. */
182
+ declare function routeLength(route: Route): number;
183
+ /**
184
+ * Where the traveller is at `t`, clamped to `[0, 1]`.
185
+ *
186
+ * By distance along the line, not by vertex index: a route whose vertices bunch up around
187
+ * a city would otherwise crawl there and sprint across the country between.
188
+ */
189
+ declare function pointAlong(route: Route, t: number): LngLat | null;
190
+ /**
191
+ * The part of the route travelled by `t`, as a line that can be drawn.
192
+ *
193
+ * Ends exactly at {@link pointAlong}, with the partial segment included, so the drawn
194
+ * line and the traveller never disagree — a gap between them is the artefact that makes a
195
+ * route animation look broken.
196
+ */
197
+ declare function routeTo(route: Route, t: number): LngLat[];
198
+ /** The box the whole route occupies. */
199
+ declare function routeBounds(route: Route): LngLatBounds | null;
200
+ interface RouteCameraOptions {
201
+ /**
202
+ * How close the camera follows, as a fraction of the whole route's extent.
203
+ *
204
+ * `1` frames the entire route and never moves — the establishing shot. Smaller keeps
205
+ * the traveller filling more of the frame. `0.25` reads as following a car.
206
+ *
207
+ * @defaultValue 0.3
208
+ */
209
+ follow?: number;
210
+ /** Margin around the framing, as a fraction. @defaultValue 0.15 */
211
+ padding?: number;
212
+ }
213
+ /**
214
+ * A camera that follows a traveller along the route.
215
+ *
216
+ * Returns a `View` rather than driving anything, so the same function serves a deck's
217
+ * rAF, a video's frame counter, and a page's scroll offset. Compose it with `flight` for
218
+ * the moves between routes, and use it directly for the travel itself.
219
+ */
220
+ declare function routeCamera(route: Route, t: number, viewport: Viewport, options?: RouteCameraOptions): View | null;
221
+
222
+ export { type LngLat, type LngLatBounds, MAX_LATITUDE, type MapLike, type MapProviderOptions, type MapView, type NamedPlace, type Route, type RouteCameraOptions, TILE_SIZE, boundsOf, boundsToView, latToY, lngToX, mapProvider, mapViewToView, pointAlong, routeBounds, routeCamera, routeFromPositions, routeLength, routeTo, viewToMapView, widthToZoom, xToLng, yToLat, zoomToWidth };
@@ -0,0 +1,184 @@
1
+ import { normalizeView } from '../chunk-RWYOYDBD.js';
2
+
3
+ // src/map/mercator.ts
4
+ var MAX_LATITUDE = 85.0511287798066;
5
+ var TILE_SIZE = 512;
6
+ function clampLatitude(lat) {
7
+ return Math.min(MAX_LATITUDE, Math.max(-MAX_LATITUDE, lat));
8
+ }
9
+ function lngToX(lng) {
10
+ return (lng + 180) / 360;
11
+ }
12
+ function latToY(lat) {
13
+ const phi = clampLatitude(lat) * Math.PI / 180;
14
+ return (1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2;
15
+ }
16
+ function xToLng(x) {
17
+ return x * 360 - 180;
18
+ }
19
+ function yToLat(y) {
20
+ const n = Math.PI * (1 - 2 * y);
21
+ return 180 / Math.PI * Math.atan(Math.sinh(n));
22
+ }
23
+ function zoomToWidth(zoom, viewport) {
24
+ return viewport.width / (TILE_SIZE * 2 ** zoom);
25
+ }
26
+ function widthToZoom(w, viewport) {
27
+ const safe = Math.max(w, Number.EPSILON);
28
+ return Math.log2(viewport.width / (TILE_SIZE * safe));
29
+ }
30
+ function mapViewToView(map, viewport) {
31
+ return normalizeView({
32
+ cx: lngToX(map.lng),
33
+ cy: latToY(map.lat),
34
+ w: zoomToWidth(map.zoom, viewport)
35
+ });
36
+ }
37
+ function viewToMapView(view, viewport) {
38
+ return {
39
+ lng: xToLng(view.cx),
40
+ lat: yToLat(view.cy),
41
+ zoom: widthToZoom(view.w, viewport)
42
+ };
43
+ }
44
+ function boundsToView(bounds, viewport, padding = 0.1) {
45
+ const x0 = lngToX(bounds.west);
46
+ const x1 = lngToX(bounds.east);
47
+ const y0 = latToY(bounds.north);
48
+ const y1 = latToY(bounds.south);
49
+ const width = Math.abs(x1 - x0);
50
+ const height = Math.abs(y1 - y0);
51
+ const aspect = viewport.height === 0 ? 1 : viewport.width / viewport.height;
52
+ const w = Math.max(width, height * aspect) * (1 + padding * 2);
53
+ return normalizeView({ cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, w });
54
+ }
55
+ function boundsOf(positions) {
56
+ if (positions.length === 0) return null;
57
+ let west = Number.POSITIVE_INFINITY;
58
+ let south = Number.POSITIVE_INFINITY;
59
+ let east = Number.NEGATIVE_INFINITY;
60
+ let north = Number.NEGATIVE_INFINITY;
61
+ for (const p of positions) {
62
+ west = Math.min(west, p.lng);
63
+ east = Math.max(east, p.lng);
64
+ south = Math.min(south, p.lat);
65
+ north = Math.max(north, p.lat);
66
+ }
67
+ return { west, south, east, north };
68
+ }
69
+
70
+ // src/map/provider.ts
71
+ function isBounds(place) {
72
+ return "west" in place;
73
+ }
74
+ function mapProvider(options = {}) {
75
+ const { map, places = {}, padding = 0.1, assumeReady = false } = options;
76
+ return {
77
+ name: "map",
78
+ bounds() {
79
+ return { x: 0, y: 0, width: 1, height: 1 };
80
+ },
81
+ resolve(ref, viewport) {
82
+ const place = places[ref] ?? places[ref.replace(/^#/, "")];
83
+ if (!place) return null;
84
+ return isBounds(place) ? boundsToView(place, viewport, padding) : mapViewToView(place, viewport);
85
+ },
86
+ ready() {
87
+ if (assumeReady) return true;
88
+ if (!map) return true;
89
+ if (map.isStyleLoaded && !map.isStyleLoaded()) return false;
90
+ if (map.areTilesLoaded && !map.areTilesLoaded()) return false;
91
+ return true;
92
+ },
93
+ apply(view, viewport) {
94
+ if (!map) return false;
95
+ const { lng, lat, zoom } = viewToMapView(view, viewport);
96
+ map.jumpTo({ center: [lng, lat], zoom });
97
+ return true;
98
+ }
99
+ };
100
+ }
101
+
102
+ // src/map/route.ts
103
+ function routeFromPositions(positions) {
104
+ return positions.map(([lng, lat]) => ({ lng, lat }));
105
+ }
106
+ function project(p) {
107
+ return { x: lngToX(p.lng), y: latToY(p.lat) };
108
+ }
109
+ function unproject(p) {
110
+ return { lng: xToLng(p.x), lat: yToLat(p.y) };
111
+ }
112
+ function measure(route) {
113
+ const at = [0];
114
+ let total = 0;
115
+ for (let i = 1; i < route.length; i++) {
116
+ const a = project(route[i - 1]);
117
+ const b = project(route[i]);
118
+ total += Math.hypot(b.x - a.x, b.y - a.y);
119
+ at.push(total);
120
+ }
121
+ return { at, total };
122
+ }
123
+ function routeLength(route) {
124
+ return measure(route).total;
125
+ }
126
+ function pointAlong(route, t) {
127
+ if (route.length === 0) return null;
128
+ if (route.length === 1) return route[0];
129
+ const clamped = Math.min(1, Math.max(0, Number.isFinite(t) ? t : 0));
130
+ if (clamped <= 0) return route[0];
131
+ if (clamped >= 1) return route[route.length - 1];
132
+ const { at, total } = measure(route);
133
+ if (total === 0) return route[0];
134
+ const target = clamped * total;
135
+ for (let i = 1; i < route.length; i++) {
136
+ if (at[i] < target) continue;
137
+ const span = at[i] - at[i - 1];
138
+ const local = span === 0 ? 0 : (target - at[i - 1]) / span;
139
+ const a = project(route[i - 1]);
140
+ const b = project(route[i]);
141
+ return unproject({ x: a.x + (b.x - a.x) * local, y: a.y + (b.y - a.y) * local });
142
+ }
143
+ return route[route.length - 1];
144
+ }
145
+ function routeTo(route, t) {
146
+ if (route.length === 0) return [];
147
+ const clamped = Math.min(1, Math.max(0, Number.isFinite(t) ? t : 0));
148
+ if (clamped <= 0) return [route[0]];
149
+ const { at, total } = measure(route);
150
+ if (total === 0) return [route[0]];
151
+ const target = clamped * total;
152
+ const out = [route[0]];
153
+ for (let i = 1; i < route.length; i++) {
154
+ if (at[i] <= target) {
155
+ out.push(route[i]);
156
+ continue;
157
+ }
158
+ const head = pointAlong(route, clamped);
159
+ if (head) out.push(head);
160
+ return out;
161
+ }
162
+ return out;
163
+ }
164
+ function routeBounds(route) {
165
+ return boundsOf(route);
166
+ }
167
+ function routeCamera(route, t, viewport, options = {}) {
168
+ const bounds = routeBounds(route);
169
+ if (!bounds) return null;
170
+ const whole = boundsToView(bounds, viewport, options.padding ?? 0.15);
171
+ const follow = options.follow ?? 0.3;
172
+ if (follow >= 1) return whole;
173
+ const head = pointAlong(route, t);
174
+ if (!head) return whole;
175
+ return {
176
+ cx: lngToX(head.lng),
177
+ cy: latToY(head.lat),
178
+ w: Math.max(whole.w * follow, Number.EPSILON)
179
+ };
180
+ }
181
+
182
+ export { MAX_LATITUDE, TILE_SIZE, boundsOf, boundsToView, latToY, lngToX, mapProvider, mapViewToView, pointAlong, routeBounds, routeCamera, routeFromPositions, routeLength, routeTo, viewToMapView, widthToZoom, xToLng, yToLat, zoomToWidth };
183
+ //# sourceMappingURL=index.js.map
184
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/map/mercator.ts","../../src/map/provider.ts","../../src/map/route.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Geography, as a coordinate space the camera already understands.\n *\n * A map looks like it needs its own camera — `flyTo` takes a centre and a zoom, not a\n * rectangle — and building one would have meant a second interpolator, a second set of\n * checker rules, and two ways for a waypoint to mean something. It does not need one.\n *\n * Web Mercator *is* a plane. Projecting the world onto the unit square makes longitude\n * and latitude into ordinary x and y, and a zoom level into a view width: at zoom `z` the\n * whole world is `512·2^z` pixels across, so a viewport `vw` pixels wide shows\n * `vw / (512·2^z)` of it. That is a `View`. The camera, the checker, the timeline and the\n * video host all work on a map with no changes at all, and the provider's whole job\n * reduces to converting on the way out.\n *\n * It is also the same space MapLibre's own `flyTo` interpolates in — it implements van\n * Wijk & Nuij too, which is where this package got the algorithm from in the first place.\n * So driving `jumpTo` from this camera is not fighting the library; it is doing what the\n * library does, on a clock the renderer controls.\n *\n * @packageDocumentation\n */\n\nimport { normalizeView, type View, type Viewport } from \"../camera/view\";\n\n/**\n * The latitude where Web Mercator stops.\n *\n * `atan(sinh(π))` in degrees. Beyond it the projection runs to infinity, so every map\n * library clamps here and so does this.\n */\nexport const MAX_LATITUDE = 85.051128779806604;\n\n/** MapLibre's tile size. The world is this many pixels across at zoom 0. */\nexport const TILE_SIZE = 512;\n\n/** A place on the earth, the way an author names one. */\nexport interface LngLat {\n lng: number;\n lat: number;\n}\n\n/** A framing of the earth: where to look, and how close. */\nexport interface MapView extends LngLat {\n /** Map zoom level, as MapLibre and Mapbox count it. */\n zoom: number;\n}\n\n/** A rectangle of the earth. */\nexport interface LngLatBounds {\n west: number;\n south: number;\n east: number;\n north: number;\n}\n\nfunction clampLatitude(lat: number): number {\n return Math.min(MAX_LATITUDE, Math.max(-MAX_LATITUDE, lat));\n}\n\n/** Longitude → the unit square's x. */\nexport function lngToX(lng: number): number {\n return (lng + 180) / 360;\n}\n\n/** Latitude → the unit square's y. Clamped at the poles, where Mercator gives up. */\nexport function latToY(lat: number): number {\n const phi = (clampLatitude(lat) * Math.PI) / 180;\n return (1 - Math.log(Math.tan(phi) + 1 / Math.cos(phi)) / Math.PI) / 2;\n}\n\n/** The unit square's x → longitude. */\nexport function xToLng(x: number): number {\n return x * 360 - 180;\n}\n\n/** The unit square's y → latitude. */\nexport function yToLat(y: number): number {\n const n = Math.PI * (1 - 2 * y);\n return (180 / Math.PI) * Math.atan(Math.sinh(n));\n}\n\n/**\n * How much of the world a view holds, at a zoom level.\n *\n * The whole conversion, in one line: the world is `TILE_SIZE·2^zoom` pixels across, so a\n * viewport that many pixels wide would show all of it.\n */\nexport function zoomToWidth(zoom: number, viewport: Viewport): number {\n return viewport.width / (TILE_SIZE * 2 ** zoom);\n}\n\n/** The zoom level at which a view holds `w` of the world. */\nexport function widthToZoom(w: number, viewport: Viewport): number {\n const safe = Math.max(w, Number.EPSILON);\n return Math.log2(viewport.width / (TILE_SIZE * safe));\n}\n\n/** A map framing → the camera's own `{ cx, cy, w }`. */\nexport function mapViewToView(map: MapView, viewport: Viewport): View {\n return normalizeView({\n cx: lngToX(map.lng),\n cy: latToY(map.lat),\n w: zoomToWidth(map.zoom, viewport),\n });\n}\n\n/** The camera's `{ cx, cy, w }` → a map framing the library can be told to hold. */\nexport function viewToMapView(view: View, viewport: Viewport): MapView {\n return {\n lng: xToLng(view.cx),\n lat: yToLat(view.cy),\n zoom: widthToZoom(view.w, viewport),\n };\n}\n\n/**\n * The framing that holds a rectangle of the earth.\n *\n * `padding` is a fraction of the box, so `0.1` leaves a tenth of it as margin — the\n * difference between a country filling the frame edge to edge and sitting in it.\n */\nexport function boundsToView(bounds: LngLatBounds, viewport: Viewport, padding = 0.1): View {\n const x0 = lngToX(bounds.west);\n const x1 = lngToX(bounds.east);\n const y0 = latToY(bounds.north);\n const y1 = latToY(bounds.south);\n\n const width = Math.abs(x1 - x0);\n const height = Math.abs(y1 - y0);\n const aspect = viewport.height === 0 ? 1 : viewport.width / viewport.height;\n\n // Same rule as a plane: widen when the box is taller than the frame's shape, so the\n // whole thing is inside rather than cropped top and bottom.\n const w = Math.max(width, height * aspect) * (1 + padding * 2);\n\n return normalizeView({ cx: (x0 + x1) / 2, cy: (y0 + y1) / 2, w });\n}\n\n/** The smallest box containing every position. Null for none. */\nexport function boundsOf(positions: readonly LngLat[]): LngLatBounds | null {\n if (positions.length === 0) return null;\n let west = Number.POSITIVE_INFINITY;\n let south = Number.POSITIVE_INFINITY;\n let east = Number.NEGATIVE_INFINITY;\n let north = Number.NEGATIVE_INFINITY;\n for (const p of positions) {\n west = Math.min(west, p.lng);\n east = Math.max(east, p.lng);\n south = Math.min(south, p.lat);\n north = Math.max(north, p.lat);\n }\n return { west, south, east, north };\n}\n","/**\n * A camera over a real map.\n *\n * The provider is thin on purpose. Because Mercator is already a plane (see\n * `./mercator.ts`), there is no second camera and no second interpolator here — the\n * `View` the shared camera produces is converted to a centre and a zoom, and the map is\n * *told* where to be. Every frame. That is the whole of `apply`.\n *\n * Telling rather than asking is the important part, and it is what makes a map work under\n * the video renderer at all. `flyTo` animates on the wall clock: it decides where the\n * camera is from `Date.now()`, which is exactly the thing a renderer advancing a frame\n * counter cannot use — Remotion's own map documentation says the two models are\n * incompatible, and Mapbox's cinematic-route write-up gave up composing `flyTo` with\n * other camera control and wrote a custom interpolator instead. `jumpTo` per frame, from\n * a camera that is a pure function of time, has neither problem.\n *\n * `maplibre-gl` is an optional peer dependency and this module never imports it. The map\n * arrives as an object with the three methods used below, so the package does not grow by\n * a megabyte for decks that have no map, and so this file is testable with a fake.\n *\n * @packageDocumentation\n */\n\nimport type { Rect, View, Viewport } from \"../camera/view\";\nimport type { SceneProvider } from \"../scene/provider\";\nimport {\n boundsToView,\n type LngLatBounds,\n type MapView,\n mapViewToView,\n viewToMapView,\n} from \"./mercator\";\n\n/**\n * What this needs from a map. Structural, so `maplibre-gl` stays optional and a test can\n * hand it a fake.\n *\n * Satisfied by a MapLibre or Mapbox `Map` as-is.\n */\nexport interface MapLike {\n jumpTo(options: { center: [number, number]; zoom: number }): unknown;\n /** False while any tile in view is still in flight. */\n areTilesLoaded?(): boolean;\n /** False until the style JSON has been parsed and its sources registered. */\n isStyleLoaded?(): boolean;\n}\n\n/** A named place a waypoint can fly to by name. */\nexport type NamedPlace = MapView | LngLatBounds;\n\nexport interface MapProviderOptions {\n /**\n * The map to drive. Omit it for a build-time or server render, where there is no map\n * and nothing to move.\n */\n map?: MapLike | null;\n /**\n * Places a waypoint may name — `at: \"reykjavik\"`.\n *\n * A closed set the author writes once, which is what keeps coordinates out of the\n * camera path: `at: \"reykjavik\"` survives someone deciding the city should be framed a\n * little wider, and `at: -21.9, 64.1, 0.004` does not.\n */\n places?: Record<string, NamedPlace>;\n /** Padding when a named place is a box rather than a point. @defaultValue 0.1 */\n padding?: number;\n /**\n * Treat the map as ready even while tiles are loading.\n *\n * The deck host ignores readiness anyway; this is for a still export where waiting\n * forever on a map that will never finish is worse than a partly-drawn frame.\n *\n * @defaultValue false\n */\n assumeReady?: boolean;\n}\n\nfunction isBounds(place: NamedPlace): place is LngLatBounds {\n return \"west\" in place;\n}\n\n/**\n * A scene backed by a map.\n *\n * The world is the Web Mercator unit square, so a waypoint's `at` is either a named place\n * or — rarely, and legibly only for someone who knows the projection — a raw\n * `{ cx, cy, w }`. Prefer names, and prefer {@link mapViewToView} when building one from\n * a longitude, a latitude and a zoom.\n */\nexport function mapProvider(options: MapProviderOptions = {}): SceneProvider {\n const { map, places = {}, padding = 0.1, assumeReady = false } = options;\n\n return {\n name: \"map\",\n\n bounds(): Rect {\n // The whole world, always. A map has no edge to fly off, so the checker's\n // outside-the-scene rule should only ever fire on a genuinely nonsensical framing.\n return { x: 0, y: 0, width: 1, height: 1 };\n },\n\n resolve(ref: string, viewport: Viewport): View | null {\n const place = places[ref] ?? places[ref.replace(/^#/, \"\")];\n if (!place) return null;\n return isBounds(place)\n ? boundsToView(place, viewport, padding)\n : mapViewToView(place, viewport);\n },\n\n ready(): boolean {\n if (assumeReady) return true;\n if (!map) return true;\n // Both, and in this order: a map can report its tiles loaded before the style has\n // registered the sources those tiles belong to, which renders an empty basemap\n // that claims to be finished.\n if (map.isStyleLoaded && !map.isStyleLoaded()) return false;\n if (map.areTilesLoaded && !map.areTilesLoaded()) return false;\n return true;\n },\n\n apply(view: View, viewport: Viewport): boolean {\n if (!map) return false;\n const { lng, lat, zoom } = viewToMapView(view, viewport);\n map.jumpTo({ center: [lng, lat], zoom });\n // Handled: the host must not also put a CSS transform on the map's container, which\n // would scale the already-correct tiles a second time.\n return true;\n },\n };\n}\n","/**\n * A route, and a camera that travels it.\n *\n * The second thing the brief asked a map for, after showing places: showing a *route*.\n * Two pieces, both pure, both testable without a map on screen — where the traveller is\n * at fraction `t`, and how much of the line to draw behind them.\n *\n * Distances are measured in projected space rather than on the sphere. That is the right\n * choice here, not a shortcut: the camera and the drawn line both live in Mercator, so a\n * traveller placed by projected distance moves at a *visually* even speed across the\n * frame. Great-circle distance would be even on the globe and visibly accelerate as the\n * route ran towards a pole, which is the wrong answer for something being watched rather\n * than navigated.\n *\n * @packageDocumentation\n */\n\nimport type { View, Viewport } from \"../camera/view\";\nimport {\n boundsOf,\n boundsToView,\n type LngLat,\n type LngLatBounds,\n latToY,\n lngToX,\n xToLng,\n yToLat,\n} from \"./mercator\";\n\n/** A route: an ordered list of positions, as a GeoJSON LineString's coordinates would be. */\nexport type Route = readonly LngLat[];\n\n/** Read `[lng, lat]` pairs — the shape a GeoJSON LineString actually carries. */\nexport function routeFromPositions(positions: readonly (readonly [number, number])[]): Route {\n return positions.map(([lng, lat]) => ({ lng, lat }));\n}\n\ninterface Projected {\n x: number;\n y: number;\n}\n\nfunction project(p: LngLat): Projected {\n return { x: lngToX(p.lng), y: latToY(p.lat) };\n}\n\nfunction unproject(p: Projected): LngLat {\n return { lng: xToLng(p.x), lat: yToLat(p.y) };\n}\n\n/** Cumulative projected distance to each vertex, and the total. */\nfunction measure(route: Route): { at: number[]; total: number } {\n const at: number[] = [0];\n let total = 0;\n for (let i = 1; i < route.length; i++) {\n const a = project(route[i - 1]);\n const b = project(route[i]);\n total += Math.hypot(b.x - a.x, b.y - a.y);\n at.push(total);\n }\n return { at, total };\n}\n\n/** How long the route is, in units of the projected world's width. */\nexport function routeLength(route: Route): number {\n return measure(route).total;\n}\n\n/**\n * Where the traveller is at `t`, clamped to `[0, 1]`.\n *\n * By distance along the line, not by vertex index: a route whose vertices bunch up around\n * a city would otherwise crawl there and sprint across the country between.\n */\nexport function pointAlong(route: Route, t: number): LngLat | null {\n if (route.length === 0) return null;\n if (route.length === 1) return route[0];\n\n const clamped = Math.min(1, Math.max(0, Number.isFinite(t) ? t : 0));\n // The ends come back exactly as the author wrote them. Interpolating them would send\n // them through the projection and back, and return coordinates a hair off their own.\n if (clamped <= 0) return route[0];\n if (clamped >= 1) return route[route.length - 1];\n\n const { at, total } = measure(route);\n if (total === 0) return route[0];\n\n const target = clamped * total;\n\n for (let i = 1; i < route.length; i++) {\n if (at[i] < target) continue;\n const span = at[i] - at[i - 1];\n const local = span === 0 ? 0 : (target - at[i - 1]) / span;\n const a = project(route[i - 1]);\n const b = project(route[i]);\n return unproject({ x: a.x + (b.x - a.x) * local, y: a.y + (b.y - a.y) * local });\n }\n\n return route[route.length - 1];\n}\n\n/**\n * The part of the route travelled by `t`, as a line that can be drawn.\n *\n * Ends exactly at {@link pointAlong}, with the partial segment included, so the drawn\n * line and the traveller never disagree — a gap between them is the artefact that makes a\n * route animation look broken.\n */\nexport function routeTo(route: Route, t: number): LngLat[] {\n if (route.length === 0) return [];\n const clamped = Math.min(1, Math.max(0, Number.isFinite(t) ? t : 0));\n if (clamped <= 0) return [route[0]];\n\n const { at, total } = measure(route);\n if (total === 0) return [route[0]];\n const target = clamped * total;\n\n const out: LngLat[] = [route[0]];\n for (let i = 1; i < route.length; i++) {\n if (at[i] <= target) {\n out.push(route[i]);\n continue;\n }\n const head = pointAlong(route, clamped);\n if (head) out.push(head);\n return out;\n }\n return out;\n}\n\n/** The box the whole route occupies. */\nexport function routeBounds(route: Route): LngLatBounds | null {\n return boundsOf(route);\n}\n\nexport interface RouteCameraOptions {\n /**\n * How close the camera follows, as a fraction of the whole route's extent.\n *\n * `1` frames the entire route and never moves — the establishing shot. Smaller keeps\n * the traveller filling more of the frame. `0.25` reads as following a car.\n *\n * @defaultValue 0.3\n */\n follow?: number;\n /** Margin around the framing, as a fraction. @defaultValue 0.15 */\n padding?: number;\n}\n\n/**\n * A camera that follows a traveller along the route.\n *\n * Returns a `View` rather than driving anything, so the same function serves a deck's\n * rAF, a video's frame counter, and a page's scroll offset. Compose it with `flight` for\n * the moves between routes, and use it directly for the travel itself.\n */\nexport function routeCamera(\n route: Route,\n t: number,\n viewport: Viewport,\n options: RouteCameraOptions = {},\n): View | null {\n const bounds = routeBounds(route);\n if (!bounds) return null;\n\n const whole = boundsToView(bounds, viewport, options.padding ?? 0.15);\n const follow = options.follow ?? 0.3;\n if (follow >= 1) return whole;\n\n const head = pointAlong(route, t);\n if (!head) return whole;\n\n return {\n cx: lngToX(head.lng),\n cy: latToY(head.lat),\n w: Math.max(whole.w * follow, Number.EPSILON),\n };\n}\n"]}