La home de CyberSenseAI cuenta una sola idea — «un miembro más del equipo» — con una sola pieza central: un organigrama con física de muelles, dibujado a mano sobre canvas 2D, que crece con el scroll y al que las Personas IA se incorporan como nodos que se enganchan a los equipos. Esta guía documenta el concepto, la técnica y las decisiones.
El producto de CyberSenseAI no es un chatbot: es una Persona IA que ocupa un puesto en el organigrama, con rol, responsabilidades y objetivos. La forma más honesta de contarlo era enseñar el organigrama mismo — y hacer que el visitante viera, en cuatro capítulos de scroll, cómo una organización normal incorpora a sus primeros colaboradores digitales:
El grafo no es un vídeo ni una ilustración: es un sistema físico que se puede agarrar y arrastrar en cualquier momento. Y si nadie lo toca en los primeros segundos, un nodo se sacude solo — un empujón de cortesía que demuestra la física sin esperar permiso. La metáfora deja de ser metáfora cuando el organigrama responde a tu mano.
Claro corporativo cálido: marfil de papel de oficina, azul tinta de organigrama impreso y un coral que marca todo lo que es nuevo — las Personas IA. Nada de neones ni gradientes genéricos: la página debe resultar creíble para un CTO o un CISO, y el espectáculo se concentra en la física.
La atmósfera se apoya en tres texturas discretas: un grano de papel (SVG feTurbulence en data-URI, multiplicado al 4,5 %), una retícula tenue enmascarada radialmente tras el grafo, y sombras largas y suaves en tarjetas que van ligeramente rotadas (−0,4° a 1,1°) para romper la rigidez del grid.
Sin librerías. El grafo es un integrador semi-implícito de Euler con dos substeps por frame y cuatro fuerzas: un muelle hacia el «sitio» que el layout asigna a cada nodo, muelles en las aristas jerárquicas, repulsión de corto alcance entre todos los pares y una gravedad suave que hace que el árbol cuelgue con naturalidad. Este es el corazón real del código de la home:
/* muelle hacia el sitio asignado + gravedad */ n.vx += (hx - n.x) * kk * dt; n.vy += (hy - n.y) * kk * dt + G * dt * 10; /* muelles de las aristas (padre ↔ hijo) */ f = (d - ar.resto) * kArista * dt; n.vx += dx/d*f; n.vy += dy/d*f; m.vx -= dx/d*f; m.vy -= dy/d*f; /* repulsión de corto alcance, O(n²) con n≈17 */ if (d < lim){ f = (lim - d) * 26 * dt; … } /* integración con amortiguación exponencial */ n.vx *= Math.exp(-2.6 * dt); n.x += n.vx * dt;
Los «sitios» no salen de un algoritmo de layout: están dirigidos artísticamente, nodo a nodo, en coordenadas relativas al viewport — así el árbol siempre compone bien a 1440 px y a 390 px. Cada sitio además respira con un par de senos desfasados, de modo que el organigrama nunca está del todo quieto.
Las aristas se dibujan como curvas cuadráticas con comba: el punto de control se desplaza en perpendicular y hacia abajo, y sobre esa misma curva se mueven los pulsos coral que viajan de los equipos a sus Personas IA:
/* comba perpendicular + un poco de “cuelgue” vertical */ var comba = Math.min(26, d * 0.14); var cx = mx - dy/d*comba*0.35, cy = my + dx/d*comba*0.35 + comba*0.55; ctx.quadraticCurveTo(cx, cy, b.x, b.y); /* punto sobre la Bézier para el pulso viajero */ x = u*u*a.x + 2*u*t*cx + t*t*b.x;
La escena es un contenedor position: sticky de 100 vh sobre el que fluyen cuatro tarjetas-capítulo. La etapa activa no depende de un observer con umbrales frágiles: se calcula de forma determinista desde la posición de scroll, así subir y bajar es siempre reversible:
/* etapa = nº de capítulos cuyo tope superó el 62 % de la pantalla */ var e = 0; for (i = 0; i < topesPasos.length; i++) if (scrollY + innerHeight*0.62 >= topesPasos[i]) e = i + 1; fijarEtapa(e);
fijarEtapa() hace un diff entre los nodos presentes y los que la etapa exige: los nuevos nacen escalonados (las Personas IA entran desde fuera del lienzo, a 220–340 px/s, y disparan una onda al engancharse); al retroceder, los sobrantes se encogen y se podan. El anillo XAIOS, la insignia de equipo híbrido y el desplazamiento del centro del grafo son valores-objetivo que se persiguen con interpolación exponencial — nada salta, todo llega.
| Decisión | Por qué |
|---|---|
| Canvas 2D, no WebGL | 17 nodos y ~21 aristas no justifican shaders; el 2D con devicePixelRatio limitado a 2 mantiene 60 fps con un solo requestAnimationFrame. |
| Pausa fuera de vista | Un IntersectionObserver detiene el bucle cuando la escena sale del viewport y al ocultar la pestaña: coste cero en el resto de la página. |
touch-action: pan-y | En móvil, el scroll vertical siempre le pertenece al navegador; el arrastre de nodos convive sin secuestrar la página. |
prefers-reduced-motion | Sin vaivén, sin pulsos, sin escalonados: los nodos aparecen ya asentados (muelles casi críticos) y las revelaciones CSS se anulan. Todo el contenido vive en HTML semántico; el lienzo es decorativo (aria-hidden). |
| Un solo archivo | CSS y JS inline; el único recurso externo son las dos familias de Google Fonts. Sin imágenes: todo es procedural. |
Una revisión de peine fino sobre capturas reales cerró doce defectos. Los que cambian técnica o decisiones:
1 − e^(−k·dt): idéntica sensación a 60 y a 120 Hz.transform: none final) aniquilaba el translateX(-50%) del centrado; ahora tiene su propio keyframe que preserva ambos ejes.:focus-visible con paridad de hover en las filas de verticales, y el outline de foco ya no deforma los botones píldora. La selección de texto en las secciones oscuras pasó a coral (antes era tinta sobre tinta: invisible).La última revisión buscó los fallos que solo aparecen con la página en producción: pestañas en segundo plano, navegadores capados, móviles reales. Quince cierres:
visibilitychange pausaba el bucle, pero nada lo reanudaba (el IntersectionObserver no re-dispara si la intersección no cambió). El canvas quedaba congelado para siempre. Ahora la visibilidad de la escena se recuerda aparte y corriendo = escenaVisible && !document.hidden se recalcula en ambos eventos.aria-expanded, panel a pantalla completa con entrada escalonada, cierre con Escape / enlace / resize, y bloqueo de scroll mientras está abierto.getContext('2d') devuelve null (webviews capadas), el script moría antes de body.lista y el héroe quedaba invisible. Ahora la escena se omite limpiamente y todo el HTML sigue funcionando; con JS desactivado, un bloque <noscript> muestra la página completa en estático.movilNo — el filtro de nodos que no caben en móvil se aplicaba al crecer por capítulos pero no en la siembra inicial: Carmen y Pablo aparecían apiñados bajo la píldora de pista en 390 px. La semilla ahora respeta el mismo filtro.animation-iteration-count: 1 las congela en su estado final.#contenido lleva tabindex="-1" para que el foco se mueva de forma fiable en todos los navegadores.shadowBlur (desenfoque gaussiano, lo más caro del render 2D) se pintaba en cada nodo y en cada frame. Ahora cada tipo de nodo — raíz, equipo, humano, Persona IA — se hornea una vez en un canvas fuera de pantalla con sombra, gradiente y estrella incluidos, y el frame solo hace drawImage escalado. Halo, escudo XAIOS y texto siguen siendo dinámicos encima.movilNo se filtraban al crear, pero girar el dispositivo no reconciliaba: cargar en horizontal y girar dejaba el grafo sobrepoblado, y al revés faltaban nodos para siempre. Un reconciliar() en cada cruce del umbral de 700 px crea o poda según el formato, y el resize re-evalúa también etapa y parallax.content (verticales, criterios, chapa «ALTA · RRHH») llevan texto alternativo vacío (content: "…" / "", con declaración de respaldo para navegadores antiguos) y el pie usa h3 — antes saltaba de h2 a h4. El grano de papel fijo pasó de inset: -50% (una capa compuesta de 4× el viewport mezclada con mix-blend-mode) a inset: 0.