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.
136 lines
6.7 KiB
HTML
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}}&intensity={{$.Intensity}}&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}}&intensity={{$i}}&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}}&intensity={{$.Intensity}}&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}}
|