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 |
| 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 | Your own fragment shaders inside the effect chain, with every other effect working on their result |
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', // Bloom, the depth of field, your own passes and capture read the map canvas back. canvasContextAttributes: { preserveDrawingBuffer: true }, },});
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 basesetKnob(effects, effectKnobCatalogue, 'bloom.intensity', 0.5); // one knobsetKnobs(effects, effectKnobCatalogue, { 'bloom.radius': 14, 'bloom.threshold': 0.6 }); // several, the rest kept
resetKnob(effects, effectKnobCatalogue, 'bloom.intensity'); // one effect offeffects.resetConfig(); // everything off- Bloom, the depth of field, your own passes and capture read the map canvas back, so they need
preserveDrawingBuffer: true. The plugin warns once if it is missing. - 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 or the depth of field. Bloom and the CSS overlays need none, so a
MapEffectswith neither 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 bloom.only, which 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 and the depth of field on the map’s pixels. Each pass reads the previous pass’s output without the picture leaving the GPU.
- Bloom is a small canvas blended in
screenmode over that result. - Overlays are CSS layers over both: grade, fog, edge blur, tint and vignette.
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, so what you capture is what was on screen.
- The chain and bloom redraw on the map’s
renderevent, at most once per animation frame. The CSS overlays need no redrawing. - Nothing takes pointer events. The effects sit below the map’s controls and leave hit-testing and SDK events alone.
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. A number knob adds a range, and bloom.only lists its accepted names in options. It shares the entry shape of the SDK’s knob catalogues, and the playground above builds its panel from it. 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 |
| Grade | grade.brightness, grade.contrast, grade.saturation |
| Fog | fog.intensity, fog.reach |
| 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
- 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, with bloom, grade and vignette over it
- 3D terrain playground - A depth of field over a valley
- Route fly-over playground - A depth of field holding the road ahead in focus