Qué es
El sitio en rodrigonegrete.es: portada, proyectos, laboratorio, trayectoria, cinco temas visuales, tres idiomas (es, ca, en) y una página de caso por proyecto.
Caso de estudio · Mi propio portfolio · 2026
Un portfolio visual puede ser un escaparate de efectos o la prueba de que sé decidir. Este caso recorre cada pieza de movimiento, tema, sonido e interacción de rodrigonegrete.es: qué problema resuelve, qué investigué, qué descarté y cuánto cuesta. Con demos que puedes probar y con lo que salió mal.
01 · Resumen y rol
Busco un rol de AI Product o UX/AI. Mi perfil es visual, y un perfil visual tiene un riesgo: que el movimiento parezca decoración. Por eso traté mi propio sitio como un producto con requisitos, medición y revisión.
El sitio en rodrigonegrete.es: portada, proyectos, laboratorio, trayectoria, cinco temas visuales, tres idiomas (es, ca, en) y una página de caso por proyecto.
Las decisiones de diseño, los criterios de movimiento, la selección de referentes, la revisión de cada entrega y las mediciones. La implementación la hicieron agentes de IA especializados (Claude Code) que yo dirigía; 37 de los 45 commits del rediseño llevan el trailer Co-Authored-By.
No hay cliente, ni analítica de uso, ni tests con usuarios. Cualquier frase sobre qué «funciona mejor» para quien visita el sitio es una hipótesis y está marcada como tal.
git log, en el código o en una medición indicada
Lectura mía interpretación de diseño
Hipótesis por validar
Lo que no lleva etiqueta dentro de una ficha es verificable en el repositorio. Los números de este caso salen de tres sitios: el historial de git, el código fuente y las mediciones de Lighthouse móvil en local que indico en cada gráfico.
02 · Problema y objetivos
Quien revisa un portfolio suele tener poco tiempo, a veces desde el móvil y a veces con el sonido apagado. Un efecto vistoso que retrasa el primer texto, tapa contenido o ignora las preferencias del sistema juega en contra del mensaje. Y a la vez, un portfolio de diseño sin criterio de interacción no demuestra lo que dice saber hacer.
Dos lectores opuestos: quien tiene prisa y necesita ver mi perfil y mis casos rápido, y quien quiere ver criterio de diseño y se detiene en los detalles. El movimiento debe servir al segundo sin estorbar al primero.
prefers-reduced-motion.scroll-snap vertical obligatorio.03 · Proceso de investigación
Antes de escribir CSS estudié sitios y librerías de referencia. Me interesaba entender qué técnica sostiene cada efecto y qué cuesta, no reproducirlos.
| Referente | Licencia | Qué aporta Lectura mía | Qué hice |
|---|---|---|---|
| darkroomengineering/lenis | MIT | Scroll suave por JavaScript | Descartado: choca con mi regla de no tocar el scroll. Sin dependencia. |
| locomotivemtl/locomotive-scroll | MIT | Animaciones ligadas al scroll con motor propio | Estudiado; lo equivalente lo hace animation-timeline nativo. |
| Cuberto/mouse-follower | MIT | Cursor propio con estados por elemento | Idea de estados (botón, tarjeta) reescrita en código propio. |
| activetheory/split-text | MIT | Separar el texto en letras para animarlo | Idea reescrita a mano para el titular de la portada. |
| darkroomengineering/satus | MIT | Estructura de proyecto y buenas prácticas | Solo lectura de referencia. |
| basementstudio/basement-laboratory | Sin licencia | Inspiración visual y de ritmo | Solo inspiración. No se copió código. |
| makemepulse/2024-kaizen-public | Sin licencia | Inspiración visual y de ritmo | Solo inspiración. No se copió código. |
Verificado package.json declara solo cinco dependencias: astro, @astrojs/react, @astrojs/sitemap, react y react-dom. Ninguna librería de animación, de scroll ni de 3D.
Casi todo lo que quería cabe en CSS moderno: animation-timeline, View Transitions y clip-path. WebGL no hacía falta. Cuando lo probé por instinto, el resultado fue peor (véase el cursor fluido en el sistema de movimiento).
04 · Principios de movimiento
Fijé siete reglas antes de implementar. Al revisar el código para este caso comparé cada regla con la realidad. No salgo limpio, y creo que esa tabla es lo más útil del caso.
| Regla | Realidad en el código |
|---|---|
| Una animación protagonista por viewport | Lectura mía Criterio de composición; no tengo una medición automática de ello. |
| Micro-interacciones de 120 a 200 ms | Sin auditoría sistemática de todas; no afirmo cumplimiento. |
| Reveals de 400 a 700 ms | Se cumple en rv-up (.7 s). Excepciones: máscara de titulares 1 s, imágenes de tarjeta .9 s, menú móvil .85 s de apertura. |
| Escalonado de 40 a 70 ms, máximo 8 elementos | Titulares: 70 ms. Excepciones: pastillas del stack 45 ms hasta 20 elementos, filas 28 ms, letras del titular 22 ms hasta 40, pie del menú hasta 1,14 s de retardo. |
| Sin secuestro de scroll ni snap obligatorio | Sin scroll-jacking. Hay scroll-snap-type: x mandatory horizontal en las rejillas proof-grid y test-g en móvil (styles.css); testimonios lo sustituye por proximity. Ninguno vertical. |
| Contenido visible por defecto | Las clases que ocultan (sfx-rev) las añade JavaScript solo con no-preference. |
Todo bajo prefers-reduced-motion | La QA halló animaciones infinitas sin guarda (corregido en 7315075). Al escribir este caso encontré el tilt 3D de tarjetas sin guarda y lo corregí (128a664). |
Los números del escalonado y las duraciones los fijé a partir de mi criterio de diseño; no son un estándar. Lo que sí es verificable es dónde se cumplen y dónde no.
05 · Sistema de movimiento
Cada ficha responde cinco preguntas: qué hace, qué problema resuelve, qué investigué, qué descarté y qué cuesta. Seis incluyen una demo real que puedes probar. Todas se detienen con su botón y, si tu sistema pide menos movimiento, empiezan paradas y las reproduces tú.
src/motion.css · src/scroll-fx.css · src/previews.css
scaleX con scroll(root)), entrada de tarjetas, parallax y zoom lento de imágenes, tinte de sección, línea de separador que se dibuja e inclinación del marco de laptop. Todo con animation-timeline, sin escuchar el evento de scroll.@supports (animation-timeline: scroll()) y de no-preference.transform, translate, opacity, clip-path). Imágenes con dos animaciones a la vez (parallax y zoom); Hipótesis en móviles de gama baja puede notarse, no lo he medido. Accesibilidad: sin no-preference o sin soporte del navegador, todo se ve estático y completo. Dos fallos reales: véase la demo y la sección de errores.Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.
Tu navegador no soporta animation-timeline: verás las tarjetas estáticas, que es justo el comportamiento de respaldo del sitio.
overflow: hidden y vuelve a desplazarte.hidden la sección pasa a ser la fuente del timeline y las tarjetas no animan..sec y que arreglé con clip.Qué observar: con clip las tarjetas entran al aparecer. Con hidden, la sección interior se convierte en el scroller del que depende la animación; como ella no se desplaza, el timeline nunca avanza.

src/main.js (split) · src/cinema.css · src/effects.js · src/scroll-fx.css
clip-path (1 s), números de sección que cuentan de 00 al valor (900 ms) y un scramble de 700 ms solo en los eyebrows.split-text de Active Theory (MIT). Lo reescribí a mano. El código re-divide el texto tras cada cambio de idioma para no romper la traducción.<em> y <br> y el cambio de idioma reescribe su HTML, así que quedó limitado a texto plano.<span> por letra, solo en el titular de la portada. Accesibilidad: Hipótesis partir el texto en spans puede hacer que algunos lectores de pantalla deletreen; el código real no añade aria-label y no lo he probado con lector. La demo sí lo hace bien. Bajo reduced el titular aparece directamente.Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.
Qué observar: con 0 ms todo entra de golpe; con más de 70 ms la frase tarda en leerse. Mi valor real es 22 ms.
src/motion.js · src/sections.js · src/islands/store.js
document.startViewTransition y clip-path. Entre el portfolio y los casos hay un fundido de 300 ms con @view-transition.html[data-theme] y se aplica con un script en línea antes del primer pintado, así no hay destello al cargar.transition todas las variables de color: cientos de nodos recalculando a la vez, sin control sobre el orden.reduced o sin soporte del navegador el cambio es instantáneo.Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.
Qué observar: el origen del círculo cambia con el botón. Con «Pausar efecto» el cambio es instantáneo, como en el sitio con reduced.
src/mobile-menu.css · src/mobile-menu.js
clip-path, 850 ms), los enlaces suben desde una máscara de línea (70 ms entre enlaces), el fondo tiene tres manchas que se mueven solo mientras está abierto, el foco queda atrapado y se cierra arrastrando.git) funcionaba, pero no explicaba de dónde venía. El origen en el botón da causa y efecto.reduced. El commit 2abde80 corrigió tres errores: cancelaba el scroll suave, el clic tras arrastrar activaba enlaces y la vibración se disparaba sin gesto.reduced: 250 ms de opacidad y nada más.clip-path animado una vez al abrir; las manchas usan will-change y quedan pausadas con el menú cerrado. Contra mi regla: con los retardos del pie, la entrada completa llega a unos 1,1 s. Accesibilidad: la etiqueta del botón cambia entre «Abrir menú» y «Cerrar menú» en es, ca y en, y Escape lo cierra. Hipótesis sin probar con VoiceOver ni TalkBack.
src/main.js · src/motion.js · src/cinema.js
mouse-follower de Cuberto (MIT), por los estados del cursor. El historial muestra un camino accidentado: se probó un cursor fluido WebGL (35f7b26 y 08ca8b7) y se retiró (ff45ab5: 1.321 líneas menos, «no funcionaba en producción»). Después, 331b464 bajó el lag del cursor.mix-blend-mode: difference en un tema, por ser de las operaciones de composición más pesadas (mismo commit).requestAnimationFrame que se detiene al parar el puntero, y un listener global en vez de uno por elemento. No he medido fotogramas. Accesibilidad: solo con puntero fino y pantalla de más de 1024 px. Pero el cursor del sistema se oculta (cursor: none); Hipótesis puede restar visibilidad a quien lo tenga ampliado. Con reduced el modo inicial del cursor es siempre Normal y el tilt se desactiva (arreglado hoy).Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.
Este efecto necesita ratón o trackpad. En pantallas táctiles el sitio real tampoco lo aplica.
Qué observar: el botón se desplaza hacia el puntero y vuelve con un muelle suave. Mi valor real es 25% con tope de 10 px; con más, el botón se escapa del dedo y parece un fallo.
src/sections.js · src/sections.css
aria-hidden, porque cada sección ya tiene su h2. Los nombres se traducen con el idioma.transform pausada fuera de vista con IntersectionObserver. Accesibilidad, una deuda abierta: WCAG 2.2.2 pide poder pausar un movimiento automático de más de 5 s. La marquesina real no tiene botón de pausa (sí respeta reduced). La demo de abajo sí lo tiene.Tu sistema pide menos movimiento: la demo empieza detenida. Si la reproduces, lo decides tú.
Qué observar: se detiene fuera de pantalla (como en el sitio) y con el botón. La demo va a 24 s por vuelta para que se aprecie; el sitio usa 48 s.
src/components/ProjectPreview.astro · src/preview.js · src/previews.css
preload="none" el vídeo no cuesta nada al cargar. Con ahorro de datos, red 2g o reduced queda solo el póster. Accesibilidad: el <video> es decorativo (aria-hidden, sin controles) y el póster lleva alt. Misma deuda que la marquesina frente a WCAG 2.2.2.

src/main.js (bloque Preloader)
reduced) y se puede saltar con clic, Escape, espacio o Enter.b5d0f7e lo descarta entero por uno minimalista.src/main.js (Music player) · src/components/Footer.astro · public/audio/
<audio> lleva preload="none": no se descarga hasta el primer clic, y ese clic es además el gesto que exigen los navegadores para reproducir.play() solo se llama desde un clic).06 · Sistema de 5 temas
El sitio se puede ver en cinco temas con data-theme (saas, editorial, cyber, diario, hud). Cambian tipografía, paleta y carácter; el contenido y la estructura son los mismos.





Lectura mía Un portfolio visual tiene que demostrar rango. Cinco temas sobre la misma base muestran que el sistema aguanta cambios de tono sin rehacer nada.
Verificado Cada tema multiplica los casos de contraste. La QA encontró fallos reales de contraste en los chips de las tarjetas por tema (66cd877). Lighthouse solo audita el tema por defecto; los demás los revisó la QA con Playwright en tres temas y dos viewports, no en los cinco. Los dos restantes quedan sin revisión automática.
La persistencia usa un script en línea que aplica el tema guardado antes del primer pintado, y el cambio pasa por la View Transition de la pieza 03. Los nombres internos no coinciden con los públicos (editorial es El Diario Digital, diario es Minimal Journal, hud es Fine Art): arrastre de versiones anteriores que no he renombrado.
07 · Accesibilidad y reduced-motion
Accesibilidad de 90 a 100 en móvil, en el tema por defecto. Auditoría previa de 199 hallazgos de diseño (703d3cc).
Los efectos viven detrás de prefers-reduced-motion: no-preference. Los clips quedan en póster, el menú se abre con fundido de 250 ms y el cambio de tema es instantáneo.
Lectores de pantalla reales (VoiceOver, NVDA, TalkBack), el ocultamiento del cursor del sistema con baja visión y la pausa de las marquesinas (WCAG 2.2.2).
Esta página aplica lo mismo: las demos empiezan paradas si tu sistema lo pide, todas tienen botón de pausa, los controles miden al menos 44 px y el contraste del texto supera 4,5:1 (lo comprobé con un cálculo sobre los colores de esta hoja de estilos).
08 · Rendimiento
Medí con Lighthouse móvil en local sobre la portada. El orden es el de mis mediciones; no conservé los informes, así que no atribuyo cada salto a un commit concreto.
Pensé que el titular tardaba en pintarse. Midiendo, el texto salía en unos 200 ms; los 4,8 s eran del simulador de Lighthouse (Lantern), que contaba unos 530 KB de imágenes de tarjetas descargadas antes de load. La solución fue diferirlas al evento load (e5ebd2c), y el LCP simulado bajó a 2,3-3,4 s.
cbeebe3).srcset mediante astro:assets.Los números de Lighthouse del párrafo anterior son los que me quedaron anotados de mis mediciones; no son un resultado repetible desde este repositorio. Las cifras de tamaño sí lo son: salen de git y de la compilación.
09 · Arquitectura
El 22 de octubre migré de una página única con Vite a Astro (369e9b5): componentes por sección, subpáginas servidas desde public/, imágenes WebP con srcset, sitemap automático. Al día siguiente, tres piezas con estado compartido pasaron a islas React con client:idle (2a231b2): el tema, el idioma y el filtro de proyectos. El resto sigue en CSS y JS sin framework.
| Opción | Coste en JS | Qué gano | Qué pierdo |
|---|---|---|---|
| Todo vanilla | 0 KB extra | Nada que hidratar | El estado de tema, idioma y filtro se reparte entre el DOM y varios manejadores. Más difícil de mantener. |
| Astro con islas React (elegida) | +72 KB gzip medidos | Una fuente de verdad (html[data-theme] y un store) compartida por el dock, el menú y el selector | 72 KB para tres componentes pequeños |
| SPA React completa | No medido | Modelo único | Lectura mía Desproporcionada para un sitio de contenido estático. |
Verificado Medido hoy tras npm run build, sumando el cliente de React (65,8 KB gzip), react (3,0), jsx-runtime (1,1) y las tres islas (2,4): unos 72 KB gzip. El script principal del sitio, ya existente, pesa 26,8 KB gzip.
Lectura mía Es una decisión de mantenimiento, no de rendimiento: la ganancia no está en la velocidad sino en no repetir el estado. Hipótesis client:idle evita que la hidratación compita con el primer pintado; la puntuación de rendimiento no cayó tras añadirlas, pero no aislé su efecto con un A/B.
10 · Agentes y QA
Trabajé con agentes especializados: uno investigaba, otros implementaban en ramas aisladas (worktrees) y otros revisaban. Mi trabajo fue decidir qué se pedía, fijar los criterios y revisar lo que volvía.
45 commits entre el 22 y el 23 de octubre, 10 de ellos fusiones de ramas de agentes. 37 llevan el trailer Co-Authored-By de Claude.
reduced.Un agente que implementa no es el que debe revisar. Los fallos más útiles los halló la QA con Playwright, a 1440 y 390 px, no la implementación original.
| Herramienta | Para qué se usó | Evidencia |
|---|---|---|
| Claude Code | Implementación y revisión por agentes que yo dirigía | Trailer Co-Authored-By en los commits (git log) |
| Git worktrees | Cada subagente trabaja en una rama aislada y se fusiona después | Fusiones worktree-agent-* en el historial |
| CLAUDE.md y graphify | Reglas del proyecto y grafo de conocimiento del código para los agentes | CLAUDE.md en la raíz. El grafo no se versiona |
| Impeccable | Detector de calidad de diseño; triage de 199 hallazgos | .impeccable/config.json (703d3cc) |
| Playwright | QA a 1440 y 390 px y capturas reales de demos | Mensaje del commit 24a9cee (capturas); QA descrita en esta sección |
| axe-core y Lighthouse | Accesibilidad y rendimiento móvil, en local | Mensaje del commit 2cd6ea0; mediciones de la sección 08 |
| HyperFrames | Proyecto base de un reel (HTML a vídeo), fuera del build | video/reel/ (1df5500). Solo el proyecto base: sin render publicado |
| Remotion | Teaser original de Capsule Corp mostrado en una tarjeta | Commit 692afe1 |
| Vercel y GitHub | Despliegue y ramas | vercel.json (c5e8238); remoto origin |
11 · Errores y aprendizajes
overflow:hidden mataba los timelines.sec{overflow:hidden} convertía la sección en la fuente del view(); como no se desplaza, ninguna animación interior corría. Cambié a overflow:clip (a9f6eee). La demo 1 lo reproduce.
animationlightningcss unía animation y animation-timeline en un shorthand que algunos Chrome descartan. Escribo longhands (d382e44, e794ac8). Lo anoto en el CSS para no repetirlo.
Tardé en medir el LCP real: el texto salía en 200 ms. El problema eran las imágenes antes de load y el simulador.
Dos commits para añadirlo, uno para quitarlo: 1.321 líneas fuera. La técnica era correcta; el resultado en producción no.
La tabla de principios muestra escalonados y duraciones fuera de mi rango, y un tilt sin guarda de reduced que descubrí al escribir este caso.
Iterar la entrada del sitio costó nueve commits y una decisión final de simplificar. El gesto más pequeño tenía el coste más alto.
12 · Qué mediría y qué falta validar
No tengo datos de uso ni he probado el sitio con personas. Todo lo que sigue son hipótesis y cómo las comprobaría.
Lo comprobaría con 5 o 6 sesiones moderadas de personas que revisan portfolios: tiempo hasta encontrar un caso y qué recuerdan después. Sin cifras previas.
Mediría la tasa de saltarlo (clic o tecla) si añadiera un contador de eventos propio. Hoy no existe.
Grabaría trazas de rendimiento en un Android modesto durante el scroll y mediría INP y tareas largas, no solo Lighthouse.
Probaría con VoiceOver, NVDA y TalkBack el titular dividido, el menú, el reproductor y la pausa de las marquesinas.
Mira el código
El repositorio es público: el historial de git, los commits que cito y el código de cada pieza. Y si te interesa otra forma de entender mi criterio, Capsule Corp convierte mi CV en un juego.