HEX, RGB, HSL e i nuovi spazi colore

Come si rapportano fra loro HEX, RGB, HSL, LCH e OKLCH, perché una palette costruita in HSL si intorbida nei toni medi, e che cosa fanno al colore che ottieni il ritaglio del gamut e la composizione con il canale alfa.

Un colore in CSS è fatto di tre o quattro numeri più una regola per leggerli: lo spazio colore. Le palette che a schermo sembrano irregolari di solito sono scalate in uno spazio che non corrisponde a come funziona la visione.

HEX e RGB sono gli stessi numeri

#3B82F6 sono tre byte in base 16: 3B è 59, 82 è 130, F6 è 246, quindi rgb(59, 130, 246) è lo stesso colore in base 10, e un convertitore di colori passa dall'uno all'altro senza perdite.

La forma abbreviata a tre cifre duplica ogni cifra invece di riempirla con degli zeri, quindi #F80 si espande in #FF8800, non in #F08000; solo 4.096 colori hanno una forma breve. #RGBA aggiunge allo stesso modo una cifra per l'alfa e #RRGGBBAA scrive per esteso tutti e quattro i byte, con quel byte alfa che va da 0 a 255 (1A è il 10%, 80 è il 50%, CC è l'80%) mentre ogni altro alfa in CSS va da 0 a 1. La sintassi moderna accetta anche componenti separate da spazi con l'alfa dopo una barra: rgb(59 130 246 / 50%).

Questi numeri non sono quantità di luce. sRGB codifica ogni canale attraverso una curva di trasferimento di circa 2,2, quindi 128 emette circa il 21,6% della luce che emette 255, non la metà, e ogni miscelazione calcolata sui valori grezzi eredita quella curva.

HSL, HSV e la luminosità che non è luminosità

HSL riorganizza lo stesso cubo RGB in un cilindro: la tinta è un angolo (0 rosso, 120 verde, 240 blu), la saturazione è la distanza dall'asse grigio centrale, e la luminosità va da 0 passando per il 50%, la tinta pura, fino al 100%, il bianco. HSV scambia la luminosità con il valore, il cui massimo è la tinta pura invece del bianco, quindi le versioni chiare si ottengono abbassando la saturazione. Nessuno dei due sa alcunché della percezione.

ColoreHSLLuminanza relativaLuminosità OKLCH
#FFFF00hsl(60 100% 50%)0,9280,97
#0000FFhsl(240 100% 50%)0,0720,45

Entrambi dichiarano il 50% di luminosità; uno emette circa tredici volte la luce dell'altro. Una fila di token con ogni tinta fissata al 50% si legge come un guazzabuglio di pesi diversi.

Scalare la luminosità HSL a tinta e saturazione fisse fallisce per lo stesso motivo, più un altro: la saturazione è misurata rispetto allo spazio disponibile a quella luminosità, quindi il gradino al 50% è il più colorato e tutto ciò che sta ai due lati sbiadisce. Anche l'angolo di tinta resta fermo mentre il croma percepito crolla, ed è questo che rende torbidi i gradini intermedi. Non sono il colore di base scurito, sono il colore di base ingrigito di una quantità che non ha scelto nessuno.

Il giallo mostra il caso peggiore. Abbassare la luminosità HSL porta un giallo dritto nel verde oliva, perché un giallo scuro a quell'angolo di tinta è semplicemente verde oliva, e tenerlo giallo significa ruotare la tinta verso l'arancione mentre si scende. Il blu ha il problema opposto: sta così in basso come luminanza che tutta la sua variazione utilizzabile vive sopra il 50%.

LCH e OKLCH

LCH è la forma polare di CIE Lab, costruita a partire da esperimenti di confronto fra colori: luminosità da 0 a 100, croma (quantità di colore, senza un massimo fisso) e un angolo di tinta. OKLCH ha la stessa forma ma poggia su Oklab, che corregge gli scarti di tinta per cui Lab è noto, i più visibili dei quali sono i blu che scivolano verso il viola mentre si schiariscono.

.a { color: oklch(0.62 0.21 260); }        /* L 0 to 1, C about 0 to 0.37, H in degrees */
.b { color: oklch(62% 0.21 260 / 50%); }   /* percentages and alpha both allowed */
.c { color: lch(52% 82 285); }             /* L runs 0 to 100 here */

Uniformità percettiva significa che una uguale distanza numerica è vicina a una uguale distanza percepita. Una rampa a gradini di 0,08 di luminosità sembra fatta di passi regolari, una fila di tinte fissate a un'unica luminosità si legge come una fila sola, e cambiare la luminosità lascia la tinta dove l'hai messa. Anche l'interpolazione può usare questo spazio, come in linear-gradient(in oklch, #0000FF, #FFFF00). Un'avvertenza: la luminosità OKLCH è molto più vicina della HSL alla brillantezza apparente, ma non è la formula di luminanza delle WCAG, quindi le coppie di testo passano comunque per una verifica del contrasto.

Gamut e ritaglio

Un gamut è l'insieme dei colori che uno spazio può rappresentare. sRGB è da tempo la scelta predefinita; Display P3 copre grossomodo un quarto in più del colore visibile, e quasi tutto il guadagno sta nei rossi e nei verdi profondi. In OKLCH il croma non ha limite superiore, quindi è facile scrivere un colore che nessuno schermo può mostrare.

@supports (color: color(display-p3 1 1 1)) {
  .vivid { background: color(display-p3 0.9 0.1 0.14); }
}

Il ritaglio canale per canale riporta dentro un colore fuori gamut fissando ogni canale al proprio limite in modo indipendente, il che sposta la tinta e appiattisce tonalità vicine su un unico valore, così i gradienti formano bande e il dettaglio sparisce. CSS Color 4 esegue invece una mappatura: tiene ferme luminosità e tinta e riduce il croma in OKLCH finché il colore non ci sta dentro.

È per questo che lo screenshot di una foto vivida può sembrare piatto. La foto porta con sé un profilo P3 e il display la mostra per intero; un'esportazione convertita in sRGB comprime quei pixel saturi in un volume più piccolo, e sono i rossi e i verdi ad avere la strada più lunga da percorrere.

Composizione con il canale alfa

Un colore semitrasparente sopra uno sfondo produce un terzo colore, opaco, un canale alla volta:

result = alpha * source + (1 - alpha) * backdrop

Il nero al 50% sopra il bianco dà (127.5, 127.5, 127.5), cioè #808080. Da questa coincidenza nasce l'idea che «il nero con opacità 50% è grigio», e vale sul bianco e da nessun'altra parte: lo stesso nero sopra #3B82F6 dà circa #1E417B, un blu scuro, e sopra una fotografia dà un colore diverso in ogni pixel. Il nero semitrasparente è un operatore che scurisce, non un grigio.

La composizione lavora sui valori codificati, quindi quel #808080 ha una luminanza relativa di circa 0,216 e non di 0,5. Misura l'esadecimale composito, mai il colore dichiarato. Nemmeno l'alfa si somma come ci si aspetta: due livelli al 50% coprono il 75%, perché il secondo agisce solo su ciò che il primo ha lasciato passare.

Schiariture, scurimenti, toni e armonie

TermineRicetta tradizionaleIn OKLCH
Schiariturabase più biancoalza L, abbassa leggermente C
Scurimentobase più neroabbassa L
Tonobase più grigioabbassa C a L costante

Le armonie sono scostamenti di tinta. Scelgono un accento; non costruiscono un sistema.

ArmoniaScostamentiUtile per
Complementare+180°un accento contro una tinta dominante, anche se vibra quando entrambe sono sature
Analoga±30°raggruppamenti tranquilli, sfondi, stati di una stessa famiglia
Triadica+120°, +240°insiemi categoriali come le serie di un grafico

Gli scostamenti sulla ruota RGB non sono percettivamente regolari, dato che quella ruota dedica al verde un arco sproporzionato, quindi tratta un'armonia calcolata come una prima bozza e correggila a occhio. Un generatore di palette che costruisce schiariture, scurimenti e armonie da un'unica base porta quella bozza a schermo in fretta.

La scala dei neutri richiede una decisione a sé. Un grigio puro, con rosso, verde e blu uguali, accanto a un'interfaccia colorata sembra spento e vagamente sporco. Dai ai neutri un croma piccolo, grossomodo da 0,005 a 0,02 in OKLCH, vicino alla tinta del marchio: i grigi caldi intorno ai 60° o 90° si leggono come carta, quelli freddi intorno ai 250° si leggono come tecnici. Fai calare quel croma verso i gradini più chiari, così gli sfondi non sembrano tinti.

Daltonismo

Circa 1 uomo su 12 e 1 donna su 200 hanno un deficit della visione dei colori. Le forme rosso-verde ne rappresentano quasi la totalità, con la deuteranomalia largamente in testa; le forme tritan, blu-giallo, riguardano meno di 1 persona su 10.000.

Le confusioni che contano nelle interfacce sono il rosso contro il verde a luminosità simile, il verde contro il marrone e l'arancione, il viola contro il blu e il rosa contro il grigio. La visione protan scurisce inoltre i rossi, quindi un rosso scelto per comunicare urgenza può finire per essere la cosa più sommessa sullo schermo.

Il colore non deve quindi mai essere l'unico portatore di significato. Affianca a ogni stato un'etichetta, un'icona, una forma o una posizione, e fai in modo che gli stati accoppiati differiscano nella luminosità oltre che nella tinta, perché un rosso e un verde alla stessa luminosità OKLCH sono la combinazione peggiore possibile. Convertire uno screenshot in scala di grigi individua queste collisioni in pochi secondi.