@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/README.md +36 -1
- package/THIRD_PARTY_NOTICES.md +25 -2
- package/dist/browser.d.ts +1217 -1
- package/dist/browser.js +10339 -2849
- package/dist/build.d.ts +15 -6
- package/dist/build.js +1234 -427
- package/dist/{capture-scene-BaAJjGq7.d.ts → capture-shot-ChPQvPyi.d.ts} +121 -1
- package/dist/capture.d.ts +1 -1
- package/dist/capture.js +218 -0
- package/dist/config-reference.json +2646 -766
- package/dist/{icon-composition-Befmn-zZ.d.ts → icon-composition-DqoZWQuz.d.ts} +318 -3
- package/dist/index.d.ts +122 -7
- package/dist/index.js +2533 -985
- package/dist/{manifest-types-E7m23eXL.d.ts → manifest-types-hmzy2bBX.d.ts} +20 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/native.js +1893 -1884
- package/dist/runtime.d.ts +1 -1
- package/docs/application-tile-reading.md +83 -0
- package/docs/authoring-model.md +13 -3
- package/docs/buildings-3d.md +172 -0
- package/docs/camera-moves-and-tile-cache.md +112 -0
- package/docs/capture-scenes.md +4 -0
- package/docs/capture-shots.md +173 -0
- package/docs/depth-markers.md +65 -0
- package/docs/public-api-and-browser-subpath.md +3 -3
- package/docs/route-drawing.md +191 -0
- package/docs/street-lights.md +107 -0
- package/docs/sun-and-light.md +125 -0
- package/docs/themes-and-module-styles.md +7 -0
- package/docs/trees-3d.md +211 -0
- package/package.json +4 -1
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,
|
|
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.
|
package/docs/authoring-model.md
CHANGED
|
@@ -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. `
|
|
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
|
|
157
|
-
|
|
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.
|
package/docs/capture-scenes.md
CHANGED
|
@@ -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.
|