An Open-source Mapbox GL JS & MapLibre GL JS custom layers package

Wind particles, smooth weather rasters, and GeoTIFF visualization

GPU-accelerated particle motion for wind: mapping wind is that easy and cost-effective.

No tile server required. Works with Mapbox GL JS and MapLibre GL JS.

Demo videos

Watch wind particles in action, then try the interactive demos below.

Wind particles on a MapLibre GL JS globe projection
Featured · v1.3.1

Wind particles on a MapLibre globe

US continental wind particle animation on MapLibre GL JS globe projection — same custom layer, draped on the globe.

Watch on YouTube →

Also see: US continental wind (Mapbox GL JS · v1.1.0) · Southern California (earlier demo)

Live demos

Three Layer Classes, Four Use Cases

Particle motion wind layer example with colored wind streaks

Wind

Particle Motion

Particles sample local u/v-component velocity, move a small displacement, and are re-colored by local speed at each update interval. A flowing animation that makes direction and relative speed easy to read.

Smooth raster temperature layer example over a regional map

Smooth, non-blocky weather display

Smooth Raster

Utilizing WebGL RGBA texture with linear filtering; the GPU bilinearly interpolates between adjacent texel values when sampling, producing a smooth gradient. Suitable for scalar (grayscale) variables such as temperature and precipitation.

Grayscale scalar GeoTIFF with temperature visualized; grid native resolution is visible

Grayscale, Scalar GeoTIFF

Smooth Raster

Visualize a small single-band GeoTIFF directly in the browser. Multi-band GeoTIFFs (each band holding a different scalar variable) are also accepted; just pick one band to display. Grid size should be no more than 4096x4096. (v1.3.1+)

RGB GeoTIFF displayed as a true-color image layer on a map

RGB or RGBA GeoTIFF

RgbGeoTiff

Display a true-color (RGB) or transparent (RGBA) GeoTIFF as a native map image layer. The file is decoded client-side and added as a Mapbox/MapLibre image source. Supports uint8 and uint16 bands. (v1.3.3+)

README

Frequently asked questions

How does it differ from other wind-mapping solutions?

Mapbox Wind Layer relies on Mapbox’s proprietary Tiling Service to convert GRIB wind data into raster arrays tiled at multiple zoom levels. At high zoom (e.g. greater than 12), covering the extent can require a very large number of tiles — processing cost grows quickly, and pipelines sometimes hang or get killed before they finish. If you cap tiles at zoom 12 instead, particles look like blurry boxes sliding across the map when users zoom in to village, neighborhood, or street level — an aesthetic that is hard to accept for detailed views.

Open-source alternatives

mapbox-exif-layer maplibre-gl-wind sakitam-gis/maplibre-wind
MapLibre v5 support Yes (with mapRuntime set to maplibre in constructor; v1.2.1+) Yes (via MapboxOverlay of deck.gl) No (only works for MapLibre v3)
MapLibre Globe Projection Yes No No
Particle colors vary by actual speed Yes Yes No
Particle has tails Yes No Yes
GeoTIFF support Yes (sample code and data are provided; v1.3.1+) No Yes (mentioned in the doc, but no sample code or data supplied)
Image source support Single JPEG or PNG Single PNG Single or tiled JPEG or PNG
Browser on mobile device support Yes (native) Yes (depends on deck.gl) No (very likely to lose WebGL context)
NA/No data cells handling Store 0 in B-band for NA cells (v1.1.0+) Store 0 in A-band for NA cells Require GeoJSON mask valid or invalid area and supply mask parameter to the layer constructor

See more in Assessing packages for mapping wind as particle motion layer in MapLibre.

How are JPEG/PNG image sources encoded?

A JPEG or PNG used as a source is not a normal photo — it stores a normalized weather grid in three 8-bit RGB bands (0–255 per channel). For wind (ParticleMotion), u-velocity is min–max normalized to 0–255 in the R band and v-velocity in the G band; no-data cells use B = 0 (valid cells use B = 255). For a smooth raster (SmoothRaster), the scalar attribute is normalized to 0–255 in the R band; no-data cells use B = 255 (valid cells use B = 0).

The layer also needs the physical min and max values used when encoding (before normalization). You can supply them in either of two ways:

  • EXIF — write min/max to JPEG ImageDescription (the classic “EXIF-enabled JPEG” path; see Method 2 in the docs).
  • Constructor (v1.3.2+) — pass scalarValueRange on SmoothRaster or velocityRange on ParticleMotion when the image has no EXIF metadata (Method 1). If EXIF is present, it takes precedence.

See docs/jpeg-source.md for encoding rules, both min/max workflows, and ready-to-use pipeline scripts.

I would like to visualize a GeoTIFF file — can I use this package?

Yes. The package supports GeoTIFF in the browser with no tile server — which kind depends on your file:

  • Scalar / grayscale (temperature, elevation, wind u/v, …) — SmoothRaster or ParticleMotion. See scalar GeoTIFF docs.
  • RGB / RGBA imagery (aerial, orthomosaic, true-color satellite) — RgbGeoTiff. See RGB GeoTIFF docs.

That path is best for small grids, demos, data previews, and other non-production use. The layer downloads the entire GeoTIFF on load (no COG range requests or tiling), so large files or maps at many zoom levels are a poor fit.

For production, use a standard Mapbox/MapLibre raster tile source instead:

  • Pre-generated MBTiles — build tiles with rio-mbtiles or QGIS Raster → Generate XYZ Tiles (MBTiles), then serve with mbtileserver. For scalar GeoTIFF, assign RGB(A) colors from the attribute first (e.g. rasterio + NumPy) before generating the tileset.
  • COG + tile serverTiTiler (or GeoServer / similar) serves tiles directly from a Cloud Optimized GeoTIFF without pre-generating MBTiles.
  • MapLibre serverless COG — host a COG on static storage and use maplibre-cog-protocol for range-request tiling in the client (no tile server).

For wind maps, should I use JPEG/PNG or GeoTIFF?

Properly encoded JPEG/PNG rasters are much smaller than GeoTIFFs. On this site’s NOAA NAM NEST CONUS demo grid (2269×976), wind is ~332 KB as JPEG vs ~8.9 MB as ZSTD-compressed float32 GeoTIFF — a big difference for storage and download when you serve many forecast timesteps. GeoTIFF is still a practical starting point: a familiar gdalwarp step gets you a map without learning custom JPEG band encoding — a quick way to validate the layer and build confidence before moving to encoded images for production.

  • Prefer JPEG/PNG when you need many timesteps (multi-hour forecasts), a large grid, or want minimal hosting cost. Min/max velocity metadata can come from EXIF or constructor velocityRange.
  • Prefer GeoTIFF (EPSG:4326, float32 u/v bands, compressed) when you want a GIS-friendly pipeline without custom image encoding, have a smaller study area, and only a few timesteps. Extra float precision is often not visible in everyday visualization though.

Can GeoTIFF be used for smooth, non-blocky, linear-interpolated display?

No — not with SmoothRaster in this package. For smooth weather display (temperature, humidity, precipitation, …), use a properly encoded JPEG/PNG source. The grid is uploaded as an 8-bit RGBA texture with linear filtering, so the GPU bilinearly blends between neighboring cells and the layer looks smooth when zoomed in.

Scalar GeoTIFF on SmoothRaster is a separate use case: a preview path with native grid resolution and nearest sampling on float32 data (WebGL does not reliably support linear filtering on float textures). Cell edges stay visible when zoomed in even though colormap stops are interpolated. See scalar GeoTIFF docs and the GeoTIFF production note above for alternatives at scale.

Can this package be used with Leaflet or OpenLayers?

No. It targets Mapbox GL JS and MapLibre GL JS only — both WebGL map SDKs with a similar custom-layer API that this package implements. Leaflet and OpenLayers use a different rendering model and are not supported.

Getting started

This package does not include a map SDK. Install one map runtime, then add mapbox-exif-layer (and optionally geotiff for .tif sources).

Mapbox GL JS

Default mapRuntime: 'mapbox'

npm install mapbox-gl

MapLibre GL JS

Set mapRuntime: 'maplibre' on each layer

npm install maplibre-gl

This package

npm install mapbox-exif-layer

GeoTIFF sources optional

Only for .tif / .tiff URLs — JPEG-only setups do not need this.

npm install geotiff

Import

import { ParticleMotion, SmoothRaster, RgbGeoTiff } from 'mapbox-exif-layer';