// scenes-v3-overrides.jsx — Institutional variants of scenes 4, 5 and 7. // // • S4Aerosols — drops Popocatépetl (out-of-context for the Valle), // moves the marine-aerosol source higher, pushes the // title below the brand strip. // • S5Resolution — embeds the real CAMS/GFS/WRF pixel figure with // proper containment so it can never crop. // • S6Downscale — three large source cards: official CAMS icon (SVG), // official WRF logo (PNG) and the SIATA stations map. // // Same scene timing / fade behaviour as v2. // ─── Scene 4 (institutional): aerosol transport — without volcano ──── function S4Aerosols() { const { localTime, duration } = useSprite(); const time = useTime(); const W = 1280, H = 720; // Sources — Valle moved to the far left; burns SE & below the Valle; // Caribbean marine aerosol upper-right of Valle (right of burns); // Sahara dust stays in its perfect upper-right position. const sources = React.useMemo(() => [ { name: 'Polvo del Sahara', tag: 'AOD 1.2', color: C.orange, x: 1010, y: 290, spread: 80, weight: 1.0, }, { name: 'Quemas · Amazonía', tag: 'BC + OC', color: C.coral, x: 600, y: 560, spread: 78, weight: 0.95, }, { name: 'Aerosol marino · Caribe', tag: 'sea salt', color: C.cyan, x: 720, y: 310, spread: 65, weight: 0.55, }, ], []); // Composite trade-wind flow field (unchanged from v2) const field = React.useMemo(() => (x, y, t) => { const lat = (H / 2 - y) / (H / 2); const trade = Math.exp(-lat * lat * 2); const vx = -1 * trade - 0.25; const vy = lat * 0.25; const tt = t * 0.08; const swirl = 0.45; const cx2 = Math.sin(x * 0.005 + tt) + Math.cos(y * 0.006 - tt * 0.7); const cy2 = Math.cos(x * 0.0055 - tt * 0.5) + Math.sin(y * 0.0048 + tt); return [vx + cx2 * swirl, vy + cy2 * swirl]; }, []); const totalW = sources.reduce((s, p) => s + p.weight, 0); const spawnFn = React.useMemo(() => (t, i) => { let pick = (i * 0.6180339 + t * 0.001) % 1; pick *= totalW; let acc = 0; for (const src of sources) { acc += src.weight; if (pick <= acc) { const r = rand(i * 13 + Math.floor(t * 100)); const ang = r() * Math.PI * 2; const rad = Math.sqrt(r()) * src.spread; return { x: src.x + Math.cos(ang) * rad, y: src.y + Math.sin(ang) * rad }; } } return { x: sources[0].x, y: sources[0].y }; }, [sources, totalW]); const tKick = Easing.easeOutCubic(clamp((localTime - 0.3) / 0.5, 0, 1)); const tHead = Easing.easeOutCubic(clamp((localTime - 0.7) / 0.7, 0, 1)); const tMap = Easing.easeOutCubic(clamp((localTime - 1.0) / 0.8, 0, 1)); const tPin = Easing.easeOutCubic(clamp((localTime - 4.0) / 0.6, 0, 1)); const tFact = Easing.easeOutCubic(clamp((localTime - 6.0) / 0.6, 0, 1)); const fadeOut = clamp((duration - localTime) / 0.5, 0, 1); const valle = { x: 240, y: 360 }; return (
{/* Source halos */} {sources.map((s, i) => ( ))} {sources.map((s, i) => ( ))} {/* Graticule */} {[ { lat: 30, label: '30°N' }, { lat: 15, label: '15°N' }, { lat: 0, label: '0°', equator: true }, { lat: -15, label: '15°S' }, { lat: -30, label: '30°S' }, ].map((row, i) => { const y = 468 - row.lat * 7.1; if (y < 0 || y > 720) return null; return ( {row.label} ); })} {[ { lon: -90, label: '90°W' }, { lon: -60, label: '60°W' }, { lon: -30, label: '30°W' }, { lon: 0, label: '0°' }, { lon: 30, label: '30°E' }, ].map((col, i) => { const x = 700 + (col.lon + 75) * 4.13; if (x < 0 || x > 1280) return null; return ( {col.label} ); })} {/* Compass — moved right and down a bit so it doesn't fight the brand strip */}
Flujo dominante
E → O
vientos alisios tropicales
{/* Header — pushed below the brand strip */}
Capítulo III · Transporte de aerosoles
Lo que ven los satélites,
CAMS lo transporta hora a hora.
CAMS Global / aerosol optical depth
level 550 nm · superficie · UTC {String(Math.floor((time * 6) % 24)).padStart(2, '0')}:00
{/* Source pins */} {sources.map((s, i) => { const ap = Easing.easeOutBack(clamp((localTime - 2.3 - i * 0.2) / 0.5, 0, 1)); return (
{s.name}
{s.tag}
); })} {/* Valle de Aburrá target */}
{[0, 1, 2].map(i => { const ph = ((time * 0.6 + i / 3) % 1); return (
); })}
Valle de Aburrá
06°15′N · 75°34′W
{/* Bottom fact */}
Por qué importa para el Valle
Polvo del Sahara, humo de quemas amazónicas y aerosol marino del Caribe pueden cruzar el continente en 3 a 5 días y modular nuestra calidad del aire local.
fuente · CAMS global forecast
aerosol_550nm · superficie
); } // ─── Scene 5 (institutional): CAMS 40 km pixels over Valle de Aburrá ── function S5Resolution() { const { localTime, duration } = useSprite(); const time = useTime(); const tKick = Easing.easeOutCubic(clamp((localTime - 0.2) / 0.5, 0, 1)); const tHead = Easing.easeOutCubic(clamp((localTime - 0.5) / 0.6, 0, 1)); const tMap = Easing.easeOutCubic(clamp((localTime - 1.0) / 0.7, 0, 1)); const tHL = Easing.easeOutCubic(clamp((localTime - 2.4) / 0.7, 0, 1)); const tFact = Easing.easeOutCubic(clamp((localTime - 3.6) / 0.6, 0, 1)); const fadeOut = clamp((duration - localTime) / 0.45, 0, 1); // Smaller image area, contained, so it can't crop. Top a bit lower // so it clears the brand strip. const mapW = 460, mapH = 520; const mapLeft = 760, mapTop = 100; const camsPixels = [ { x: 0.04, y: 0.31 }, { x: 0.62, y: 0.31 }, { x: 0.04, y: 0.86 }, { x: 0.50, y: 0.86 }, { x: 0.71, y: 0.86 }, { x: 0.92, y: 0.86 }, ]; return (
Capítulo IV · El reto de la escala
CAMS describe
el planeta
en celdas de 40 km.
Cada celda promedia ~1 600 km² de atmósfera. El Valle de Aburrá, con sus ~60 km de norte a sur, queda atravesado por sólo 3 o 4 píxeles CAMS.
Resoluciones superpuestas
{[ { c: '#A04949', sym: 'square', l: 'CAMS', sub: '40 km · global' }, { c: '#8AA4D4', sym: 'square', l: 'GFS', sub: '28 km · global' }, { c: '#7E7793', sym: 'diamond', l: 'WRF', sub: '2 km · regional' }, { c: '#3D1F4A', sym: 'circle', l: 'Estaciones', sub: '20 puntos · in situ' }, ].map((row, i) => (
{row.l}
{row.sub}
))}
{/* Right: institutional pixel map — object-contain so it never crops */}
CAMS, GFS, WRF pixels y estaciones SIATA sobre el Valle de Aburrá {/* Highlight pulses on the red CAMS pixels */} {camsPixels.map((p, i) => { const ph = ((time * 0.55 + i * 0.18) % 1); const r = 2 + ph * 5; return ( ); })}
Fig. 1  · Píxeles de modelos meteorológicos sobre el Valle de Aburrá
); } // ─── Scene 7 (institutional): Ensamble local — three big source cards ── function S6Downscale() { const { localTime, duration } = useSprite(); const time = useTime(); const tKick = Easing.easeOutCubic(clamp((localTime - 0.2) / 0.5, 0, 1)); const tHead = Easing.easeOutCubic(clamp((localTime - 0.6) / 0.6, 0, 1)); const tCams = Easing.easeOutCubic(clamp((localTime - 1.2) / 0.6, 0, 1)); const tWrf = Easing.easeOutCubic(clamp((localTime - 1.8) / 0.6, 0, 1)); const tStn = Easing.easeOutCubic(clamp((localTime - 2.4) / 0.6, 0, 1)); const tArr = Easing.easeOutCubic(clamp((localTime - 3.4) / 0.7, 0, 1)); const tOut = Easing.easeOutCubic(clamp((localTime - 4.4) / 0.7, 0, 1)); const tCap = Easing.easeOutCubic(clamp((localTime - 5.5) / 0.6, 0, 1)); const fadeOut = clamp((duration - localTime) / 0.45, 0, 1); // Cards stack: each is "highlighted" briefly as its arrow fires. const cardW = 320, cardH = 150, cardX = 56; const inputs = [ { key: 'cams', label: 'CAMS · 40 km', sub: 'background químico', color: C.orange, t: tCams, highlight: 1.2, y: 170, kind: 'cams-icon', }, { key: 'wrf', label: 'WRF · 2 km', sub: 'meteorología regional', color: C.cyan, t: tWrf, highlight: 1.8, y: 340, kind: 'wrf-logo', }, { key: 'stn', label: '20 estaciones SIATA', sub: 'PM2.5 in situ', color: C.amber, t: tStn, highlight: 2.4, y: 510, kind: 'stations-map', }, ]; // CAMS icon comes from the official Copernicus Atmosphere SVG — // see assets/cams-icon.svg. We load it via so its original // teal (#55bfd0) is preserved untouched. return ( {/* Header */}
Capítulo VI · Ensamble local
Del modelo global a cada estación SIATA
{/* Input cards — large, with real institutional refs */} {inputs.map((inp, i) => { const pulse = Math.max(0, 1 - Math.abs(localTime - inp.highlight) * 1.4); const lift = pulse * 6; return (
{/* Card */}
0.05 ? '0 0 28px ' + hexWithAlpha(inp.color, pulse * 0.45) : 'none', transition: 'box-shadow 0.2s', }} /> {/* Big institutional thumbnail (left half of card) */}
{inp.kind === 'cams-icon' && ( Copernicus Atmosphere · CAMS )} {inp.kind === 'wrf-logo' && ( WRF logo )} {inp.kind === 'stations-map' && ( Mapa de estaciones SIATA )}
{/* Label on right */}
{inp.label}
{inp.sub}
{inp.key === 'cams' && (
Copernicus · ECMWF
química global, hora a hora
)} {inp.key === 'wrf' && (
NCAR · UCAR
viento, T, humedad, capa límite
)} {inp.key === 'stn' && (
Red Calidad del Aire AMVA
muestreo horario, 24/7
)}
); })} {/* Arrows + ML node */} {inputs.map((inp) => { const sy = inp.y + cardH / 2; return ( ); })} {inputs.map((inp, i) => { const ph = ((time * 0.5 + i * 0.33) % 1); const x = (cardX + cardW + 6) + (600 - (cardX + cardW + 6)) * ph; const y = (inp.y + cardH / 2) + (400 - (inp.y + cardH / 2)) * ph; return ; })} {/* ML node */}
Machine
Learning
ensamble
{/* Output heatmap */}
Pronóstico · 20 estaciones × 72 h
T+0+24h+48h+72h
{Array.from({ length: 20 }, (_, row) => (
E{String(row + 1).padStart(2, '0')}
{Array.from({ length: 24 }, (_, col) => { const stationLevel = 0.4 + (Math.sin(row * 1.3) + 1) / 2 * 0.5; const phase = col / 24 * Math.PI * 6 + row * 0.18; const diurnal = (Math.sin(phase - Math.PI / 2) + 1) / 2; const drift = (Math.sin(col * 0.18 + row * 0.07) + 1) / 2 * 0.3; const v = stationLevel * (0.4 + diurnal * 0.5) + drift; const color = v > 0.75 ? C.coral : v > 0.55 ? C.orange : v > 0.35 ? C.amber : C.green; const ap = Easing.easeOutCubic(clamp((localTime - 4.4 - (row + col) * 0.02) / 0.4, 0, 1)); return (
); })}
))}
0µg/m³50+
{/* Bottom caption */}
Modelos de machine learning aprenden a corregir el sesgo de CAMS en la coordenada exacta de cada estación, usando WRF como contexto meteorológico y los datos históricos de PM2.5 como anclaje real.
); } // ─── Scene 6 (institutional): ¿Qué es WRF? — equations + 2 km ───────── function S6WRF() { const { localTime, duration } = useSprite(); const time = useTime(); const tKick = Easing.easeOutCubic(clamp((localTime - 0.2) / 0.5, 0, 1)); const tHead = Easing.easeOutCubic(clamp((localTime - 0.6) / 0.6, 0, 1)); const tBody = Easing.easeOutCubic(clamp((localTime - 1.1) / 0.7, 0, 1)); const tEq = Easing.easeOutCubic(clamp((localTime - 1.8) / 0.6, 0, 1)); const tD1 = Easing.easeOutCubic(clamp((localTime - 2.2) / 0.5, 0, 1)); const tD2 = Easing.easeOutCubic(clamp((localTime - 2.7) / 0.5, 0, 1)); const tD3 = Easing.easeOutCubic(clamp((localTime - 3.2) / 0.5, 0, 1)); const tWinds = Easing.easeOutCubic(clamp((localTime - 3.9) / 0.6, 0, 1)); const tStats = Easing.easeOutCubic(clamp((localTime - 4.7) / 0.6, 0, 1)); const fadeOut = clamp((duration - localTime) / 0.45, 0, 1); const cx = 920, cy = 400; const d1 = { w: 320, h: 360, color: C.purple, label: 'D01 · 18 km', sub: 'NW de Sudamérica' }; const d2 = { w: 200, h: 230, color: C.cyan, label: 'D02 · 9 km', sub: 'Antioquia' }; const d3 = { w: 110, h: 140, color: C.amber, label: 'D03 · 2 km', sub: 'Valle de Aburrá' }; return ( {/* Left text column */}
Capítulo V · ¿Qué es WRF?
La meteorología
del Valle,
hora a hora.
WRF (Weather Research & Forecasting, NCAR / UCAR) resuelve, en cada celda del modelo, las ecuaciones del movimiento del aire: conservación de masa, energía, momento y humedad.
{/* Equation block */}
Ecuaciones primitivas
u/∂t = − v·∇u − fk×v − (1/ρ)∇p + F
θ/∂t = − v·∇θ + Q/Π
q/∂t = − v·∇q + Sq
viento · temperatura potencial · humedad
SIATA opera WRF con tres dominios anidados que bajan de 18 km a 2 km sobre el Valle, capturando el efecto de las montañas que CAMS no puede ver.
{/* Stats pills */}
{[ { v: '2 km', l: 'dominio interno' }, { v: '16', l: 'variables' }, { v: 'horaria', l: 'salida' }, ].map((s, i) => (
{s.v}
{s.l}
))}
{/* Right diagram: nested WRF domains — D01 18 km, D02 9 km, D03 3 km */}
{/* D01 */}
{Array.from({ length: 8 }, (_, i) => ( ))} {Array.from({ length: 7 }, (_, i) => ( ))}
{d1.label}
{d1.sub}
{/* D02 */}
{Array.from({ length: 12 }, (_, i) => ( ))} {Array.from({ length: 10 }, (_, i) => ( ))}
{d2.label}
{d2.sub}
{/* D03 — Valle de Aburrá at 2 km */}
{Array.from({ length: 14 }, (_, i) => ( ))} {Array.from({ length: 11 }, (_, i) => ( ))} {tWinds > 0 && (() => { const arrows = []; for (let i = 0; i < 5; i++) { for (let j = 0; j < 7; j++) { const ax = 10 + (d3.w - 20) * (i + 0.5) / 5; const ay = 12 + (d3.h - 24) * (j + 0.5) / 7; const phase = time * 0.6 + j * 0.3; const ang = -Math.PI / 2 + Math.sin(phase) * 0.5; const len = 6 + 3 * Math.sin(phase + i); const ex = ax + Math.cos(ang) * len; const ey = ay + Math.sin(ang) * len; const opa = tWinds * (0.45 + Math.sin(phase + i * 0.7) * 0.25); arrows.push( ); } } return arrows; })()}
{d3.label}
); } // ─── Scene 9 (institutional): Outro — final phrase with WRF nod ─────── function S8Outro() { const { localTime, duration } = useSprite(); const time = useTime(); const field = React.useMemo(() => directionalField(Math.PI * 0.98, 0.0035, 0.15, 0.55, 0.4), []); const tKick = Easing.easeOutCubic(clamp((localTime - 0.3) / 0.5, 0, 1)); const tHead = Easing.easeOutCubic(clamp((localTime - 0.7) / 0.7, 0, 1)); const tFoot = Easing.easeOutCubic(clamp((localTime - 3.0) / 0.5, 0, 1)); const fadeOut = clamp((duration - localTime) / 0.6, 0, 1); const stats = [ { v: '4', l: 'fuentes integradas', c: C.cyan, d: 0 }, { v: '20', l: 'estaciones SIATA', c: C.amber, d: 0.15 }, { v: '72 h', l: 'horizonte de pronóstico', c: C.orange, d: 0.3, nowrap: true }, { v: '>80%', l: 'acierto categoría ICA', c: C.green, d: 0.45 }, ]; return (
El círculo se cierra
CAMS observa el planeta.
SIATA lo aterriza al Valle con ciencia y modelos propios.
{/* Stats grid */}
{stats.map((s, i) => { const ap = Easing.easeOutBack(clamp((localTime - 1.5 - s.d) / 0.6, 0, 1)); return (
{s.v}
{s.l}
); })}
{/* Footer */}
Publicado cada 6 horas en siata.gov.co · PM2.5 horario, 20 estaciones, 3 días de pronóstico.
CCT 261/2025 · SIATA · Área Metropolitana del Valle de Aburrá
); } Object.assign(window, { S4Aerosols, S5Resolution, S6WRF, S6Downscale, S8Outro });