Cómo se hizo · ADN
Guía de diseño y técnica de la home de CyberSenseAI: una doble hélice 3D escrita a mano en WebGL cuyos pares de bases son los cuatro valores CREI, y que se desenrolla a medida que el visitante hace scroll.
01 / Concepto
CyberSenseAI afirma que sus Personas IA son Confiables, Responsables, Éticas e Inclusivas. La mayoría de webs enterprise colocan esos valores en una lista de viñetas; aquí son la estructura física de la página: las cuatro bases nitrogenadas de una doble hélice que preside todo el recorrido.
02 / Dirección visual
Atmósfera de laboratorio nocturno de secuenciación: fondo azul profundo con viñetas radiales, grano de placa fotográfica (SVG feTurbulence fijo sobre toda la página), platos de vidrio con backdrop-filter y una regla métrica fija a la izquierda que mide el avance en «kilopares de bases».
03 / Técnica
El fondo es un único <canvas> WebGL 1 escrito a mano: matrices propias, dos shaders GLSL y geometría recalculada por frame en CPU (dos hebras de 380 puntos, 38 peldaños de 9 cuentas, 150 motas de polvo — un tercio con tinte CREI — y un cabezal de lectura de 18 puntos que recorre la hebra como una polimerasa, haciendo destellar cada base a su paso; trivial a 60 fps). El desenrollado es un solo parámetro: la tasa de giro k tiende a cero con el scroll. Al cargar, la hélice no aparece «puesta»: las hebras encienden y los peldaños se secuencian por orden durante ~2,5 s, como una lectura real de bases.
// La espiral es una función del scroll: k(p) → 0 la vuelve escalera
function buildFrame(t, p, aspect) {
var unwind = unwindAt(p); // keyframes suavizados 0→0.95
var k = (Math.PI * 2 * TURNS / SPAN) * (1 - unwind); // twist por unidad de altura
var rot = t * 0.42 * (1 - unwind * 0.72) + p * 3.0; // el giro también se calma
for (var strand = 0; strand < 2; strand++) {
var phase = strand * Math.PI; // hebras en oposición
// x = cos(y·k + rot + fase)·R · z = sin(y·k + rot + fase)·R
}
}
// Vertex shader: proyección propia + tamaño por distancia (con tope de fill-rate)
gl_PointSize = min(a_size * u_dpr * (26.0 / w), 44.0 * u_dpr);
// Fragment shader: halo + núcleo. Alfa CERO: con blend(ONE, ONE) el canvas
// premultiplicado compone como luz emisiva pura sobre el fondo CSS.
float halo = smoothstep(0.5, 0.0, d);
float core = smoothstep(0.16, 0.0, d);
gl_FragColor = vec4(v_col * (halo * 0.62 + core * 1.32) * v_depth, 0.0);
El resaltado por sección es un vector de cuatro pesos interpolado cada frame: un IntersectionObserver sobre cada tarjeta de valor llama a window.__helix.setHighlight(i) y los peldaños de esa base crecen y brillan mientras el resto baja a un 22 %.
// Keyframes del desenrollado, ligados al progreso p de scroll
function unwindAt(p) {
if (p < 0.12) return 0; // hero: espiral íntegra
if (p < 0.5) return ease((p - 0.12) / 0.38) * 0.55; // CREI: se abre
if (p < 0.85) return 0.55 + ease((p - 0.5) / 0.35) * 0.4;
return 0.95; // cierre: escalera
}
04 / Decisiones
alpha 1.0 con blending aditivo sobre un canvas premultiplicado. La corrección — emitir alpha 0.0 — convierte cada punto en luz pura que se suma al fondo CSS. Es el detalle que separa «partículas» de «bioluminiscencia».sessionStorage): la espectacularidad no debe cobrarse dos veces.drift % 2.4): un diente de sierra que producía un salto visible al cruzar cada múltiplo. Se sustituyó por una deriva senoidal acotada — continua por construcción, sin pops.lighter). Si el contexto WebGL se pierde, se espera el restored; si no llega en 2 s, se reemplaza el canvas por uno 2D (el mismo nodo no admite dos tipos de contexto). Con prefers-reduced-motion, se pinta un único fotograma estático a medio desenrollar y todos los reveals quedan visibles.feTurbulence). Único recurso externo: Google Fonts.--tinta-faint subió de #5A6C8E (≈3.6:1) a #7186AB (≈4.8:1 incluso sobre los platos de vidrio): todo el texto mono pequeño cumple WCAG AA. La ficha lateral del hero ganó además un scrim (fondo translúcido + blur) para leerse sobre las partículas de la hélice.opacity/transform, compositor puro); menú móvil con fade + stagger por ítem; hover en la marca del nav; skip-link accesible; metadatos OG/canonical/theme-color completos; mul() con scratch fijo (cero asignaciones por frame); y arreglo del caso reduced-motion + resize, que dejaba el canvas en negro al no repintarse el fotograma estático.resize() dejó de ejecutarse por frame (leía innerWidth/innerHeight y subía el uniform DPR 60 veces/s) y ahora ignora los resizes de solo-altura <130 px que dispara la barra del navegador móvil durante el scroll — sin reasignar el framebuffer en pleno gesto; scrollHeight cacheado y recalculado en resize/load/fonts.ready. Robustez: si fallan WebGL y Canvas 2D el bucle rAF se apaga (y no resucita al volver de una pestaña en segundo plano); prefers-reduced-motion se escucha en vivo con matchMedia.change — parar o arrancar el bucle y revelar todo al instante. Accesibilidad: menú móvil con Escape, foco al primer enlace al abrir, retorno de foco al botón al cerrar y fondo inert; scrim degradado permanente bajo el nav para leerse sobre las partículas. Detalle: filas de criterios apiladas en ≤560 px, indicador de scroll oculto en viewports de poca altura, JSON-LD de Organization y castellano peninsular consistente («ROI medio estimado»)..fin era .rv con todos sus hijos .rv, así que el escalonado de los hijos ocurría dentro de un padre todavía con blur(7px) — paint caro sobre una tarjeta de ~900 px y la tarjeta llegaba borrosa a cámara; ahora revelan solo los hijos. Scrim radial tras el texto del hero en ≤1060 px (donde la hélice pasa por detrás del párrafo). El resaltado por pointerenter ignora punteros no-ratón (un tap lo dejaba enganchado contra el observer). #menu es un <nav> con aria-label; fallback 100vh previo a 100svh; flex-wrap en los criterios; y JSON-LD de Organization en el <head>.