Files
Pete/internal/web/templates/weather.html
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

136 lines
6.7 KiB
HTML

{{define "title"}}Weather demo · {{.SiteTitle}}{{end}}
{{define "main"}}
<div class="space-y-8">
<section class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
<div class="flex flex-wrap items-start justify-between gap-3 mb-1">
<h1 class="font-display text-3xl font-bold">Weather demo</h1>
<div class="flex items-center gap-2 text-xs font-semibold">
<span data-demo-renderer class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1 uppercase tracking-wider text-[color:var(--ink)]/60">renderer: …</span>
<span data-demo-fps class="rounded-full border-2 border-[color:var(--ink)]/10 px-3 py-1 tabular-nums text-[color:var(--ink)]/60">— fps</span>
</div>
</div>
<p class="text-sm text-[color:var(--ink)]/60 mb-5">Pick a variant, intensity, and phase. Switching is instant, no reload.</p>
<div class="space-y-3 text-sm">
<div class="flex flex-wrap items-center gap-2">
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-[color:var(--ink)]/60">Variant</span>
{{range $v := .Variants}}
<a href="?variant={{$v}}&amp;intensity={{$.Intensity}}&amp;phase={{$.PhaseSel}}"
data-demo-variant="{{$v}}"
class="rounded-full px-3 py-1 transition border-2 border-[color:var(--ink)]/10
{{if eq $.Variant $v}}bg-[color:var(--accent)] text-white font-semibold{{else}}hover:bg-[color:var(--ink)]/5{{end}}">{{$v}}</a>
{{end}}
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-[color:var(--ink)]/60">Intensity</span>
{{range $i := .Intensities}}
<a href="?variant={{$.Variant}}&amp;intensity={{$i}}&amp;phase={{$.PhaseSel}}"
data-demo-intensity="{{$i}}"
class="rounded-full px-3 py-1 transition border-2 border-[color:var(--ink)]/10
{{if eq $.Intensity $i}}bg-[color:var(--accent)] text-white font-semibold{{else}}hover:bg-[color:var(--ink)]/5{{end}}">{{$i}}</a>
{{end}}
</div>
<div class="flex flex-wrap items-center gap-2">
<span class="w-20 shrink-0 font-semibold uppercase tracking-wider text-[color:var(--ink)]/60">Phase</span>
{{range $p := .Phases}}
<a href="?variant={{$.Variant}}&amp;intensity={{$.Intensity}}&amp;phase={{$p}}"
data-demo-phase="{{$p}}"
class="rounded-full px-3 py-1 transition border-2 border-[color:var(--ink)]/10
{{if eq $.PhaseSel $p}}bg-[color:var(--accent)] text-white font-semibold{{else}}hover:bg-[color:var(--ink)]/5{{end}}">{{$p}}</a>
{{end}}
</div>
</div>
<div class="mt-5 text-xs text-[color:var(--ink)]/50">
Current: <code data-demo-current class="font-semibold">{{.Variant}} / {{.Intensity}} / {{.PhaseSel}}</code>
<span class="mx-1">·</span> Tip: aurora and clear night want the night phase; wind and haze are the new autumn gusts and Saharan calima.
</div>
</section>
<section class="rounded-3xl bg-[color:var(--card)] p-10 shadow-pete border-2 border-[color:var(--ink)]/10 text-center">
<p class="font-display text-lg">Sample card</p>
<p class="mt-2 text-sm text-[color:var(--ink)]/60">Particles should drift in the margins around this block, not over the top of it.</p>
</section>
<section class="grid gap-4 sm:grid-cols-2">
<div class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
<p class="font-display">Card A</p>
<p class="text-sm text-[color:var(--ink)]/60 mt-1">Two cards with a gap so you can see the canvas between them.</p>
</div>
<div class="rounded-3xl bg-[color:var(--card)] p-6 shadow-pete border-2 border-[color:var(--ink)]/10">
<p class="font-display">Card B</p>
<p class="text-sm text-[color:var(--ink)]/60 mt-1">Make your window taller to give the particles more room.</p>
</div>
</section>
</div>
<script>
// Instant switching: the pill links stay real URLs for no-JS visitors, but
// with JS we swap the effect in place via PeteWeather.set and keep the URL
// in sync with replaceState.
document.addEventListener("DOMContentLoaded", function () {
var root = document.documentElement;
var state = {
variant: {{.Variant}},
intensity: {{.Intensity}},
phase: {{.PhaseSel}}
};
function markActive(attr, value) {
document.querySelectorAll("[" + attr + "]").forEach(function (el) {
var on = el.getAttribute(attr) === value;
el.classList.toggle("bg-[color:var(--accent)]", on);
el.classList.toggle("text-white", on);
el.classList.toggle("font-semibold", on);
});
}
function apply() {
root.dataset.phase = state.phase;
if (window.PeteWeather) window.PeteWeather.set(state.variant, state.intensity);
markActive("data-demo-variant", state.variant);
markActive("data-demo-intensity", state.intensity);
markActive("data-demo-phase", state.phase);
var cur = document.querySelector("[data-demo-current]");
if (cur) cur.textContent = state.variant + " / " + state.intensity + " / " + state.phase;
var label = document.querySelector("[data-phase-label]");
if (label) label.textContent = state.phase;
history.replaceState(null, "", "?variant=" + state.variant + "&intensity=" + state.intensity + "&phase=" + state.phase);
}
document.addEventListener("click", function (e) {
var a = e.target.closest && e.target.closest("[data-demo-variant],[data-demo-intensity],[data-demo-phase]");
if (!a || !window.PeteWeather) return;
e.preventDefault();
if (a.hasAttribute("data-demo-variant")) state.variant = a.getAttribute("data-demo-variant");
if (a.hasAttribute("data-demo-intensity")) state.intensity = a.getAttribute("data-demo-intensity");
if (a.hasAttribute("data-demo-phase")) state.phase = a.getAttribute("data-demo-phase");
apply();
});
var rEl = document.querySelector("[data-demo-renderer]");
if (rEl && window.PeteWeather && window.PeteWeather.renderer) {
rEl.textContent = "renderer: " + window.PeteWeather.renderer();
}
// Lightweight FPS meter, demo page only. Counts frames on its own rAF and
// refreshes the pill once a second.
var fpsEl = document.querySelector("[data-demo-fps]");
if (fpsEl) {
var frames = 0, lastStamp = performance.now();
function tick(now) {
frames++;
if (now - lastStamp >= 1000) {
fpsEl.textContent = Math.round(frames * 1000 / (now - lastStamp)) + " fps";
frames = 0;
lastStamp = now;
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
}
});
</script>
{{end}}