Trees
import { TreesLayer, setTheme } from 'maplibre-landmarks';
map.addLayer(new TreesLayer({ id: 'trees', source: 'protomaps' }), 'pois');
setTheme(map, 'dusk'); // day | dawn | dusk | night: lights + tree/landmark palettes, map-wideTrees come from Protomaps pois points with kind=tree (zoom 15+ tiles). Green landuse polygons (forest, wood, park by default) are filled with seeded, stable trees only where mapped trees are sparse. The nearest maxTrees (4000) are drawn from zoom 16, with cheap impostors beyond lodDistanceM (300 m). The source must be used by at least one style layer (MapLibre only loads tiles for used sources; a basemap source always is). Trees sway with wind: { strength, directionDeg, fps } (setWind). Each sway frame redraws the whole map, so while nothing else moves the sway runs at fps (30); calm wind stops the repaint loop.
| Option | Default | Description |
|---|---|---|
source | required | Vector (Protomaps schema) or GeoJSON source id |
sourceLayers | { points: 'pois', polygons: 'landuse', water: 'water' } | '' for GeoJSON (water then off unless named) |
models | [deciduous, conifer, birch] | Any TreeModel |
weights | { deciduous: .6, conifer: .2, birch: .2 } | Pick probability per model id |
maxTrees / minZoom / lodDistanceM | 4000 / 14 / 300 | Budget (trees appear with the 3D buildings at zoom 14) |
fullDensityZoom | 16 | Below it scattered trees are thinned: ¼ two zooms below, ½ one zoom below |
scatter | { forest: 1/120, wood: 1/120, park: 1/400 } | Trees per m² by landuse kind; false = points only |
density | 0.6 | Share of trees drawn, mapped and scattered (a fixed subset per tree) |
thinBeyondM | 250 | Scattered trees thin out past this distance from the view centre, as (distance ÷ it)⁻², and stop at 8× it, so the budget reaches far into dense forest (Infinity: off) |
farCutoff | true | Pitched views (past 45°) draw trees only to about three screen heights away, sinking them near the edge, and skip tiles beyond |
avoidWater | true | No scattered tree in water (lakes and streams inside parks and woods); mapped trees stay |
riseMs | 400 | A newly shown tree rises from the ground over this time (0: appears at once) |
scatterSkipRatio | 0.25 | Don't scatter a tile piece whose mapped trees reach this share of its target |
wind | { strength: 1, directionDeg: 250, fps: 30 } | Direction the wind blows from; fps caps the sway frame rate while the map is otherwise still (0: every frame) |
season | 'summer' | 'spring', 'summer', 'autumn', 'winter', 'auto' or a number (0 spring … 3 winter); see Seasons |
snow | true | Snow on evergreens and bare branches in winter |
theme | — | Sets the map-wide theme |
Methods: setWind(wind), setDensity(density), setTheme(theme), setSeason(season), getSeason(), setSnow(snow), getStats().
Seasons
const trees = new TreesLayer({ id: 'trees', source: 'protomaps', season: 'autumn' });
trees.setSeason('winter'); // eases forward through the year over 1.5 s
trees.setSeason(2.4, { durationMs: 0 }); // 0 spring, 1 summer, 2 autumn, 3 winter; wraps at 4
trees.setSeason('auto'); // today's date at the map's latitude
trees.setSnow(false); // bare winter trees without snow| Season | Deciduous trees | Evergreens |
|---|---|---|
| Spring | Fresh yellow-green; a share of them in pink or white blossom; crowns at 80% | Slightly lighter (new growth) |
| Summer | The theme's foliage colour (the default, as before seasons existed) | The theme's foliage colour |
| Autumn | Each tree red, orange, amber or late green; crowns thinning | Slightly darker |
| Winter | Leaves fall clump by clump, leaving bare branches | Darker, with snow on top |
Each tree turns at a slightly different time, so a park changes gradually rather than all at once. Everything happens in the shader: changing the season rebuilds no tiles. Seasonal colours are set for the day theme and scaled for the others, so they follow setTheme.
'auto' places the year so each season peaks mid-way through it (spring around 15 April, autumn around 15 October), half a year on south of the equator. Between the tropics it stays summer. It is also available as seasonAt(date, lat).
The basemap's ground is not part of this layer. The sample above tints its park fill itself.
Custom tree models
A model returns trunk and foliage geometry (metres, Y up, base at 0). Colours always come from the theme palette, so every model follows setTheme. Foliage can be an array of leaf clumps, which fall one by one in autumn; a single geometry shrinks as one crown. Put bare branches in the trunk geometry: the crown hides them until the leaves fall. Set leafCycle: 'evergreen' on models that keep their leaves (default 'deciduous'), and blossom (0–1) for the share of trees that blossom in spring.
import { treeModelFromGLB, type TreeModel } from 'maplibre-landmarks';
const palm = treeModelFromGLB('/models/palm.glb', {
trunk: ['bark'],
foliage: ['leaves'],
leafCycle: 'evergreen',
});
const lollipop: TreeModel = {
id: 'lollipop',
build: (seed) => ({ trunk: myTrunkGeometry(seed), foliage: myCrownGeometry(seed) }),
};
map.addLayer(new TreesLayer({ id: 'trees', source: 'protomaps', models: [palm, lollipop] }));The built-in procedural trees are adapted from the "maplibre-shaders" CodePen.