Animacao de particulas de vento

Wind Particle Animation

Os dados de vento do GFS sao servidos como JSON com componentes U e V em um grid. Neste tutorial, usamos esses dados para criar uma animacao de particulas no Canvas — o mesmo efeito usado em mapas de vento como earth.nullschool.net e Windy.

GFS wind data is served as JSON with U and V components on a grid. In this tutorial, we use this data to create a particle animation on Canvas — the same effect used in wind maps like earth.nullschool.net and Windy.

1. Buscar dados de vento / Fetch wind data

const API_KEY = 'YOUR_API_KEY';
const BASE = 'https://imagery.api.insyde.one';

async function getWindData(forecastHour = 0) {
  const h = String(forecastHour).padStart(3, '0');
  const res = await fetch(`${BASE}/gfs/wind/latest/f${h}.json?key=${API_KEY}`);
  return res.json();
}

2. Estrutura do JSON / JSON structure

O arquivo de vento contem um grid com dimensoes width x height e arrays flat de componentes U (leste-oeste) e V (norte-sul):

{
  "source": "GFS 0.25deg",
  "date": "2026-02-19T12:00:00Z",
  "width": 720,
  "height": 361,
  "uMin": -28.5,
  "uMax": 32.1,
  "vMin": -21.3,
  "vMax": 19.8,
  "u": [0.12, -1.34, 2.56, ...],   // width * height values
  "v": [-0.45, 0.78, -1.23, ...]
}
Campo / FieldDescricao / Description
width, heightDimensoes do grid / Grid dimensions (lon x lat)
u[]Componente leste-oeste (m/s) / East-west component (m/s)
v[]Componente norte-sul (m/s) / North-south component (m/s)
uMin/uMax/vMin/vMaxValores min/max para normalizacao / Min/max values for normalization

3. Interpolar vento no grid / Interpolate wind on grid

Para obter o vento em qualquer ponto do canvas, interpolamos bilinearmente entre os 4 pontos mais proximos do grid:

function interpolateWind(wind, lon, lat) {
  // Convert lon/lat to grid coordinates
  const x = (lon + 180) / 360 * wind.width;
  const y = (90 - lat) / 180 * wind.height;

  const x0 = Math.floor(x), y0 = Math.floor(y);
  const x1 = x0 + 1, y1 = y0 + 1;
  const fx = x - x0, fy = y - y0;

  function get(arr, gx, gy) {
    const cx = ((gx % wind.width) + wind.width) % wind.width;
    const cy = Math.max(0, Math.min(wind.height - 1, gy));
    return arr[cy * wind.width + cx];
  }

  function lerp(arr) {
    const v00 = get(arr, x0, y0);
    const v10 = get(arr, x1, y0);
    const v01 = get(arr, x0, y1);
    const v11 = get(arr, x1, y1);
    return v00 * (1 - fx) * (1 - fy)
         + v10 * fx * (1 - fy)
         + v01 * (1 - fx) * fy
         + v11 * fx * fy;
  }

  return { u: lerp(wind.u), v: lerp(wind.v) };
}

4. Sistema de particulas / Particle system

function createParticles(count, bounds) {
  return Array.from({ length: count }, () => ({
    lon: bounds.west + Math.random() * (bounds.east - bounds.west),
    lat: bounds.south + Math.random() * (bounds.north - bounds.south),
    age: Math.floor(Math.random() * 80),
    maxAge: 60 + Math.floor(Math.random() * 40),
  }));
}

function advanceParticle(p, wind, dt) {
  const w = interpolateWind(wind, p.lon, p.lat);
  const speed = Math.sqrt(w.u * w.u + w.v * w.v);

  // Advance position (scale for visual effect)
  p.lon += w.u * dt * 0.01;
  p.lat -= w.v * dt * 0.01;  // V is flipped (north = up)
  p.age++;

  return speed;
}

5. Renderizar no Canvas / Render on Canvas

const canvas = document.getElementById('wind-canvas');
const ctx = canvas.getContext('2d');

function speedToColor(speed) {
  const t = Math.min(speed / 15, 1);
  const r = Math.round(59 + t * 180);
  const g = Math.round(130 + t * 80 - t * t * 200);
  const b = Math.round(246 - t * 200);
  return `rgba(${r},${g},${b},0.7)`;
}

function draw(wind, particles, bounds) {
  // Fade previous frame
  ctx.fillStyle = 'rgba(5,5,16,0.04)';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  for (const p of particles) {
    const prevX = lonToX(p.lon, bounds, canvas.width);
    const prevY = latToY(p.lat, bounds, canvas.height);

    const speed = advanceParticle(p, wind, 1);

    const x = lonToX(p.lon, bounds, canvas.width);
    const y = latToY(p.lat, bounds, canvas.height);

    ctx.beginPath();
    ctx.moveTo(prevX, prevY);
    ctx.lineTo(x, y);
    ctx.strokeStyle = speedToColor(speed);
    ctx.lineWidth = 0.8;
    ctx.stroke();

    // Reset particle if out of bounds or too old
    if (p.age > p.maxAge || outOfBounds(p, bounds)) {
      p.lon = bounds.west + Math.random() * (bounds.east - bounds.west);
      p.lat = bounds.south + Math.random() * (bounds.north - bounds.south);
      p.age = 0;
    }
  }

  requestAnimationFrame(() => draw(wind, particles, bounds));
}
Dica: O truque principal e usar fillRect com alpha baixo para criar trilhas. Alpha de 0.03–0.05 cria trilhas longas; 0.1+ cria trilhas curtas. Ajuste lineWidth e contagem de particulas conforme o zoom.
Tip: The key trick is using fillRect with low alpha to create trails. Alpha of 0.03–0.05 creates long trails; 0.1+ creates short trails. Adjust lineWidth and particle count based on zoom level.

6. Exemplo completo / Complete example

async function main() {
  const wind = await getWindData(0);

  const bounds = {
    west: -80, east: -30,
    south: -35, north: 5,
  };

  const particles = createParticles(3000, bounds);
  draw(wind, particles, bounds);
}

main();

Para animar diferentes horas de previsao, busque os dados com getWindData(hour) e substitua o objeto wind. As particulas se adaptam automaticamente ao novo campo de vento.

To animate different forecast hours, fetch data with getWindData(hour) and replace the wind object. Particles automatically adapt to the new wind field.

Back to Imagery API docs