@tileflow/core 0.1.0-alpha.32 → 0.1.0-alpha.33

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.
@@ -0,0 +1,80 @@
1
+ # Application tile reading
2
+
3
+ Some application features need the map's data themselves rather than what MapLibre draws: a route
4
+ over the streets, the name of the place under the camera, buildings for a custom 3D layer.
5
+ MapLibre answers `querySourceFeatures` only for the tiles it has loaded for the current view, at
6
+ the zoom it shows. `createTileflowTileReader` reads chosen layers of a vector source for any area
7
+ at a fixed zoom instead, with the map's own tile address and request rules.
8
+
9
+ ## Read an area
10
+
11
+ This example assumes an existing MapLibre map named `map` whose style has loaded a vector source
12
+ named `tileflow`:
13
+
14
+ <!-- docs:check -->
15
+
16
+ ```ts
17
+ import {
18
+ createTileflowTileReader,
19
+ readTileflowMapTileSource,
20
+ tileflowTilePointToLngLat,
21
+ type TileflowTileSourceMap,
22
+ } from '@tileflow/core/browser';
23
+
24
+ export async function streetNamesNear(map: TileflowTileSourceMap, lng: number, lat: number) {
25
+ const source = readTileflowMapTileSource(map, 'tileflow');
26
+ if (!source) return [];
27
+ const reader = createTileflowTileReader({...source, layers: ['transportation_name']});
28
+ const tiles = await reader.readArea({
29
+ bounds: [lng - 0.005, lat - 0.005, lng + 0.005, lat + 0.005],
30
+ zoom: 14,
31
+ });
32
+ const names = new Map<string, [number, number]>();
33
+ for (const tile of tiles) {
34
+ const layer = tile.layers.transportation_name;
35
+ for (const feature of layer?.features ?? []) {
36
+ const name = feature.properties.name;
37
+ const [x, y] = feature.geometry[0] ?? [];
38
+ if (typeof name === 'string' && x !== undefined && y !== undefined)
39
+ names.set(name, tileflowTilePointToLngLat(tile, x, y, layer!.extent));
40
+ }
41
+ }
42
+ return [...names];
43
+ }
44
+ ```
45
+
46
+ `readArea` reads the tiles covering the bounds at the zoom, clamped to the source's `minzoom` and
47
+ `maxzoom`. Deeper zooms read the source's last zoom. It leaves out tiles the source does not have
48
+ and refuses an area of more than `maxTiles` tiles (64 by default). `readTile({z, x, y})` reads one
49
+ tile. Both take an `AbortSignal`.
50
+
51
+ ## What the reader returns
52
+
53
+ Each tile holds the chosen layers. Each layer holds its `extent` (tile units across the tile,
54
+ usually 4096) and its features. A feature has `type` (`Point`, `LineString` or `Polygon`),
55
+ `properties`, an `id` when the tile gives one, and `geometry`: one flat `[x0, y0, x1, y1, …]` array
56
+ per point set, line or ring, in tile units. Rings are closed and keep the vector tile winding:
57
+ exterior rings run clockwise in tile units, holes anticlockwise. Coordinates in a tile's buffer
58
+ fall outside `0` to `extent`, and lines are cut at the buffer's edge, so a feature crossing tiles
59
+ comes in pieces. `tileflowTilePointToLngLat(tile, x, y, extent)` converts a point to longitude and
60
+ latitude.
61
+
62
+ Feature ids read as MapLibre reads them, including ids beyond 2^53, so they can address feature
63
+ state on the map.
64
+
65
+ ## Requests and cache
66
+
67
+ - **One request per tile.** However many reads wait for a tile, it is requested once. A read whose
68
+ signal aborts stops waiting, but the request finishes for the cache and the other reads.
69
+ - **Cache.** The reader keeps up to `cacheSize` decoded tiles (128 by default), the most recently
70
+ read first. It remembers a tile the source does not have (404 or 204) as absent, and does not
71
+ remember errors. `clear()` forgets everything.
72
+ - **Concurrency.** At most `concurrency` requests (6 by default) run at once; the rest wait their
73
+ turn.
74
+ - **Requests.** `transformRequest` rewrites each request as a MapLibre map's option does. Pass the
75
+ map's own so tile reads follow the same rules. The browser's HTTP cache applies as for any
76
+ request.
77
+
78
+ The reader has no DOM dependency, so it also runs in a module worker: post the tile address and
79
+ zooms to the worker and create the reader there. `decodeTileflowVectorTile(bytes, {layers})`
80
+ decodes tile bytes an application already has.
@@ -153,8 +153,17 @@ independent place, water, aerodrome, and POI labels remain under the labels modu
153
153
  Every styling module supports semantic shortcuts and exact semantic targets. Visual style values
154
154
  accept typed token refs, documented `fixed(value, {reason})` values, closed data expressions through
155
155
  `expr.*` plus `field(...)`, and zoom functions through `zoom.step(...)`, `zoom.linear(...)`, or
156
- `zoom.exponential(...)`. Refs and fixed leaves also work inside typed expressions and zoom stops.
157
- Structural controls such as presets, visibility, and zoom gates remain ordinary literals. Exact
156
+ `zoom.exponential(...)`. Refs and fixed leaves also work as the results of typed expressions and
157
+ as zoom-stop values; conditions and other operands take plain values, such as `3` rather than
158
+ `fixed(3)` in a rank test inside a colour expression. Structural controls such as presets,
159
+ visibility, text fields, and zoom gates remain ordinary literals. Expression builders take at most
160
+ 16 branches, stops, or operands; an error names the received count and the config line of the
161
+ call, and a larger lookup composes nested groups.
162
+
163
+ Layout properties, such as text fields, text and icon sizes, and symbol placement, are evaluated at
164
+ each tile's integer zoom: a step at zoom 17.5 takes effect at zoom 18. Paint properties follow the
165
+ camera's exact zoom. `createStyleResult()` reports a `TF_LAYOUT_FRACTIONAL_ZOOM_STEP` warning with
166
+ the semantic target for every layout step at a fractional zoom. Exact
158
167
  controls address stable concepts such as `roads.classes.primary.surface.fill`, not renderer layer
159
168
  IDs.
160
169
 
@@ -0,0 +1,168 @@
1
+ # 3D buildings
2
+
3
+ `createTileflowBuildings3D` raises the buildings of a Tileflow map's vector tiles round the camera
4
+ and lights them from the theme controller's sun: walls and roofs take the light by how they face
5
+ it, buildings cast shadows on each other and on the flat map, the ground darkens at the foot of the
6
+ walls, and at night flood lights wash the foot of shops, busy places, landmarks and towers while
7
+ the places busy at night light their ground floors. Showing, hiding and growing the city lays out
8
+ no source, so it works as a 3D switch and as an animated height.
9
+
10
+ It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
11
+ labels. It needs WebGL2 and the Mercator projection, which MapLibre's globe turns into from about
12
+ zoom 12; the buildings stand from zoom 14.5 by default.
13
+
14
+ ## Show the buildings
15
+
16
+ This example assumes an existing MapLibre map showing a Tileflow map whose vector source is called
17
+ `tileflow`, as Tileflow's maps call World, and a theme controller for it:
18
+
19
+ <!-- docs:check -->
20
+
21
+ ```ts
22
+ import {
23
+ createTileflowBuildings3D,
24
+ type TileflowBuildings3DMap,
25
+ type TileflowThemeController,
26
+ } from '@tileflow/core/browser';
27
+
28
+ export function raiseTheCity(map: TileflowBuildings3DMap, controller: TileflowThemeController) {
29
+ const buildings = createTileflowBuildings3D({map, controller});
30
+ // The buildings slide out of the ground over a second.
31
+ void buildings.show({transition: {duration: 1000}});
32
+ return buildings;
33
+ }
34
+ ```
35
+
36
+ `show()` raises the buildings, `hide()` sinks them back into the flat map, and `grow(scale)` eases
37
+ them to a share of their height, from 0 to 1. Each returns a promise that resolves once the motion
38
+ ends; a newer call takes over from where the buildings stand. A browser preference for reduced
39
+ motion changes at once. `dispose()` takes the layer off the map.
40
+
41
+ The renderer reads the source's z15 tiles round the camera (z15 is where World's buildings carry
42
+ their heights; a source that stops earlier is read at its own maximum zoom), up to `maxTiles`
43
+ tiles (25 by default), and meshes them a few milliseconds per frame. It reads World's
44
+ `render_height`, `render_min_height`, `hide_3d`, `building_tone` and `night_use`, and the places in
45
+ World's `poi` layer (see [night light](#night-light)).
46
+
47
+ ## Light and shadows
48
+
49
+ The light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
50
+ application places by direction or by date and place (see
51
+ [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md)).
52
+ A sunlit roof keeps the building's colour, a wall turned to a low sun glows, and shade takes the
53
+ sky's colour. As the sun sets, shadows lengthen while they fade; a sun below the horizon casts none.
54
+
55
+ A wall sees half the sky, so at night, when the ground gives back little light, walls in shade
56
+ stay darker than roofs. A theme's night `lighting.sun` set high (60° or more) lights roofs more than
57
+ any wall, so roofs and walls read apart under the moon.
58
+
59
+ MapLibre's own layers take no shadows, so the renderer darkens the map it is drawn over: after its
60
+ buildings, it multiplies the ground by the shadows and by a band at the foot of the walls. Labels
61
+ and icons drawn above it keep their colours. Layers kept under 3D content with
62
+ `createTileflowGroundLayers` are darkened with the ground.
63
+
64
+ `shadows` and `occlusion` (the band at the foot of the walls and the darker tight corners) default
65
+ to true and can be turned off. `windows: true` adds invented night windows, lit by rooms; they are
66
+ decoration, not data, and are off by default.
67
+
68
+ ## Edges
69
+
70
+ Where one building ends and the next starts stays legible at any distance: each roof's outline and
71
+ each building's outer corners catch the light in a line one CSS pixel wide, whatever the zoom, and
72
+ each building is a little lighter or darker than its colour (up to 6%), so neighbours of one colour
73
+ stay apart. The lines are strongest at night; diagonals inside a roof, inner corners and the cuts at
74
+ a tile's edge draw none. `edges` sets their strength from 0 (none) to 1, the default.
75
+
76
+ `edgeColor` gives the lines a colour of the application's own, as a CSS colour or per theme: a dark
77
+ outline as on a printed map, or a neon one at night. It is drawn as it is, unlit, at the full
78
+ `edges` strength by day and night. The renderer asks for each theme's colour once and mixes the
79
+ colours of the themes either side of a blend's position; a theme without a valid colour keeps the
80
+ default.
81
+
82
+ <!-- docs:check -->
83
+
84
+ ```ts
85
+ import {
86
+ createTileflowBuildings3D,
87
+ type TileflowBuildings3DMap,
88
+ type TileflowThemeController,
89
+ } from '@tileflow/core/browser';
90
+
91
+ export function neonEdges(map: TileflowBuildings3DMap, controller: TileflowThemeController) {
92
+ return createTileflowBuildings3D({
93
+ map,
94
+ controller,
95
+ // Cyan outlines at night, a dark ink by day.
96
+ edgeColor: (theme) => (theme === 'night' ? '#7fd8ff' : '#2b2a33'),
97
+ });
98
+ }
99
+ ```
100
+
101
+ ## Night light
102
+
103
+ At night (as the sun's `brightness` falls), the buildings light the street from data, quietly by
104
+ default:
105
+
106
+ - Landmarks (World's `building_tone` `destination`) and towers of 40 m or more, one in two, are
107
+ flood-lit at their foot, on the wall and the ground around it.
108
+ - Buildings that hold places busy at night light the windows of their ground floor and spill their
109
+ light a few metres onto the pavement: bars and clubs in a neon of their own or a warm light,
110
+ theatres and cinemas in a bright white, places to eat in amber. A building takes the strongest
111
+ use of its places, nightlife before entertainment before dining. Walls round a courtyard stay
112
+ dark.
113
+
114
+ The night use comes from World's `night_use` on each building. In tiles whose buildings do not
115
+ carry it, the renderer finds it in the tile's places (`placesLayer`, `poi` by default): a place
116
+ inside a building, or within a few metres of its facade, gives the building its use.
117
+
118
+ `nightLight` sets how bright all of this shines, from 0 (none) to 2; it defaults to 1.
119
+
120
+ For the streets themselves, see
121
+ [street lights](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/street-lights.md),
122
+ which draws over the buildings so they hide the lamps behind them; both layers share each tile's
123
+ download.
124
+
125
+ ## Colours
126
+
127
+ Without `color`, buildings take the theme's 3D building colour, the colour Tileflow's buildings
128
+ module compiles for its extrusion, and follow theme changes and blends.
129
+
130
+ `color(properties, theme)` colours each building from its feature properties for each theme, as a
131
+ CSS colour. The renderer calls it once per building and theme when a tile or the set of themes
132
+ changes, and mixes the colours of the themes either side of a blend's position on the GPU, so a
133
+ moving blend costs nothing per frame:
134
+
135
+ <!-- docs:check -->
136
+
137
+ ```ts
138
+ import {createTileflowBuildings3D, type TileflowBuildings3DMap} from '@tileflow/core/browser';
139
+
140
+ const tints: Record<string, readonly string[]> = {
141
+ day: ['#f1d4cd', '#d3e8da', '#f6e0b5', '#e2d5f0', '#cfe3f2'],
142
+ night: ['#353d41', '#2f3a3d', '#3a3b44', '#33363f', '#2d3540'],
143
+ };
144
+
145
+ export function tintedCity(map: TileflowBuildings3DMap) {
146
+ return createTileflowBuildings3D({
147
+ map,
148
+ color(properties, theme) {
149
+ const floors = Math.floor(Number(properties.render_height ?? 5) / 3.2);
150
+ const palette = tints[theme] ?? tints.day!;
151
+ return palette[floors % palette.length]!;
152
+ },
153
+ });
154
+ }
155
+ ```
156
+
157
+ ## Cost
158
+
159
+ The shadow map (up to 4096 × 4096) is drawn again only when the sun, the buildings or their height
160
+ change, not when the camera moves. In a campaign over Miami at 1512 × 860 at 2×, a city lit this
161
+ way kept the frame pacing of the same city drawn without shadows.
162
+
163
+ ## Limits
164
+
165
+ - Mercator only; the layer draws nothing below its `minzoom`.
166
+ - No terrain: buildings stand on flat ground.
167
+ - One building colour per feature and theme; facades, roof shapes and building parts as separate
168
+ shapes are not drawn.
@@ -0,0 +1,112 @@
1
+ # Camera moves and the tile cache
2
+
3
+ A long camera move outruns the network: the new view's tiles take longer to arrive than the move
4
+ takes. Some examples are a flight from a street to a region and back, a guided tour, or a scripted
5
+ opening. MapLibre keeps the tiles it no longer shows in an out-of-view cache, so a camera that
6
+ comes back finds them ready. This guide covers what keeps that cache useful and what empties it,
7
+ as MapLibre GL JS 6 behaves.
8
+
9
+ ## What the cache keeps
10
+
11
+ The out-of-view cache holds about five views' worth of tiles per source:
12
+
13
+ - **Size.** It is the tiles across the map times `maxTileCacheZoomLevels`, 5 by default.
14
+ `maxTileCacheSize` caps it.
15
+ - **Too small for a flight.** A move that crosses many zooms (a street, the region, the street
16
+ again) needs more. Raise `maxTileCacheZoomLevels` on the map, for example to 12, so tiles
17
+ loaded early in a sequence are still there at its end.
18
+ - **Exact tiles only.** While a tile the camera needs is loading, MapLibre falls back on its loaded
19
+ parents or children only among the tiles it shows. It does not fall back on tiles it keeps out
20
+ of view. A cached tile helps only when it is the very tile a frame needs.
21
+
22
+ ## What empties it
23
+
24
+ Changes that make MapLibre lay a source's tiles out again also empty that source's out-of-view
25
+ cache. The tiles in view are parsed again; the cached ones are dropped and come back from the
26
+ network when the camera returns.
27
+
28
+ **Changes that lay tiles out:**
29
+
30
+ - Any layout property, `visibility` included, through `setLayoutProperty`.
31
+ - A filter, through `setFilter`.
32
+ - A new value, through `setPaintProperty`, for a paint property whose value reads feature data,
33
+ such as `['get', ...]` or `['feature-state', ...]`.
34
+ - `setStyle` with layers that differ. A style that is not diffed starts every source afresh.
35
+ - In a theme blend, a change of the nearest theme over values like these. `setBlend({hold: true})`
36
+ keeps them back during a move; see
37
+ [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md).
38
+
39
+ **Changes that keep the cache:**
40
+
41
+ - A constant paint value, or one that reads only the zoom.
42
+ - `setFeatureState`, which rewrites the paint of the features it names.
43
+ - Global state read by values that read no feature data.
44
+
45
+ During a camera move, change only what keeps the cache. Make layout changes, filters and theme
46
+ crossings before the move starts or after it ends. To mark features during a move, prefer feature
47
+ state over a filter.
48
+
49
+ ## Warm cameras before a move
50
+
51
+ MapLibre GL JS 6 has no public way to load a camera's tiles without showing it. Until it does,
52
+ warm cameras while something covers the map, such as a title card or a loading screen. Visit each
53
+ camera with `jumpTo`, wait until its tiles are in, then go on to the next:
54
+
55
+ <!-- docs:check -->
56
+
57
+ ```ts
58
+ type WarmableMap = {
59
+ jumpTo(options: {
60
+ center: [number, number];
61
+ zoom: number;
62
+ bearing?: number;
63
+ pitch?: number;
64
+ }): unknown;
65
+ loaded(): boolean;
66
+ on(event: 'render', listener: () => void): unknown;
67
+ off(event: 'render', listener: () => void): unknown;
68
+ triggerRepaint(): void;
69
+ };
70
+
71
+ type Camera = {center: [number, number]; zoom: number; bearing?: number; pitch?: number};
72
+
73
+ /** Resolves once the tiles of the camera on screen are in, or false after `ms`. */
74
+ function tilesIn(map: WarmableMap, ms: number): Promise<boolean> {
75
+ return new Promise((resolve) => {
76
+ const check = () => {
77
+ if (map.loaded()) done(true);
78
+ };
79
+ const done = (ok: boolean) => {
80
+ clearTimeout(timer);
81
+ map.off('render', check);
82
+ resolve(ok);
83
+ };
84
+ const timer = setTimeout(() => done(false), ms);
85
+ map.on('render', check);
86
+ map.triggerRepaint();
87
+ });
88
+ }
89
+
90
+ export async function warmCameras(map: WarmableMap, cameras: readonly Camera[], rest: Camera) {
91
+ for (const camera of cameras) {
92
+ map.jumpTo(camera);
93
+ await tilesIn(map, 1500);
94
+ }
95
+ map.jumpTo(rest);
96
+ }
97
+ ```
98
+
99
+ Four details matter:
100
+
101
+ - **Check after a frame.** `map.loaded()` can read false on the frame in which the last tile
102
+ arrived. So check it after a frame, on `render`, not once right after `jumpTo`.
103
+ - **Do not wait for `idle`.** `idle` also waits for labels to fade in, which adds time and loads
104
+ nothing.
105
+ - **Visit every zoom the move crosses.** A cached tile helps only when it is the exact tile a frame
106
+ needs, so warm each zoom, not just the destination.
107
+ - **Make cache-emptying changes first.** Make the changes listed above before the first visit.
108
+ Otherwise the warm-up is lost.
109
+
110
+ Mapbox GL JS offers `preloadOnly` on `jumpTo`, `easeTo` and `flyTo`, which loads a move's tiles
111
+ without showing it. A MapLibre release with an equivalent would replace the visits above with one
112
+ call per move.
@@ -34,3 +34,7 @@ external fixtures may identify their explicit revision.
34
34
  Every exact World V1 release ID is already canonical at the producer boundary: 12–128 characters
35
35
  matching `^world-v1-[a-z0-9][a-z0-9._-]*[a-z0-9]$`. Core validates the literal value and never trims,
36
36
  lowercases, or upgrades it to another World generation.
37
+
38
+ To record a timed camera move instead of one image, commit a
39
+ [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
40
+ and render it with `tileflow capture --shot <name>`.
@@ -0,0 +1,173 @@
1
+ # Capture shots
2
+
3
+ Start with the [@tileflow/core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md) for installation and a complete first example.
4
+
5
+ A shot is a short, timed camera move committed on the singular exported map. Capture renders it
6
+ frame by frame into a WebM video or a directory of numbered PNG frames. Like
7
+ [capture scenes](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-scenes.md),
8
+ shots implicitly target their map, do not inherit, and are removed before cartographic compilation;
9
+ they affect visual evidence, not style or manifest identity.
10
+
11
+ ## Commit a shot
12
+
13
+ This shot moves from central Madrid toward the east over four seconds while the map passes from
14
+ the `light` theme to the `dark` theme:
15
+
16
+ <!-- docs:check -->
17
+
18
+ ```ts
19
+ import {defineMap} from '@tileflow/core';
20
+ import {streets} from '@tileflow/maps';
21
+
22
+ export default defineMap({
23
+ id: 'madrid',
24
+ version: 1,
25
+ extends: streets,
26
+ shots: {
27
+ 'dusk-flyover': {
28
+ theme: 'light',
29
+ viewport: {width: 1280, height: 720, dpr: 1},
30
+ fps: 30,
31
+ keyframes: [
32
+ {
33
+ at: 0,
34
+ camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12},
35
+ blend: {themes: ['light', 'dark'], position: 0},
36
+ },
37
+ {
38
+ at: 4000,
39
+ camera: {type: 'center', center: [-3.68, 40.42], zoom: 14, bearing: 30, pitch: 45},
40
+ blend: {themes: ['light', 'dark'], position: 1},
41
+ },
42
+ ],
43
+ },
44
+ },
45
+ });
46
+ ```
47
+
48
+ A shot has these fields:
49
+
50
+ - `theme`: a concrete theme of the map; `system` is invalid. The map loads this theme first, and a
51
+ shot without a blend shows it throughout.
52
+ - `viewport`: CSS pixels and a `dpr` of 1 or 2, with the same limits as a scene viewport.
53
+ - `fps`: 24, 25, 30, 50, or 60 frames per second. The default is 30.
54
+ - `easing`: `smooth` (the default) eases into and out of each keyframe; `linear` moves at a constant
55
+ rate between keyframes.
56
+ - `keyframes`: 2 to 64 entries. `at` is the whole number of milliseconds from the start of the
57
+ shot. The first keyframe is at 0, every later keyframe is later than the one before it, and the
58
+ last is at most 60000.
59
+
60
+ Keyframe cameras use the scene `center` form: `[longitude, latitude]`, zoom, and optional bearing
61
+ and pitch. The `bounds` form is not accepted, because the camera that fits bounds depends on the
62
+ renderer's viewport and projection, and a shot interpolates between exact cameras.
63
+
64
+ ## Timing and interpolation
65
+
66
+ A shot renders one frame at the start and one every `1 / fps` seconds up to its last keyframe:
67
+ `floor(lastKeyframe.at × fps / 1000) + 1` frames, at most 3600. Frame `i` shows the shot at
68
+ `i × 1000 / fps` milliseconds, so the last keyframe is itself a frame when its time is a whole
69
+ number of frame intervals, as 4000 ms is at 30 frames per second.
70
+
71
+ Between two keyframes the center moves in Web Mercator coordinates, along the shorter way around
72
+ the world when it crosses the antimeridian. Zoom and pitch change linearly, and bearing turns along
73
+ the shorter arc. Easing applies to every value in the same way. A frame exactly at a keyframe shows
74
+ that keyframe's camera.
75
+
76
+ `tileflowCaptureShotFrames` returns the camera and blend position of every frame without rendering
77
+ anything, so tools can inspect a shot before capture:
78
+
79
+ <!-- docs:check -->
80
+
81
+ ```ts
82
+ import {tileflowCaptureShotFrames, type TileflowMapShot} from '@tileflow/core';
83
+
84
+ const shot = {
85
+ theme: 'light',
86
+ viewport: {width: 640, height: 360},
87
+ keyframes: [
88
+ {at: 0, camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12}},
89
+ {at: 1000, camera: {type: 'center', center: [-3.68, 40.42], zoom: 13}},
90
+ ],
91
+ } satisfies TileflowMapShot;
92
+
93
+ const frames = tileflowCaptureShotFrames(shot);
94
+ console.log(frames.length); // 31
95
+ console.log(frames[15]?.camera.zoom); // 12.5
96
+ ```
97
+
98
+ ## Blend themes along a shot
99
+
100
+ Add `blend` to every keyframe, or to none. Each blend lists the same two to eight concrete themes in
101
+ the same order, and a `position` from 0 to one less than the number of themes, as in a
102
+ [theme blend](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md).
103
+ The position moves between keyframes with the same easing as the camera.
104
+
105
+ Capture prepares the blend once with Core's theme controller and moves its position on every frame.
106
+ The controller applies a new position once it differs by at least a four-hundredth of a theme, so a
107
+ very slow blend changes the ground colours every few frames. Labels and icons change at the nearest
108
+ theme, without a cross-fade, because capture renders with reduced motion.
109
+
110
+ ## Render a shot
111
+
112
+ Render committed shots with the CLI. The first command writes
113
+ `.tileflow/captures/shots/dusk-flyover.webm` and its receipt; the second writes numbered PNG frames
114
+ instead:
115
+
116
+ ```sh
117
+ npx tileflow capture --shot dusk-flyover --json
118
+ npx tileflow capture --shot dusk-flyover --format frames --json
119
+ ```
120
+
121
+ Each frame waits until MapLibre has loaded the tiles and resources it needs, so a slow machine or
122
+ network makes the render take longer but never drops or repeats frames. See the
123
+ [capture guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/capture/README.md) for
124
+ the Node API and the
125
+ [capture contract](https://github.com/tileflow/tileflow-sdk/blob/main/docs/contracts/local-visual-capture.md)
126
+ for output paths, encoding, and the shot receipt.
127
+
128
+ ## Play a shot in the browser
129
+
130
+ `playTileflowShot(map, shot, options)` from `@tileflow/core/browser` plays the same shot live. The
131
+ camera follows the keyframes frame by frame, with the same interpolation Capture renders, so the
132
+ page and the video show the same film. This example assumes an existing MapLibre map named `map`,
133
+ a theme controller, and the runtime styles of the blended themes:
134
+
135
+ <!-- docs:check -->
136
+
137
+ ```ts
138
+ import {
139
+ playTileflowShot,
140
+ type TileflowShotMotion,
141
+ type TileflowShotPlayerMap,
142
+ type TileflowThemeController,
143
+ } from '@tileflow/core/browser';
144
+ import type {TileflowRuntimeStyle} from '@tileflow/core/runtime';
145
+
146
+ export function playDuskFlyover(
147
+ map: TileflowShotPlayerMap,
148
+ controller: TileflowThemeController,
149
+ themes: readonly TileflowRuntimeStyle[],
150
+ shot: TileflowShotMotion,
151
+ showTitle: () => void,
152
+ ) {
153
+ return playTileflowShot(map, shot, {
154
+ blend: (position) => void controller.setBlend({themes, position}),
155
+ cues: [{at: 2500, run: showTitle}],
156
+ });
157
+ }
158
+ ```
159
+
160
+ - **Blend.** `blend` receives the blend position at each frame of a shot that blends themes.
161
+ - **Cues.** `cues` run once each, in time order, as the shot reaches them, for application events
162
+ such as drawing a route or showing a title.
163
+ - **Stopping.** `stop()` or an aborted `signal` stops the shot where it is; `finished` resolves
164
+ `true` once the shot reaches its last keyframe and `false` when it was stopped first.
165
+ - **Reduced motion.** With reduced motion requested, the shot shows its last keyframe at once and
166
+ runs every cue in order.
167
+
168
+ `tileflowShotStateAt(shot, at)` returns the camera and blend position at any time, for players of
169
+ one's own.
170
+
171
+ Live playback runs at the browser's pace and does not wait for tiles, unlike a render. Load the
172
+ tiles of the cameras a shot crosses before playing it; see
173
+ [camera moves and the tile cache](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/camera-moves-and-tile-cache.md).
@@ -0,0 +1,65 @@
1
+ # Depth markers
2
+
3
+ DOM markers float above the map: in a 3D scene a pin behind a tower still shows in front of it.
4
+ `createTileflowDepthMarkers` draws markers in the map's 3D scene instead. A MapLibre custom layer
5
+ draws them against the depth the map's extrusions write, so nearer buildings hide them as the camera
6
+ moves.
7
+
8
+ ## Add markers
9
+
10
+ This example assumes an existing MapLibre map named `map` with 3D buildings, and two pictures drawn
11
+ by the application:
12
+
13
+ <!-- docs:check -->
14
+
15
+ ```ts
16
+ import {createTileflowDepthMarkers, type TileflowDepthMarkerMap} from '@tileflow/core/browser';
17
+
18
+ export function pinDestination(
19
+ map: TileflowDepthMarkerMap,
20
+ at: [number, number],
21
+ pin: HTMLCanvasElement,
22
+ ring: HTMLCanvasElement,
23
+ ) {
24
+ const markers = createTileflowDepthMarkers(map);
25
+ // A pin facing the viewer, 28 × 40 CSS pixels, standing on the position.
26
+ markers.add({lngLat: at, image: pin, size: [28, 40], anchor: 'bottom'});
27
+ // A ring lying on the ground, 60 metres across.
28
+ const halo = markers.add({lngLat: at, image: ring, size: [60, 60], alignment: 'ground'});
29
+ return {
30
+ pulse: (opacity: number) => halo.update({opacity}),
31
+ dispose: () => markers.dispose(),
32
+ };
33
+ }
34
+ ```
35
+
36
+ A marker has:
37
+
38
+ - **Position.** `lngLat`, and `altitude` in metres above the ground.
39
+ - **Picture.** `image`: an image, an image bitmap, a canvas or image data. After drawing into a
40
+ canvas again, pass it to `update({image})` so the marker uploads it once more. Markers that share
41
+ a picture share its upload.
42
+ - **Alignment.** `screen` (the default) faces the viewer at `size` in CSS pixels, like a pin.
43
+ `ground` lies flat on the ground at `size` in metres, in the camera's perspective, like a ring or
44
+ an arrow painted on the street.
45
+ - **Anchor and placement.** `anchor` (`center` or `bottom`), `offset` in CSS pixels for screen
46
+ markers, and `rotation` in degrees clockwise: from the screen's up for screen markers, from north
47
+ for ground markers.
48
+ - **Opacity.** From 0 to 1.
49
+ - **`occlude`.** Defaults to true: nearer 3D content hides the marker. Set it to false for a marker
50
+ that must always show.
51
+
52
+ Markers test against the scene's depth without writing to it, so they never hide one another. A
53
+ screen marker takes the depth of its position: a tower in front of the position hides the whole
54
+ pin, a tower in front of part of it hides that part.
55
+
56
+ ## Layer and lifecycle
57
+
58
+ The markers share one custom layer, `tileflow-depth-markers` unless `id` names another, added with
59
+ the first marker. `beforeId` places it under a layer, given as an id or as a function that finds the
60
+ layer again after a style change. After a style change the layer puts itself back, and pictures
61
+ are uploaded again. `dispose()` removes the layer and releases its GPU resources.
62
+
63
+ Depth markers need MapLibre GL JS on WebGL 2 (GL JS 5 or later). They work on globe and Mercator
64
+ projections. They draw pictures only: draw text and effects into the picture, or keep DOM markers
65
+ for interactive content.
@@ -16,9 +16,9 @@ inside a named `maplibreOverlay()` bound to a named Team source.
16
16
 
17
17
  Advanced cartography remains in the same public language: `withRenderStack()`, `renderPass()`, and
18
18
  `refineRenderTarget()` address owner-local semantic targets without exposing physical layer IDs,
19
- sources, source layers, raw filters, or before/after anchors. Typed `field()` references and
20
- `expr.*` builders stay semantic until the compiler's single lowering boundary. There is no recipe
21
- subpath or alternate compiler integration API.
19
+ sources, source layers, raw filters, or physical before/after layer anchors. Typed `field()`
20
+ references and `expr.*` builders stay semantic until the compiler's single lowering boundary. There
21
+ is no recipe subpath or alternate compiler integration API.
22
22
 
23
23
  `mapRevisionSha256` hashes only resolved cartography: the effective design after `extends`, the
24
24
  semantic language, compiler-owned effective contributions, and exact source icon/font identities.