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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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,80 @@
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 `color`, a warm white by default.
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
+ `estimate` chooses where the layer adds light the data does not map as lamps:
56
+
57
+ - `lit` (the default): along the streets and pavements mapped as lit.
58
+ - `streets`: also along streets the data says nothing about, in tiles where the data maps few
59
+ lamps. This invents light, as night windows do, for maps that want every street lit; a tile whose
60
+ data maps its lamps keeps to them.
61
+ - `none`: only mapped lamps shine.
62
+
63
+ Estimated light keeps to the middle of its street and shows no lamp head, so it reads as a lit
64
+ street rather than as lamps that may not stand there.
65
+
66
+ ## Options
67
+
68
+ `intensity` scales the light from 0 to 2 (1 by default). `minzoom` (14 by default) and `maxTiles`
69
+ (25 by default, at most 64) bound where and how far the lights shine. `beforeId` places the layer
70
+ before another one; `source` names a vector source other than `tileflow`; `transformRequest`
71
+ applies the map's request rules to the tiles the layer reads.
72
+
73
+ Layers that read the same source share each tile's download, so the street lights and the 3D
74
+ buildings fetch a tile once between them.
75
+
76
+ ## Limits
77
+
78
+ - Mercator only; the layer draws nothing below its `minzoom`.
79
+ - The light falls on the ground; walls the lamps face are not lit by them.
80
+ - 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/core",
3
- "version": "0.1.0-alpha.32",
3
+ "version": "0.1.0-alpha.33",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Typed Tileflow config and MapLibre style compiler.",
6
6
  "type": "module",
@@ -81,7 +81,10 @@
81
81
  "zod": "^4.4.3"
82
82
  },
83
83
  "devDependencies": {
84
+ "@mapbox/vector-tile": "3.0.0",
85
+ "earcut": "3.2.4",
84
86
  "maplibre-contour": "0.1.0",
87
+ "pbf": "5.1.2",
85
88
  "pmtiles": "4.4.0",
86
89
  "whatwg-url": "15.1.0"
87
90
  },