La Matrice : Alphabet Géométrique en CSS

Comment j'ai créé un alphabet brutaliste génératif avec une grille de carrés imbriqués et le pouvoir du clip-path en CSS

Tout a commencé par une fascination pour le projet de design graphique de Nguyen Gobber et Hofmann. Leur approche de la typographie modulaire repose sur un système de grille proportionnelle où des cercles parfaits s'entremêlent pour former des lettres d'une fluidité cool et la lisibilité discutable :D.

C'est un concept de géométrie incroyable car la proportionalité est automatisée. Mais pour mon espace expérimental, j'ai une autre ambition : le brutalisme (poum poum poum). Comment reprendre cette logique de proportionnalité stricte, mais en éliminant la moindre courbe ? Comment remplacer la douceur d'un cercle par la violence d'un angle droit ou d'un biseau tranchant ?

La réponse ne se trouve pas dans un logiciel de dessin, mais dans l'architecture même du CSS : la grille, les variables mathématiques, et la propriété clip-path. C'est partie pour le tunel dans la création d'un alphabet brutaliste génératif.

1. La Théorie : Tuer la courbe, inventer la matrice

Dans le projet original, les concepteurs utilisaient une grille de 4x4 cellules contenant des cercles. Le défi du brutalisme est que l'absence de courbes nous prive des intersections naturelles qu'offrent les arcs de cercle pour tracer des diagonales.

Si nous utilisions de simples carrés pleins dans une grille 4x4, nous obtiendrions du pixel-art basique des années 80. Pas du brutalisme moderne.

Le concept du "Carré dans le Carré"

Pour obtenir des pleins, des déliés et des angles obliques (des chanfreins), il faut complexifier la grille interne de chaque module. La solution ? Une matrice de carrés imbriqués.

Imagine une grille classique de 4x4. Entre chaque cellule, il y a un espace vide (le gap). Maintenant, à l'intérieur de chaque grande cellule, dessine un carré plus petit (par exemple, occupant 60% de l'espace). En prolongeant virtuellement les lignes de ce petit carré vers les bords extérieurs, tu vas avoir une une sous-grille de 3x3 à l'intérieur de chaque cellule.

Nous obtenons alors une cartographie d'une précision complexe, mais fiable. Les intersections de ces lignes ne sont plus de simples coins, mais des points de pivot qui vont nous permettre de tracer des diagonales franches et des épaisseurs de trait mathématiquement parfaites.

⬜ Grille externe
Les 4x4 cellules maîtresses avec l'espacement
◽ Carré interne
Le carré de 60% qui crée nos points d'ancrage intérieurs
⬛ Points de coordonnées
Les 16 points où nous pouvons accrocher nos lignes

2. Le CSS : Jouer à la Bataille Navale

Là, c'est chaud. Si on regarde mon html, on a une suite de div... mais c'est violent. Imagine écrire en dur dans le html 250 points de ta grille. PAR LETTRE.
Avant de "dessiner", nous devons coder notre plan de travail. C'est ici que les Variables CSS (Custom Properties) vont être utiles. Elles entrent en jeu pour créer notre système de coordonnées, sans les écrire. En gros, elles vont automatiser la grille, je l'explique plus bas.

Puisque la propriété CSS clip-path: polygon() utilise des paires de coordonnées (X et Y), il faut nommer les lignes comme sur un plateau de bataille navale :

Chaque cellule possède 4 repères sur son axe (les bords extérieurs 1 et 4, les bords intérieurs 2 et 3). Ainsi, le point supérieur gauche du petit carré central de la première cellule en haut à gauche s'appellera : var(--A-x2) var(--R1-y2). Première colonne (A), deuxième repère (x2), première ligne (R1), deuxième repère (y2)
C'est comme si on lui disait va à la colone 1 et avance de 2 puis va la ligne 2 et avance de 2. (pfiou, c'est chaud hein ?)

L'Échafaudage Mathématique

Voici le code "racine" qui s'applique au conteneur de notre lettre. C'est lui qui calcule tout seul les distances en fonction de la taille souhaitée et du ratio de la grille interne.

[data-matrice-char] {

/* --- LES FONDATIONS --- */
--size: 40vmin;
/* Taille totale de la lettre */
--gap-size: 0.5vmin;
/* L'espace entre nos blocs principaux */
--inner-ratio: 0.6;
/* Le petit carré occupe 60% du grand */
/* Le moteur de calcul automatique */
--cell-size: calc((var(--size) - (var(--gap-size) * 3)) / 4);
--inner-size: calc(var(--cell-size) * var(--inner-ratio));
--offset: calc((var(--cell-size) - var(--inner-size)) / 2);
}
--cell-size
La taille d'une case individuelle (le premier var()). On retire d'abord les 3 gaps qui séparent nos 4 colonnes (le 2ème var() fois 3), puis on divise par 4. Rien n'est laissé au hasard.
--inner-size
Le petit carré intérieur. C'est simplement une multiplication du ratio (ici 0.6 = 60%). On change cette valeur et tout l'alphabet mute.
--offset
La distance magique. C'est l'espace blanc qui sépare le bord extérieur de la case du début de notre carré interne. C'est lui qui crée tous nos biseaux et angles parfaits. Il est toujours égal à la moitié de la différence entre les deux carrés. C'est pas très claire je crois (bonne chance).
/* Mise en page de la grille maître */
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: var(--gap-size);
width: var(--size);
height: var(--size);
position: relative;
/* --- CARTOGRAPHIE DE L'AXE X (Exemple avec A et B) --- */
/* Colonne A (gauche) */
--A-x1: 0;
--A-x2: var(--offset);
--A-x3: calc(var(--offset) + var(--inner-size));
--A-x4: var(--cell-size);
}
x1 / y1
Le bord extérieur absolu. C'est le bord dur de la case, le point 0. Il n'y a rien avant ça.
x2 / y2
Le bord intérieur gauche. Début du carré central. C'est le début de notre épaisseur de trait.
x3 / y3
Le bord intérieur droit. Fin du carré central. C'est la fin de notre épaisseur de trait.
x4 / y4
Le bord extérieur droit. Fin de la case. Après ça c'est le gap.
La règle de la mort qui tues
Tu ne traces JAMAIS entre deux repères. Tous tes points obligatoirement sur 1, 2, 3 ou 4. C'est la contrainte qui fait la beauté du système. Comme l'article source.
/* Colonne B (milieu gauche - on prend en compte le gap !) */
--B-start: calc(var(--A-x4) + var(--gap-size));
--B-x1: var(--B-start);
--B-x2: calc(var(--B-start) + var(--offset));
--B-x3: calc(var(--B-start) + var(--offset) + var(--inner-size));
--B-x4: calc(var(--B-start) + var(--cell-size));

/* ... (C et D suivent la même logique mathématique) ... */
/* --- CARTOGRAPHIE DE L'AXE Y (Exemple avec R1) --- */
/* Ligne 1 (haut) */
--R1-y1: 0;
--R1-y2: var(--offset);
--R1-y3: calc(var(--offset) + var(--inner-size));
--R1-y4: var(--cell-size);

/* ... (R2, R3, R4 descendent avec le même calcul) ... */
}

3. la méta class [data-char] :
la sorcellerie pure et dure

Maintenant qu'on l'explication de la structure, qu'on peut à peu prêt expliquer comment la grille est construite. On revient tout en haut. Re regarde la première ligne tu es peut-être passé à côté.
Tu imagines déjà un <div> pour chaque lettre ? genre <div class="lettre-N"> pour la lettre N, <div class="H"> pour la lettre H, etc ?
Mais pas du tout. Il faut comprendre ce qu'est [data-]. Définition : moi dans mon css j'ai écris : [data-matrice-char] { ... }.
Ce n'est pas une class, un selecteur d'attribut. C'est à dire qu'on dit au navigateur : "Yo, à chaque fois que tu vois un élément HTML qui est data-matrice-char, applique lui cette logique de construction".
Mais pas que, une classe on lui dira "ressemble à ça", une classe décore et sculte. Là,on lui dit aussi, il EST une information/identité, il est de la donnée, une identité.
Donc le navigateur va le calculer et lui appliquer les styles et l'identité.
Par exemple, plus bas, tu verras un N. il est dans <div data-matrice-char="N">. Donc le navigateur va lire "Ah, c'est un élément avec l'attribut data-matrice-char,
Il a la valeur N et il EST[data-matrice-char],
C'est comme si on avait une classe "N", mais en plus on peut faire du CSS conditionnel en fonction de la valeur de cet attribut. Par exemple, on peut dire : [data-matrice-char="N"]::after { clip-path: polygon(...); }. Et là, le navigateur va aller chercher les données du clip-path uniquement à l'élément qui a data-matrice-char="N". Et découper la forme de la lettre N sur l'élément virtuel ::after
C'est la le twist du CSS qui nous permet d'avoir un seul conteneur générique pour TOUTES les lettres, et de les différencier grâce à leurs attributs. C'est pratique, c'est super propre, car on n'a pas besoin de créer une classe pour chaque lettre, ni de dupliquer du code.
Tu sais créer un API !!!!!!! ET en CSS. C'est de la magie de dingue.
Maintenant tu sais où va agir le clip-path et où il va chercher la donnée pour l'appliquer à la classe. Et on va aller le voir de plus près.

4. La sculture artistique incomprise : clip-path: polygon()

Il y a une confusion courante avec le CSS que j'ai apris à mes dépends : clip-path ne dessine pas un trait, il découpe une forme (donc pas du tout comme un stroke en SVG). Donc, les coordonées qu'on a défini dans [data-] servent à découper une forme dans un bloc de couleur. clip-path agit comme une paire de ciseaux sur une feuille de papier définie par la propriété background. Qui est dans ? [data-matrice-char] ou si on veut être précis dans [data-matrice-char]::after

Pourquoi cette approche ? Le "remplissage" de la lettre est totalement indépendant de sa forme. Tu veux une lettre noire ? Les propriétés sont définies dans [data-matrice-char]::after. le clip-path découpe la forme hérité de [data-matrice-char] et des propriétés comme background: var(--color-black);. Tu veux une lettre en texture de bruit ou un GIF animé ? background: var(--color-texture);. La forme restera intacte. C'est toi qui les définis si tu en as envie. MAIS attention, avec des var(--variable)

Le masque global s'applique sur un pseudo-élément qui recouvre toute la grille :

[data-matrice-char]::after {
content: '';
position: absolute;
inset: 0;
background: var(--bg, var(--clr-brand))/* La couleur de la lettre */
/* clip-path défini lettre par lettre ci-dessous */
}

5. Pratique : Sculpter la lettre "N"

C'est ici que la sorcellerie du clip-path de la matrice opère.

Prenons la lettre "N". Sa diagonale est le cauchemar de la grille stricte. Mais grâce à nos "petits carrés" internes, nous avons les points d'ancrage parfaits pour créer une diagonale épaisse et tranchante.

Dans mon premier essai, le H était cassé. Pourquoi ? Parce que le tracé se croisait. J'avais dit à la barre horizontale de commencer en R2-y4 (le bas de la ligne 2) et de finir en R3-y1 (le haut de la ligne 3). Sauf que l'axe Y descend de haut en bas... donc `R2` est au-dessus de `R3` ! Le bas s'est retrouvé plus haut que le sommet, vrillant le polygone sur lui-même.
C'est un style tu me diras, et tu as raison. Les voies du brutalisme sont impénétrables.

Voici la correction parfaite pour un tracé propre (le fameux H fonctionnel) :

[data-matrice-char="H"]::after {
clip-path: polygon(
/* Fût Gauche */
var(--A-x1) var(--R1-y1),
var(--A-x1) var(--R4-y4),
var(--A-x3) var(--R4-y4),

/* On remonte jusqu'au bas de la barre horizontale */
var(--A-x3) var(--R3-y3),

/* On traverse vers le Fût Droit */
var(--D-x2) var(--R3-y3),
var(--D-x2) var(--R4-y4),
var(--D-x4) var(--R4-y4),
var(--D-x4) var(--R1-y1),
var(--D-x2) var(--R1-y1),

/* On redescend jusqu'au haut de la barre horizontale */
var(--D-x2) var(--R2-y2),

/* On retraverse pour fermer */
var(--A-x3) var(--R2-y2),
var(--A-x3) var(--R1-y1)
);
}

6. L'Ombre Brutaliste et la technique de la "Fente"

Je me suis fait avoir...en CSS : Est-ce qu'on est obligé d'utiliser ::before et ::after pour percer un trou (comme au milieu d'un O) ?

C'est de la triche, mais mal faites en plus : ::after pour dessiner un bloc plein de couleur, et ::before par dessus, peint de la couleur du fond (le `background`), pour donner "l'illusion" d'un trou.

Mais ça crée un énorme problème : On ne peut plus mettre d'ombres !

Si tu tentes de mettre un box-shadow, il se fera couper net par le clip-path.
La solution est d'appliquer un filtre filter: drop-shadow(...) sur le conteneur parent (comme c'est le cas sur l'exemple des lettres ci-dessus !). Sauf que... si ton trou n'est qu'une tricherie de couleur pleine, le navigateur considère ta lettre comme un gros bloc rectangulaire et l'ombre bave n'importe comment.

La solution du slit : La Fente

Pour pouvoir mettre une ombre, il faut que le polygone soit percé nativement, sans ::before.
Pour cela, on utilise la règle du Winding Number (la règle de remplissage).

  1. Tu dessines le contour extérieur dans le Sens Horaire.
  2. Tu crées une "fente" mathématique invisible vers l'intérieur.
  3. Tu dessines le trou intérieur dans le Sens Anti-Horaire.
  4. Tu ressors par ta fente.

Le navigateur calcule que "1 - 1 = 0", et vide magiquement le centre !
Le pseudo-élément ::before disparaît du code CSS, tout tient dans le ::after.
Le résultat ? Une vraie transparence au centre et une ombre (drop-shadow) nette et tranchante qui projette toute la complexité de ta forme.

[data-char="O"]::after {
clip-path: polygon(
/* Extérieur octogonal (Sens HORAIRE) */
var(--A-x3) var(--R1-y1),
var(--D-x2) var(--R1-y1),
var(--D-x4) var(--R1-y3),
/* ... On fait le tour complet ... */
var(--A-x3) var(--R1-y1),

/* LA FENTE (entrée) */
var(--B-x2) var(--R2-y2),

/* Trou carré (Sens ANTI-HORAIRE) */
var(--B-x2) var(--R3-y3),
var(--C-x3) var(--R3-y3),
var(--C-x3) var(--R2-y2),
var(--B-x2) var(--R2-y2),

/* LA FENTE (sortie) */
var(--A-x3) var(--R1-y1)
);
}

Le pouvoir de la contrainte et des maths. À toi de trouver les coordonnées de la lettre Z ! (et pour le A... oulala)
Le niveau suivant te permettra de comprendre à quoi cela va servir. Et qu'est-ce qu'on peut faire avec.
spoiler : t'imagines quoi ? ... le Morphing... mix-blend-mode: multiply;...