CyberSenseAI · Cómo se hizo
Anatomía de la home «Piensan, colaboran, ejecutan: el espejo humano–IA». Todo procedural: cero imágenes, un shader, dos mundos y una coreografía de scroll.
← Volver a la homeLa promesa de CyberSenseAI es que una Persona IA no sustituye a nadie: se sienta al lado. La página lo hace físico dividiendo el viewport en dos mundos completos que conviven en todo momento:
No es una metáfora decorativa: los dos mundos renderizan el mismo contenido en el hero (mismo H1, mismos CTAs) con dos sistemas tipográficos opuestos, y una costura de luz los separa. Al mover el puntero, la costura se desplaza (±17%) y revela más de un mundo o del otro — el espejo responde. La respuesta tiene tres capas: la costura persigue al cursor, el filamento se abomba a su altura, y las anotaciones al margen derivan suavemente con él mientras sus reflejos OLED derivan en sentido contrario (contra-parallax: un espejo no copia, invierte).
Regla aprendida a golpes: el corte solo funciona sobre geometría idéntica. Kicker y CTAs usan la misma métrica en ambos mundos, así que la costura los corta limpio. El subtítulo no podía (dos fuentes ⇒ dos ajustes de línea ⇒ papilla en la costura), así que es un único elemento en una capa central sin recortar con mix-blend-mode: difference; los mundos llevan «fantasmas» invisibles de métrica idéntica para que las tres columnas midan lo mismo y todo quede alineado. Bonus del blending: el mismo fósforo del subtítulo se lee cian sobre el OLED y rojo sello sobre el papel — cada mundo lo tiñe con su propia tinta.
El hero contiene dos capas hijas a viewport completo con contenido idéntico. Cada una se recorta con clip-path contra una variable CSS compartida, --seam. Como cada capa está completa, la costura simplemente «corta» las letras por donde pasa: la mitad izquierda de una palabra es Fraunces italic sobre papel y la derecha, Archivo expandida sobre negro.
/* CSS — el corte del espejo */ .mundo--papel{ clip-path: inset(0 calc(100% - var(--seam)) 0 0); } .mundo--oled { clip-path: inset(0 0 0 var(--seam)); } // JS — la costura persigue al cursor con inercia (lerp en rAF) hero.addEventListener('pointermove', e => { seamTarget = 50 + ((e.clientX / r.width) - 0.5) * 34; // ±17% }); seamPct = lerp(seamPct, seamTarget, 0.08); hero.style.setProperty('--seam', seamPct + '%');
Ventaja clave: el hit-testing de clip-path también recorta los eventos de puntero, así que los CTAs duplicados funcionan en ambos mundos sin lógica extra. La copia OLED lleva aria-hidden y tabindex="-1": los lectores de pantalla solo leen un mundo.
La línea que separa los mundos es un canvas WebGL de solo 180 px de ancho (barato en fill-rate) con un fragment shader propio: un filamento de luz desplazado por FBM (fractal brownian motion), que se abomba cerca del cursor y chispea. El color se funde de cálido a frío al cruzar el eje — la propia costura es mitad papel, mitad fósforo.
// GLSL — núcleo del filamento (extracto real) float w = (fbm(vec2(y*5.0, u_t*0.25)) - 0.5) * 0.34; // ondulación lenta w += (fbm(vec2(y*14.0, u_t*0.7 )) - 0.5) * 0.10; // vibración fina float d = abs(x - w); float core = exp(-d * 26.0); // filamento float halo = exp(-d * 4.5) * 0.5; float bulge = exp(-abs(y - u_my) * 7.0) * exp(-d * 8.0) * 0.7; // cursor vec3 col = mix(vec3(1.0,.62,.34), vec3(.31,.91,.83), smoothstep(-0.5, 0.5, x)); // cálido → frío
ready), recta y con un destello que decae (u_gain), «aprendiendo» a ondular al rampear u_amp de 0 a 1 en 1,6 s.u_gain se multiplica por un acumulador que crece con el error del lerp (|target − actual|) y decae al 90% por frame. Un barrido rápido enciende el filamento; al pararse, se calma en ~medio segundo.prefers-reduced-motion: degrada a una línea CSS con glow (html.no-webgl .seam-fallback). Y si el GPU resetea a mitad de sesión, webglcontextlost hace preventDefault() y entrega el relevo a esa misma línea — nunca una costura en negro.La sección firmada del sitio: 520 vh de scroll con un viewport fijado (position: sticky) partido en dos paneles. El scroll avanza un día narrativo — 09:41, 03:12, lunes 09:00 — y en cada escena los dos mundos ejecutan la misma tarea en paralelo:
| Hora | Mundo papel (humano) | Mundo OLED (SoulBot) |
|---|---|---|
09:41 | Elena subraya un contrato de 84 páginas; las notas al margen aparecen a mano. | Terminal jurídica: 212 cláusulas, riesgo en la 14.2, informe en 4 min 12 s. |
03:12 | El papel se oscurece: el equipo duerme. | Incidente VPN contenido en 38 s. Vende el 24/7 sin decirlo. |
09:00 | El informe ya está en el comité; sello «Aprobado». | Los resultados suben en barras: −40%, ×10, 171% ROI. |
Todo se escrubea: el progreso de scroll (0→1) se reparte en tres ventanas de escena, y dentro de cada una se derivan sub-progresos con ventanas propias. Los subrayados son SVG con pathLength="1" y stroke-dashoffset continuo; las líneas de terminal cruzan umbrales (data-at) que disparan una animación de tecleo por pasos, reversible al subir.
// El reparto del día (extracto real) var raw = p * 3; var idx = Math.min(2, Math.floor(raw)); // escena 0 | 1 | 2 var sp = clamp01(raw - idx); // progreso de la escena var cp = clamp01((sp - 0.06) / 0.82); // ventana de contenido // subrayado del contrato: dibujo continuo dentro de su ventana var k = windowP(cp, +path.dataset.a, +path.dataset.b); path.style.strokeDashoffset = (1 - k); // línea de terminal: umbral reversible el.classList.toggle('on', cp >= +el.dataset.at);
El riel central es la tercera voz: una línea que se llena de tinta roja a fósforo con paradas horarias en mix-blend-mode: difference, de modo que la hora se lee sobre ambos fondos. En móvil (≤860 px) el espejo rota: paneles apilados y riel horizontal.
Dos dominantes espejados (papel/OLED) y un acento afilado por mundo: el rojo sello de las anotaciones a mano y el verde fósforo de las terminales. Nada de gradientes genéricos: el único degradado protagonista es la propia costura.
Fraunces (serif con eje óptico, cursiva cálida) contra Archivo expandida en mayúsculas, más IBM Plex Mono para las terminales. La pareja no convive: se enfrenta — misma frase, dos voces.
feTurbulence en data-URI, la lluvia de datos son 26 nodos CSS, y la costura es un shader. Peso total: un solo HTML.mix-blend-mode: difference. Un solo nav blanco que se invierte solo al cruzar de mundo — cero media queries de color, y funciona incluso mientras la costura se mueve.clip-path a viewport completo solo se reescriben mientras el lerp de la costura no ha convergido.:focus-visible es fósforo sobre OLED pero rojo sello sobre papel (el cian da ~1,5:1 sobre crema), y blanco sobre los botones de fondo fósforo — el indicador de teclado siempre supera contraste sobre su propio fondo.difference sobre un degradado suave produce gris-sobre-gris en la transición; sobre la partición 50/50 el texto siempre invierte con contraste pleno. Y los botones viven fuera del wrapper con blending: una pastilla de fósforo sólida se lee igual de bien sobre papel que sobre OLED.--plx/--ply) sobre transform, decorativo, y quieto con reduced motion.--hair-b — un gris azulado único en vez de seis rgba casi iguales.visibilitychange: nadie debería perderse la primera escena por culpa de un middle-click.pointermove no llama a getBoundingClientRect: ancho/alto de viewport y el corte móvil (≤860 px) se cachean en resize. Y si el GPU pierde el contexto WebGL, el fallback CSS releva al shader sin pantalla en negro.aria-hidden: fuera del flujo del discurso, un lector las leería como ruido sin contexto.Assets externos: únicamente Google Fonts. Todo lo demás — texturas, shader, iconografía, luna, flechas manuscritas — está generado en el propio documento.
La última revisión no añadió espectáculo: lo blindó. Lo que cambió al pasar el peine fino por móvil, rendimiento y accesibilidad:
html.rm .marq > div + div).getBoundingClientRect de la sección del día solo se lee con la sección en pantalla (IntersectionObserver), y los pulsos de larga vida — la costura del CTA final, el estado de SoulBot — se pausan fuera de viewport.viewport-fit=cover más env(safe-area-inset-*) en el nav y el pie del hero: en pantallas con notch nada queda bajo el hardware.div con aria-label (que los lectores ignoran sin rol) a section real, y el anillo de foco dentro del nav en difference es blanco — el único color que invierte con contraste garantizado sobre ambos mundos.