Map effects plugin
The Map Effects plugin adds practical post-processing to a TomTomMap. Its effects are computed from the rendered map's pixels, not its style, so they work on every style and survive style releases. MapLibre itself has no post-processing. The playground below puts every effect on one panel:
Each effect is named after the problem it solves, is off by default and has its own guide:
| Guide | Effects | Solves |
|---|---|---|
| Bloom | bloom.* |
Emphasis on dark and driving styles. Lit roads, traffic tubes and route lines glow, optionally only in the colours you name |
| Glare | glare.* |
Lights that read as lights. The colours you name, the major roads by default, cast streaks as lamps do through a lens |
| Depth of field | depthOfField.* |
Depth on a tilted map. One band of distance stays sharp, and the rest defocuses as through a lens |
| Grade, tint, fog, edge blur and vignette | grade.*, tint.*, fog.*, edgeBlur.*, vignette.* |
Muting the base map under your data, and depth and focus at the rim of the view |
| Custom GPU passes | addPass, addLayer |
Your own fragment shaders inside the effect chain, with every other effect working on their result, and your own 2D layers between the overlays |
Installation
npm install @tomtom-org/maps-sdk maplibre-gl @tomtom-org/maps-sdk-plugin-map-effects
@tomtom-org/maps-sdk and maplibre-gl are peer dependencies.
Usage
import { resetKnob, setKnob, setKnobs, TomTomMap } from '@tomtom-org/maps-sdk/map';
import { effectKnobCatalogue, MapEffects } from '@tomtom-org/maps-sdk-plugin-map-effects';
const map = new TomTomMap({
style: 'streetDark',
mapLibre: { container: 'map' },
});
const effects = new MapEffects(map); // or new MapEffects(map, config)
effects.updateConfig({ grade: { saturation: 0.4, brightness: 0.85 }, tint: { opacity: 0.15 } }); // a quiet base
setKnob(effects, effectKnobCatalogue, 'bloom.intensity', 0.5); // one knob
setKnobs(effects, effectKnobCatalogue, { 'bloom.radius': 14, 'bloom.threshold': 0.6 }); // several, the rest kept
resetKnob(effects, effectKnobCatalogue, 'bloom.intensity'); // one effect off
effects.resetConfig(); // everything off
- The configuration (
MapEffectsConfig) nests each knob along its id, sobloom.intensityisbloom: { intensity }.getConfig()returns the knobs set, andresetConfig()turns everything off. MapEffectsfollows the same rules as a map module. For values your code writes, useapplyConfig(config), which replaces them all, orupdateConfig(config), which replaces each effect you name and keeps the others. Note thatupdateConfig({ bloom: { threshold: 0.3 } })drops the bloom's other knobs. TypeScript checks these values, and nothing checks them at run time.- For values held as data, from a control, storage or a language model, use the SDK's
setKnobandsetKnobswitheffectKnobCatalogue. They check every value before anything changes, then set each knob by its id and keep every other one, including the rest of its effect. A refused value throwsRangeError, and an unknown id throwsError.getKnobreturns the value set, otherwise the catalogue default, andresetKnobunsets one. remove()detaches the effects, removes their elements from the page and releases the chain's WebGL context. This matters because browsers cap the contexts per page.- The chain takes that context only when it first has something to run: your own pass, the depth of field, the glare or a bloom scoped with
bloom.only. Bloom without a scope and the CSS overlays need none, so aMapEffectswith none of those on holds no context. Once taken, it's held untilremove().
How it renders
No effect knob writes the MapLibre style. Every effect reads the map's rendered pixels, so the knobs change the frame rather than what the map draws. The one link to the style is the colour scopes, bloom.only and glare.only. Each keys on the colours the style paints for the style colour knobs it names, whichever module holds them:
The effects draw in three kinds of layer, in this order:
- The GPU chain is a WebGL2 canvas under everything else. It runs your passes, the depth of field, the glare and a bloom scoped by colour on the map's pixels, in that order. Each pass reads the previous pass's output without the picture leaving the GPU.
- Bloom without a scope is a small canvas blended in
screenmode over that result. - Overlays are CSS layers over both: grade, fog, edge blur, tint and vignette. Your own 2D layers from
addLayersit between them, each right above the overlay itsplacementnames. A layer atafter-bloomsits above a scoped bloom too, since that glow is part of the chain.
As a result:
- Bloom reads the chain's output, not the raw map. A defocused highlight glows as the wide soft disc a photograph gives it, and a hue-shifting pass of yours runs before bloom's colour key.
- The overlays work on the glow too. A grade, a tint or a vignette changes the bloom along with the map under it.
- Capture runs the same chain at the requested pixel ratio, then composites the same overlays and your layers in the same order, so what you capture is what was on screen.
- The effects draw in the same frame as the map, with no
preserveDrawingBuffer. The chain, bloom and your layers draw on the map'srenderevent, while the frame the map just drew is still readable. The CSS overlays need no redrawing. - A change to bloom, the glare or the depth of field, or a pass you add or remove, asks the map for one frame, so an idle map redraws once. A change to the CSS overlays asks for none. A pass or layer that changes on its own still asks for frames with
map.mapLibreMap.triggerRepaint(). - Nothing takes pointer events. The effects sit below the map's controls and leave hit-testing and SDK events alone.
- HTML markers sit above the effects, as popups and the map's controls do. To have a pin take the effects, draw it as a symbol layer instead.
What each effect costs
An effect costs nothing while it's off. Once it's on, its cost each frame depends on the layer it draws in:
| Effect | Draws as | Each frame |
|---|---|---|
| Grade, tint, vignette | A CSS layer | The browser composites one more full-size layer. The plugin draws nothing. |
| Fog, edge blur | A CSS layer with a backdrop-filter |
The browser blurs what's under the layer, then composites it. |
| Bloom | A canvas at a quarter of the pixels | The plugin draws the map into it through a CSS filter, and the browser blends it in screen mode. |
Bloom with bloom.only |
Three stages of the GPU chain | Three full-size passes. The scope's colours reach the GPU only when the scope or the style changes. |
| Glare | Two stages of the GPU chain | Two full-size passes. The second reads 32 samples per pixel for each pair of opposite rays. |
| Depth of field | One stage of the GPU chain | One full-size pass, which samples the pixels around each pixel. |
| Your own passes | One stage of the GPU chain each | One full-size pass, as costly as your shader. |
- Once any stage runs, the chain adds a cost of its own. It copies the whole map canvas into its WebGL context every frame, and its canvas is one more full-size layer.
- The map keeps its default drawing buffer, which the browser swaps every frame rather than copies, whichever effects are on.
- The plugin reads no pixels back to the CPU while the map runs.
capture()reads the frame back once per call. - Every cost grows with the device pixels. On a map that redraws every frame, such as one under an animated 3D layer,
map.mapLibreMap.setPixelRatio()trades sharpness for all of them at once.
The catalogue
effectKnobCatalogue lists every knob as static data, which you can read without a map. Each entry has an id, a kind (number, color or colors), a description, a default and a useCase. It shares the entry shape of the SDK's knob catalogues, and the playground above builds its panel from it.
- A number knob adds a
range. Itsboundsishardonly where the maths breaks past it. Otherwise it'shard-min: the minimum holds, and the maximum is where a slider ends. bloom.onlyandglare.onlylist their accepted names inoptions.fog.colorhas nodefault, because the air in force follows the map's light/dark theme.
effectKnobIds lists the ids, EffectKnobId is their union and EffectKnobValueOf<ID> is the value one takes.
Each effect's guide lists its ranges and defaults:
| Effect | Knob ids |
|---|---|
| Bloom | bloom.intensity, bloom.radius, bloom.threshold, bloom.only, bloom.onlyTolerance |
| Glare | glare.intensity, glare.length, glare.rays, glare.angle, glare.threshold, glare.only, glare.onlyTolerance |
| Grade | grade.brightness, grade.contrast, grade.saturation |
| Fog | fog.intensity, fog.reach, fog.color |
| Edge blur | edgeBlur.intensity, edgeBlur.reach |
| Depth of field | depthOfField.intensity, depthOfField.focus, depthOfField.band, depthOfField.bokeh |
| Tint | tint.color, tint.opacity |
| Vignette | vignette.intensity, vignette.reach |
Capture
capture({ pixelRatio }) renders the current view, map and effects, into one canvas. The plugin re-renders the map at the requested ratio rather than resampling it, reads it back once every tile is drawn, and then sets it back to its previous ratio. MapLibre clamps to its maxCanvasSize, so measure the ratio you got from the returned canvas.
const canvas = await effects.capture({ pixelRatio: 3 });
canvas.toBlob((blob) => upload(blob), 'image/png');
Relation to the Styling Foundations module
The Styling Foundations module changes the style (what the map draws), and effects change the frame (how the drawn map looks). The two combine. The data-viz map preset quiets the map's content, and grade.saturation and tint.opacity mute what remains.
API reference
For complete documentation see the MapEffects API Reference and effectKnobCatalogue.
Related guides and examples
Related guides
- Map styling - The styling knobs, and the map-wide colour ids
bloom.onlynames - Map modules - The knob kinds the effect catalogue shares with the SDK's own catalogues
Related examples
- Map effects playground - Every effect on one panel, with glare and bloom as street lights
- Bloom playground - Every bloom knob, the colour scope included
- Traffic flow with bloom - Live traffic glowing over a dark map
- Mixed map effects - A watercolour pass of your own on a paper texture, with bloom, grade and vignette over it and a paper margin layer on top
- 3D terrain playground - A depth of field over a valley
- Route fly-over playground - A depth of field holding the road ahead in focus
- Cinematic city orbit - Bloom and a depth of field over 3D buildings and landmarks, in eight looks