@tileflow/core 0.1.0-alpha.4 → 0.1.0-alpha.41
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/LICENSE +201 -0
- package/README.md +318 -26
- package/THIRD_PARTY_NOTICES.md +444 -0
- package/dist/3d-CzLTD0RK.d.ts +1092 -0
- package/dist/3d.d.ts +1 -0
- package/dist/3d.js +12660 -0
- package/dist/browser.d.ts +2278 -0
- package/dist/browser.js +21826 -0
- package/dist/build.d.ts +595 -0
- package/dist/build.js +29737 -0
- package/dist/capture-shot-ChPQvPyi.d.ts +291 -0
- package/dist/capture.d.ts +9 -0
- package/dist/capture.js +510 -0
- package/dist/config-reference.json +40527 -0
- package/dist/icon-composition-HUh_Lj8Z.d.ts +2516 -0
- package/dist/index.d.ts +1805 -162
- package/dist/index.js +26400 -3488
- package/dist/manifest-types-DnhSJjCM.d.ts +902 -0
- package/dist/manifest.d.ts +9 -0
- package/dist/manifest.js +449 -0
- package/dist/native-profile.d.ts +206 -0
- package/dist/native-profile.js +9339 -0
- package/dist/native.d.ts +365 -0
- package/dist/native.js +5970 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +1445 -0
- package/dist/worker/camera-tile-worker.global.js +20 -0
- package/docs/application-tile-reading.md +86 -0
- package/docs/authoring-model.md +269 -0
- package/docs/boundary-areas-and-choropleths.md +217 -0
- package/docs/buildings-3d.md +608 -0
- package/docs/camera-moves-and-tile-cache.md +112 -0
- package/docs/capture-scenes.md +40 -0
- package/docs/capture-shots.md +173 -0
- package/docs/cartographic-defaults.md +210 -0
- package/docs/compiled-style-performance.md +286 -0
- package/docs/data-is-separate-from-design.md +610 -0
- package/docs/depth-markers.md +65 -0
- package/docs/dynamic-style-images.md +100 -0
- package/docs/globe-atmosphere.md +113 -0
- package/docs/hosted-session-authorization.md +106 -0
- package/docs/map-and-terrain-examples.md +156 -0
- package/docs/map-emphasis.md +89 -0
- package/docs/native-artifact-profile.md +152 -0
- package/docs/native-resource-urls.md +369 -0
- package/docs/nautical-marks.md +308 -0
- package/docs/public-api-and-browser-subpath.md +101 -0
- package/docs/route-drawing.md +191 -0
- package/docs/shared-visual-primitives.md +101 -0
- package/docs/street-lights.md +127 -0
- package/docs/street-sky.md +156 -0
- package/docs/style-states.md +73 -0
- package/docs/sun-and-light.md +167 -0
- package/docs/theme-transitions-and-blends.md +317 -0
- package/docs/themes-and-module-styles.md +156 -0
- package/docs/trees-3d.md +295 -0
- package/package.json +67 -8
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Theme transitions and blends
|
|
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 Tileflow map can animate a theme change, or show any point between two or more of its
|
|
6
|
+
themes. Use a transition when people switch between appearances, such as light and dark. Use a
|
|
7
|
+
blend when the application controls the appearance continuously, such as a slider or a day that
|
|
8
|
+
passes from night to day.
|
|
9
|
+
|
|
10
|
+
Both work on the one MapLibre map the application already has. They create no second map, add no
|
|
11
|
+
map views, and request the same tiles as a map that does not change theme. The React, Vue, and
|
|
12
|
+
Svelte maps offer them with MapLibre GL JS, and the React Native map with MapLibre Native on iOS
|
|
13
|
+
and Android; see [React Native](#react-native) for what differs there.
|
|
14
|
+
|
|
15
|
+
## Animate a theme change
|
|
16
|
+
|
|
17
|
+
Theme and `timeOfDay` changes animate over 450 ms by default in React, Vue, and Svelte.
|
|
18
|
+
Pass `themeTransition` to customize the duration. This React example assumes a map named
|
|
19
|
+
`city` with `light` and `dark` themes in the published manifest:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import {Map} from '@tileflow/react';
|
|
23
|
+
import {useState} from 'react';
|
|
24
|
+
|
|
25
|
+
export function CityMap() {
|
|
26
|
+
const [theme, setTheme] = useState<'dark' | 'light'>('light');
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Switch theme</button>
|
|
30
|
+
<Map source={{map: 'city'}} theme={theme} />
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Compatible themes interpolate on the live map, including sky, lighting, buildings and vegetation.
|
|
37
|
+
The globe and 3D camera can keep moving throughout the change. Temporary blend layers are removed
|
|
38
|
+
when the destination theme is reached. No image of the previous map is projected over the scene.
|
|
39
|
+
`onThemeChange` reports `preloading`, `applying`, and finally `ready` after the transition.
|
|
40
|
+
|
|
41
|
+
Structurally different styles, entering a blend, or leaving one use a stationary snapshot while
|
|
42
|
+
the new style loads, then fade it out. Moving the camera removes that cover immediately. A
|
|
43
|
+
`theme="system"` change follows the same rules as an explicit theme selection.
|
|
44
|
+
|
|
45
|
+
`duration` is in milliseconds, from 0 to 5000. It defaults to 450, including when `themeTransition`
|
|
46
|
+
is omitted or empty. Set `themeTransition={{duration: 0}}` to change the theme at once.
|
|
47
|
+
When the browser asks for reduced motion, every change happens at once.
|
|
48
|
+
|
|
49
|
+
## Blend themes
|
|
50
|
+
|
|
51
|
+
Pass `themeBlend` with the themes to blend, in order, and a `position` between them. Position 0
|
|
52
|
+
shows the first theme, 1 the second, and 1.25 a quarter of the way from the second to the third:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import {Map} from '@tileflow/react';
|
|
56
|
+
import {useState} from 'react';
|
|
57
|
+
|
|
58
|
+
export function DayMap() {
|
|
59
|
+
const [position, setPosition] = useState(0);
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<input
|
|
63
|
+
aria-label="Time of day"
|
|
64
|
+
max={2}
|
|
65
|
+
min={0}
|
|
66
|
+
onChange={(event) => setPosition(Number(event.target.value))}
|
|
67
|
+
step={0.01}
|
|
68
|
+
type="range"
|
|
69
|
+
value={position}
|
|
70
|
+
/>
|
|
71
|
+
<Map source={{map: 'city'}} themeBlend={{position, themes: ['night', 'dusk', 'day']}} />
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The first blend of a set of themes loads each theme's style and fonts, prepares one style that
|
|
78
|
+
holds every theme, and replaces the map's style once, using the default or configured transition.
|
|
79
|
+
Until then the map shows the theme nearest to the position. After that, changing `position` updates the map at
|
|
80
|
+
once without loading anything, so it can follow a slider, a clock, or an animation frame by frame.
|
|
81
|
+
Changing `themes` prepares a new blend. Removing `themeBlend` returns to `theme`.
|
|
82
|
+
|
|
83
|
+
While a blend is shown, `data-tileflow-theme` names the theme nearest to the position.
|
|
84
|
+
`onThemeChange` reports the preparation of a blend; moving its position reports nothing.
|
|
85
|
+
|
|
86
|
+
### What follows the position
|
|
87
|
+
|
|
88
|
+
- Colours and numbers of the ground, such as land, water, roads, buildings, and their widths and
|
|
89
|
+
opacities, follow the position continuously. Colours mix in the OKLab colour space, so halfway
|
|
90
|
+
between two themes looks halfway to the eye.
|
|
91
|
+
- Patterns, the sky, and light follow the position too. Pattern artwork is mixed in its pixels;
|
|
92
|
+
artwork two neighbouring themes share, by name or pixel for pixel, is not uploaded again.
|
|
93
|
+
- Labels, icons, and other symbols change when the nearest theme changes, with a short cross-fade.
|
|
94
|
+
Light lettering on a dark plate and dark lettering on a light plate would meet in the same grey
|
|
95
|
+
halfway, so symbols do not follow the position.
|
|
96
|
+
- A value that depends on feature state, such as a caption that changes colour when highlighted,
|
|
97
|
+
changes with the nearest theme. MapLibre lays its source out again for that change; a stationary
|
|
98
|
+
cross-fade covers it until the camera moves.
|
|
99
|
+
|
|
100
|
+
### Which themes can be blended
|
|
101
|
+
|
|
102
|
+
Blend themes of the same map. Every theme of a map exposes the same token keys, so its compiled
|
|
103
|
+
styles share sources, layers, filters, and expressions, and differ only in colours, numbers, and
|
|
104
|
+
image names. Up to eight themes can be blended. A blend fails, and the map keeps its current
|
|
105
|
+
appearance, when the styles differ in structure, sources, sprite, glyphs, projection, or terrain.
|
|
106
|
+
|
|
107
|
+
Icon and pattern artwork is mixed in place of the first theme's image when every theme's version
|
|
108
|
+
has the same size. Otherwise that image changes with the nearest theme instead.
|
|
109
|
+
|
|
110
|
+
### Cost
|
|
111
|
+
|
|
112
|
+
The prepared style holds every theme. A value that depends on feature data, such as a land-use
|
|
113
|
+
colour chosen by class, becomes one layer per class while the blend is shown, so the prepared
|
|
114
|
+
style has more layers than one theme. Physical layer IDs are diagnostic, not stable; semantic
|
|
115
|
+
interactions are not affected.
|
|
116
|
+
|
|
117
|
+
Entering a blend replaces the map's style once. MapLibre then takes about as long as it takes to
|
|
118
|
+
load the map: for a detailed city map, around half a second on a desktop computer and longer on a
|
|
119
|
+
phone, while the transition snapshot, if any, covers the map. Start a blend before people need it
|
|
120
|
+
to move, for example when the map loads.
|
|
121
|
+
|
|
122
|
+
Each position change after that sets the map's themed values directly, without expression parsing.
|
|
123
|
+
For a detailed city map this takes a few milliseconds on a mid-range phone. Positions closer than a
|
|
124
|
+
four-hundredth of a theme are not applied, so a slowly moving position leaves the map at rest
|
|
125
|
+
between changes.
|
|
126
|
+
|
|
127
|
+
## React Native
|
|
128
|
+
|
|
129
|
+
The `Map` of `@tileflow/react-native` accepts the same `themeTransition` and `themeBlend` props.
|
|
130
|
+
Theme and `timeOfDay` changes also cross-fade over 450 ms by default:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import {Map} from '@tileflow/react-native';
|
|
134
|
+
|
|
135
|
+
export function DayMap({position}: {position: number}) {
|
|
136
|
+
return (
|
|
137
|
+
<Map
|
|
138
|
+
source={{map: 'city', manifestUrl: 'https://maps.example.com/tileflow/manifest.json'}}
|
|
139
|
+
themeBlend={{position, themes: ['night', 'dusk', 'day']}}
|
|
140
|
+
style={{flex: 1}}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A transition covers the drawn map with a snapshot of its current frame, below markers and map
|
|
147
|
+
controls. The snapshot follows the camera and fades out once the new style is drawn.
|
|
148
|
+
`onReadinessChange` and `onThemeChange` report `ready` after the fade. The device's reduce-motion
|
|
149
|
+
setting (iOS) or removed animations (Android) change at once.
|
|
150
|
+
|
|
151
|
+
A blend loads every blended theme's style once and replaces the map's style once. Position changes
|
|
152
|
+
then apply in batches on the main thread, at most one at a time, and later positions replace any
|
|
153
|
+
that are still waiting. MapLibre Native's style transition smooths values between batches.
|
|
154
|
+
|
|
155
|
+
What follows the position differs from the browser in a few ways:
|
|
156
|
+
|
|
157
|
+
- Ground colours and numbers, pattern artwork, and light follow the position. Native maps have no
|
|
158
|
+
sky.
|
|
159
|
+
- Labels, icons, and switched values change at the nearest theme under a short cross-fade of the
|
|
160
|
+
whole map. Icon artwork changes by image name instead of mixing pixels, so stretchable images
|
|
161
|
+
keep their stretch and content areas.
|
|
162
|
+
- A value that depends on feature data and cannot be split by class, such as a road casing chosen
|
|
163
|
+
by access and zoom together, is drawn once per theme. Only the nearest theme's copy is shown.
|
|
164
|
+
The map has a few more layers while the blend is shown.
|
|
165
|
+
|
|
166
|
+
If the application's native build predates these props, for example after an over-the-air
|
|
167
|
+
JavaScript update, themes change at once and a blend shows its nearest theme.
|
|
168
|
+
|
|
169
|
+
## Use the core controller
|
|
170
|
+
|
|
171
|
+
Framework adapters use `createTileflowThemeController` from `@tileflow/core/browser`. A direct
|
|
172
|
+
MapLibre integration can use it too. Resolve runtime styles from the published manifest with
|
|
173
|
+
`resolveTileflowRuntimeStyle` and `resolveTileflowRuntimeThemeBlend` from `@tileflow/core/runtime`:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import type {Map as MapLibreMap} from 'maplibre-gl';
|
|
177
|
+
import {createTileflowThemeController} from '@tileflow/core/browser';
|
|
178
|
+
import {
|
|
179
|
+
resolveTileflowRuntimeStyle,
|
|
180
|
+
resolveTileflowRuntimeThemeBlend,
|
|
181
|
+
} from '@tileflow/core/runtime';
|
|
182
|
+
import type {TileflowRuntimeManifestMap} from '@tileflow/core/runtime';
|
|
183
|
+
|
|
184
|
+
export async function moveThroughTheDay(map: MapLibreMap, manifestMap: TileflowRuntimeManifestMap) {
|
|
185
|
+
const source = {map: 'city'};
|
|
186
|
+
const styleOf = (theme: string) => {
|
|
187
|
+
const style = resolveTileflowRuntimeStyle({manifestMap, source, theme});
|
|
188
|
+
if (!style) throw new Error(`The manifest has no ${theme} theme for ${source.map}.`);
|
|
189
|
+
return style;
|
|
190
|
+
};
|
|
191
|
+
// `initial` is the runtime style the map was created with.
|
|
192
|
+
const controller = createTileflowThemeController({initial: styleOf('day'), map});
|
|
193
|
+
|
|
194
|
+
await controller.setTheme(styleOf('night'));
|
|
195
|
+
|
|
196
|
+
const themes = resolveTileflowRuntimeThemeBlend({
|
|
197
|
+
blend: {position: 0, themes: ['night', 'dusk', 'day']},
|
|
198
|
+
manifestMap,
|
|
199
|
+
source,
|
|
200
|
+
});
|
|
201
|
+
await controller.setBlend({position: 0.5, themes});
|
|
202
|
+
// The same themes: the position applies at once.
|
|
203
|
+
await controller.setBlend({position: 0.6, themes});
|
|
204
|
+
// {held: false, position: 0.6, relayoutAt: [...], themes: ['night', 'dusk', 'day']}
|
|
205
|
+
return controller.getBlend();
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`setTheme` and `setBlend` accept `{transition: {duration}}`;
|
|
210
|
+
`createTileflowThemeController({transition})` overrides the 450 ms default for themes and blends.
|
|
211
|
+
Sun, emphasis, and style-state changes remain immediate unless a transition is configured.
|
|
212
|
+
Requests are serialized and the latest wins. A failed change restores the previous appearance
|
|
213
|
+
and reports `failed` with its error.
|
|
214
|
+
|
|
215
|
+
The controller loads blended themes' style documents with the same request policy it uses for
|
|
216
|
+
style font metadata: same-origin credentials, no redirects, and a bounded size. Pass `loadStyle`
|
|
217
|
+
when styles need another loader.
|
|
218
|
+
|
|
219
|
+
The controller also shows [map emphasis](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/map-emphasis.md),
|
|
220
|
+
which stays through theme changes and follows every blend position.
|
|
221
|
+
Its [dynamic style images](dynamic-style-images.md) receive theme, blend and declared state inputs
|
|
222
|
+
under one resource owner, with fixed geometry and restoration after a style reload.
|
|
223
|
+
|
|
224
|
+
## Fast sweeps on slower devices
|
|
225
|
+
|
|
226
|
+
Each position change makes MapLibre evaluate the paint of every layer whose value changed. A clock
|
|
227
|
+
that moves slowly changes few values per frame, and positions closer than a four-hundredth of a
|
|
228
|
+
theme are skipped. A sweep that crosses a whole theme in a few seconds while the camera also moves
|
|
229
|
+
changes most ground layers on every frame. Measured in Chromium on Apple silicon at 1440 × 900 for
|
|
230
|
+
a detailed city map, such a sweep kept 76 frames a second; with the processor slowed four times, as
|
|
231
|
+
on a mid-range phone, it fell to about 11, against 28 for the same camera move without the sweep.
|
|
232
|
+
On slower devices, prefer short sweeps or move the position less often.
|
|
233
|
+
|
|
234
|
+
## Camera moves and recordings
|
|
235
|
+
|
|
236
|
+
Some changes make MapLibre lay tiles out again: when the nearest theme changes, the values that
|
|
237
|
+
switch with it (icon names, layout values, and values that read feature data or feature state),
|
|
238
|
+
and each step of pattern or icon artwork that is mixed in pixels. A camera move or a screen
|
|
239
|
+
recording shows that work as dropped frames. `getBlend().relayoutAt` lists the positions where the
|
|
240
|
+
nearest theme changes over such values, so a sequence can be planned around them.
|
|
241
|
+
|
|
242
|
+
Pass `hold: true` to hold that work back. Colours, numbers, the sky, and light keep following the
|
|
243
|
+
position; the nearest theme, the values that switch with it, and mixed artwork wait.
|
|
244
|
+
`getBlend().held` reports whether anything waits. The next request without `hold` brings it in at
|
|
245
|
+
once, so make it while the camera rests:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import type {TileflowThemeController} from '@tileflow/core/browser';
|
|
249
|
+
import type {TileflowRuntimeStyle} from '@tileflow/core/runtime';
|
|
250
|
+
|
|
251
|
+
export async function duskWhileFlying(
|
|
252
|
+
controller: TileflowThemeController,
|
|
253
|
+
themes: readonly TileflowRuntimeStyle[],
|
|
254
|
+
flight: Promise<void>,
|
|
255
|
+
) {
|
|
256
|
+
// Through the camera move the ground darkens towards night with no layout work.
|
|
257
|
+
for (const position of [0.25, 0.5, 0.75])
|
|
258
|
+
await controller.setBlend({hold: true, position, themes});
|
|
259
|
+
await flight;
|
|
260
|
+
// The camera rests: labels, icons, and artwork follow at once.
|
|
261
|
+
await controller.setBlend({position: 0.75, themes});
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The React, Vue, and Svelte maps take the same flag in `themeBlend`: pass
|
|
266
|
+
`themeBlend={{hold: true, position, themes}}` while the camera moves and drop `hold` when it rests.
|
|
267
|
+
The React Native `Map` blends natively and does not take `hold`.
|
|
268
|
+
|
|
269
|
+
## Time of day
|
|
270
|
+
|
|
271
|
+
`timeOfDay` is an optional map capability. It maps a theme and an artistic preset (`day`,
|
|
272
|
+
`dawn`, `dusk`, `night`) to a complete theme already in `themes`:
|
|
273
|
+
|
|
274
|
+
<!-- docs:check -->
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import {defineMap} from '@tileflow/core';
|
|
278
|
+
import {streets, streetsThemes} from '@tileflow/maps';
|
|
279
|
+
|
|
280
|
+
export default defineMap({
|
|
281
|
+
id: 'city',
|
|
282
|
+
version: 1,
|
|
283
|
+
extends: streets,
|
|
284
|
+
themes: {
|
|
285
|
+
paper: streetsThemes.light,
|
|
286
|
+
sunrise: streetsThemes.dawn,
|
|
287
|
+
evening: streetsThemes.dusk,
|
|
288
|
+
ink: streetsThemes.dark,
|
|
289
|
+
},
|
|
290
|
+
defaultTheme: 'paper',
|
|
291
|
+
systemThemes: {light: 'paper', dark: 'ink'},
|
|
292
|
+
timeOfDay: {
|
|
293
|
+
paper: {day: 'paper', dawn: 'sunrise', dusk: 'evening', night: 'ink'},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Applications keep `theme` and optionally add `timeOfDay`: `theme="paper" timeOfDay="dusk"`
|
|
299
|
+
selects `evening`. Compilation supports `createStyle(map, {theme: 'paper', timeOfDay: 'dusk'})`;
|
|
300
|
+
`resolveTileflowRuntimeTheme(map, theme, colorScheme, timeOfDay)` uses the same resolution.
|
|
301
|
+
Omitting the preset preserves the existing selection. `system` resolves its light/dark theme
|
|
302
|
+
first. A concrete variant such as `ink` also identifies its family when a preset is supplied.
|
|
303
|
+
Maps or themes without a family ignore this option. Within a declared family, an unavailable
|
|
304
|
+
preset is an error; controls should offer only declared presets. Families must include their own
|
|
305
|
+
name in their presets, and a concrete theme can belong to only one family.
|
|
306
|
+
|
|
307
|
+
The catalog inherits atomically; `timeOfDay: {}` disables it on an extension. Replacing `themes`
|
|
308
|
+
clears an inherited catalog. `defaultTheme`, `systemThemes`, and `themeTransition` retain their
|
|
309
|
+
behavior. An explicit `themeBlend` owns its concrete theme sequence and takes precedence over
|
|
310
|
+
`timeOfDay`. Changing the preset does not start a clock or call `setSun`; lighting is whatever the
|
|
311
|
+
selected theme already declares, and an application's sun override retains its existing behavior.
|
|
312
|
+
Captures, style URLs, and transition events use the resolved concrete theme name.
|
|
313
|
+
|
|
314
|
+
Maps with a catalog publish runtime manifest version 2. Updated browser and native readers accept
|
|
315
|
+
both versions. Old readers need a version 1 manifest: `createManifest(project, {manifestVersion: 1})`
|
|
316
|
+
omits the catalog and keeps every concrete theme available. Maps without a catalog still emit
|
|
317
|
+
version 1. Update readers before delivering a version 2 manifest.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Themes and module styles
|
|
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 map declares complete named appearances in `themes`, chooses one deterministic
|
|
6
|
+
`defaultTheme`, and may map the browser's light/dark preference through `systemThemes`. `system` is
|
|
7
|
+
never a compiled theme name: it is a runtime request that resolves immediately to one concrete
|
|
8
|
+
name. Disk builds write `styles/<map>/<theme>.json` beneath their output root. Public and
|
|
9
|
+
content-addressed Style URLs belong to the runtime manifest and may include a deployment prefix;
|
|
10
|
+
capture scenes and receipts store the concrete name rather than `system`.
|
|
11
|
+
|
|
12
|
+
Themes are flat, JSON-safe documents with `colorScheme`, identity, typography, lighting, and four
|
|
13
|
+
typed token catalogs: `color`, `font`, `image`, and `number`. Every theme on one map must expose the
|
|
14
|
+
same token keys, so changing a theme can never change map structure or silently lose a semantic
|
|
15
|
+
role. `defineTheme(base, definition)` is an authoring convenience that returns a fully materialized
|
|
16
|
+
document; it does not leave an inheritance edge in the resolved config.
|
|
17
|
+
|
|
18
|
+
## Start with one appearance
|
|
19
|
+
|
|
20
|
+
A map can have one appearance for its entire lifetime. Neither `systemThemes` nor `timeOfDay` is
|
|
21
|
+
required. This complete theme supplies the twelve base color roles the semantic compiler needs;
|
|
22
|
+
copy it and change its colors to start an independent design without inheriting an official map:
|
|
23
|
+
|
|
24
|
+
<!-- docs:check -->
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {defineTheme} from '@tileflow/core';
|
|
28
|
+
|
|
29
|
+
export const paper = defineTheme({
|
|
30
|
+
id: 'paper',
|
|
31
|
+
version: 1,
|
|
32
|
+
colorScheme: 'light',
|
|
33
|
+
tokens: {
|
|
34
|
+
color: {
|
|
35
|
+
'surface.background': '#F6F7F3',
|
|
36
|
+
'surface.land': '#F1F3ED',
|
|
37
|
+
'surface.water': '#A9D3F5',
|
|
38
|
+
'surface.park': '#CDE8B5',
|
|
39
|
+
'surface.building': '#E6E3DA',
|
|
40
|
+
'roads.default': '#FFFFFF',
|
|
41
|
+
'roads.major': '#F4C95D',
|
|
42
|
+
'roads.casing': '#D9DEE2',
|
|
43
|
+
'boundaries.default': '#C9D1D9',
|
|
44
|
+
'labels.primary': '#3C4043',
|
|
45
|
+
'labels.muted': '#727B84',
|
|
46
|
+
'labels.halo': '#FFFFFF',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
An existing map definition can select this appearance with `themes: {paper}` and
|
|
53
|
+
`defaultTheme: 'paper'`. Supply that map's data and font assets separately; the default typography
|
|
54
|
+
uses `Noto Sans Regular`. A theme without these base roles is a valid appearance document but cannot
|
|
55
|
+
compile a semantic map. Colors above are an editable starting point, including for monochrome,
|
|
56
|
+
historical, dark-only, or imagined worlds; `colorScheme` describes the appearance for consumers and
|
|
57
|
+
does not generate a palette or choose a time of day.
|
|
58
|
+
|
|
59
|
+
The compiler derives more specific roles when they are absent. For example, primary and motorway
|
|
60
|
+
roads initially use `roads.major`; declaring `roads.motorway` replaces only that derived color.
|
|
61
|
+
Land-use fills, road casings, and label colors have their own semantic roles, so an explicit role
|
|
62
|
+
always wins over its fallback. Use `color.mix()` and `token.color()` in a theme when the relationship
|
|
63
|
+
itself should be authored and inspectable. Derivations resolve during compilation, without a clock
|
|
64
|
+
or a per-frame palette calculation.
|
|
65
|
+
|
|
66
|
+
Add named appearances when the map needs them. A day/night pair does not need dawn or dusk, and a
|
|
67
|
+
theme named `paper` or `fantasy` does not need a temporal family. Declare `timeOfDay` only when the
|
|
68
|
+
application should offer those artistic moments; see
|
|
69
|
+
[time-of-day selection](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md#time-of-day).
|
|
70
|
+
|
|
71
|
+
## Compose and replace appearance values
|
|
72
|
+
|
|
73
|
+
Some optional tokens are for the application rather than the map's own layers. Route inks
|
|
74
|
+
(`route.line`, `route.casing`, `route.core`, `route.glow`, `route.alternative`,
|
|
75
|
+
`route.glow.opacity` and `route.core.opacity`) set how an application draws its routes; a theme
|
|
76
|
+
without them uses its colour scheme's defaults. The compiler publishes them in the style's metadata
|
|
77
|
+
and the theme controller returns them; see
|
|
78
|
+
[route drawing](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/route-drawing.md).
|
|
79
|
+
|
|
80
|
+
The key in the `themes` record is its concrete runtime selector and output-path segment. The
|
|
81
|
+
theme's own `id` and `version` are its editorial identity and build provenance, so they need not
|
|
82
|
+
equal that key. Concrete keys may be names such as `day` and `night`; every `systemThemes.light` or
|
|
83
|
+
`systemThemes.dark` value must name an existing theme whose `colorScheme` matches that branch.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import {color, defineMap, defineTheme, fixed, roads, token} from '@tileflow/core';
|
|
87
|
+
import {streets, streetsThemes} from '@tileflow/maps';
|
|
88
|
+
|
|
89
|
+
const dark = defineTheme(streetsThemes.dark, {
|
|
90
|
+
id: 'company-dark',
|
|
91
|
+
version: 1,
|
|
92
|
+
colorScheme: 'dark',
|
|
93
|
+
tokens: {
|
|
94
|
+
color: {
|
|
95
|
+
'surface.land': '#101722',
|
|
96
|
+
'labels.primary': color.mix('#eef3fb', token.color('surface.background'), {amount: 0.08}),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export default defineMap({
|
|
102
|
+
id: 'company',
|
|
103
|
+
version: 1,
|
|
104
|
+
extends: streets,
|
|
105
|
+
themes: {light: streetsThemes.light, dark},
|
|
106
|
+
defaultTheme: 'light',
|
|
107
|
+
systemThemes: {light: 'light', dark: 'dark'},
|
|
108
|
+
modules: {
|
|
109
|
+
roads: roads({
|
|
110
|
+
classes: {
|
|
111
|
+
primary: {
|
|
112
|
+
surface: {
|
|
113
|
+
fill: {
|
|
114
|
+
color: fixed('#ff2d78', {reason: 'Regulatory primary-road ink is invariant'}),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
secondary: {
|
|
119
|
+
surface: {
|
|
120
|
+
fill: {
|
|
121
|
+
color: token.color('roads.secondary'),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Semantic modules own structure and behavior; visual fields accept typed token references.
|
|
132
|
+
Use a visual literal only inside a theme. If a module value intentionally must not vary, wrap it in
|
|
133
|
+
`fixed(value, {reason})`. `tileflow inspect --json` reports stable `THEME_IMPLICIT_FIXED`
|
|
134
|
+
diagnostics: implicit color, font, and image literals block Style compilation, while direct visual
|
|
135
|
+
number literals are warnings so an agent can document their invariance deliberately. Structural
|
|
136
|
+
numbers such as zoom bounds and ranks are not visual-theme diagnostics. `color.alpha()` and
|
|
137
|
+
deterministic OKLCH `color.mix()` keep derived palette logic inspectable. Unknown refs, cycles,
|
|
138
|
+
cross-category refs, token-schema drift, and unresolved visual nodes fail before Style JSON is
|
|
139
|
+
emitted.
|
|
140
|
+
|
|
141
|
+
Theme lighting also supports explicit removal during `defineTheme` composition. Use
|
|
142
|
+
`lighting: reset()` to remove the entire inherited lighting configuration, or
|
|
143
|
+
`lighting: {sun: reset(), ambient: reset()}` to remove those presets while retaining other light
|
|
144
|
+
properties. The same operation can remove a direct property such as `position` or `color`.
|
|
145
|
+
Omitting a property, or supplying an empty object, continues to inherit it. Reset operations are
|
|
146
|
+
materialized immediately and never enter the resolved theme or Style JSON. Removing a lighting
|
|
147
|
+
preset restores the renderer's fallback; it does not disable 3D shadows or occlusion. Those remain
|
|
148
|
+
explicit building settings. See
|
|
149
|
+
[sun and light](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/sun-and-light.md#replace-inherited-lighting)
|
|
150
|
+
for a complete example.
|
|
151
|
+
|
|
152
|
+
There is no physical-layer override for the Tileflow basemap or separate official-map authoring
|
|
153
|
+
surface. Basemap behavior belongs to a typed control or owner-local render stack. Every
|
|
154
|
+
official and application map therefore follows the same schema, inheritance rules, diagnostics,
|
|
155
|
+
and lowering path. `createStyle()` validates the final planned Style JSON with the MapLibre style
|
|
156
|
+
spec and never returns an invalid style.
|