// weather-vis.jsx — atmospheric visualizations & chart primitives // All exports attached to window at the bottom. // ───────────────────────────────────────────────────────────────────── // Atmospheric background — three layered SVGs that compose the "weather // you can feel" effect: isobars (pressure curves), wind flow field, // precipitation/condensation overlay. // ───────────────────────────────────────────────────────────────────── function IsobarField({ palette, opacity = 0.5, seed = 1 }) { // Concentric blobby isobars in the palette's accent line color. const cx1 = 30 + seed * 7, cy1 = 35; const cx2 = 75, cy2 = 70 - seed * 5; const ring = (i, base) => { const r = 12 + i * 7; return `M ${base.cx - r} ${base.cy} ` + `C ${base.cx - r} ${base.cy - r * 0.8}, ${base.cx + r} ${base.cy - r}, ${base.cx + r} ${base.cy} ` + `C ${base.cx + r} ${base.cy + r * 0.7}, ${base.cx - r} ${base.cy + r * 0.9}, ${base.cx - r} ${base.cy}`; }; return ( {[0, 1, 2, 3, 4].map((i) => ( ))} {[0, 1, 2, 3, 4, 5].map((i) => ( ))} {/* Faint pressure labels */} 1012 1008 ); } function WindField({ palette, direction = 268, intensity = 4.1, opacity = 0.55 }) { // Streamlines flowing in `direction` (meteorological — direction wind // comes from; we render the flow going TO direction+180). const flowDir = ((direction + 180) % 360) * Math.PI / 180; const dx = Math.cos(flowDir - Math.PI / 2); const dy = Math.sin(flowDir - Math.PI / 2); const lines = []; const density = Math.min(28, 14 + intensity); for (let i = 0; i < density; i++) { const t = i / density; // start points scattered across an oblique band const sx = -10 + t * 120 + Math.sin(i * 7.3) * 8; const sy = -10 + Math.cos(i * 3.1) * 110 + t * 30; // 3 control points along the flow vector with slight curl const pts = []; let x = sx, y = sy; for (let k = 0; k < 5; k++) { pts.push([x, y]); const wob = Math.sin((x + y + i) * 0.07) * 4; x += dx * 30 + wob * dy * 0.3; y += dy * 30 - wob * dx * 0.3; } const path = "M " + pts.map(p => p.join(" ")).join(" L "); lines.push( ); } return ( {lines} ); } function PrecipOverlay({ palette, intensity = 0.3, mood = "mist" }) { // Mist = soft horizontal washes. Rain = vertical streaks. Storm = streaks + flashes. if (mood === "mist" || mood === "overcast") { return ( {[12, 28, 44, 62, 80].map((y, i) => ( ))} ); } if (mood === "rain" || mood === "storm") { const drops = []; for (let i = 0; i < 60; i++) { const x = (i * 17) % 100; const y = (i * 23) % 100; const len = 3 + (i % 4); drops.push( ); } return ( {drops} ); } return null; } function AtmosphericBackdrop({ palette, weathercode = 55, hour = 8, windDir = 268, windSpd = 4 }) { const mood = palette.mood; return (
{/* film grain via SVG noise */}
); } // ───────────────────────────────────────────────────────────────────── // Sun arc — sunrise/sunset arc with current sun position dot. // ───────────────────────────────────────────────────────────────────── function SunArc({ palette, sunrise, sunset, now, width = 320, height = 70 }) { const t0 = new Date(sunrise + ":00").getTime(); const t1 = new Date(sunset + ":00").getTime(); const tn = new Date(now + ":00").getTime(); const p = Math.max(0, Math.min(1, (tn - t0) / (t1 - t0))); const w = width, h = height; // half-ellipse arc const cx = w / 2, cy = h * 0.95, rx = w * 0.42, ry = h * 0.78; const ang = Math.PI * (1 - p); // 0..PI from left (sunrise) to right (sunset) const sx = cx - rx * Math.cos(ang); const sy = cy - ry * Math.sin(ang); return ( {sunrise.slice(11)} {sunset.slice(11)} ); } // ───────────────────────────────────────────────────────────────────── // Moon glyph — pure SVG given phase fraction 0..1. // ───────────────────────────────────────────────────────────────────── function MoonGlyph({ phase, size = 26, color = "#fff", bg = "#000" }) { // phase 0=new 0.5=full const r = size / 2; const lit = phase; // simple cos blend // x offset for terminator ellipse const k = Math.cos(phase * 2 * Math.PI); return ( ); } // ───────────────────────────────────────────────────────────────────── // SeriesChart — multi-variable hourly sparkline. // ───────────────────────────────────────────────────────────────────── function Sparkline({ values, width, height, color, fill, strokeWidth = 1.5, smooth = true, minPad = 0.1 }) { const min = Math.min(...values), max = Math.max(...values); const range = Math.max(max - min, 0.001); const pad = range * minPad; const lo = min - pad, hi = max + pad, span = hi - lo; const xs = (i) => (i / (values.length - 1)) * width; const ys = (v) => height - ((v - lo) / span) * height; if (!smooth) { const d = values.map((v, i) => `${i === 0 ? 'M' : 'L'} ${xs(i)} ${ys(v)}`).join(' '); return ; } // Catmull-Rom -> cubic const pts = values.map((v, i) => [xs(i), ys(v)]); let d = `M ${pts[0][0]} ${pts[0][1]}`; for (let i = 0; i < pts.length - 1; i++) { const p0 = pts[Math.max(0, i - 1)]; const p1 = pts[i]; const p2 = pts[i + 1]; const p3 = pts[Math.min(pts.length - 1, i + 2)]; const c1x = p1[0] + (p2[0] - p0[0]) / 6; const c1y = p1[1] + (p2[1] - p0[1]) / 6; const c2x = p2[0] - (p3[0] - p1[0]) / 6; const c2y = p2[1] - (p3[1] - p1[1]) / 6; d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${p2[0]} ${p2[1]}`; } return ( <> {fill && ( )} ); } // Bar series — for precipitation function Bars({ values, width, height, color, max }) { const top = max ?? Math.max(...values, 1); const bw = width / values.length; return values.map((v, i) => { const h = (v / top) * height; return ; }); } // ───────────────────────────────────────────────────────────────────── // Wind compass — rose with cardinals + current dir/speed. // ───────────────────────────────────────────────────────────────────── function WindCompass({ direction = 268, speed = 4.1, gusts = 10.1, palette, size = 140, lang = "pt" }) { const r = size / 2; const c = size / 2; const ang = (direction - 90) * Math.PI / 180; const ax = c + (r - 18) * Math.cos(ang); const ay = c + (r - 18) * Math.sin(ang); const card = lang === "pt" ? ["N", "L", "S", "O"] : ["N", "E", "S", "W"]; return ( {/* outer ring */} {/* tick marks */} {Array.from({ length: 36 }).map((_, i) => { const t = (i * 10 - 90) * Math.PI / 180; const r1 = r - 2, r2 = r - (i % 9 === 0 ? 10 : 6); return ; })} {/* cardinals */} {[0, 90, 180, 270].map((deg, i) => { const t = (deg - 90) * Math.PI / 180; return {card[i]}; })} {/* arrow */} {/* center value */} {speed.toFixed(1)} m/s · {gusts.toFixed(0)}↑ ); } // ───────────────────────────────────────────────────────────────────── // Tide curve — synthesized sinusoid for a port near the city. // ───────────────────────────────────────────────────────────────────── function TideCurve({ palette, width = 340, height = 110, lang = "pt" }) { const N = 96; const data = Array.from({ length: N }, (_, i) => { const t = (i / N) * Math.PI * 4; return 0.5 + 0.4 * Math.sin(t + 0.6) + 0.05 * Math.sin(t * 3.2); }); const xs = (i) => (i / (N - 1)) * width; const ys = (v) => height - 10 - v * (height - 25); let d = `M ${xs(0)} ${ys(data[0])}`; for (let i = 1; i < N; i++) d += ` L ${xs(i)} ${ys(data[i])}`; const nowI = Math.floor(N * (8.5 / 24)); return ( {lang === "pt" ? "AGORA" : "NOW"} {[0, 6, 12, 18, 24].map((h) => { const i = (h / 24) * (N - 1); return {String(h).padStart(2, "0")}h ; })} ); } // Air-quality ring (AQI 0-200) — concentric meter. function AQIRing({ value = 38, palette, size = 110, lang = "pt" }) { const max = 300; const pct = Math.min(1, value / max); const r = size / 2 - 8; const c = size / 2; const circ = 2 * Math.PI * r; const color = value < 50 ? "#5e9c6a" : value < 100 ? "#c5a23a" : value < 150 ? "#c97c3a" : "#a8463a"; const label = value < 50 ? (lang === "pt" ? "Bom" : "Good") : value < 100 ? (lang === "pt" ? "Moderado" : "Moderate") : value < 150 ? (lang === "pt" ? "Sensíveis" : "Sensitive") : (lang === "pt" ? "Ruim" : "Unhealthy"); return ( {value} AQI · {label} ); } Object.assign(window, { IsobarField, WindField, PrecipOverlay, AtmosphericBackdrop, SunArc, MoonGlyph, Sparkline, Bars, WindCompass, TideCurve, AQIRing, });