Explicación visual
La imagen que el usuario ve es el fotograma final. El botón de reproducción vuelve a ejecutar la animación una vez. Por lo tanto, la imagen estática debe explicarse por sí sola; el movimiento solo muestra el orden de las cosas.
Este es el estilo interno: tinta oscura, un acento verde y tiempos calmados, sobre blanco con líneas finas en lugar de sombras.
Legibilidad ante todo
La imagen se dibuja 1:1 junto al texto del terminal de aproximadamente 15px. Cualquier cosa más pequeña se lee peor que el chat que la rodea, por lo que el usuario no puede leerla.
- Sin texto por debajo de 16px (código 15px). Si no cabe, recorta contenido o divídelo en dos imágenes; nunca reduzcas el tamaño de la tipografía.
- Fuente: Atkinson Hyperlegible (diseñada para legibilidad), con las alternativas a continuación. Cuerpo 400, títulos 700.
- Menos elementos por imagen: aproximadamente 5 nodos por fila, dos filas como máximo.
Tokens
:root{
--bg:#fff; /* siempre blanco */
--ink:#1d2520; /* texto principal, trazos principales */
--muted:#5f6561; /* texto secundario, etiquetas en líneas */
--faint:#9aa19c; /* nodos inactivos, rutas secundarias/opcionales */
--line:#e4e4e0; /* bordes de tarjetas, divisores, cuadrícula */
--tint:#f4f2ef; /* relleno poco común: chips de código, una fila resaltada */
--accent:#3b7a57; /* el ÚNICO color: la ruta que importa */
--bad:#9a4b3c; /* solo para una marca de "no / bloqueado", nunca decoración */
}
body{font:400 18px/1.4 "Atkinson Hyperlegible",Inter,system-ui,sans-serif;color:var(--ink)}
code{font:15px "Intel One Mono",ui-monospace,monospace}
- Escala tipográfica: título 26px/700, etiqueta de nodo 18px, subetiqueta y etiquetas de línea 16px. Título con
letter-spacing:-.01em. Los números usanfont-variant-numeric:tabular-nums. - Trazos: bordes 1px. Conectores 1.5px,
stroke-linecap:round. Nada más grueso que 2px. - Radios: tarjetas 10px, chips 8px, píldora de token 999px. Sin otros valores.
- Espaciado: rejilla de 8px. Relleno del cuerpo 24px. Relleno de tarjeta 14px 18px. Separación entre nodos 96-120px, para que quepa una etiqueta de línea.
- Uso del acento: una ruta, un borde o un chip. Si dos cosas son verdes, una de ellas no debería serlo.
Lienzo
- Ancho = contenido + 48px de relleno. Típico 600-1100. Nunca ancho completo "para llenar". El contenido comienza arriba a la izquierda.
- Filas: alto del contenido + 48px, dividido por 19, redondeado hacia arriba. Un flujo de una fila ocupa 9-11 filas; dos niveles 16-20. Máximo 30.
- Ancho y bajo es mejor que alto. Prefiere un flujo de izquierda a derecha; salta a una segunda fila antes de volverse alto.
- El estilo base es blanco, 16px de relleno,
box-sizing:border-box,overflow:hidden. Establecebody{padding:24px}y el fondo tú mismo de todas formas.
Diseño
- Orden de lectura: título arriba a la izquierda, luego de izquierda a derecha, luego de arriba a abajo. La animación sigue el mismo orden.
- El título es una oración simple que expresa la conclusión ("Un acierto de caché omite la base de datos"), no un tema ("Caché").
- Tarjetas solo para cosas que existen (un servicio, una tabla, una cuenta, un paso). Las etiquetas, valores y notas son texto sin formato.
- Conectores: superposición SVG (
position:absolute;inset:0;overflow:visible) en el mismo padre posicionado que los nodos. Mide los extremos congetBoundingClientRect(); nunca codifiques coordenadas. Deja un espacio de 8px entre la línea y la tarjeta. - Punta de flecha: un camino de chevron abierto
l5 4-5 4en el color de la línea. Sin triángulos rellenos, sin valores predeterminados de<marker>. - Líneas rectas primero. Curvas solo para rodear algo: una cúbica, tangentes horizontales (
C midX y1, midX y2, x2 y2). - Una ruta que no ocurre: color
--faint, discontinua2 5. Una ruta bloqueada: igual, más una pequeña x de color--baden su centro. - Las etiquetas de línea se sitúan centradas sobre la línea en
--muted16px, 2-3 palabras. - Datos en movimiento (una solicitud, una cantidad, un mensaje) son una pequeña píldora oscura: fondo
--ink, texto blanco 16px, radio 999px. Termina estacionado donde llegó, para que la imagen estática lo muestre. - Gráficos: un eje, línea de 2px o barras finas, líneas de cuadrícula en
--line, la serie que importa en--accenty el resto en--faint, etiquetas directas en lugar de leyenda, valores en tinta (nunca en el color de la serie). Si un número es el punto, muestra el número, no un gráfico.
Animación
- Cada escena termina en un fotograma final significativo. Sin salidas, sin bucles, sin desvanecimientos. La animación total termina al menos 200ms antes de
duration_ms. - Total típico 2-4s. Quédate por debajo de 6s a menos que sea una historia de varios pasos.
duration_ms= último final + 300ms. - Orden: título, luego nodos en orden de lectura, luego se dibujan los conectores, luego las etiquetas, luego el token en movimiento, luego el cambio de estado final. Una cosa comienza mientras la anterior se asienta (superposición ~0.1-0.3s); nunca todo a la vez, nunca estrictamente uno tras otro.
- Valores:
| Qué | Desde | Duración | Facilidad (GSAP / cubic-bezier) |
|---|---|---|---|
| Título | opacidad 0, y 8 | 0.6s | power3.out / .16,1,.3,1 |
| Tarjeta | opacidad 0, y 12, escala .97 | 0.8s, escalonado 0.12s | expo.out / .16,1,.3,1 |
| Dibujo del conector | dashoffset L+4 | 0.7s, escalonado 0.1-0.3s | power2.inOut / .65,0,.35,1 |
| Punta de flecha | opacidad 0 | 0.2s, cuando termina la línea | lineal |
| Etiqueta de línea | opacidad 0, y 4 | 0.5s | power3.out |
| Viaje del token | a lo largo de la línea | 1.0s | power2.inOut |
| Intercambio de valor | antiguo: y -8, desenfoque 3px, fuera 0.35s; nuevo: y 8, desenfoque 3px, dentro 0.5s | power2.in / power3.out |
- Animar solo
transform,opacity,filter: blury elstroke-dashoffsetde SVG. - Nunca desde
scale(0); comienza en .97. Sin rebote, sin sobrepaso, sin elástico. La única excepción: un pequeño popback.out(1.6)para una marca de "no". - Nunca animes texto que el lector deba leer mientras se mueve; deja que aterrice, luego mueve lo siguiente.
- Metáfora física cuando encaje: si la cosa es una carta, una factura o una tarjeta, muévela como el objeto real.
Librerías
Elige la librería según lo que sea la imagen, antes de escribir cualquier HTML. Hacer a mano lo que una librería ya hace (un diseño de fuerza, una fórmula matemática, resaltado de código) se ve peor y se rompe más.
| La imagen es | Usa | Por qué |
|---|---|---|
| Tarjetas y flechas, un flujo, una secuencia de pasos | Tarjetas HTML + superposición SVG + timeline GSAP | Control total del estilo; GSAP ordena los pasos |
| Una entrada corta, una lista que aparece | Animación animate + stagger, o @keyframes CSS | Menos código que una timeline |
| Un gráfico con muchos nodos cuyas posiciones no puedes elegir a mano | elkjs (diseño en capas) o d3-force, luego dibuja en nuestro estilo | La matemática del diseño es difícil de hacer bien a mano |
| Un gráfico real (ejes, muchos puntos, una escala) | d3 (escalas, ejes, formas) estilizado a mano | Chart.js solo para un bar/line rápido; reestiliza sus valores predeterminados |
| Matemáticas, fórmulas | KaTeX | Tipografía matemática real |
| Código | highlight.js con el tema github, fuente 15px | Colores legibles sobre blanco |
| Muchas partículas, una simulación, física | <canvas> con rAF, impulsado por performance.now() | El DOM es demasiado lento para cientos de elementos |
| Algo 3D | three.js (módulo ES + importmap) | Solo cuando la profundidad es el punto |
| Una sensación de "idea / boceto" | rough.js | Líneas dibujadas a mano a propósito |
Nunca Mermaid para explicaciones: su apariencia predeterminada lucha contra este estilo. Usa elkjs para el diseño en su lugar.
Fijadas, todas verificadas en cdn.jsdelivr.net (prefijo https://cdn.jsdelivr.net/npm/):
- GSAP
[email protected]/dist/gsap.min.js(globalgsap). No usespaused:true; déjalo reproducir al cargar. - Motion
[email protected]/dist/motion.js(globalMotion;const {animate, stagger} = Motion). - d3
[email protected]/dist/d3.min.js(globald3). - elkjs
[email protected]/lib/elk.bundled.js(globalELK;new ELK().layout(graph)devuelve una Promise). - Chart.js
[email protected]/dist/chart.umd.min.js(globalChart). Estableceanimation:falseo su propia duración bajoduration_ms. - KaTeX
[email protected]/dist/katex.min.js+[email protected]/dist/katex.min.css. - highlight.js
@highlightjs/[email protected]/highlight.min.js+.../styles/github.min.css. - rough.js
[email protected]/bundled/rough.js(globalrough). - three.js
[email protected]/build/three.module.min.jsvía<script type="importmap">.
Diseño asíncrono (elkjs, fuentes, KaTeX): construye la escena, luego inicia la timeline. El fotograma final
se toma en duration_ms, por lo que todo debe terminar a tiempo.
Problemas comunes:
- Motion: el fotograma clave final debe usar las mismas funciones de transformación que el inicial.
['translateY(12px) scale(.97)','none']renderizascale(0), por lo que el elemento desaparece. Escribe'translateY(0px) scale(1)'. - Dibujo de línea: nunca uses
pathLength="1"; el tween de offset de GSAP lo ignora y la línea aparece completa. MideL=p.getTotalLength(), establecestrokeDasharray:${L} ${L+4}``,strokeDashoffset:L+4(el +4 oculta el punto de la tapa redonda), anima el offset a 0. Una ruta discontinua no se puede dibujar de esta manera; desvanécela. - Inserta el SVG medido y las etiquetas antes de iniciar la animación, y mide antes de aplicar cualquier transformación.
- Scripts al final de
<body>; carga la etiqueta CDN antes del script en línea.
Nunca
- Gradientes, brillo, neón, glassmorphism, sombras pesadas o de color.
- Púrpura, o más de un tono de acento. Fondos de color en áreas grandes.
- Emoji o decoración de fuente de iconos. Un pequeño SVG en línea con trazo de 1.5px si un icono es realmente necesario.
- Todo en cajas. Tarjeta héroe centrada. Una tarjeta dentro de una tarjeta.
- Un lienzo estirado a todo lo ancho con el contenido flotando en el medio.
- Flechas que se cruzan entre sí, líneas que tocan tarjetas, etiquetas que se superponen a líneas.
- Leyendas que repiten el título. Más de ~12 palabras de prosa dentro de la imagen.
- Texto por debajo de 16px, o una fuente diferente a la anterior, en una explicación.
Después del renderizado, lee el PNG y verifica: espacios, superposiciones, texto recortado, lado derecho vacío.
Esqueleto
<style>/* tokens arriba */ h2{font-size:26px;font-weight:700;letter-spacing:-.01em;margin:0 0 32px}
.row{position:relative;display:flex;gap:112px;width:max-content}
.node{padding:14px 18px;border:1px solid var(--line);border-radius:10px}
.node small{display:block;color:var(--muted);font-size:16px}
.hot{border-color:var(--accent)} svg{position:absolute;inset:0;width:100%;height:100%;overflow:visible}
path{fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}</style>
<h2>Un acierto de caché omite la base de datos</h2>
<div class="row" id="row">
<div class="node" id="a">Solicitud<small>GET /user/42</small></div>
<div class="node hot" id="b">Caché<small>Redis, TTL 5 min</small></div>
<svg id="s"></svg>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
const R=row.getBoundingClientRect();
function link(p,q,color){ const a=p.getBoundingClientRect(),b=q.getBoundingClientRect();
const y=a.top+a.height/2-R.top, x1=a.right-R.left+8, x2=b.left-R.left-8;
s.insertAdjacentHTML('beforeend',`<path class="l" stroke="${color}" d="M${x1} ${y}H${x2}"/><path class="h" opacity="0" stroke="${color}" d="M${x2-5} ${y-4}l5 4-5 4"/>`); }
link(a,b,'#3b7a57');
s.querySelectorAll('.l').forEach(p=>{const L=p.getTotalLength();gsap.set(p,{strokeDasharray:`${L} ${L+4}`,strokeDashoffset:L+4})});
gsap.timeline()
.from('h2',{opacity:0,y:8,duration:.6,ease:'power3.out'})
.from('.node',{opacity:0,y:12,scale:.97,duration:.8,ease:'expo.out',stagger:.12},.2)
.to('.l',{strokeDashoffset:0,duration:.7,ease:'power2.inOut'},1)
.to('.h',{opacity:1,duration:.2},1.6);
</script>
Llámalo con width ≈ 560, rows: 10, duration_ms: 2200.