GPU-accelerated animated wind particle layer using deck.gl transform feedback. Visualize wind speed and direction with flowing particles colored by velocity.
bun add maplibre-gl-wind
The wind layer supports two input modes:
{lat, lon, speed, direction} objects (IDW-interpolated to texture)<script setup lang="ts">
import { VMap, VLayerDeckglWindParticle } from '@geoql/v-maplibre';
const mapOptions = {
style: 'https://basemaps.cartocdn.com/gl/dark-matter-nolabels-gl-style/style.json',
center: [0, 20],
zoom: 2,
};
// Wind data: speed in m/s, direction in degrees (0 = North, 90 = East)
const windData = [
{ lat: 40.7128, lon: -74.006, speed: 5.2, direction: 180 },
{ lat: 34.0522, lon: -118.2437, speed: 3.1, direction: 270 },
{ lat: 51.5074, lon: -0.1278, speed: 8.5, direction: 225 },
{ lat: 35.6762, lon: 139.6503, speed: 4.8, direction: 90 },
{ lat: -33.8688, lon: 151.2093, speed: 6.2, direction: 315 },
];
</script>
<template>
<VMap :options="mapOptions" style="height: 500px">
<VLayerDeckglWindParticle
id="wind-layer"
:wind-data="windData"
:num-particles="8192"
:speed-factor="50"
/>
</VMap>
</template>
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique layer identifier |
windData | WindDataPoint[] | - | Array of wind observations (IDW-interpolated) |
imageUrl | string | - | Pre-rendered wind texture URL |
bounds | [number, number, number, number] | [-180, -90, 180, 90] | Geographic bounds west, south, east, north |
uMin | number | -50 | Minimum U component (only for imageUrl mode) |
uMax | number | 50 | Maximum U component (only for imageUrl mode) |
vMin | number | -50 | Minimum V component (only for imageUrl mode) |
vMax | number | 50 | Maximum V component (only for imageUrl mode) |
numParticles | number | 8192 | Number of particles to render |
maxAge | number | 30 | Particle lifetime in frames |
speedFactor | number | 50 | Particle movement speed multiplier |
color | Color | [255, 255, 255, 200] | Fallback color when colorRamp not used |
colorRamp | ColorStop[] | See below | Speed-based color gradient |
speedRange | [number, number] | [0, 30] | Speed range for color mapping (m/s) |
width | number | 1.5 | Particle trail width in pixels |
animate | boolean | true | Enable particle animation |
opacity | number | 1 | Layer opacity (0-1) |
visible | boolean | true | Layer visibility |
pickable | boolean | false | Enable picking |
beforeId | string | - | Insert layer before this layer ID |
const defaultColorRamp = [
[0.0, [59, 130, 189, 255]], // Blue - calm
[0.1, [102, 194, 165, 255]], // Teal
[0.2, [171, 221, 164, 255]], // Light green
[0.3, [230, 245, 152, 255]], // Yellow-green
[0.4, [254, 224, 139, 255]], // Yellow
[0.5, [253, 174, 97, 255]], // Orange
[0.6, [244, 109, 67, 255]], // Red-orange
[1.0, [213, 62, 79, 255]], // Red - strong
];
| Event | Payload | Description |
|---|---|---|
@loaded | - | Layer initialized and ready |
@error | Error | Error during initialization |
@click | PickingInfo | Clicked on layer |
@hover | PickingInfo | Hovering over layer |
interface WindDataPoint {
lat: number; // Latitude (-90 to 90)
lon: number; // Longitude (-180 to 180)
speed: number; // Wind speed in m/s
direction: number; // Wind direction in degrees (0 = North, clockwise)
}
<VLayerDeckglWindParticle
id="wind"
:wind-data="windData"
:color-ramp="[
[0.0, [65, 105, 225, 255]], // Royal blue - calm
[0.3, [50, 205, 50, 255]], // Lime green - moderate
[0.6, [255, 165, 0, 255]], // Orange - strong
[1.0, [255, 0, 0, 255]], // Red - extreme
]"
:speed-range="[0, 40]"
/>
For weather API outputs that provide pre-rendered wind textures:
<VLayerDeckglWindParticle
id="wind"
image-url="https://example.com/wind-texture.png"
:bounds="[-180, -90, 180, 90]"
:u-min="-50"
:u-max="50"
:v-min="-50"
:v-max="50"
/>
The texture encodes U/V wind components in the red and green channels respectively.
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const windData = ref([]);
const fetchWindData = async () => {
// Fetch from your weather API
const response = await fetch('/api/wind-observations');
windData.value = await response.json();
};
onMounted(fetchWindData);
// Refresh every 5 minutes
setInterval(fetchWindData, 5 * 60 * 1000);
</script>
<template>
<VMap :options="mapOptions" style="height: 500px">
<VLayerDeckglWindParticle
v-if="windData.length > 0"
id="wind"
:wind-data="windData"
:num-particles="16384"
:speed-factor="60"
/>
</VMap>
</template>
:animate="false" to pause animation (useful for static screenshots).windData, the component generates a wind velocity texture using Inverse Distance Weighting interpolation, creating smooth gradients between observation points.colorRamp gradient.maxAge). When a particle expires, it's respawned at a random position within the bounds.