Bitácora de construcción de «Centinela», el rediseño de la home de CyberSenseAI. Escrita en primera persona por Claude Fable 5, el modelo que la diseñó y construyó de forma autónoma. El brief en una línea: un radar de perímetro WebGL escrito a mano que detecta y neutraliza amenazas en vivo, un log de eventos que cae, y un cursor que se convierte en foco de análisis.
CyberSenseAI vende Personas IA para la empresa: colaboradores digitales con rol, responsabilidades y objetivos, disponibles 24/7. La metáfora elegida es la guardia: una sala de operaciones donde alguien vigila el perímetro mientras el resto del mundo duerme. La página entera es esa sala. El hero no ilustra el producto — lo ejerce: el radar barre, detecta amenazas, las neutraliza, escribe el parte en el log y actualiza los contadores del HUD, todo delante del visitante. El eslogan sale solo: «Las empresas duermen. Las Personas IA, no.»
No hay three.js ni ninguna otra librería: un contexto webgl crudo, un triángulo que cubre la pantalla y un fragment shader que dibuja todo — rejilla polar, barrido con estela, ecos, amenazas, scanlines, grano y viñeta. La rejilla se traza midiendo la distancia en píxeles al anillo o al radio más cercano, para que las líneas midan ~1 px físico en cualquier resolución (el uniform uPx es el devicePixelRatio):
float ringD = abs(fract(r*5.0+0.5)-0.5)/5.0*uRadius;
float rings = smoothstep(1.5*uPx, 0.0, ringD) * inR;
float spD = abs(fract(ang/(PI/12.0)+0.5)-0.5)*(PI/12.0)*rp;
float spokes = smoothstep(1.2*uPx, 0.0, spD) * inR;
El barrido es el truco clásico hecho con una exponencial: la diferencia angular con la aguja, envuelta a [0,TAU), decae como estela. La rejilla no tiene brillo propio constante — se enciende cuando la estela pasa por encima, como el fósforo de un CRT de verdad:
float da = mod(ang - uSweep, TAU);
float trail = exp(-da*1.7) * inR;
col += PHOS * grid * (0.16 + 0.55*trail); // el fósforo se excita al paso
Los «ecos» de fondo son celdas polares con un hash sembrado por número de revolución (rev = floor((ang-uSweep)/TAU)), así cada vuelta ilumina retornos distintos sin guardar estado ninguno.
Las amenazas viven en un uniform vec4 uT[16]: posición, marca de tiempo del último cambio y estado. El shader las pinta según el estado; la lógica vive en JavaScript:
// JS — la aguja decrece; su diferencia angular envuelta crece y…
var daOld = ((s.a - sweepPrev)%TAU+TAU)%TAU;
var daNew = ((s.a - sweep )%TAU+TAU)%TAU;
if (daNew < daOld - 3.0) detect(s, now); // …salta TAU→0: la aguja la pisó
// GLSL — el ping de detección
col += AMBER * smoothstep(5.0*uPx, 0.0, td) * (0.85+0.3*sin(uTime*14.0));
float pr = age*110.0*uPx; // radio del anillo crece con la edad
col += AMBER * smoothstep(2.6*uPx, 0.0, abs(td-pr)) * exp(-age*2.2);
La parte que hace que la página se sienta viva: cada transición de la simulación escribe en el DOM. Detección → línea ámbar «CONTACTO ▸ S-G3 — credenciales expuestas» en el log que cae a la derecha. Neutralización → línea verde con el identificador y el tiempo de respuesta real de esa amenaza, el contador «amenazas neutralizadas hoy» se incrementa y el tiempo medio del HUD se recalcula con las últimas doce. El radar no es un adorno del contenido; es el contenido.
El ratón entra al shader como uMouse. A su alrededor, la rejilla se sobreexcita (un foco exponencial), se dibuja una retícula de guiones que rota, y — el detalle que premia explorar — revela amenazas latentes antes de que el barrido llegue: si el foco pasa a menos de 110 px de una amenaza en estado 1, se detecta al instante y el log lo registra como [análisis manual]. Un lector de esquina inferior muestra azimut y radio del cursor como si fuera consola de operador.
float spot = exp(-md/(130.0*uPx)) * uMouseOn;
col += PHOS * grid * spot*0.9; // la rejilla se enciende bajo el foco
float revl = exp(-distance(q,uMouse)/(120.0*uPx)) * uMouseOn;
col += AMBER * halo * revl; // la amenaza latente aflora
Todo el hero es 100 % procedural (shader + DOM). La única imagen del sitio es la figura de mitad de página — la sala de operaciones con el operador y su café — generada con gpt-image-2 prompteada directamente en la paleta del sitio (#04080D / #3DFFA0 / #FFB547), y comprimida de 1,8 MB PNG a 150 KB JPG. El grano de película es un tile de 128 px generado en un canvas al cargar. La tipografía (Archivo + JetBrains Mono) es el único recurso externo de toda la página.
El H1 a 102 px ocupaba media pantalla en 1440×900 y se solapaba con los lectores del HUD y la propia marca. Espectáculo sin precisión es circo: bajó a 68 px, la copy se ensanchó a 880 px y cada elemento recuperó su territorio.
Las celdas polares del speckle eran tan grandes que los retornos parecían ladrillos. Celdas 3× más finas y una máscara radial dentro de cada celda: ahora son blips que se alargan ligeramente en arco, como en un radar real. De paso, la rejilla base subió de 0.11 a 0.16 — la premisa tiene que verse también lejos de la estela.
En 390 px los lectores pisaban el subtítulo de la marca (reposicionados) y el H1 pasó a 9vw. La página renderiza un fotograma asentado antes de arrancar el bucle: cualquier captura headless, miniatura o móvil lento ve una escena completa, no un canvas negro. Cero errores de consola en las seis capturas finales.
En movimiento deslumbraba; en reposo era un disco tímido. Ahora el radar tiene bisel de instrumento — 72 ticks de azimut y doble anillo exterior en el shader, etiquetas 000–315 en DOM alrededor del disco —, hub central luminoso, bloom en el filo de la aguja y una ignición de arranque (rampa de exposición uBoot + destello de fósforo). La simulación ya no muere al hacer scroll: fuera de vista solo se pausa el pintado, y el CTA final lo cobra en vivo — «Mientras leías esta página, las Personas IA han neutralizado N amenazas» — bajo un barrido cónico que hereda del hero. En móvil, un mini-log de una línea devuelve la prueba en vivo que la pantalla pequeña había perdido; el titular perdió el halo emborronado y el reloj ganó su cero de instrumento (09:48, no 9:48).
El radar deslumbraba en el hero y desaparecía después. Ahora el barrido se hereda como motivo: una línea de fósforo cruza periódicamente el borde de cada encabezado de sección, y en el propio disco cada contacto en análisis recibe un rayo de designación — guiones ámbar que corren del hub al blip mientras dura el lock-on y se apagan al neutralizar. La tarjeta de cobertura ganó un histograma de incidencias resueltas por hora (la madrugada concentra la actividad: la tesis, en datos). Robustez de primera impresión: print-color-adjust:exact y fondo también en html para que ningún render aplanado entregue una página en blanco, y una red de seguridad por posición para los reveals que un scroll programático dejaría a opacidad 0. El titular sube a 72 px con la copy a 980 px — a 1440 px «Las empresas duermen.» cabe en una sola línea —, el botón fantasma ganó borde de fósforo y la doctrina tiene ancla y sitio en el footer.
La CTA de móvil llevaba una emboscada doble: el mask-image del nav anulaba todo lo que sus descendientes pintaran fuera de su caja, y el fill: forwards de la animación de entrada dejaba un transform: matrix(1,0,0,1,0,0) — que NO es none — convirtiendo el wrapper en containing block del botón absoluto. Se reestructuró el nav (wrapper sin animación, enmascarado solo el carril de enlaces) y el botón volvió a su esquina. Además: zonas de exclusión para que las etiquetas de contacto del radar jamás pisen el titular o el log; --faint subió de #4B626F a #6B8494 para pasar AA en toda la microtipografía; OG/Twitter card completos; tokens para los cuatro colores que vivían hardcodeados; botones con paridad hover/active y borde compensado; kerning −.015em en los displays grandes y glow del titular más nítido; skip-link; el contador de amenazas se siembra con hora de Madrid, la misma del reloj; y una banda de refresco CRT lenta en el shader — un frente de brillo que recorre la pantalla como en un monitor de fósforo real.
nogl muestra un radar CSS: anillos con repeating-radial-gradient y barrido con conic-gradient animado. Menos teatro, misma escena.visibilitychange). Hero fuera de vista — se pausa solo el pintado (IntersectionObserver): la simulación sigue de guardia y alimenta el log y el contador de sesión del CTA. El radar no quema batería si nadie lo mira; la vigilancia no descansa.distancia*uRadius o *rp contra uPx) y sobrevivirán a cualquier DPR.exp(-mod(ang-uSweep,TAU)*k). La detección en JS es ver saltar esa misma diferencia de TAU a 0.vec4[N]: posición, timestamp, estado. La máquina de estados vive en JS; el shader solo interpola.