Expliqueur visuel
L'image que l'utilisateur voit est la dernière image. Le bouton de lecture rejoue le mouvement une fois. Donc l'image fixe doit s'expliquer d'elle-même ; le mouvement montre seulement l'ordre des choses.
Voici le style maison : encre sombre, un accent vert et un timing calme, sur fond blanc avec des traits fins au lieu d'ombres.
Lisibilité d'abord
L'image est dessinée à côté du texte du terminal en 15px environ. Tout ce qui est plus petit se lit moins bien que le chat autour, donc l'utilisateur ne peut pas le lire.
- Pas de texte en dessous de 16px (code 15px). Si ça ne tient pas, réduisez le contenu ou divisez-le en deux visuels ; ne réduisez jamais la police.
- Police : Atkinson Hyperlegible (conçue pour la lisibilité), avec les polices de secours ci-dessous. Corps 400, titres 700.
- Moins de choses par image : environ 5 nœuds par rangée, deux rangées au maximum.
Tokens
:root{
--bg:#fff; /* toujours blanc */
--ink:#1d2520; /* texte principal, traits principaux */
--muted:#5f6561; /* texte secondaire, étiquettes sur les lignes */
--faint:#9aa19c; /* nœuds inactifs, chemins de retour/optionnels */
--line:#e4e4e0; /* bordures de cartes, séparateurs, grille */
--tint:#f4f2ef; /* remplissage rare : puces de code, ligne surlignée */
--accent:#3b7a57; /* la SEULE couleur : le chemin qui compte */
--bad:#9a4b3c; /* seulement pour une marque "non / bloqué", jamais décoratif */
}
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}
- Échelle de police : titre 26px/700, étiquette de nœud 18px, sous-étiquette et étiquettes de ligne 16px. Titre
letter-spacing:-.01em. Les nombres reçoiventfont-variant-numeric:tabular-nums. - Traits : bordures 1px. Connecteurs 1.5px,
stroke-linecap:round. Rien de plus épais que 2px. - Rayons : cartes 10px, puces 8px, pilule de token 999px. Pas d'autres valeurs.
- Espacement : grille de 8px. Remplissage du corps 24px. Remplissage de carte 14px 18px. Écart entre les nœuds 96-120px, pour qu'une étiquette de ligne tienne.
- Utilisation de l'accent : un chemin, une bordure, ou une puce. Si deux choses sont vertes, l'une d'elles ne devrait pas l'être.
Canvas
- Largeur = contenu + remplissage de 48px. Typiquement 600-1100. Jamais en pleine largeur "pour remplir". Le contenu commence en haut à gauche.
- Lignes : hauteur du contenu + 48px, divisé par 19, arrondi au supérieur. Un flux d'une ligne fait 9-11 lignes ; deux niveaux 16-20. Maximum 30.
- Large et court bat long et étroit. Préférez un flux de gauche à droite ; passez à une deuxième rangée avant de devenir trop haut.
- Le style de base est blanc, remplissage 16px,
box-sizing:border-box,overflow:hidden. Définissezbody{padding:24px}et le fond vous-même de toute façon.
Mise en page
- Ordre de lecture : titre en haut à gauche, puis de gauche à droite, puis de haut en bas. L'animation suit le même ordre.
- Le titre est une phrase simple énonçant le message à retenir ("Un cache hit contourne la base de données"), pas un sujet ("Mise en cache").
- Les cartes seulement pour les choses qui existent (un service, une table, un compte, une étape). Les étiquettes, valeurs et notes sont du texte nu.
- Connecteurs : superposition SVG (
position:absolute;inset:0;overflow:visible) dans le même parent positionné que les nœuds. Mesurez les extrémités avecgetBoundingClientRect(); ne codez jamais en dur les coordonnées. Laissez un écart de 8px entre la ligne et la carte. - Pointes de flèche : un chemin chevron ouvert
l5 4-5 4dans la couleur de la ligne. Pas de triangles remplis, pas de valeurs par défaut<marker>. - Lignes droites d'abord. Courbes seulement pour contourner quelque chose : une cubique, tangentes horizontales (
C midX y1, midX y2, x2 y2). - Un chemin qui ne se produit pas : couleur
--faint, pointillé2 5. Un chemin bloqué : pareil, plus un petit x--baden son milieu. - Les étiquettes de ligne sont centrées au-dessus de la ligne en
--muted16px, 2-3 mots. - Données en mouvement (une requête, un montant, un message) sont une petite pilule sombre : fond
--ink, texte blanc 16px, rayon 999px. Elle se termine garée là où elle est arrivée, pour que l'image fixe la montre. - Graphiques : un axe, ligne de 2px ou barres fines, lignes de grille en
--line, la série qui compte en--accentet les autres en--faint, étiquettes directes au lieu d'une légende, valeurs en encre (jamais dans la couleur de la série). Si un nombre est le point important, montrez le nombre, pas un graphique.
Mouvement
- Chaque scène se termine par une image finale significative. Pas de sorties, pas de boucles, pas de fondus sortants. L'animation totale se termine au moins 200ms avant
duration_ms. - Durée totale typique 2-4s. Restez sous 6s sauf s'il s'agit d'une histoire en plusieurs étapes.
duration_ms= dernière fin + 300ms. - Ordre : titre, puis nœuds dans l'ordre de lecture, puis les connecteurs se dessinent, puis les étiquettes, puis le token en mouvement, puis le changement d'état final. Une chose commence pendant que la précédente se calme (chevauchenement ~0.1-0.3s) ; jamais tout en même temps, jamais strictement l'une après l'autre.
- Valeurs :
| Quoi | Depuis | Durée | Ease (GSAP / cubic-bezier) |
|---|---|---|---|
| Titre | opacité 0, y 8 | 0.6s | power3.out / .16,1,.3,1 |
| Carte | opacité 0, y 12, échelle .97 | 0.8s, stagger 0.12s | expo.out / .16,1,.3,1 |
| Tracé du connecteur | dashoffset L+4 | 0.7s, stagger 0.1-0.3s | power2.inOut / .65,0,.35,1 |
| Pointe de flèche | opacité 0 | 0.2s, quand la ligne se termine | linéaire |
| Étiquette de ligne | opacité 0, y 4 | 0.5s | power3.out |
| Trajet du token | le long de la ligne | 1.0s | power2.inOut |
| Échange de valeur | ancien : y -8, blur 3px, sortie 0.35s ; nouveau : y 8, blur 3px, entrée 0.5s | power2.in / power3.out |
- Animez seulement
transform,opacity,filter: bluret lesstroke-dashoffsetSVG. - Jamais à partir de
scale(0); commencez à .97. Pas de rebond, pas de dépassement, pas d'élasticité. La seule exception : un petit popback.out(1.6)pour une marque "non". - N'animez jamais un texte que le lecteur doit lire pendant qu'il bouge ; laissez-le atterrir, puis déplacez la chose suivante.
- Métaphore physique quand elle convient : si la chose est une lettre, une facture ou une carte, déplacez-la comme l'objet réel.
Bibliothèques
Choisissez la bibliothèque en fonction de ce qu'est l'image, avant d'écrire du HTML. Réinventer ce qu'une bibliothèque fait déjà (un layout de force, une formule mathématique, un surlignage de code) est moins beau et plus cassant.
| L'image est | Utilisez | Pourquoi |
|---|---|---|
| Cartes et flèches, un flux, une séquence d'étapes | Cartes HTML + superposition SVG + timeline GSAP | Contrôle total du style ; GSAP ordonne les étapes |
| Une courte entrée, une liste qui apparaît | animate de Motion + stagger, ou @keyframes CSS | Moins de code qu'une timeline |
| Un graphe avec de nombreux nœuds dont vous ne pouvez pas choisir les positions à la main | elkjs (layout en couches) ou d3-force, puis dessinez dans notre style | Le calcul de layout est difficile à faire correctement à la main |
| Un vrai graphique (axes, de nombreux points, une échelle) | d3 (échelles, axes, formes) stylisé à la main | Chart.js seulement pour un barre/ligne rapide ; restylez ses valeurs par défaut |
| Maths, formules | KaTeX | Un vrai composition mathématique |
| Code | highlight.js avec le thème github, police 15px | Couleurs lisibles sur fond blanc |
| Beaucoup de particules, une simulation, de la physique | <canvas> sur rAF, piloté par performance.now() | Le DOM est trop lent pour des centaines d'éléments |
| Quelque chose en 3D | three.js (module ES + importmap) | Seulement quand la profondeur est le sujet |
| Une sensation "idée vague / esquisse" | rough.js | Lignes dessinées à la main intentionnellement |
Jamais Mermaid pour les explicateurs : son apparence par défaut combat ce style. Utilisez elkjs pour la mise en page à la place.
Épinglés, tous vérifiés sur cdn.jsdelivr.net (préfixe https://cdn.jsdelivr.net/npm/) :
- GSAP
[email protected]/dist/gsap.min.js(globalgsap). Ne mettez paspaused:true; laissez-le jouer au chargement. - 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)renvoie une Promesse). - Chart.js
[email protected]/dist/chart.umd.min.js(globalChart). Définissezanimation:falseou sa propre durée sousduration_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.jsvia<script type="importmap">.
Mise en page asynchrone (elkjs, polices, KaTeX) : construisez la scène, puis démarrez la timeline. L'image finale
est prise à duration_ms, donc le tout doit quand même finir à temps.
Pièges :
- Motion : la keyframe de fin doit utiliser les mêmes fonctions de transformation que le début.
['translateY(12px) scale(.97)','none']rendscale(0), donc l'élément disparaît. Écrivez'translateY(0px) scale(1)'. - Dessin de ligne : jamais
pathLength="1"; le tween de décalage de GSAP l'ignore et la ligne apparaît d'un coup. MesurezL=p.getTotalLength(), définissezstrokeDasharray:"${L} ${L+4}",strokeDashoffset:L+4(le +4 cache le point d'arrondi), tweenez le décalage vers 0. Un chemin pointillé ne peut pas être dessiné de cette façon ; fondez-le en entrée. - Insérez les SVG et étiquettes mesurés avant de démarrer l'animation, et mesurez avant toute transformation appliquée.
- Scripts à la fin de
<body>; chargez la balise CDN avant le script en ligne.
Jamais
- Dégradés, lueur, néon, glassmorphisme, ombres lourdes ou colorées.
- Violet, ou plus d'une teinte d'accent. Arrière-plans colorés sur de grandes zones.
- Emoji ou décoration par police d'icônes. Un petit SVG en ligne avec trait de 1.5px si une icône est vraiment nécessaire.
- Tout encadré. Carte héros centrée. Une carte dans une carte.
- Un canvas étiré en pleine largeur avec le contenu flottant au milieu.
- Flèches qui se croisent, lignes qui touchent les cartes, étiquettes qui chevauchent les lignes.
- Légendes qui répètent le titre. Plus de ~12 mots de prose à l'intérieur de l'image.
- Texte sous 16px, ou une police autre que celle ci-dessus, dans un expliqueur.
Après le rendu, lisez le PNG et vérifiez : espaces, chevauchements, texte coupé, côté droit vide.
Squelette
<style>/* tokens ci-dessus */ 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 cache hit contourne la base de données</h2>
<div class="row" id="row">
<div class="node" id="a">Requête<small>GET /user/42</small></div>
<div class="node hot" id="b">Cache<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>
Appelez avec width ≈ 560, rows: 10, duration_ms: 2200.