{/* 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' && (
)}
{inp.kind === 'wrf-logo' && (
)}
{inp.kind === 'stations-map' && (
)}
{/* 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 */}
{/* 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) => (
))}
{/* 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 (
);
})}
{/* 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 });