A browser wind visualiser in the style of earth.nullschool.net: an orthographic globe with animated wind streamlines coloured by speed, plus time animation over preloaded GFS analysis and forecast frames.
Repo: https://github.com/andyg2/nulledschool-earth
Requires Node 18+ (uses the built-in fetch). No dependencies, no build step.
git clone https://github.com/andyg2/nulledschool-earth
cd nulledschool-earth
node server.mjs # then open http://localhost:8080The server serves the static app and proxies /data/* to gaia.nullschool.net
(nullschool's data host sends no CORS headers, so the browser can't fetch it
directly). Set PORT to change the port.
- Drag to rotate the globe, scroll to zoom, hover for a wind readout.
- Level / height — surface, or isobaric levels up to the 250 hPa jet stream.
- History (back) — hours of past analysis to preload (6-120h).
- Forecast (ahead) — hours of GFS forecast to preload (up to +96h, as the data allows). Playback starts at the present and runs into the forecast; forecast frames are tagged in the time label.
- ▶ / scrubber — play through time or scrub to a specific hour.
- Particles — how many streamlines to draw.
- Particle speed — how fast the streamlines flow (visual only).
- Trail length — how long the streaks persist.
- Frame speed — how fast playback steps between hourly frames.
All settings (plus the globe's orientation and zoom) are saved to localStorage,
so the view comes back exactly as you left it. Delete the nulledschool-earth:v1
key to reset to defaults.
On a first run (nothing saved yet) it opens on the surface level, centred on the central Philippines, with 120 h of history + 96 h of forecast preloaded — around 200 hourly frames. That first load is heavy, but the on-disk cache below makes every later load fast. Drop the history/forecast windows in the panel for a lighter start.
The proxy mirrors every dated frame it fetches to a local .cache/ folder
(atomic writes; served with an X-Proxy-Cache: HIT/MISS header). Dated frames are
immutable so they're cached indefinitely; the hourly "current" symlink is never
cached. Delete .cache/ to clear it.
server.mjs static server (root: public/) + /data proxy with on-disk caching
public/
index.html page shell, styles, control panel
js/
app.mjs wires it all together + the UI + settings persistence
globe.mjs orthographic projection, rotation/zoom, coastline + graticule
field.mjs bilinear sampling of a decoded u/v wind grid
particles.mjs advects particles along the field, fading coloured trails
palette.mjs wind-speed → colour ramp
data.mjs builds dated frame URLs, fetches/decodes, preloads a window
epak.mjs decoder for nullschool's .epak container (varpack + 2D delta)
assets/
coastline.json Natural Earth 110 m coastlines
.cache/ proxied frames mirrored to disk (git-ignored)
The browser modules under public/js/ are plain ES modules with no build step;
server.mjs runs on Node with no dependencies.
Data: GFS via nullschool, hourly, 0.5°. Frames are available for roughly the past month back and ~4 days of forecast ahead; missing hours are skipped automatically.