@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.
@@ -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.
@@ -0,0 +1,191 @@
1
+ # Route drawing
2
+
3
+ An application that draws a route on a Tileflow map can take the route's colours from the theme and
4
+ draw the line on with a moving head. Themes set route inks; the theme controller returns them for
5
+ the theme on screen; `createTileflowLineProgress` animates a group of line layers along their
6
+ length; `createTileflowGroundLayers` keeps them on the ground, under the map's 3D buildings.
7
+
8
+ ## Set route inks in a theme
9
+
10
+ Route inks are optional theme tokens. A theme that sets none uses defaults for its colour scheme:
11
+ a blue line on a dark edge for light themes, a lighter blue with a glow and a bright core for dark
12
+ themes.
13
+
14
+ | Token | Kind | Use |
15
+ | -------------------- | ------ | ----------------------------------------------------- |
16
+ | `route.line` | colour | The chosen route. |
17
+ | `route.casing` | colour | The edge each side of the line, apart from the roads. |
18
+ | `route.core` | colour | A thin line along the middle. |
19
+ | `route.glow` | colour | A soft halo around the route. |
20
+ | `route.alternative` | colour | Other routes offered beside the chosen one. |
21
+ | `route.glow.opacity` | number | The halo's opacity, from 0 (none) to 1. |
22
+ | `route.core.opacity` | number | The core's opacity, from 0 (none) to 1. |
23
+
24
+ Every theme of a map carries the same tokens, so a map that sets route inks sets them in each of
25
+ its themes:
26
+
27
+ <!-- docs:check -->
28
+
29
+ ```ts
30
+ import {defineTheme, type TileflowTheme} from '@tileflow/core';
31
+
32
+ export function withNeonRoute(base: TileflowTheme, id: string): TileflowTheme {
33
+ return defineTheme(base, {
34
+ id,
35
+ version: 1,
36
+ colorScheme: base.colorScheme,
37
+ tokens: {
38
+ color: {
39
+ 'route.line': '#ff4fae',
40
+ 'route.casing': '#0b0f12',
41
+ 'route.core': '#ffe0f0',
42
+ 'route.glow': '#ff4fae',
43
+ },
44
+ number: {'route.glow.opacity': 0.55, 'route.core.opacity': 0.9},
45
+ },
46
+ });
47
+ }
48
+ ```
49
+
50
+ The compiler publishes the resolved inks in the style's `tileflow:route` metadata.
51
+ `readTileflowRouteInks(style.metadata)` reads them from a style document;
52
+ `tileflowStyleRouteInks(style)` also falls back on the defaults for a style compiled without them.
53
+
54
+ ## Draw a route in the theme's inks
55
+
56
+ The theme controller's `getRouteInks()` returns the inks of the theme on screen. During a blend it
57
+ mixes them between the neighbouring themes at the blend's position, in the same colour space as
58
+ the map's own colours. Read them again after `setTheme` or `setBlend`.
59
+
60
+ `createTileflowLineProgress(map, {layers})` owns the `line-gradient` and `line-opacity` of a group of
61
+ line layers. Their source must be GeoJSON with `lineMetrics: true`. The controller shows the line
62
+ from `start` to `end` (shares of its length), a highlight at the head while the line is being drawn,
63
+ and an optional light at `light`. It writes only what changed, paints a layer again when a style
64
+ change re-creates it, and skips layers that are not on the map.
65
+
66
+ This example assumes an existing MapLibre map named `map` whose style already holds four line
67
+ layers over a GeoJSON source with `lineMetrics: true`:
68
+
69
+ <!-- docs:check -->
70
+
71
+ ```ts
72
+ import {
73
+ createTileflowLineProgress,
74
+ type TileflowLineProgressMap,
75
+ type TileflowRouteInks,
76
+ } from '@tileflow/core/browser';
77
+
78
+ const layers = (inks: TileflowRouteInks) => [
79
+ {id: 'route-glow', color: inks.glow, highlight: '#ffffff', opacity: inks.glowOpacity},
80
+ {id: 'route-casing', color: inks.casing},
81
+ {id: 'route-line', color: inks.line, highlight: inks.core},
82
+ {id: 'route-core', color: inks.core, highlight: '#ffffff', opacity: inks.coreOpacity},
83
+ ];
84
+
85
+ export async function drawRoute(
86
+ map: TileflowLineProgressMap,
87
+ inks: TileflowRouteInks,
88
+ lengthInMetres: number,
89
+ ) {
90
+ const route = createTileflowLineProgress(map, {
91
+ layers: layers(inks),
92
+ // A 90 m highlight, whatever the route's length.
93
+ headLength: Math.min(0.2, 90 / lengthInMetres),
94
+ initial: {end: 0},
95
+ });
96
+ await route.animate({to: {end: 1}, duration: 2500});
97
+ // A light runs along the drawn route.
98
+ await route.animate({to: {light: 1}, duration: 1500});
99
+ route.set({light: undefined});
100
+ return {
101
+ recolour: (next: TileflowRouteInks) => route.configure({layers: layers(next)}),
102
+ fadeOut: () => route.animate({to: {opacity: 0}, duration: 400}),
103
+ dispose: () => route.dispose(),
104
+ };
105
+ }
106
+ ```
107
+
108
+ `animate` resolves `true` when the line gets where it was sent, and `false` when `set`, another
109
+ animation, or `dispose` stops it first; the line stays where it was stopped. A browser preference
110
+ for reduced motion shows the target at once. `tileflowLineProgressGradient(state, options)` returns
111
+ the same gradient for a single state, for styles built ahead of time.
112
+
113
+ ## Keep a route under 3D buildings
114
+
115
+ MapLibre draws a 2D layer that comes after a 3D layer, such as extruded buildings or a custom layer
116
+ drawn in 3D, without testing depth. A route added under the labels therefore crosses in front of
117
+ the buildings. A route that lies on the ground goes before the first 3D layer: every building then
118
+ hides it exactly where it stands in front of it.
119
+
120
+ `tileflowGroundBeforeId(map)` returns that layer: the first 3D layer above the roads, or the start
121
+ of the vegetation when the buildings are flat, so a ground layer still covers the roads and the
122
+ flat buildings. Pass it as `beforeId` to `map.addLayer` for a single layer.
123
+
124
+ `createTileflowGroundLayers(map, {layers, occludedOpacity})` keeps a group of layers there, bottom
125
+ to top as given:
126
+
127
+ - It adds the layers, or moves those already on the map. A layer whose source is not on the map yet
128
+ is added when the source is.
129
+ - After a style change, it brings back the layers the change dropped, with the paint and layout
130
+ last written through the group.
131
+ - When a 3D layer lands above the group, such as a custom layer added later, the group moves under
132
+ it.
133
+ - `occludedOpacity` lets a line show through 3D content: by layer id, how much of the line shows
134
+ where buildings hide it, from 0 (nothing, the default) to 1. The group draws a copy of the line
135
+ over the 3D content at the line's opacity times that share, as a navigation app shows a route
136
+ through buildings. The copy lies over the group's other layers too, so where the line shows, it
137
+ tints the layers drawn above the line slightly; a line at the top of the group shows no change.
138
+ Only line layers whose `line-opacity` is a number can show through.
139
+
140
+ Paint the group's layers through the group, so the copies follow them. A helper that paints layers
141
+ takes the group in place of the map:
142
+
143
+ <!-- docs:check -->
144
+
145
+ ```ts
146
+ import {
147
+ createTileflowGroundLayers,
148
+ createTileflowLineProgress,
149
+ type TileflowGroundLayersMap,
150
+ type TileflowRouteInks,
151
+ } from '@tileflow/core/browser';
152
+
153
+ export function addRoute(map: TileflowGroundLayersMap, inks: TileflowRouteInks) {
154
+ // The map already holds a GeoJSON source named 'route' with `lineMetrics: true`.
155
+ const line = (id: string, width: number) => ({
156
+ id,
157
+ type: 'line',
158
+ source: 'route',
159
+ layout: {'line-cap': 'round', 'line-join': 'round'},
160
+ paint: {'line-width': width},
161
+ });
162
+ const ground = createTileflowGroundLayers(map, {
163
+ layers: [line('route-casing', 12), line('route-line', 8), line('route-core', 2)],
164
+ occludedOpacity: {'route-line': 0.35},
165
+ });
166
+ const route = createTileflowLineProgress(ground, {
167
+ layers: [
168
+ {id: 'route-casing', color: inks.casing},
169
+ {id: 'route-line', color: inks.line, highlight: inks.core},
170
+ {id: 'route-core', color: inks.core, opacity: inks.coreOpacity},
171
+ ],
172
+ initial: {end: 0},
173
+ });
174
+ return {
175
+ draw: () => route.animate({to: {end: 1}, duration: 2500}),
176
+ remove: () => {
177
+ route.dispose();
178
+ ground.dispose();
179
+ },
180
+ };
181
+ }
182
+ ```
183
+
184
+ `dispose` takes the group's layers and copies off the map. The copies keep the layer's id with
185
+ `:occluded` after it.
186
+
187
+ ## Cost
188
+
189
+ Each frame of an animation writes one `line-gradient` per layer, and MapLibre rebuilds that layer's
190
+ gradient texture. A route of a few layers costs little; avoid animating dozens of long lines at
191
+ once.
@@ -0,0 +1,107 @@
1
+ # Street lights
2
+
3
+ `createTileflowStreetLights` lights a Tileflow map's streets at night from its own vector tiles. The
4
+ lamps the map's data holds shine where they stand, each with a pool of light on the ground and a
5
+ glowing head. Streets and pavements the data maps as lit, but whose lamps it does not hold, take a
6
+ soft light along them that claims no lamp's place. As the theme controller's sun darkens the map,
7
+ the lights switch on one by one; they go out as day comes.
8
+
9
+ It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
10
+ labels. When `createTileflowBuildings3D` raises the buildings, they hide the lamps behind them. It
11
+ needs WebGL2 and the Mercator projection, and shines from zoom 14 by default.
12
+
13
+ ## Light the streets
14
+
15
+ This example assumes an existing MapLibre map showing a Tileflow map whose vector source is called
16
+ `tileflow`, as Tileflow's maps call World, and a theme controller for it:
17
+
18
+ <!-- docs:check -->
19
+
20
+ ```ts
21
+ import {
22
+ createTileflowStreetLights,
23
+ type TileflowStreetLightsMap,
24
+ type TileflowThemeController,
25
+ } from '@tileflow/core/browser';
26
+
27
+ export function lightTheStreets(map: TileflowStreetLightsMap, controller: TileflowThemeController) {
28
+ // The lights switch on as the theme's or the application's sun darkens the map.
29
+ return createTileflowStreetLights({map, controller});
30
+ }
31
+ ```
32
+
33
+ The lights follow `controller.getSun()`: they switch on as its `brightness` falls through dusk, each
34
+ lamp at its own moment, and are all on at night. A theme whose night has no `lighting.sun` keeps
35
+ the default daylight sun, so its lights stay off; see
36
+ [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md).
37
+ `on` reports how far they have switched on, from 0 to 1, `stats` the lamps on screen, and
38
+ `dispose()` takes the layer off the map.
39
+
40
+ ## What the light comes from
41
+
42
+ The layer reads the source's z15 tiles round the camera, the zoom at which World carries its street
43
+ detail:
44
+
45
+ - Lamps from World's `street_furniture` points whose `subclass` is `street_lamp`. Where the data
46
+ gives them, a lamp's `height`, `lamp_mount`, `light_count`, `lamp_type` and `light_colour` set how
47
+ high it stands, how far its light reaches and its colour: sodium lamps shine orange, LED and
48
+ metal-halide lamps white. Other lamps take the map's light colour.
49
+ - Streets in World's `transportation` layer, and pavements in its `sidewalk` layer, that the data
50
+ maps as lit (`lit`). A street mapped as unlit never takes light.
51
+
52
+ How much of this a city's data holds varies widely: some map most of their lamps or most of their
53
+ lit streets, others almost none. Where the data says nothing, the streets stay dark.
54
+
55
+ ## Design the lights in the map
56
+
57
+ The map describes its street lights in its roads module, next to the other details drawn from
58
+ street furniture, and publishes them with its style for every theme; `stats.declared` tells whether
59
+ it does. Without a description the lights take the defaults below:
60
+
61
+ <!-- docs:check -->
62
+
63
+ ```ts
64
+ import {defineMap, refine} from '@tileflow/core';
65
+ import {streets} from '@tileflow/maps';
66
+
67
+ export default defineMap({
68
+ id: 'night-city',
69
+ name: 'Night city',
70
+ version: 1,
71
+ extends: streets,
72
+ modules: {
73
+ // Sodium-orange light, a little brighter, on every street of tiles that map few lamps.
74
+ roads: refine({streetLights: {color: '#ffb35c', intensity: 1.3, estimate: 'streets'}}),
75
+ },
76
+ });
77
+ ```
78
+
79
+ `estimate` chooses where the layer adds light the data does not map as lamps:
80
+
81
+ - `lit` (the default): along the streets and pavements mapped as lit.
82
+ - `streets`: also along streets the data says nothing about, in tiles where the data maps few
83
+ lamps. This invents light, as night windows do, for maps that want every street lit; a tile whose
84
+ data maps its lamps keeps to them.
85
+ - `none`: only mapped lamps shine.
86
+
87
+ Estimated light keeps to the middle of its street and shows no lamp head, so it reads as a lit
88
+ street rather than as lamps that may not stand there.
89
+
90
+ `color` is the light of the lamps whose data gives no colour, a warm white by default, and
91
+ `intensity` scales the light from 0 to 2 (1 by default); both may be theme tokens and mix during a
92
+ blend. `streetLights: {enabled: false}` gives the map none.
93
+
94
+ ## Options
95
+
96
+ The layer's own options say only how it is delivered: `minzoom` (14 by default) and `maxTiles` (25
97
+ by default, at most 64) bound where and how far the lights shine, `beforeId` places the layer before
98
+ another one, and `transformRequest` applies the map's request rules to the tiles it reads.
99
+
100
+ Layers that read the same source share each tile's download, so the street lights and the 3D
101
+ buildings fetch a tile once between them.
102
+
103
+ ## Limits
104
+
105
+ - Mercator only; the layer draws nothing below its `minzoom`.
106
+ - The light falls on the ground; walls the lamps face are not lit by them.
107
+ - No terrain: lamps stand on flat ground.
@@ -0,0 +1,125 @@
1
+ # Sun and light
2
+
3
+ A theme can describe its sun and the light from its sky, as a light preset for that theme: a high
4
+ sun in white light for a day theme, a low amber sun in the west for an evening theme, a cold moon
5
+ with faint shadows for a night theme. In a browser, the theme controller returns the sun the map
6
+ shows, mixed between neighbouring themes during a blend, so an application's own 3D layers can light
7
+ their models and cast their shadows from it. An application can also place the sun itself, by
8
+ direction or by the real position of the sun for a date and place.
9
+
10
+ MapLibre lights its `fill-extrusion` layers with one directional light and casts no shadows. When a
11
+ theme describes a sun, the map's light is anchored to the map and points from the sun, so Tileflow's
12
+ 3D buildings are shaded from where it stands. Shadows, ambient occlusion and lit windows are for the
13
+ application's own WebGL layers to draw, from the values `getSun()` returns.
14
+
15
+ ## Describe a theme's sun
16
+
17
+ Set `lighting.sun` and `lighting.ambient` in the theme. Every value is optional; unset values take
18
+ the defaults in `tileflowDefaultSunPreset` (azimuth 210°, altitude 60°, white light at 0.5, full
19
+ shadows, a white sky at 0.5):
20
+
21
+ <!-- docs:check -->
22
+
23
+ ```ts
24
+ import {defineTheme} from '@tileflow/core';
25
+ import {streetsThemes} from '@tileflow/maps';
26
+
27
+ export const evening = defineTheme(streetsThemes.dark, {
28
+ id: 'city-evening',
29
+ version: 1,
30
+ colorScheme: 'dark',
31
+ lighting: {
32
+ // Degrees clockwise from north, and above the horizon.
33
+ sun: {azimuth: 285, altitude: 9, color: '#ff9a50', intensity: 1, shadowIntensity: 0.9},
34
+ ambient: {color: '#5a4290', intensity: 0.3},
35
+ },
36
+ });
37
+ ```
38
+
39
+ `intensity` values run from 0 to 1. `shadowIntensity` says how dark the shadows the sun casts fall,
40
+ from 0 (none) to 1. A theme derived from another changes single values of its sun and sky light; the
41
+ others are inherited. The compiled style carries the resolved sun in its metadata
42
+ (`tileflow:sun`) and a map-anchored `light` that points from it; MapLibre's `anchor` and the
43
+ direction in `lighting.position` no longer apply to a theme with a sun. A theme without `sun` or
44
+ `ambient` compiles exactly as before.
45
+
46
+ ## Read the sun in an application
47
+
48
+ `themeController.getSun()` returns the sun the map shows:
49
+
50
+ - `azimuth` and `altitude`, in degrees, and `direction`, a unit vector towards the sun with x east,
51
+ y north and z up.
52
+ - `color` and `rgb` (sRGB channels from 0 to 1) of the direct light, and its `intensity`, which fades
53
+ to 0 over the sun's last degrees above the horizon: a sun below it lights nothing.
54
+ - `ambient`, the light from the sky, with its own `color`, `rgb` and `intensity`.
55
+ - `shadowIntensity`, how dark the sun's shadows fall.
56
+ - `brightness`, how bright flat, open ground is under this light, from 0 to 1: the sky's light plus
57
+ the sun's at its height, each weighted by its colour's luminance. Use it to switch lamps or windows
58
+ on as a scene darkens, whatever theme or sun lights it.
59
+ - `source`: `theme`, `direction`, or `date`.
60
+
61
+ During a blend the sun is mixed between the neighbouring themes' suns: the azimuth turns the short
62
+ way round, colours mix in OKLab as the map's colours do, and numbers mix linearly. Read it when a
63
+ custom layer draws; the map redraws whenever the sun changes.
64
+
65
+ ## Place the sun
66
+
67
+ `setSun` places the sun by direction, or by the real sun of a date over a place given as
68
+ `[longitude, latitude]`, the map's centre when it is left out. The themes keep the sun's colours,
69
+ strengths and shadow strength; only its direction changes. `setSun(undefined)` returns it to the
70
+ themes. This example assumes an existing MapLibre map and a controller for it:
71
+
72
+ <!-- docs:check -->
73
+
74
+ ```ts
75
+ import type {TileflowThemeController} from '@tileflow/core/browser';
76
+
77
+ export function followTheRealSun(controller: TileflowThemeController) {
78
+ // Where the sun stands over Miami Beach now; a later call moves it on.
79
+ void controller.setSun({date: Date.now(), location: [-80.13, 25.79]});
80
+ }
81
+
82
+ export function lowWesternSun(controller: TileflowThemeController) {
83
+ // Turns the short way round to the west over a second.
84
+ return controller.setSun({azimuth: 270, altitude: 8}, {transition: {duration: 1000}});
85
+ }
86
+ ```
87
+
88
+ A request that follows an animation, such as a clock that moves every frame, passes no transition.
89
+ A request with a transition supersedes one still in progress, which resolves with
90
+ `status: 'superseded'`; a browser preference for reduced motion changes at once.
91
+
92
+ `tileflowSunPosition(date, [longitude, latitude])` returns the sun's azimuth and apparent altitude,
93
+ with atmospheric refraction, from NOAA's solar equations (accurate to about a hundredth of a degree
94
+ for years 1800 to 2200). It runs anywhere, without a map:
95
+
96
+ <!-- docs:check -->
97
+
98
+ ```ts
99
+ import {tileflowSunPosition} from '@tileflow/core/browser';
100
+
101
+ // Solar noon in Miami at the June solstice, 17:22 UTC: the sun stands about 87.7° high, due south.
102
+ export const noon = tileflowSunPosition(Date.UTC(2026, 5, 21, 17, 22), [-80.19, 25.77]);
103
+ ```
104
+
105
+ ## Light a custom layer
106
+
107
+ For a custom WebGL layer that draws 3D content, light each surface with the sky's light, plus the
108
+ sun's where the surface faces it and nothing stands in the way:
109
+
110
+ light = ambient.rgb × ambient.intensity + rgb × intensity × max(normal · direction, 0) × (1 − shadowIntensity × shadowed)
111
+
112
+ Convert the colours to linear light before adding them and back to sRGB after. MapLibre's own 2D
113
+ layers take no shadows, so a layer that casts shadows on the ground darkens the map it is drawn over:
114
+ draw the ground under the scene, after the scene's own geometry, with multiplying blending and a
115
+ depth test against it.
116
+
117
+ ## Limits
118
+
119
+ - MapLibre shades extrusions with its one light; it has no shadow maps, ambient occlusion or
120
+ emissive light. The sun gives custom layers what they need to draw them.
121
+ - Themes interpolate the sun linearly between presets; a request by date follows the real sun, not
122
+ the themes' presets.
123
+ - Style expressions cannot read the sun yet. An application that wants a theme to react to it sets a
124
+ [style state](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/style-states.md)
125
+ from `getSun().brightness`.
@@ -15,6 +15,13 @@ same token keys, so changing a theme can never change map structure or silently
15
15
  role. `defineTheme(base, definition)` is an authoring convenience that returns a fully materialized
16
16
  document; it does not leave an inheritance edge in the resolved config.
17
17
 
18
+ Some optional tokens are for the application rather than the map's own layers. Route inks
19
+ (`route.line`, `route.casing`, `route.core`, `route.glow`, `route.alternative`,
20
+ `route.glow.opacity` and `route.core.opacity`) set how an application draws its routes; a theme
21
+ without them uses its colour scheme's defaults. The compiler publishes them in the style's metadata
22
+ and the theme controller returns them; see
23
+ [route drawing](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/route-drawing.md).
24
+
18
25
  The key in the `themes` record is its concrete runtime selector and output-path segment. The
19
26
  theme's own `id` and `version` are its editorial identity and build provenance, so they need not
20
27
  equal that key. Concrete keys may be names such as `day` and `night`; every `systemThemes.light` or
@@ -0,0 +1,211 @@
1
+ # 3D trees
2
+
3
+ `createTileflowTrees3D` stands the trees of a Tileflow map's vector tiles round the camera as 3D
4
+ trees, lit by the theme controller's sun. Each tree takes the form and the size its data gives:
5
+ palms, conifers, columns and three broadleaf forms, at their mapped height and crown where the data
6
+ has them. Trees cast shadows on the ground and, with `createTileflowBuildings3D`, on the buildings,
7
+ and take the buildings' shadows; nearer buildings hide them.
8
+
9
+ The map describes how its trees look; the renderer only stands them. A map that declares
10
+ `vegetation({mode: '3d'})`, as Streets does, publishes its tree colours, scales and forms in the
11
+ style it compiles, for every theme, and the renderer follows theme changes and blends.
12
+
13
+ It is a custom WebGL2 layer for MapLibre GL JS, drawn above the map's buildings and below its
14
+ labels. It needs WebGL2 and the Mercator projection, and the trees stand from zoom 15 by default.
15
+ Elsewhere (Capture, Static Maps, Native, or a browser without WebGL2) the map draws the vegetation
16
+ module's flat trees.
17
+
18
+ ## Stand the trees
19
+
20
+ This example assumes an existing MapLibre map showing a Tileflow map whose vector source is called
21
+ `tileflow`, as Tileflow's maps call World, and a theme controller for it:
22
+
23
+ <!-- docs:check -->
24
+
25
+ ```ts
26
+ import {
27
+ createTileflowTrees3D,
28
+ type TileflowThemeController,
29
+ type TileflowTrees3DMap,
30
+ } from '@tileflow/core/browser';
31
+
32
+ export function standTheTrees(map: TileflowTrees3DMap, controller: TileflowThemeController) {
33
+ const trees = createTileflowTrees3D({map, controller});
34
+ // The trees grow out of the ground over a second.
35
+ void trees.show({transition: {duration: 1000}});
36
+ return trees;
37
+ }
38
+ ```
39
+
40
+ `show()` grows the trees, `hide()` sinks them back into the ground, and `grow(scale)` eases them to
41
+ a share of their size, from 0 to 1. Each returns a promise that resolves once the motion ends; a
42
+ newer call takes over from where the trees stand. A browser preference for reduced motion changes at
43
+ once. `dispose()` takes the layer off the map.
44
+
45
+ While they stand, the 3D trees stand in for the map's flat trees: the layers the vegetation module
46
+ draws them with are hidden when the trees are shown, and come back as they were once the trees have
47
+ sunk or the layer leaves the map.
48
+
49
+ `stats.declared` tells whether the map's style describes 3D trees. When it is false the layer draws
50
+ nothing: declare `vegetation({mode: '3d'})` in the map. `stats` also counts the tiles, the trees
51
+ placed and the trees drawn in the last frame.
52
+
53
+ ## What the trees come from
54
+
55
+ The renderer reads the source's z15 tiles round the camera:
56
+
57
+ - World's `tree` layer: the trees OpenStreetMap maps one by one, as points, and its rows of trees,
58
+ as lines, along which a tree stands every 8 metres. A tree's `leaf_type`, `genus`, `species` and
59
+ `taxon` choose its form; its `height` and `diameter_crown` its size, and a trunk's
60
+ `circumference` sizes a tree with neither. A row's attributes apply to each of its trees.
61
+ - World's `canopy` layer, where the data has it: decorative points in urban parks and gardens, each
62
+ standing one to three trees by its canopy cover, inside the room it leaves before a path, a
63
+ building or the park's edge. They stand for measured canopy, not for mapped trees.
64
+
65
+ | Form | From the data |
66
+ | ---------- | ---------------------------------------------------------------------------- |
67
+ | `palm` | `leaf_type` `palm`, `taxon` `Arecaceae`, or a palm genus such as `Roystonea` |
68
+ | `columnar` | the cypress genera `Cupressus`, `Calocedrus` and `Thuja` |
69
+ | `conifer` | `leaf_type` `needleleaved`, or a conifer genus such as `Pinus` or `Cedrus` |
70
+ | `open` | spreading genera such as `Quercus`, `Olea`, `Ficus` or `Jacaranda` |
71
+ | `tall` | upright genera such as `Platanus`, `Populus`, `Ulmus` or `Ginkgo` |
72
+ | `round` | rounded genera such as `Acer`, `Tilia`, `Aesculus` or `Prunus` |
73
+
74
+ A tree whose data gives no form, and every canopy tree, takes one of the three broadleaf forms; a
75
+ tree the data calls broadleaved keeps to them. Each tree's form, size, turn and shade come from a
76
+ stable hash of where it stands, so it looks the same at every zoom and on every device. Points in a
77
+ tile's buffer belong to the neighbouring tile, so no tree stands twice.
78
+
79
+ ## Design the trees in the map
80
+
81
+ Colours come from the theme: `vegetation.tree.bark`, `vegetation.tree.broadleaf.a` to `.d`,
82
+ `vegetation.tree.conifer.a` to `.c` and `vegetation.tree.palm.a` to `.c`, sunlit, from darkest to
83
+ lightest. A crown facing a sunlit sky takes its colour; shade takes the sky's light, as on 3D
84
+ buildings. The vegetation module's `threeDimensional` options refine them for one map:
85
+
86
+ <!-- docs:check -->
87
+
88
+ ```ts
89
+ import {defineMap, token, vegetation} from '@tileflow/core';
90
+ import {streets} from '@tileflow/maps';
91
+
92
+ export default defineMap({
93
+ id: 'subtropical-city',
94
+ name: 'Subtropical city',
95
+ version: 1,
96
+ extends: streets,
97
+ modules: {
98
+ vegetation: vegetation({
99
+ mode: '3d',
100
+ threeDimensional: {
101
+ barkColor: token.color('vegetation.tree.bark'),
102
+ heightScale: 1.1,
103
+ finish: 'toon',
104
+ edges: 1,
105
+ // Along this coast most trees the data does not describe are palms.
106
+ regions: [{bounds: [-81.0, 24.4, -79.8, 26.9], unknownForms: {palm: 3, round: 1, open: 1}}],
107
+ },
108
+ }),
109
+ },
110
+ });
111
+ ```
112
+
113
+ `finish` sets how trees take the light: `faceted` (the default), low-poly facets softened towards a
114
+ round crown; `smooth`, round crowns; or `toon`, three bands of light and a crisp shadow, as a drawn
115
+ tree has them. `edges` rings each tree with an outline a constant width on screen, from 0 (none,
116
+ the default) to 1, in `edgeColor` or the crown's darkest colour deepened; buildings use the same
117
+ two words for their outlines.
118
+
119
+ `heightScale` and `crownScale` stretch every tree, and `forms` each form on top of them, such as
120
+ `forms: {palm: {heightScale: 1.2}}` for taller palms. `unknownForms` sets, as relative weights, the
121
+ forms of trees whose data gives none, and `regions` sets them inside `[west, south, east, north]`
122
+ bounds in degrees, the first region holding a tree applying. They are a choice of look, not data:
123
+ they never change a tree whose data gives its form, and as mapping improves they apply to fewer
124
+ trees.
125
+
126
+ ## Your own trees
127
+
128
+ A map can draw a form with its own models. Put glTF binaries (`.glb`) in a directory, declare it in
129
+ the map's `models`, and name the models a form draws by their file names:
130
+
131
+ <!-- docs:check -->
132
+
133
+ ```ts
134
+ import {defineMap, vegetation} from '@tileflow/core';
135
+ import {streets} from '@tileflow/maps';
136
+
137
+ export default defineMap({
138
+ id: 'palm-coast',
139
+ name: 'Palm coast',
140
+ version: 1,
141
+ extends: streets,
142
+ // ./models holds sabal-palm.glb and royal-palm.glb, and sabal-palm.far.glb for far trees.
143
+ models: ['./models'],
144
+ modules: {
145
+ vegetation: vegetation({
146
+ mode: '3d',
147
+ threeDimensional: {
148
+ forms: {palm: {models: ['sabal-palm', 'royal-palm'], heightScale: 1.1}},
149
+ },
150
+ }),
151
+ },
152
+ });
153
+ ```
154
+
155
+ Each tree of the form draws one of its models, one to four, by its stable hash, so a palm keeps its
156
+ model wherever it is read. The data still chooses each tree's form and size: a model replaces how a
157
+ form looks, not which trees take it.
158
+
159
+ How to prepare a model:
160
+
161
+ - **Materials name the parts.** Name each material `bark`, `leaves`, `fronds` or `stem` (an
162
+ exporter's `.001` suffix is ignored). Each part takes its theme colours, as Core's trees do; the
163
+ material's colour and any vertex colours set how light or dark each part of the model is within
164
+ them, so the model follows the theme from day to night.
165
+ - **The origin is where the trunk meets the ground**, with the tree above it. Tileflow stands the
166
+ model as a unit tree: its top is the tree's height, and its widest leaf half its crown.
167
+ - **Budgets.** A model has at most 1,000 triangles and 4,096 vertices; `<id>.far.glb`, drawn
168
+ beyond the nearest tiles, at most 120 triangles. Without a far model, the form's own far tree
169
+ stands in. A map draws at most 16 models.
170
+ - **What is read.** Triangles of the default scene with their node transforms; textures,
171
+ animations and compressed geometry (Draco or meshopt) are not. Export without compression.
172
+
173
+ `tileflow preview` and every build read the models, report a problem with the file and what to
174
+ change, and watch the directories. The models travel inside the compiled style, so they need no
175
+ files of their own when the map is deployed. A later directory's model replaces an earlier one of
176
+ the same name; omitting `models` inherits them, and `[]` declares none.
177
+
178
+ ## Light and shadows
179
+
180
+ The light comes from `controller.getSun()`: the theme's sun, mixed during a blend, or the sun an
181
+ application places by direction or by date and place (see
182
+ [sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md)).
183
+ Leaves take light round their edges and glow a little against a low sun; the underside and the
184
+ inside of a crown stay darker.
185
+
186
+ Trees and 3D buildings on one map share one shadow map: trees cast shadows on the ground and on the
187
+ buildings, and stand in the buildings' shade. The flat map darkens under both. Without 3D buildings
188
+ the trees darken the map themselves.
189
+
190
+ ## Options
191
+
192
+ `minzoom` (15 by default) and `maxTiles` (25 by default, at most 64) bound where and how far trees
193
+ stand; trees thin out and fade with distance from the map's centre, and near trees take more detail
194
+ than far ones. `canopySource` names a vector source with canopy points when they are not in the
195
+ map's World source. `beforeId` places the layer before another one; `transformRequest` applies the
196
+ map's request rules to the tiles the layer reads. Layers that read the same source share each
197
+ tile's download.
198
+
199
+ ## Cost
200
+
201
+ A near tree has 60 to 120 triangles and a far one about 20, and each form is one instanced draw per
202
+ tile. The shadow map is drawn again only when the sun, the trees on screen or their size change, not
203
+ when the camera moves. Over the Champ de Mars in Paris, where OpenStreetMap maps every tree, about
204
+ 13,000 trees stand in a pitched view at zoom 16.6.
205
+
206
+ ## Limits
207
+
208
+ - Mercator only; the layer draws nothing below its `minzoom`.
209
+ - No terrain: trees stand on flat ground.
210
+ - No wind or seasons; one form per tree.
211
+ - Models replace a form's look; a model per genus or species is not available.