Typographie Réactive

Variable Font brutaliste et interactive, sans SVG

Pré-requis : Cette expérience repose entièrement sur le système de matrice 4×4. Si tu n'as pas lu l'article fondateur, les coordonnées --A-x1 à --D-x4 et le principe du clip-path te sembleront incompréhensibles.

Cette page pousse le concept un cran plus loin : au lieu de dessiner des lettres à épaisseur fixe, nous allons animer la matrice elle-même. Une seule propriété --offset contrôle l'épaisseur du trait, et le navigateur recalcule toute la géométrie des 256 points d'ancrage (16 repères sur l'axe X × 16 repères sur l'axe Y). Et ce, à 60 images par seconde s'il te plaît.

Table des matières

1. Le Problème : Est-ce qu'on peut animer ?

Habituellement, pour animer l'épaisseur d'une typographie, deux solutions existent :

Notre ambition est différente : aucun fichier externe, aucun JS, juste du CSS et des maths. La forme n'est pas dessinée, elle est calculée. Pour de l'ornemental, cela pourrait le faire non ?

2. Créer sa propre propriété : @property --offset

Dans la page matrice, --offset est une valeur fixe : c'est l'espace entre le bord extérieur de la cellule et le début du carré intérieur. Cet espace, multiplié par 2, donne l'épaisseur du trait.

En CSS classique, les variables sont des tokens de texte. Le navigateur ne sait pas interpoler 12px vers 20px pendant une transition.

La propriété @property ( CSS Houdini ) permet d'enregistrer --offset auprès du navigateur avec un type length :

@property --offset {
  syntax: '<length>';
  inherits: true;
  initial-value: 12px;
}
syntax: '<length>'
Le navigateur sait que c'est une longueur (px, em, etc.). Il peut l'interpoler (12 → 20 → 21 px).
inherits: true
Les pseudo-éléments ::after héritent de la valeur. Quand --offset change, les 256 points d'ancrage de la matrice recalculent.
initial-value: 12px
Style "Light" par défaut. C'est notre graisse de départ.

Une fois enregistrée, --offset devient une propriété modifiable :

[data-matrice-char] {
  --offset: 12px;     /* Épaisseur Light */
  transition: --offset 0.4s cubic-bezier(0.25, 1, 0.5, 1);
}
[data-matrice-char]:hover {
  --offset: 20px;     /* Au survol, Bold */
}

Le navigateur ne fait pas un fondu. À chaque frame de la transition, il recalcule toutes les coordonnées de la matrice :

--cell-size: calc((var(--size) - (3 * var(--offset))) / 4);
--inner-size: calc(var(--cell-size) - (2 * var(--offset)));
--A-x2: var(--offset);
--A-x3: calc(var(--cell-size) - var(--offset));
...
--R4-y3: calc(var(--R4-start) + var(--cell-size) - var(--offset));

La variable --offset est le pivot central. Elle contrôle le gap (espacement entre cellules), l'épaisseur du trait (x2, y2, x3, y3), et indirectement la taille des cellules. C'est du morphing géométrique natif, sans canvas, sans SVG, sans JS.

C'est de la typographie réactive de sorcelerie de malade !!!!

3. Démo : Survole pour voir la graisse muter

Passe ta souris sur chaque lettre. La propriété --offset passe de 12px à 20px, et toute la matrice se recalcule. Note l'ombre filter: drop-shadow qui suit fidèlement la forme découpée — c'est normal puisque le navigateur recalcule toutes les coordonnées à chaque frame :

4. Les Lettres Complexes : Diagonales et Trous

La vraie force du système 4×4 apparaît sur les lettres complexes :

Le N utilise une diagonale qui relie le coin intérieur du fût gauche au coin intérieur du fût droit. Quand --offset augmente, la diagonale s'épaissit proportionnellement sans jamais casser l'angle.

[data-matrice-char="N"]::after {
  clip-path: polygon(
    var(--A-x1) var(--R1-y1),  /* Haut fût gauche */
    var(--A-x1) var(--R4-y4),  /* Bas fût gauche */
    var(--A-x3) var(--R4-y4),  /* Pied gauche */
    var(--D-x2) var(--R1-y3),  /* Diagonale : coin intérieur droit */
    var(--D-x2) var(--R4-y4),  /* Bas fût droit */
    var(--D-x4) var(--R4-y4),  /* Pied droit */
    var(--D-x4) var(--R1-y1),  /* Haut fût droit */
    var(--D-x2) var(--R1-y1),  /* Retour intérieur droit */
    var(--A-x3) var(--R4-y2),  /* Diagonale retour */
    var(--A-x3) var(--R1-y1)   /* Fermeture */
  );
}

5. Cartographie : Quels points pour quelles constructions ?

Chaque lettre n'utilise qu'une fraction des 256 points d'ancrage, mais elle démontre une construction géométrique unique. Voici le catalogue :

L - 6 points
A-x1, A-x3, D-x4, R1-y1, R4-y3, R4-y4
Construction : fût vertical + pied horizontal. Tous les points sont alignés sur les bords. Pas de diagonale, pas de trou.
I - 4 points
A-x2, A-x3, R1-y1, R4-y4
Construction : barre verticale isolée. Le fût n'occupe que le carré intérieur d'une seule cellule.
T - 8 points
A-x1, B-x2, B-x3, D-x4, R1-y1, R1-y2, R4-y4
Construction : barre horizontale sur toute la largeur + fût central. La barre utilise les bords extérieurs (A-x1, D-x4), le fût utilise les bords intérieurs (B-x2, B-x3).
C - 8 points
A-x1, A-x3, D-x4, R1-y1, R1-y2, R4-y3, R4-y4
Construction : arc ouvert (3 côtés d'un carré). L'ouverture est créée en sautant un côté complet du polygone.
N - 10 points
A-x1, A-x3, D-x2, D-x4, R1-y1, R1-y3, R4-y2, R4-y4
Construction : diagonale épaisse entre deux fûts. Les points D-x2 R1-y3 et A-x3 R4-y2 créent une diagonale qui traverse 3 colonnes et 3 lignes.
H - 12 points
A-x1, A-x3, D-x2, D-x4, R1-y1, R2-y2, R3-y3, R4-y4
Construction : barre transversale à travers deux fûts. La barre relie les bords intérieurs de la colonne A à ceux de la colonne D, en traversant les lignes R2 et R3.
M - 14 points
A-x1, A-x3, B-x4, C-x1, D-x2, D-x4, R1-y1, R2-y1, R2-y2, R3-y2, R4-y1, R4-y4
Construction : double pente avec pieds asymétriques. Les points B-x4 R4-y1 et C-x1 R4-y1 créent un pied central large, tandis que les pentes montent vers A-x3 R2-y1 et D-x2 R2-y1.
O - 16 points (fente / Non-Zero Winding)
A-x1, A-x3, D-x2, D-x4, B-x2, B-x3, C-x2, C-x3, R1-y1, R1-y3, R2-y2, R2-y3, R3-y2, R3-y3, R4-y2, R4-y4
Construction : trou unique octogonal. L'extérieur est un octogone (8 coins). Le trou intérieur est aussi un octogone (8 coins sur les repères B et C × R2 et R3), parfaitement parallèle à l'extérieur. La fente invisible pénètre depuis le coin sup-gauche et ressort par le même chemin.
A - 10 points (fente / Non-Zero Winding)
A-x1, A-x2, B-x2, C-x3, D-x3, D-x4, R1-y1, R2-y2, R2-y3, R3-y3, R4-y4
Construction : trou triangulaire. La pointe est un segment étroit en haut (A-x2 à D-x3). Le trou est un triangle inversé centré sur B-x2 C-x3. La fente descend verticalement depuis le sommet intérieur.
B - 26 points (fentes / Non-Zero Winding)
Construction : deux trous indépendants. Le polygone contient l'extérieur, le trou du haut et le trou du bas, reliés par deux fentes invisibles. La lettre B est pour le moment celle que est la plus complexe à construire: chaque trou a son propre aller-retour de fente.

La règle qui unifie tout : le nombre de points n'a aucune importance. Chaque point s'accroche sur un repère de la matrice (x1, x2, x3, x4 × y1, y2, y3, y4). La seule contrainte à respecter pour les lettres avec trou est la fente.

ATTENTION : On parle d'animation par modification de --offset. On ne parle pas de transition vers une seconde lettre.
C'est possible mais cela va nécessiter des réglage puisque cela veut dire que chaque point doit être identiques par lettre...
On y reviendra surement dans un prochain article.

6. Le Secret des Trous : La Fente invisible

Découper un "L" avec un polygone est trivial. Mais comment percer le trou central d'un "O" ou les deux trous d'un "B" sans que le polygone ne s'effondre ?

Dans ma première expérimentation, j'utilisais la fente : tracer le contour extérieur dans un sens, créer une entaille invisible vers l'intérieur, tracer le trou dans le sens inverse, ressortir par la même fente. C'est la Non-Zero Winding Rule.
La fente est invisible car elle est parcourue deux fois dans des sens opposés : le navigateur calcule "1 − 1 = 0" et ne remplit pas cette ligne.

Pourquoi ne pas utiliser evenodd qui semble plus simple ?

clip-path: polygon(evenodd,
  /* 1. Extérieur */
  var(--A-x3) var(--R1-y1)...

  /* 2. Trou */
  var(--B-x2) var(--R2-y2)...
);

Le problème : polygon() n'a pas de commande move-to comme en SVG. Le navigateur trace une ligne de connexion entre la fin du contour extérieur et le début du trou. Cette ligne traverse la matière de la lettre et crée un angle visible qui la casse.

La fente (Non-Zero Winding) reste donc la seule solution propre avec clip-path: polygon() : l'entrée et la sortie du trou passent par le même chemin, en sens opposé. Le navigateur les annule mathématiquement.

On a testé avec des lettres toutes moches, maintenant on va essayer avec des lettres plus élaborés dans leurs découpages, pour voir...

La Fente
Une ligne parcourue deux fois dans des sens opposés a une somme de remplissage de 0. Le navigateur l'efface : elle devient une porte invisible vers le trou.
Non-Zero Winding
Contour extérieur dans un sens (+1), trou dans le sens inverse (−1). À l'intérieur du trou : 1 − 1 = 0 → vide. Pas de ::before, pas de JS, juste des maths.

7. Le Toggle CSS Pur : Comment on clique sans JavaScript

Le LAB a un concept sacralisé : interdit d'utiliser JavaScript. Comment faire un bouton "Activer le mode Ultra Black" sans addEventListener ?

La réponse est le Checkbox Hack, une technique CSS classique qui exploite le comportement natif du HTML :

<!-- L'input est caché visuellement mais reste fonctionnel -->
<input type="checkbox" id="blackToggle" class="sr-only">

<!-- Le label agit comme bouton (cliquable partout) -->
<label for="blackToggle" class="brutal-button">Mode Black</label>

<!-- Le conteneur ciblé par le sélecteur CSS :checked -->
<div class="typo-demo">...lettres...</div>

Quand on clique sur le <label>, le navigateur coche automatiquement l'<input> grâce à l'attribut for. Le CSS utilise alors le sélecteur de voisinage ~ :

/* Quand la checkbox est cochée... */
#blackToggle:checked ~ .typo-demo [data-matrice-char] {
  --offset: 21px;  /* Toutes les lettres passent en Ultra Black */
}

/* ...le bouton s'enfonce visuellement */
#blackToggle:checked ~ .typo-controls .brutal-button {
  background: var(--clr-brand);
  transform: translate(6px, 6px);
  box-shadow: 0 0 0 var(--clr-border);
}

Le texte du bouton change aussi sans JS : deux <span> superposés ("Activer" / "Désactiver"). On utilise la classe .sr-only plutôt que display: none pour que les lecteurs d'écran n'annoncent qu'un seul texte à la fois.

État : Light État : Ultra Black
Clique le bouton
La checkbox se coche. Le sélecteur :checked ~ s'active. Toutes les lettres passent de --offset: 12px à --offset: 21px en 0.4s. 21px est la limite physique de la grille avant que les trous ne s'effondrent.
Re-clique
La checkbox se décoche. Retour à la graisse Light. Aucun état n'est perdu au rechargement (c'est du HTML natif).
Pas de JS
Ce site fonctionnera dans 20 ans. Pas de dépendance, pas de build, pas de framework. Juste un input HTML et un sélecteur CSS.

8. Variantes de Style

La matière de la lettre (couleur, texture) est totalement indépendante de sa forme. Le clip-path découpe, le background remplit. On peut donc superposer n'importe quel style :

[data-matrice-char].accent   { --bg: var(--clr-analogue); }
[data-matrice-char].inverted { --bg: var(--clr-text); filter: drop-shadow(8px 8px 0 var(--clr-brand)); }
[data-matrice-char].texture  { /* background-image: radial-gradient(...) */ }

Même principe que sur la page matrice : la forme reste intacte, seule la matière change. C'est le principe qui consiste à séparer le masque (clip-path) du remplissage (background).

9. Accessibilité et Robustesse

Le système respecte les préférences de l'utilisateurice :

@media (prefers-reduced-motion: reduce) {
  [data-matrice-char] { transition: none; }
}

Si l'utilisateurice demande moins de mouvement, les transitions disparaissent instantanément. Les lettres changent toujours de graisse, mais sans animation. Le site reste pleinement fonctionnel.

prefers-reduced-motion
Les transitions sont supprimées. Pas d'animation saccadée ni de vertige.
prefers-contrast: more
Hérité de style.css. Le texte s'agrandit à 120%.
Responsive
Sous 600px, les lettres passent de 240px à 160px. Le layout flex-wrap s'adapte automatiquement. --offset reste en px, donc l'épaisseur relative augmente : les lettres deviennent plus massives sur mobile.
Limite connue : sur des écrans très étroits, la contraction de la grille peut inverser les points dans les fentes.

Comme [data-matrice-char] est un élément HTML natif, il est reconnu par les technologies d'assistance.
Il est donc possible de naviguer et d'interagir avec ces éléments de manière accessible.
data-matrice-char="M" class="accent texture" role="img" aria-label="Lettre M" J'ai ajouté un role d'img et une étiquette ARIA pour que les lecteurs d'écran annoncent la lettre correctement.
Après, c'est ton choix, si tu décides de mettre des points pour faire un coeur, c'est toi qui vois. Là il est décidé d'en faire un ornement avec un role image.

Bravo d'être arrivé jusqu'ici !<
cadeau : La lettre A