Files
Pete/internal/web/static/js/weather.js
prosolis 9b20040b49 Rebuild weather effects on WebGL2 with new variants and a live demo page
The background weather is now GPU-rendered: one instanced-quad draw call
over a baked sprite atlas plus a fullscreen sky shader (fog, Saharan
haze, aurora, sun rays, storm gloom and lightning flash). The old
Canvas2D renderer stays as weather-2d.js and kicks in automatically
when WebGL2 is missing; weather.js is now a thin controller that owns
the toggle, prefs and the PeteWeather API.

Effect upgrades: shared wind with gust pulses leans the whole scene
together, rain gets depth, splash pops and velocity-aligned streaks,
storms grow procedural branched lightning bolts, snow mixes soft motes
with spinning six-arm crystals, clouds drift in two parallax layers,
clear nights get a moon with maria, twinkling stars and the occasional
shooting star, blossoms and leaves tumble with a faked 3D flip.

New variants: haze (Saharan calima), wind (autumn gusts with streak
lines), hail (bouncing stones with drizzle) and aurora. The /weather
demo page switches variant, intensity and phase in place without a
reload and shows the active renderer plus an FPS meter.
2026-07-07 23:53:45 -07:00

85 lines
3.2 KiB
JavaScript

// Weather controller. Two drivers feed the background canvas:
//
// 1. The server picks a *seasonal* variant + intensity from the Lisbon-local
// date and writes them to <html data-weather / data-intensity>. This is the
// default when the visitor hasn't set a location.
// 2. weather-forecast.js, when a postal code is saved, fetches the real
// forecast and calls PeteWeather.set(variant, intensity) to override the
// background with live conditions.
//
// Rendering is delegated to an engine: the WebGL2 one (weather-gl.js) when the
// browser supports it — GPU sprites, shader fog/aurora, real lightning — with
// the original Canvas2D renderer (weather-2d.js) as the fallback. This file
// only owns the toggle button, the on/off preference and the public API.
(function () {
var root = document.documentElement;
var canvas = document.getElementById("pete-weather");
if (!canvas) return;
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var engines = window.PeteWeatherEngines || {};
var engine = (engines.webgl2 && engines.webgl2(canvas)) || null;
if (!engine && engines.canvas2d) engine = engines.canvas2d(canvas);
if (!engine) return;
var STORAGE_KEY = "pete-weather-off";
var toggleBtn = document.querySelector("[data-weather-toggle]");
var star = document.querySelector("[data-weather-star]");
function userDisabled() {
try { return localStorage.getItem(STORAGE_KEY) === "1"; } catch (e) { return false; }
}
function setDisabled(v) {
try {
if (v) localStorage.setItem(STORAGE_KEY, "1");
else localStorage.removeItem(STORAGE_KEY);
} catch (e) {}
if (window.PetePrefs) window.PetePrefs.push();
}
function syncBtn(enabled) {
if (!toggleBtn) return;
toggleBtn.setAttribute("aria-pressed", enabled ? "true" : "false");
toggleBtn.title = enabled ? "Turn off weather animation" : "Turn on weather animation";
if (star) {
star.style.color = "var(--accent)";
star.style.opacity = enabled ? "1" : "0.3";
star.style.filter = enabled ? "none" : "grayscale(1)";
}
}
var enabled = !userDisabled() && !reducedMotion;
// Public hook so weather-forecast.js (and the /weather demo) can swap the
// seasonal default for other conditions. Passing a falsy variant clears the
// canvas.
window.PeteWeather = {
set: function (v, inten) {
root.dataset.weather = v || "";
if (inten) root.dataset.intensity = inten;
engine.set(v || null, inten || root.dataset.intensity || "heavy");
if (enabled && v) engine.start();
},
isEnabled: function () { return enabled; },
renderer: function () { return engine.name; }
};
syncBtn(enabled);
engine.set(root.dataset.weather || null, root.dataset.intensity || "heavy");
if (enabled && root.dataset.weather) engine.start();
if (toggleBtn) {
toggleBtn.addEventListener("click", function () {
enabled = !enabled;
setDisabled(!enabled);
syncBtn(enabled);
if (enabled) engine.start(); else engine.stop();
});
}
document.addEventListener("visibilitychange", function () {
if (!enabled) return;
if (document.hidden) engine.stop();
else engine.start();
});
})();