Cómo se hizo
Quirófano — la línea que dibuja la página
La home de CyberSenseAI se construyó sobre una sola idea: salud es precisión que respira. Todo el sitio es un quirófano luminoso — blanco cálido, verde quirófano, azul pulso — recorrido de arriba abajo por un trazo de electrocardiograma que nace en el monitor del hero, baja serpenteando por las secciones, late una vez junto a cada titular y termina reposando, con su punto luminoso, sobre la llamada final. No hay ni una sola imagen: todo es SVG y canvas procedural en un único archivo HTML.
1 · La forma de onda: P-QRS-T como suma de gaussianas
En vez de trocear un dibujo, la onda cardiaca es una función continua: cinco campanas de Gauss — onda P, complejo QRS (Q, R, S) y onda T — sumadas sobre una fase normalizada. La misma función alimenta el monitor del hero, las cuatro mini-ondas del panel de métricas y el ritmo general de la página.
// p ∈ [0,1) → desplazamiento vertical (negativo = arriba)
function gauss(p,c,w,a){var d=(p-c)/w;return a*Math.exp(-d*d/2)}
function ecg(p){
return gauss(p,.16,.022,-7) // onda P
+ gauss(p,.235,.008,6) // Q
+ gauss(p,.25,.011,-46) // R
+ gauss(p,.268,.009,13) // S
+ gauss(p,.45,.05,-11); // onda T
}
El monitor del hero es multiparamétrico, como uno real: al canal II del ECG (azul, a 54 ppm) le acompañan la pletismografía de SpO₂ (verde, sincronizada con el corazón con su retardo fisiológico) y la curva de respiración (12 rpm, casi imperceptible), cada una con su forma de onda, su amplitud y su etiqueta mono en el margen derecho. La carga está orquestada sobre ese ritmo: las tres líneas entran planas (isoeléctricas), a los 900 ms llega el primer latido real — y es ese latido el que despierta la página — y después los canales se conectan uno a uno (SpO₂ a los 1,6 s, respiración a los 2,1 s). Titular, entradilla y botones se revelan en escalera; el punto del logotipo y el CTA principal pulsan en sincronía con cada latido mientras el hero está a la vista, y la palabra respira respira de verdad, en el mismo ciclo de 9 s que el fondo.
2 · La línea vital: un path que se construye leyendo el DOM
La columna vertebral del sitio no está dibujada a mano: se calcula en tiempo de ejecución. Cada sección coloca un marcador invisible (.pulse-node) donde quiere recibir su latido. Un script recoge esos puntos, los ordena por altura y teje entre ellos curvas Bézier verticales; al llegar a cada nodo inserta el glifo P-QRS-T como segmento relativo del propio path:
function beatGlyph(x,y,mobile){
var s = mobile? .55 : 1;
var seg=function(dx,dy){return ' l '+(dx*s)+' '+(dy*s)};
var d=' L '+x+' '+y;
d+=seg(12,0);
d+=' q '+(5*s)+' '+(-9*s)+' '+(10*s)+' 0'; // onda P
d+=seg(7,0);
d+=seg(4,7)+seg(7,-46)+seg(7,56)+seg(5,-17); // QRS
d+=seg(8,0);
d+=' q '+(7*s)+' '+(-13*s)+' '+(14*s)+' 0'; // onda T
return d+seg(12,0);
}
Al redimensionar, el path entero se reconstruye: en escritorio la línea zigzaguea de un margen a otro (92 % → 7 % → 92 %…); por debajo de 900 px se pliega a una columna de 12 px junto al borde con glifos compactos. Los nodos viven en los márgenes reales y dentro del colchón de padding superior de cada sección: el latido cae junto al titular, nunca encima del texto. El trazo se pinta con un gradiente userSpaceOnUse cuyas paradas también se calculan: verde quirófano en campo abierto, menta al cruzar la sección oscura de XAIOS y azul pulso en el tramo final.
3 · Dibujado por scroll con muestreo y→longitud
El clásico stroke-dasharray revela la línea, pero la longitud del path no es lineal respecto a la altura (los latidos añaden recorrido). Para que el frente del trazo coincida siempre con la línea de lectura del viewport, el path se muestrea una vez (420 puntos longitud→coordenada) y en cada frame se busca la longitud cuya y alcanza el 72 % del viewport, con interpolación para que el avance respire:
var readLine = scrollY + innerHeight*0.72;
target = lenAtY(readLine); // lookup en las muestras
current += (target-current)*0.09; // lerp: el trazo respira
trace.style.strokeDashoffset = L - current;
var p = trace.getPointAtLength(current);
dot.setAttribute('cx',p.x); dot.setAttribute('cy',p.y);
El punto azul que corona el frente del trazo es la «cabeza» del electrocardiógrafo: acompaña el scroll todo el viaje y se queda encendido al final, reposando sobre «Tu próximo fichaje ya tiene pulso».
4 · Iconos que se dibujan: el truco pathLength="1"
Todos los iconos (verticales, escudo XAIOS, sellos ENS/ISO/GDPR, checks de la checklist, el equipo híbrido y el retrato de SoulBot) son SVG de un solo trazo que se dibujan al entrar en pantalla. Sin medir nada en JS: declarando pathLength="1", cualquier path se normaliza y basta CSS puro con retardos en cascada:
.draw path{stroke-dasharray:1;stroke-dashoffset:1;
transition:stroke-dashoffset 1.7s cubic-bezier(.4,0,.2,1);
transition-delay:var(--d,0s)}
.draw.in path{stroke-dashoffset:0}
Un único IntersectionObserver gestiona reveals, dibujos y contadores (los números del panel — 40 %, 10×, 24/7, 171 % — cuentan con easing cúbico al entrar).
5 · Lenguaje visual
| Voz | Fuente | Papel |
|---|---|---|
| Display | Instrument Serif (+ itálica) | Titulares finos y humanistas; la itálica verde marca la palabra clínica («respira», «quirófano», «pulso») |
| Texto | Instrument Sans | Cuerpo humanista, legible para un comprador enterprise |
| Instrumental | Fragment Mono | Kickers, constantes, fichas y sellos: la voz del monitor |
La metáfora se sostiene en el copy sin volverse circo: XAIOS es «el quirófano de tus datos» (aislamiento, gobernanza, soberanía), SoulBot llega con «ficha de incorporación» y constantes estables, los valores CREI son un «protocolo» — cada valor firma con su propio mini-trazado de ECG que se dibuja al entrar — y los 8 criterios enterprise, una «checklist preoperatoria» con el instrumental contado dos veces. Atmósfera: gradientes radiales que respiran (ciclo de 9 s en el hero), retícula de campo quirúrgico enmascarada, grano de gasa por feTurbulence, y cartas ligeramente rotadas (ficha −1,1°, equipo +1,2°) sobre un grid roto en las verticales.
6 · Rendimiento y respeto
- Un solo
requestAnimationFramepor sistema; las mini-ondas solo se pintan si su tile está en viewport;devicePixelRatiolimitado a 2. Cada canal del monitor tiene su propia persistencia (ECG 4,6 s, SpO₂ 5,6 s, respiración 7 s) y desvanece por edad, de modo que el barrido anterior sigue visible por delante de la cabeza — como en un monitor real. Con el hero fuera de pantalla el canvas deja de dibujar (solo sigue el reloj del latido para el punto del logo), y la línea vital no toca el DOM cuando el trazo está en reposo — 60 fps estables. prefers-reduced-motion: monitor congelado en un trazo completo, línea vital dibujada de inicio, iconos ya trazados, contadores en su valor final, sin respiración ni pulsos. La página entera funciona sin animación.- En móvil los canales secundarios del monitor se apagan, las constantes vitales se leen sobre una gasa translúcida (nada de texto sobre la onda) y la navegación completa vive en un menú plegable accesible (
aria-expanded, cierre con Escape y al navegar). Hay skip-link, Open Graph ytheme-color. - Sin librerías, sin build, sin imágenes: un HTML autocontenido; el único recurso externo es Google Fonts. Cero errores de consola y cero scroll horizontal verificados a 1440 px y 390 px con navegador headless.
7 · Decisiones que importaron
- El latido como evento de carga. Descartamos el fade-in genérico: la página literalmente no despierta hasta que el monitor late. Con tres redes de seguridad (temporizador,
load, primer scroll) para que nadie espere ante una pantalla vacía. - Path calculado, no dibujado. Hacer que la línea lea el DOM permite que sobreviva a cualquier reflow, idioma o viewport — y que el latido caiga siempre exactamente junto al titular de su sección.
- Final con destino. La primera versión prolongaba la línea hasta el footer atravesando el CTA; ahora termina su último latido y el punto se queda quieto encima del cierre: la línea te entrega, no te cruza.
- Espectáculo con credibilidad. Cada floritura sostiene un argumento de compra: las constantes son las cifras reales (40 % · 10× · 24/7 · 171 %), el campo estéril es la arquitectura de XAIOS y la checklist son los 8 criterios que firma un CTO/CISO/CIO.
8 · Segunda pasada: el peine fino
- Contraste AA en serio. El azul pulso
#1f6df2quedaba en ≈4,3:1 sobre hueso como texto pequeño; nació--azul-texto #1a5fd6(≈5,3:1) para kickers, constantes y etiquetas — los trazos y puntos conservan el azul original. Se subieron también la numeración de las tarjetas de verticales y la línea de sellos de XAIOS. - El ritmo se ve latir. El número de RITMO marca cada latido con un tick sincronizado, y en móvil solo pulsa un CTA (dos anillos a la vez eran ruido).
- Cierre cromático. La línea vital llega al final en azul; ahora la palabra pulso del titular de cierre también es azul: la historia de color termina donde termina la línea.
- Cuatro firmas CREI distintas. Confiable, Responsable (latido doble), Ética (trazo sereno) e Inclusiva (latido amplio) firman cada una con su propio ECG, no con el mismo path repetido.
- Estados impecables. El focus de teclado ya no deforma los botones-píldora, todos los botones tienen estado de pulsación, el CTA fantasma se asienta sobre una gasa translúcida y el menú móvil cierra al tocar fuera y devuelve el foco con Escape.
- Scatter sin saltos. El desnivel de las tarjetas de verticales pasó de
transforma márgenes: el hover eleva desde la posición real y la entrada en cascada vuelve a funcionar en las siete tarjetas. - Sin JavaScript también hay página. Un bloque
<noscript>revela todo el contenido, los iconos aparecen ya trazados y las métricas viajan con su valor final en el HTML (el script las pone a cero justo antes de contar). Nadie mira una pantalla vacía. - Despertar más rápido. El primer latido sigue mandando, pero las redes de seguridad se tensaron (1,5 s máximo, 0,7 s tras
load), la escalera de reveals se comprimió y la palabra respira ya no baja de 3:1 de contraste en el valle de su respiración. - Canales que se leen como canales. La persistencia corta hacía que SpO₂ y respiración parecieran fragmentos sueltos flotando; con persistencia por canal el barrido previo permanece, tenue, y las tres curvas se leen como un monitor multiparamétrico de verdad.
- Detalles de borde. La barra de navegación gana opacidad y saturación al posarse sobre la sala oscura de XAIOS, el botón del menú móvil tiene hover, el CTA fantasma se asienta en gasa sólida al pasar por encima, y las constantes vitales en móvil forman una retícula 2×2 sin huérfanos.
9 · Peine de púas finas
- El punto también late. La cabeza de la línea vital ya no es estática: pulsa a 54 ppm — el mismo ritmo que el monitor del hero — con
transform-box:fill-boxpara escalar sobre su propio centro. Y cuando el frente del trazo cruza cada glifo P-QRS-T, el nodo emite un eco: un anillo que se expande y se apaga (verde en campo abierto, menta dentro de XAIOS). Ambos gestos se anulan conprefers-reduced-motion. - Paño estéril en XAIOS. La línea vital cruzaba el texto de la sección oscura sin nada que la velara. El campo estéril tiene ahora un paño translúcido (gradiente oscuro +
backdrop-filter: blur(3px)): la línea pasa por detrás, difuminada, y el texto gana fondo y contraste. - Reduced-motion sin lagunas. Las mini-ondas del panel de métricas se pintaban una sola vez, antes de que ningún tile fuese visible: quedaban en blanco. Ahora cada onda se dibuja, estática, al entrar en viewport (y se repinta al redimensionar).
- El nav cabe en 390 px. Marca, botón y hamburguesa se apretaban hasta romper el CTA en dos líneas; con gaps, tipografía y padding recalibrados por debajo de 480 px todo respira en una sola línea.
- Anillos con mesura. El pulso de los CTAs sonaba el 72 % del tiempo (0,8 s de anillo por latido de 1,11 s); ahora dura 0,65 s, se expande menos y es más tenue — y el anillo del CTA del nav se apaga en móvil también por CSS, no solo por JS.
- Todo lo interactivo responde. La marca ganó hover (halo azul en el punto del logo, aspas de la hamburguesa en verde) y el skip-link usa la misma curva de easing que el resto del sistema.
- Semántica y metadatos completos. Landmark
<main>,rel=canonical,theme-colorpara modo oscuro y JSON-LD de organización (schema.org) embebido.
10 · Tercera pasada: la lupa final
- La página despierta incluso sin canvas. Si
getContext('2d')no está disponible, antes el módulo del monitor abortaba y el hero — orquestado porbody.alive— se quedaba invisible para siempre. Ahora ese caso añade la clase y sigue: sin monitor, pero con página. - De ~800 strokes por frame a ~70. El desvanecido por edad trazaba cada segmento del barrido con su propio
beginPath/strokepara variar el alpha. Como el alpha crece monótono hacia la cabeza, cuantizarlo a pasos de 1/24 permite agrupar segmentos contiguos en un solo trazo: mismo aspecto, una décima parte de las llamadas, 60 fps con margen en GPUs modestas. - Resize de verdad, no de barra de URL. En móvil, ocultar la barra del navegador dispara
resizesin cambiar nada relevante. Tanto el monitor como la línea vital comprueban ahora dimensiones reales antes de reiniciar el barrido o reconstruir el path: se acabó el salto a mitad de scroll. - Volver a la pestaña enciende el monitor, no lo rompe. Tras un rato en segundo plano los puntos del ECG caducaban a trozos y el trazo reaparecía fragmentado. En
visibilitychangeel reloj se pone a cero y el barrido arranca limpio desde el margen, como al encender un monitor real. - El nav se asienta sobre la sala oscura. Lo que la pasada 2 prometía existe ahora de verdad: al cruzar XAIOS la barra gana opacidad (0,9 → 0,96), saturación y una sombra más honda, con un toggle por
getBoundingClientRecten el mismo listener de scroll pasivo. - Accesibilidad sin flecos. La hamburguesa anuncia su estado («Abrir menú» ↔ «Cerrar menú»), el skip-link mueve el foco de verdad (
tabindex="-1"en su destino, sin marco espurio), las constantes del hero son unrole="group"etiquetado, el diagrama del equipo híbrido esrole="img", y cada enlace que abre pestaña nueva lo dice a los lectores de pantalla con un texto.sr-only. - CTAs a ancho completo en 390 px. Los dos botones apilados con anchos desiguales rompían el ritmo vertical del hero y del cierre; por debajo de 480 px se apilan en columna, estirados y con el texto centrado.
- El panel también se conecta. Las cuatro mini-ondas de métricas entraban ya latiendo a pleno ritmo; ahora cada una nace isoeléctrica y su latido se incorpora con easing cúbico (~1,4 s) al hacerse visible — la orquestación flatline→latido del hero, en eco. Con
prefers-reduced-motionse pintan completas de inicio, y si un canvas no entrega contexto 2D las ondas se retiran en silencio. - La línea vital descansa de verdad. El bucle de scroll llamaba a
getPointAtLengthy escribíastroke-dashoffseten cada frame aunque el frente estuviera quieto; con el trazo en reposo ya no se toca el DOM — la promesa que la sección 6 hacía, ahora cumplida por el código. - Fallback sin JS reparado. Una regresión había dejado los contadores del panel a
0en el HTML (−0 %, 0×…) — justo lo que la sección 8 presumía de haber resuelto. Los valores finales (40 · 10 · 24 · 171) viajan de nuevo en el marcado. - SpO₂ se escribe SpO₂. El
text-transform: uppercasegritaba la etiqueta como «SPO₂»; las etiquetas de canal usan ahora la notación de un monitor real (SpO₂ · RESP · II · ppm). - Microcopy sin arrugas. «Sin turnos, sin vacaciones y ningún pico sin cubrir» (adiós al triple «sin»), y «ROI promedio esperado» quedó en «ROI esperado» con nota «Retorno medio estimado en el primer ciclo»: cada idea, una sola vez.