Mes dégradés sont plus beaux que les vôtres

Mes dégradés sont plus beaux que les vôtres

☠️ La Zone Grise de la Mort

Dans les années 2010, nous avons eu le plaisir de découvrir les fonctions linear-gradient et radial-gradient qui permettent de définir des dégradés directement en CSS.

Je me souviens d'avoir expérimenté diverses combinaisons, avec des résultats assez inégaux. En utilisant certaines couleurs, je voyais une bande grisâtre qui ne semblait pas coller avec l'effet recherché.

Si je vous dis que l'on mélange du bleu et du jaune, on obtient du vert, pas vrai ?

🟡 + 🔵 = 🟢

Apparemment pas dans un navigateur, avec CSS.

background: linear-gradient(to right, #FFFF00, #0000FF);

Représentation d'un dégradé linéaire allant du jaune vers le bleu

J'ai bien tenté de refaire l'expérience avec un outil tel qu'Illustrator et j'ai été surpris d'obtenir un résultat similaire. A l'époque, cela m'avait semblé être une curiosité, un peu décevante mais assez négligeable pour être rapidement oubliée. Elle l'est restée jusqu'à ce que je découvre en 2022 ce tweet de Fin Mourhouse :

Dans ce fil, le chercheur nous explique les origines mathématiques de ce phénomène, parfois appelé la Zone Grise de la Mort ☠️

En RVB, on sait comment définir une couleur grise : il suffit de déclarer le même niveau de rouge, de vert et de bleu.

tableau présentant 11 niveaux de gris allant du blanc au noir avec les quantités correspondantes de RVB

Si le niveau de chaque couleur est élevé, on s'approchera du blanc et inversement, s'il est bas, notre gris tendra vers le noir.

En représentant le RVB sous la forme d'un cube, on peut visualiser "la diagonale du gris" qui le traverse : de l'arrière-plan vers l'avant et du bas vers le haut

Représentation du modèle RVB sous la forme d'un cube

Source : Wikimedia

Maintenant, pensons à la manière dont notre dégradé est construit.

À chaque pixel situé sur le parcours du dégradé, le navigateur va afficher une couleur qui sera calculée en fonction des valeurs de départ et des valeurs d'arrivée. Il va faire ce calcul pour le rouge, le vert et le bleu : il augmentera ou diminuera régulièrement leur valeur de départ, jusqu'à leur valeur d'arrivée.

Dans notre exemple du jaune et du bleu, nos points de départ et d'arrivée sont soit 0, soit 255. À mi-chemin, il y a un point où toutes les valeurs sont égales. À cet endroit, la synthèse RVB est nécessairement grise :

La valeur RVB médiane lors d'un dégradé linéaire du jaune vers le bleu et de 127 si l'on va de 255 à 0

Notre méthode de calcul du dégradé est facile à comprendre, mais elle nous pousse vers la diagonale du gris et les couleurs semblent plus ternes, moins lumineuses à mesure que nous nous en approchons.

Dans la suite de l'article, nous allons chercher à obtenir un meilleur dégradé, tout en privilégiant une méthode de calcul simple et si possible intuitive.

Voici les problèmes à résoudre :

  • éviter la zone grise de la mort

  • préserver la luminosité des couleurs

🌈 HSL : oui... Mais non 🌧️

Intuitivement, on peut penser que l'écriture HSL (ou le HWB qui est très proche) est un bon moyen d'éviter la zone grise de la mort.

HSL signifie Hue, Saturation, Lightness (en français : Teinte, Saturation, Lumière). Ce système représente les couleurs sur un disque :

  • les teintes (H) sont réparties de 0 à 360°.

  • la saturation (S) de la teinte est exprimée par le rayon du disque de 0 (la couleur totalement desaturée, donc grise) à 100 (la saturation est maximale).

On y ajoute une troisième dimension :

  • la lumière (L) qui va de 0 (aucune luminosité, la couleur est noire) à 100 (luminosité maximale, la couleur est blanche).

On peut alors imaginer un dégradé qui ferait varier uniquement la teinte, tout en gardant une saturation et une luminosité constante. Voici la visualisation d'un déplacement sur le disque qui évite la zone grise :

Disque chromatique représentant le système HSL avec une flèche traçant le parcours d'un dégradé allant du jaune au bleu

Source : Wikimedia

Cela semble une bonne piste. Mais attention, déclarer nos couleurs avec hsl() au lieu de rgb() ne va pas suffire. hsl() est un "sucre syntaxique" : la valeur déclarée sera systématiquement convertie en RVB et nous obtiendrons le même résultat qu'avant.

Ce qui nous intéresse, ce n'est pas comment on déclare une couleur, mais comment on calcule notre dégradé.

C'est là qu'intervient la spécification CSS Color Module Level 4 qui introduit la fonction color-interpolation-method qui nous permet de spécifier un mode de calcul.

Cela nous donne :

background: linear-gradient(to right in hsl, #0000ff, #ffff00);

Dégradé CSS du jaune au bleu qui utilise une interpolation HSL

⚠️ A ce jour (mars 2024) la fonction color-interpolation-method pour les dégradés n'est pas disponible dans tous les navigateurs.

On voit que nous n'avons même pas besoin de déclarer nos couleurs en HSL, ce qui importe c'est la déclaration in hsl.

Grâce à cette méthode, nous évitons la zone grise et nous obtenons enfin notre couleur verte entre le bleu et le jaune.

Mais le résultat est un peu déroutant. En allant vers le bleu, on passe par une zone turquoise qui parait franchement plus lumineuse que l'ensemble du dégradé. Ça ne semble pas très naturel.

💡 sRGB : des problèmes de luminosité

Quand nous utilisons les méthodes rgb(), hsl(), hwb(), nous définissons des couleurs contenues dans l'espace colorimétrique sRGB (Le "s" signifie standard). Cet espace sert à déclarer les couleurs disponibles sur la plupart des écrans que nous utilisons. Il est très pratique, car il permet d'en définir un très grand nombre en combinant seulement 3 signaux lumineux. Mais il a aussi ses défauts.

Les couleurs de cet espace ont été définies pour nos yeux, mais elles sont aussi contraintes par la technologie, par la conception même des écrans.

Il en va que le sRGB contient des ajustements spécifiques par rapport à la luminosité réelle : la correction Gamma.

Mais aussi que les valeurs maximales de rouge, vert et bleu ne sont pas égales en termes de luminosité perçue par nos yeux (on parlera alors de "luminance").

Ces deux points ont un impact important sur les calculs que nous voulons effectuer pour créer notre dégradé.

La correction Gamma 🕶️

Pour des raisons pratiques la répartition de la luminosité dans le modèle RVB est artificiellement corrigée par rapport à la luminosité naturelle.

Nous percevons mieux les nuances sombres que claires. Or, si l'on découpait linéairement la lumière selon sa luminosité, nous disposerions de beaucoup plus de nuances claires et pas assez de nuances sombres.

Voici un schéma avec à gauche le découpage corrigé et à droite un découpage linéaire :

Tableau comparant le découpage de la luminosité, selon qu'il soit linéaire ou si on lui a appliqué une correction Gamma

Dans la version corrigée, la répartition des nuances sombres nous semble plus adaptée à nos besoins. Dans le découpage linéaire, la quantité de nuances claires paraît trop importante.

La correction est de plus en plus importante au fur et à mesure que la luminosité augmente. Cette non-linéarité implique des calculs plus compliqués pour réaliser un dégradé avec une luminosité constante. Il serait plus efficace d'effectuer nos calculs à partir des valeurs du RVB linéaire.

Nous retrouvons alors notre fonction color-interpolation-method. Elle nous permet de spécifier des espaces de couleur différents dans nos dégradés :

background:linear-gradient(to right in srgb, #FFFF00, #0000FF);
background:linear-gradient(to right in srgb-linear, #FFFF00, #0000FF);

Comparaison d'un dégradé du jaune vers le bleu, le premier utilise l'espace RVB classique et le second, le RVB linéaire ce qui permet de garder une luminosité constante des couleurs dans le dégradé

Le premier dégradé utilise l'espace RVB "classique" (sRGB), le second utilise le RVB linéaire.

La différence de luminosité entre les deux méthodes de calcul est flagrante : sombre pour la première, plus lumineuse pour la seconde... Mais dans les deux cas subsiste notre premier problème : nous passons par le gris.

La luminosité perçue 👁️

Deuxième problème de notre espace sRGB : les valeurs maximales n'ont pas une luminosité équivalente, en tout cas dans notre perception. C'est flagrant si l'on compare le vert et le bleu :

Comparaison d'un aplat rgb vert pur et d'un autre bleu pur avec indication des codes RGB et HSL correspondants

Comparaison d'un aplat rgb vert et d'un autre bleu, tous les deux convertis en niveau de gris pour montrer la différence de luminosité

La conversion en niveaux de gris rend cette différence encore plus évidente. Ces différences de luminosité ne sont pas prises en compte sur le disque chromatique que nous utilisons avec HSL.

Le modèle HSL est destiné à l'espace sRGB, et donc les valeurs de luminosité et de saturation sont harmonisées afin de créer un disque chromatique cohérent avec cet espace.

Ce schéma représente horizontalement les teintes (Hue) et verticalement la luminosité (Lightness) :

Schéma représentant la répartition de la luminosité pour chaque teinte, en utilisant la méthode hsl()

Source : Wikimedia

Si l'on se place à 50% de luminosité (axe vertical L) et que l'on parcourt les différentes couleurs, la luminosité est loin de nous paraître constante.

Ceci explique le manque de consistance dans notre dégradé HSL.

À ce stade, on se dit que prendre en compte l'ensemble des caractéristiques du sRGB et les intégrer dans nos calculs de dégradé risque d'être un peu compliqué. En s'appliquant, on doit pouvoir y arriver, mais ce n'est pas aussi intuitif qu'on le voudrait et difficilement automatisable.

Mais alors que faire ?

Prenons un peu de recul et revenons sur cette histoire d'espace colorimétrique.

👩‍🚀 L'espace colorimétrique sRGB

Nous travaillons en sRGB car c'est ce dont les écrans ont besoin et historiquement nous avons les outils CSS pour transmettre cette information dans cet espace de couleur. Mais cet espace ne contient pas toutes les couleurs possibles, il est restreint. Cet ensemble de couleurs, parfois appelé gamut, peut être élargi. Certains appareils vont proposer des gamut couvrant une plage de couleur plus importante. C'est le cas de certains Macbook qui utilisent le gamut P3 ou d'écrans de télé HD qui utilisent rec2020.

Le schéma CIE 1931 ci-dessous propose une représentation mathématique des couleurs visibles. Sur cette version, on a indiqué les plages des différents gamuts :

schéma CIE 1931 avec représentation des principaux gamut disponibles

Source : Wikimedia

Au milieu, notre sRGB est un triangle, avec ses trois sommets : rouge, vert et bleu. Chacune de ces valeurs est envoyée à l'écran et sera affichée par un pixel donné, ce qui produira pour nos yeux la couleur finale.

Pour explorer cet espace, nous utilisons rgb() et hsl(). Mais comme on le disait, certains appareils nous permettent d'accéder à d'autres espaces colorimétriques. Alors comment y définir des couleurs ?

👽 Plus d'espace : plus de modèle !

C'est là qu'intervient de nouveau le CSS Color Module Level 4. Cette spécification nous met à disposition des méthodes permettant de définir des couleurs dans et hors du sRGB. Ces nouveaux modèles ne sont pas contraints par un gamut spécifique et ne sont pas conçus comme le sRGB.

Nous allons nous intéresser à :

  • lab()

  • oklab()

  • lch()

  • oklch()

Ces fonctions permettent de déclarer une couleur dans l'espace colorimétrique Lab*. Cet espace couvre toutes les couleurs visibles par nos yeux et il a été créé pour prendre en compte notre perception, en particulier la différence de luminosité que nous percevons en fonction des teintes.

La fonction lab() permet de déclarer une couleur selon sa luminosité (l) et sa teinte selon deux axes (a et b) qui vont respectivement du vert au rouge et du bleu au jaune.

lch() permet d'exploiter cet espace mais sous une forme cylindrique (comme le HSL).

Les versions "ok" (oklab et oklch) corrigent certains défauts du lab (notamment dans la luminosité des bleus).

Avec ce modèle, nous contrôlons :

  • la luminosité (l)

  • le chroma (c) qui indique la quantité de couleur et que l'on peut rapprocher de la saturation

  • la teinte (h) qui désigne une couleur sur un cercle

Nous allons donc refaire notre dégradé en okLCH qui semble être le candidat parfait :

  • Interpolation circulaire

  • Prise en compte de la luminosité perçue

🍭 Bon ! Et notre dégradé alors ?

background:linear-gradient(to right in okLCH, #ffff00, #0000ff)

Dégradé CSS du jaune au bleu qui utilise une interpolation okLCH

Et enfin ! On peut dire que ce dégradé correspond à l'idée que nous en étions faite. Ça été un peu plus long que prévu, mais cela nous a permis de comprendre l’intérêt des différents modèles à notre disposition.

Je ne vais pas rentrer plus en avant sur les qualités de okLCH, cela me demanderait beaucoup de travail et cet article serait interminable. Et surtout cela a déjà été très bien fait par l'agence Evil Martians, dont je ne saurais trop vous conseiller l'excellent article :

Ainsi que leur color picker qui permet de convertir des couleurs en okLCH et aussi de visualiser nos couleurs dans les limites de différents gamuts :

🎨 okLCH au service du Design System

Au-delà des dégradés, ce qui est intéressant, c'est la consistance d'un modèle comme okLCH pour systématiser nos déclinaisons de couleur. Générer une palette de couleur cohérente à partir d'un ensemble de teintes sera plus simple, notamment si l'on fait usage de la fonction CSS color-mix().

Pour découvrir ce sujet, je vous conseille la vidéo de Kevin Powell sur la fonction CSS color-mix() qui nous explique comment décliner ses couleurs et comment les méthodes d'interpolation vont influencer les résultats.

🛠️ Et en prod, ça marche ?

color-interpolation-method

1. dans les dégradés

Rappelons-le, color-interpolation-method, n'est pas disponible partout pour les dégradés (notamment dans Firefox). Si vous l'utilisez, vous devrez employer un fallback.

Il est aussi possible d'utiliser un générateur de dégradé qui simule des interpolations spécifiques, par exemple :

2. dans color-mix()

L'usage d'une méthode d'interpolation avec color-mix() est supporté par les navigateurs récents

oklch()

Il est possible de déclarer vos couleurs directement en utilisant oklch(). La compatibilité semble plutôt bonne.

Conclusion

En partant de cette curiosité graphique qu'est "la zone grise de la mort", je ne pensais pas découvrir autant de problématiques liées à la couleur. En tant que dev front, la couleur peut sembler être un sujet annexe qui concerne essentiellement le design. Mais en creusant, on comprend très vite que l'usage de la couleur sur nos écrans est très lié à la technologie et aux évolutions techniques. Comprendre cette histoire et ces contraintes, c'est aussi mieux comprendre et exploiter les outils à notre disposition.

Les fonctionnalités récentes du CSS en matière de couleur peuvent largement impacter le travail des design.ers.euses et leur ouvrir de nouvelles perspectives créatives.

Il est donc important de réfléchir ce sujet ensemble, à ce stade les possibilités offertes par CSS et les navigateurs semblent plus en avance que celles offertes par les outils de design. Ainsi, dans Figma, il n'est pas possible d'utiliser nativement des modèles "exotiques" comme okLCH ou d'utiliser des méthodes d'interpolation autre que le RVB pour les dégradés. Il existe cependant des plugins, reste à voir s'ils sont efficaces.

Bibliographie

Gamut / Espaces colorimétriques

Dégradés

Gamma

Luminosité perçue

Models

Color-mix

Illustration :Susan Wilkinson