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.
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();
}
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 / Field | Descricao / Description |
|---|---|
width, height | Dimensoes 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/vMax | Valores min/max para normalizacao / Min/max values for normalization |
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) };
}
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;
}
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));
}
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.
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.
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.