Layers

Wind Layers

Animated wind particle visualization with speed-based color ramps
Live Demo - GPU-accelerated wind particle flow visualization

VLayerDeckglWindParticle

GPU-accelerated animated wind particle layer using deck.gl transform feedback. Visualize wind speed and direction with flowing particles colored by velocity.

Installation

bun add maplibre-gl-wind

Features

  • GPU-accelerated particle animation via transform feedback
  • Speed-based color ramps for wind intensity visualization
  • IDW interpolation from sparse data points to full-coverage texture
  • Pre-rendered wind texture support (e.g., from weather APIs)
  • Configurable particle count, lifetime, and speed scaling

Data Sources

The wind layer supports two input modes:

  1. Wind Data Points - Array of {lat, lon, speed, direction} objects (IDW-interpolated to texture)
  2. Pre-rendered Image - Wind velocity texture URL (for weather API outputs)

Basic Usage

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

Props

PropTypeDefaultDescription
idstringrequiredUnique layer identifier
windDataWindDataPoint[]-Array of wind observations (IDW-interpolated)
imageUrlstring-Pre-rendered wind texture URL
bounds[number, number, number, number][-180, -90, 180, 90]Geographic bounds west, south, east, north
uMinnumber-50Minimum U component (only for imageUrl mode)
uMaxnumber50Maximum U component (only for imageUrl mode)
vMinnumber-50Minimum V component (only for imageUrl mode)
vMaxnumber50Maximum V component (only for imageUrl mode)
numParticlesnumber8192Number of particles to render
maxAgenumber30Particle lifetime in frames
speedFactornumber50Particle movement speed multiplier
colorColor[255, 255, 255, 200]Fallback color when colorRamp not used
colorRampColorStop[]See belowSpeed-based color gradient
speedRange[number, number][0, 30]Speed range for color mapping (m/s)
widthnumber1.5Particle trail width in pixels
animatebooleantrueEnable particle animation
opacitynumber1Layer opacity (0-1)
visiblebooleantrueLayer visibility
pickablebooleanfalseEnable picking
beforeIdstring-Insert layer before this layer ID

Default Color Ramp

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
];

Events

EventPayloadDescription
@loaded-Layer initialized and ready
@errorErrorError during initialization
@clickPickingInfoClicked on layer
@hoverPickingInfoHovering over layer

WindDataPoint Type

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)
}

Custom Color Ramp

<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]"
/>

Pre-rendered Wind Texture

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.

Real-time Wind Data

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

Performance Tips

  • Particle Count: Start with 8192 particles. Increase to 16384+ for denser coverage on large screens.
  • Max Age: Lower values (15-20) create shorter trails, higher values (40-60) create longer flowing trails.
  • Speed Factor: Adjust based on your speed range. Higher factors make particles move faster.
  • Animation: Set :animate="false" to pause animation (useful for static screenshots).

How It Works

  1. IDW Interpolation: When using windData, the component generates a wind velocity texture using Inverse Distance Weighting interpolation, creating smooth gradients between observation points.
  2. Transform Feedback: Particles are animated using GPU transform feedback shaders. Each particle reads the wind velocity at its position and moves accordingly.
  3. Color Mapping: Particle colors are determined by the wind speed at their position, mapped through the colorRamp gradient.
  4. Particle Lifecycle: Each particle has a maximum age (maxAge). When a particle expires, it's respawned at a random position within the bounds.