// Canvas-based seasonal weather. The server picks variant + intensity from // the Lisbon-local date; this script reads those off and // renders the appropriate particles. Each variant has a hand-drawn shape // so silhouettes are recognizable instead of a generic blob. (function () { var root = document.documentElement; var canvas = document.getElementById("pete-weather"); if (!canvas) return; var variant = root.dataset.weather; if (!variant) return; if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; var ctx = canvas.getContext("2d"); var W = 0, H = 0, DPR = 1; function resize() { DPR = Math.min(window.devicePixelRatio || 1, 2); W = canvas.clientWidth || window.innerWidth; H = canvas.clientHeight || window.innerHeight; canvas.width = Math.floor(W * DPR); canvas.height = Math.floor(H * DPR); ctx.setTransform(DPR, 0, 0, DPR, 0, 0); } resize(); window.addEventListener("resize", resize); var counts = { rain: 160, petals: 50, jacaranda: 55, motes: 70, leaves: 38 }; var mults = { light: 0.6, medium: 1.0, heavy: 1.4 }; var N = Math.round((counts[variant] || 0) * (mults[root.dataset.intensity] || 1)); function rand(a, b) { return a + Math.random() * (b - a); } function spawn(initial) { var p = {}; p.x = rand(0, W); p.y = initial ? rand(0, H) : rand(-40, -10); p.z = rand(0.7, 1.3); // depth — affects size + speed + alpha if (variant === "rain") { p.vy = rand(700, 1100) * p.z; p.vx = -rand(60, 120); p.len = rand(10, 18) * p.z; p.alpha = rand(0.25, 0.55); } else if (variant === "petals" || variant === "jacaranda") { p.vy = rand(60, 120); p.swayAmp = rand(20, 50); p.swayFreq = rand(0.4, 0.9); p.swayPhase = rand(0, Math.PI * 2); p.rot = rand(0, Math.PI * 2); p.vrot = rand(-1.2, 1.2); p.size = rand(8, 16) * p.z; p.alpha = rand(0.75, 1.0); if (variant === "jacaranda") { p.hue = rand(265, 285); // blue-violet — jacaranda uses the petal silhouette in purple p.sat = rand(55, 75); p.light = rand(70, 82); } else { p.hue = rand(335, 355); // pink (almond/cherry) p.sat = rand(60, 80); p.light = rand(78, 88); } } else if (variant === "motes") { p.vx = rand(-15, 25); p.vy = rand(8, 25); p.size = rand(1.2, 2.8) * p.z; p.alpha = rand(0.35, 0.7); p.twinklePhase = rand(0, Math.PI * 2); p.twinkleFreq = rand(0.5, 1.5); } else if (variant === "leaves") { p.vy = rand(70, 130); p.swayAmp = rand(30, 70); p.swayFreq = rand(0.3, 0.6); p.swayPhase = rand(0, Math.PI * 2); p.rot = rand(0, Math.PI * 2); p.vrot = rand(-2.0, 2.0); p.size = rand(10, 18) * p.z; p.alpha = rand(0.8, 1.0); p.hue = rand(18, 42); // orange→amber→brown p.light = rand(38, 55); } return p; } var particles = []; for (var i = 0; i < N; i++) particles.push(spawn(true)); var night = root.dataset.phase === "night"; function refreshNight() { night = root.dataset.phase === "night"; } function drawRain(p) { // Day/dawn/dusk backgrounds are warm and pale, so the rain needs a darker, // more saturated blue plus a slightly thicker line to stay visible. Night // keeps a paler tone so streaks read against the dark palette. if (night) { ctx.strokeStyle = "rgba(200,215,240," + p.alpha + ")"; ctx.lineWidth = 1; } else { ctx.strokeStyle = "rgba(60,95,150," + Math.min(1, p.alpha + 0.25) + ")"; ctx.lineWidth = 1.3; } ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - p.len * 0.12, p.y + p.len); ctx.stroke(); } function drawPetals(p) { // Five-petal almond/cherry blossom viewed face-on, with a yellow center. var s = p.size; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); ctx.fillStyle = "hsla(" + p.hue + "," + p.sat + "%," + p.light + "%," + p.alpha + ")"; for (var i = 0; i < 5; i++) { var a = (i / 5) * Math.PI * 2 - Math.PI / 2; var cx = Math.cos(a) * s * 0.32; var cy = Math.sin(a) * s * 0.32; ctx.beginPath(); ctx.ellipse(cx, cy, s * 0.38, s * 0.24, a, 0, Math.PI * 2); ctx.fill(); } ctx.fillStyle = "hsla(48,90%,68%," + p.alpha + ")"; ctx.beginPath(); ctx.arc(0, 0, s * 0.13, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function drawLeaf(p) { var s = p.size; ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot); var g = ctx.createLinearGradient(-s * 0.5, 0, s * 0.5, 0); g.addColorStop(0, "hsla(" + p.hue + ",70%," + (p.light - 10) + "%," + p.alpha + ")"); g.addColorStop(1, "hsla(" + p.hue + ",75%," + (p.light + 10) + "%," + p.alpha + ")"); ctx.fillStyle = g; // Almond/lozenge leaf — pointed at both ends. ctx.beginPath(); ctx.moveTo(-s * 0.55, 0); ctx.quadraticCurveTo(0, -s * 0.38, s * 0.55, 0); ctx.quadraticCurveTo(0, s * 0.38, -s * 0.55, 0); ctx.closePath(); ctx.fill(); // Midrib vein. ctx.strokeStyle = "hsla(" + p.hue + ",60%," + (p.light - 22) + "%," + (p.alpha * 0.7) + ")"; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(-s * 0.5, 0); ctx.lineTo(s * 0.5, 0); ctx.stroke(); // Small stem nub at the base. ctx.strokeStyle = "hsla(" + p.hue + ",55%," + (p.light - 25) + "%," + (p.alpha * 0.7) + ")"; ctx.lineWidth = 1.0; ctx.beginPath(); ctx.moveTo(-s * 0.55, 0); ctx.lineTo(-s * 0.7, -s * 0.04); ctx.stroke(); ctx.restore(); } function drawMote(p, t) { var twinkle = 0.5 + 0.5 * Math.sin(t * p.twinkleFreq + p.twinklePhase); // Night keeps a pale glowing mote against the dark palette. Day/dawn/dusk // use a deeper saturated honey so the motes stand out against warm cream // and peach backgrounds; alpha also gets a bump. var color, a; if (night) { color = "255,240,180"; a = p.alpha * (0.45 + 0.55 * twinkle); } else { color = "130,80,180"; a = Math.min(1, (p.alpha + 0.2) * (0.6 + 0.4 * twinkle)); } // Soft glow halo. var g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.size * 5); g.addColorStop(0, "rgba(" + color + "," + (a * 0.55) + ")"); g.addColorStop(1, "rgba(" + color + ",0)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * 5, 0, Math.PI * 2); ctx.fill(); // Bright core. ctx.fillStyle = "rgba(" + color + "," + a + ")"; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); } var last = performance.now(); var raf = 0; function step(now) { var dt = Math.min(0.05, (now - last) / 1000); last = now; var t = now / 1000; ctx.clearRect(0, 0, W, H); refreshNight(); for (var i = 0; i < particles.length; i++) { var p = particles[i]; if (variant === "rain") { p.x += p.vx * dt; p.y += p.vy * dt; if (p.y > H + 20 || p.x < -40) { particles[i] = spawn(false); particles[i].x = rand(0, W + 60); continue; } drawRain(p); } else if (variant === "motes") { p.x += p.vx * dt; p.y += p.vy * dt; if (p.y > H + 10 || p.x < -10 || p.x > W + 10) { particles[i] = spawn(false); continue; } drawMote(p, t); } else { // Drifting variants: petals, jacaranda, leaves. p.y += p.vy * dt; p.rot += p.vrot * dt; var sway = Math.sin(t * p.swayFreq + p.swayPhase) * p.swayAmp; var origX = p.x; p.x = origX + sway; if (p.y > H + 30) { particles[i] = spawn(false); continue; } if (variant === "jacaranda" || variant === "petals") drawPetals(p); else drawLeaf(p); p.x = origX; } } raf = requestAnimationFrame(step); } raf = requestAnimationFrame(step); document.addEventListener("visibilitychange", function () { if (document.hidden) { cancelAnimationFrame(raf); raf = 0; } else if (!raf) { last = performance.now(); raf = requestAnimationFrame(step); } }); })();