Skip to content

Trees ​

ts
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-wide

Trees 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.

OptionDefaultDescription
sourcerequiredVector (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 / lodDistanceM4000 / 14 / 300Budget (trees appear with the 3D buildings at zoom 14)
fullDensityZoom16Below 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
density0.6Share of trees drawn, mapped and scattered (a fixed subset per tree)
thinBeyondM250Scattered 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)
farCutofftruePitched views (past 45°) draw trees only to about three screen heights away, sinking them near the edge, and skip tiles beyond
avoidWatertrueNo scattered tree in water (lakes and streams inside parks and woods); mapped trees stay
riseMs400A newly shown tree rises from the ground over this time (0: appears at once)
scatterSkipRatio0.25Don'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
snowtrueSnow 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 ​

ts
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
SeasonDeciduous treesEvergreens
SpringFresh yellow-green; a share of them in pink or white blossom; crowns at 80%Slightly lighter (new growth)
SummerThe theme's foliage colour (the default, as before seasons existed)The theme's foliage colour
AutumnEach tree red, orange, amber or late green; crowns thinningSlightly darker
WinterLeaves fall clump by clump, leaving bare branchesDarker, 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.

ts
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.

MIT licensed. Models: Open Landmarks; map data © OpenStreetMap contributors and Overture Maps Foundation.