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

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/dist/runtime.d.ts CHANGED
@@ -1 +1 @@
1
- export { z as TileflowAnalytics, $ as TileflowManifestFetchOptions, a0 as TileflowManifestLoadOptions, a1 as TileflowMapMode, a2 as TileflowMapModeOptions, ae as TileflowResolvedRuntimeTheme, aB as TileflowRuntimeCenterLike, a as TileflowRuntimeColorScheme, T as TileflowRuntimeManifest, aC as TileflowRuntimeManifestMap, b as TileflowRuntimeManifestMapEntry, c as TileflowRuntimeManifestTheme, aD as TileflowRuntimeSource, aE as TileflowRuntimeSourceValidation, aF as TileflowRuntimeStyle, aG as TileflowRuntimeStyleOptions, d as TileflowRuntimeSystemThemes, aH as TileflowRuntimeViewOptions, aI as TileflowSessionController, aJ as TileflowSessionGrantResponse, aM as TileflowStyleFontFace, b5 as TileflowThemeBlendSelection, bc as TileflowThemeSelection, bk as assertValidTileflowRuntimeSource, bl as clearTileflowManifestCache, bn as createTileflowSessionController, bo as createTileflowSessionId, bp as defaultTileflowManifestUrl, bq as defaultTileflowRuntimeView, bs as getTileflowAnalyticsApiUrl, bt as getTileflowStyleFontFaces, bu as inferTileflowAnalyticsFromStyleUrl, bv as loadTileflowManifest, bw as mergeTileflowAnalytics, bx as nearestTileflowBlendTheme, by as normalizeTileflowRuntimeCenter, bz as normalizeTileflowStaticImageSize, bX as normalizeTileflowSurfaceId, bA as normalizeTileflowUrl, bB as resolveTileflowAnalyticsRequestUrl, bC as resolveTileflowManifestMap, bD as resolveTileflowMapMode, bE as resolveTileflowRuntimeStyle, bF as resolveTileflowRuntimeTheme, bG as resolveTileflowRuntimeThemeBlend, bH as resolveTileflowRuntimeView, bI as resolveTileflowStaticImageUrl, bJ as shouldLoadTileflowManifest, bK as startTileflowSession, bM as tileflowStyleFontFaceLimits, bN as tileflowStyleFontFacesMetadataKey, bO as tileflowThemeBlendLimits, bR as validateTileflowRuntimeSource, bS as validateTileflowThemeBlendSelection, bT as validateTileflowThemeSelection } from './manifest-types-E7m23eXL.js';
1
+ export { z as TileflowAnalytics, $ as TileflowManifestFetchOptions, a0 as TileflowManifestLoadOptions, a1 as TileflowMapMode, a2 as TileflowMapModeOptions, ae as TileflowResolvedRuntimeTheme, aC as TileflowRuntimeCenterLike, a as TileflowRuntimeColorScheme, T as TileflowRuntimeManifest, aD as TileflowRuntimeManifestMap, b as TileflowRuntimeManifestMapEntry, c as TileflowRuntimeManifestTheme, aE as TileflowRuntimeSource, aF as TileflowRuntimeSourceValidation, aG as TileflowRuntimeStyle, aH as TileflowRuntimeStyleOptions, d as TileflowRuntimeSystemThemes, aI as TileflowRuntimeViewOptions, aJ as TileflowSessionController, aK as TileflowSessionGrantResponse, aN as TileflowStyleFontFace, b6 as TileflowThemeBlendSelection, bd as TileflowThemeSelection, bl as assertValidTileflowRuntimeSource, bm as clearTileflowManifestCache, bo as createTileflowSessionController, bp as createTileflowSessionId, bq as defaultTileflowManifestUrl, br as defaultTileflowRuntimeView, bt as getTileflowAnalyticsApiUrl, bu as getTileflowStyleFontFaces, bv as inferTileflowAnalyticsFromStyleUrl, bw as loadTileflowManifest, bx as mergeTileflowAnalytics, by as nearestTileflowBlendTheme, bz as normalizeTileflowRuntimeCenter, bA as normalizeTileflowStaticImageSize, bY as normalizeTileflowSurfaceId, bB as normalizeTileflowUrl, bC as resolveTileflowAnalyticsRequestUrl, bD as resolveTileflowManifestMap, bE as resolveTileflowMapMode, bF as resolveTileflowRuntimeStyle, bG as resolveTileflowRuntimeTheme, bH as resolveTileflowRuntimeThemeBlend, bI as resolveTileflowRuntimeView, bJ as resolveTileflowStaticImageUrl, bK as shouldLoadTileflowManifest, bL as startTileflowSession, bN as tileflowStyleFontFaceLimits, bO as tileflowStyleFontFacesMetadataKey, bP as tileflowThemeBlendLimits, bS as validateTileflowRuntimeSource, bT as validateTileflowThemeBlendSelection, bU as validateTileflowThemeSelection } from './manifest-types-hmzy2bBX.js';
@@ -0,0 +1,83 @@
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
+ - **Archives.** A source served from a Tileflow PMTiles archive (a `tileflow-pmtiles://` address)
78
+ is read through Core's PMTiles protocol, sharing the archives the page's map has opened. A
79
+ `fetch` of your own replaces both.
80
+
81
+ The reader has no DOM dependency, so it also runs in a module worker: post the tile address and
82
+ zooms to the worker and create the reader there. `decodeTileflowVectorTile(bytes, {layers})`
83
+ decodes tile bytes an application already has.
@@ -89,7 +89,8 @@ TypeScript imports.
89
89
 
90
90
  `icons` is an intentional exception to implicit array composition: omission inherits the exact
91
91
  parent array, any declaration replaces it atomically, and `[]` disables map icons. Compose with a
92
- spread when the child should keep a parent's directories. `fonts` and `glyphs` are mutually
92
+ spread when the child should keep a parent's directories. `models`, the directories of a map's own
93
+ 3D models, follows the same rule. `fonts` and `glyphs` are mutually
93
94
  exclusive text providers; declaring either atomically replaces an inherited provider of either
94
95
  kind. Unknown keys and former compatibility shapes are rejected.
95
96
 
@@ -153,8 +154,17 @@ independent place, water, aerodrome, and POI labels remain under the labels modu
153
154
  Every styling module supports semantic shortcuts and exact semantic targets. Visual style values
154
155
  accept typed token refs, documented `fixed(value, {reason})` values, closed data expressions through
155
156
  `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
157
+ `zoom.exponential(...)`. Refs and fixed leaves also work as the results of typed expressions and
158
+ as zoom-stop values; conditions and other operands take plain values, such as `3` rather than
159
+ `fixed(3)` in a rank test inside a colour expression. Structural controls such as presets,
160
+ visibility, text fields, and zoom gates remain ordinary literals. Expression builders take at most
161
+ 16 branches, stops, or operands; an error names the received count and the config line of the
162
+ call, and a larger lookup composes nested groups.
163
+
164
+ Layout properties, such as text fields, text and icon sizes, and symbol placement, are evaluated at
165
+ each tile's integer zoom: a step at zoom 17.5 takes effect at zoom 18. Paint properties follow the
166
+ camera's exact zoom. `createStyleResult()` reports a `TF_LAYOUT_FRACTIONAL_ZOOM_STEP` warning with
167
+ the semantic target for every layout step at a fractional zoom. Exact
158
168
  controls address stable concepts such as `roads.classes.primary.surface.fill`, not renderer layer
159
169
  IDs.
160
170
 
@@ -0,0 +1,172 @@
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)). `stats.declared` tells whether the map's
46
+ style describes its 3D buildings; a style compiled before this description gets the defaults below.
47
+
48
+ ## Design the buildings in the map
49
+
50
+ The map describes how its 3D buildings look, in its buildings module, and publishes it with its
51
+ style for every theme; the renderer follows theme changes and blends. The design is visible in
52
+ `tileflow preview` and validated with the rest of the map:
53
+
54
+ <!-- docs:check -->
55
+
56
+ ```ts
57
+ import {defineMap, expr, field, refine, refineRenderTarget} from '@tileflow/core';
58
+ import {streets} from '@tileflow/maps';
59
+
60
+ const floors = expr.divide(expr.toNumber(expr.get(field('renderHeight')), 0), 3.2);
61
+
62
+ export default defineMap({
63
+ id: 'lit-city',
64
+ name: 'Lit city',
65
+ version: 1,
66
+ extends: streets,
67
+ modules: {
68
+ buildings: refine({
69
+ threeDimensional: {windows: true, nightLight: 0.6, edgeColor: '#2b2a33'},
70
+ renderStack: {
71
+ // Core's 3D buildings take the colour of the map's 3D building pass.
72
+ tints: refineRenderTarget({
73
+ target: 'buildings.render.extrusion',
74
+ renderer: 'extrusion',
75
+ style: {
76
+ color: expr.step(expr.modulo(floors, 3), '#e8d9c4', [
77
+ [1, '#d6e2d8'],
78
+ [2, '#e5d4e6'],
79
+ ]),
80
+ },
81
+ }),
82
+ },
83
+ }),
84
+ },
85
+ });
86
+ ```
87
+
88
+ - Colour: each building takes the colour the map gives its 3D buildings: Streets' 3D building pass
89
+ (the theme's `buildings.extrusion`), else the buildings module's extrusion, else its roofs. A
90
+ colour that depends on the building's data is evaluated per building and theme when a tile or the
91
+ set of themes changes, and the themes either side of a blend's position mix on the GPU, so a
92
+ moving blend costs nothing per frame.
93
+ - `threeDimensional.shadows` and `occlusion` (the band at the foot of the walls and the darker tight
94
+ corners) default to true. `windows: true` adds invented night windows, lit by rooms; they are
95
+ decoration, not data, and are off by default.
96
+ - `threeDimensional.edges` and `edgeColor` set the [edges](#edges), and `nightLight` the
97
+ [night light](#night-light). Numbers and colours may be theme tokens.
98
+
99
+ ## Light and shadows
100
+
101
+ The light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
102
+ application places by direction or by date and place (see
103
+ [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md)).
104
+ A sunlit roof keeps the building's colour, a wall turned to a low sun glows, and shade takes the
105
+ sky's colour. As the sun sets, shadows lengthen while they fade; a sun below the horizon casts none.
106
+
107
+ A wall sees half the sky, so at night, when the ground gives back little light, walls in shade
108
+ stay darker than roofs. A theme's night `lighting.sun` set high (60° or more) lights roofs more than
109
+ any wall, so roofs and walls read apart under the moon.
110
+
111
+ MapLibre's own layers take no shadows, so the renderer darkens the map it is drawn over: after its
112
+ buildings, it multiplies the ground by the shadows and by a band at the foot of the walls. Labels
113
+ and icons drawn above it keep their colours. Layers kept under 3D content with
114
+ `createTileflowGroundLayers` are darkened with the ground. Other Core 3D layers on the map, such as
115
+ [3D trees](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/trees-3d.md), share
116
+ the buildings' shadows.
117
+
118
+ ## Edges
119
+
120
+ Where one building ends and the next starts stays legible at any distance: each roof's outline and
121
+ each building's outer corners catch the light in a line one CSS pixel wide, whatever the zoom, and
122
+ each building is a little lighter or darker than its colour (up to 6%), so neighbours of one colour
123
+ stay apart. The lines are strongest at night; diagonals inside a roof, inner corners and the cuts at
124
+ a tile's edge draw none. `edges` sets their strength from 0 (none) to 1, the default.
125
+
126
+ `edgeColor` gives the lines a colour of the map's own, per theme through a token: a dark outline as
127
+ on a printed map, or a neon one at night. It is drawn as it is, unlit, at the full `edges` strength
128
+ by day and night, mixed between the themes either side of a blend's position.
129
+
130
+ ## Night light
131
+
132
+ At night (as the sun's `brightness` falls), the buildings light the street from data, quietly by
133
+ default:
134
+
135
+ - Landmarks (World's `building_tone` `destination`) and towers of 40 m or more, one in two, are
136
+ flood-lit at their foot, on the wall and the ground around it.
137
+ - Buildings that hold places busy at night light the windows of their ground floor and spill their
138
+ light a few metres onto the pavement: bars and clubs in a neon of their own or a warm light,
139
+ theatres and cinemas in a bright white, places to eat in amber. A building takes the strongest
140
+ use of its places, nightlife before entertainment before dining. Walls round a courtyard stay
141
+ dark.
142
+
143
+ The night use comes from World's `night_use` on each building. In tiles whose buildings do not
144
+ carry it, the renderer finds it in the tile's places: a place inside a building, or within a few
145
+ metres of its facade, gives the building its use.
146
+
147
+ `nightLight` sets how bright all of this shines, from 0 (none) to 2; it defaults to 1.
148
+
149
+ For the streets themselves, see
150
+ [street lights](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/street-lights.md),
151
+ which draws over the buildings so they hide the lamps behind them; both layers share each tile's
152
+ download.
153
+
154
+ ## Options
155
+
156
+ The renderer's own options say only how it is delivered: `minzoom` (14.5 by default), `maxTiles`,
157
+ `beforeId` to place the layer before another one, and `transformRequest` to apply the map's request
158
+ rules to the tiles it reads.
159
+
160
+ ## Cost
161
+
162
+ The shadow map (up to 4096 × 4096) is drawn again only when the sun, the buildings or their height
163
+ change, not when the camera moves. In a campaign over Miami at 1512 × 860 at 2×, a city lit this
164
+ way kept the frame pacing of the same city drawn without shadows.
165
+
166
+ ## Limits
167
+
168
+ - Mercator only; the layer draws nothing below its `minzoom`.
169
+ - No terrain: buildings stand on flat ground.
170
+ - One building colour per feature and theme; facades, roof shapes and building parts as separate
171
+ shapes are not drawn.
172
+ - A colour expression is evaluated at zoom 16.
@@ -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.