HEX, RGB, HSL e os espaços de cor mais recentes

Como HEX, RGB, HSL, LCH e OKLCH se relacionam entre si, porque é que uma paleta HSL fica turva nos tons médios, e o que o recorte de gamute e a composição alfa fazem à cor que sai.

Uma cor em CSS são três ou quatro números mais uma regra para os ler: o espaço de cor. As paletas que parecem irregulares no ecrã costumam ter os passos definidos num espaço que não corresponde ao funcionamento da visão.

HEX e RGB são os mesmos números

#3B82F6 são três bytes em base 16: 3B é 59, 82 é 130 e F6 é 246, por isso rgb(59, 130, 246) é a mesma cor em base 10, e um conversor de cor passa de uma para a outra sem perda.

A forma abreviada de três dígitos duplica cada dígito em vez de o preencher, por isso #F80 expande para #FF8800 e não para #F08000; só 4096 cores têm forma curta. #RGBA acrescenta um dígito de alfa da mesma maneira e #RRGGBBAA escreve os quatro bytes por extenso, com esse byte de alfa a ir de 0 a 255 (1A é 10%, 80 é 50%, CC é 80%), ao passo que todos os outros valores de alfa em CSS vão de 0 a 1. A sintaxe moderna também aceita componentes separados por espaços com o alfa a seguir a uma barra: rgb(59 130 246 / 50%).

Estes números não são quantidades de luz. O sRGB codifica cada canal através de uma curva de transferência de aproximadamente 2,2, por isso 128 emite cerca de 21,6% da luz que 255 emite, e não metade, e qualquer mistura calculada sobre os valores em bruto herda essa curva.

HSL, HSV e a luminosidade que não é luminosidade

O HSL reorganiza o mesmo cubo RGB num cilindro: o matiz é um ângulo (0 vermelho, 120 verde, 240 azul), a saturação é a distância ao eixo cinzento central, e a luminosidade vai de 0 até 100%, o branco, passando pelos 50%, o matiz puro. O HSV troca a luminosidade pelo valor, cujo topo é o matiz puro e não o branco, pelo que as tintas se obtêm baixando a saturação. Nenhum dos dois sabe seja o que for sobre perceção.

CorHSLLuminância relativaLuminosidade em OKLCH
#FFFF00hsl(60 100% 50%)0,9280,97
#0000FFhsl(240 100% 50%)0,0720,45

As duas dizem ter 50% de luminosidade; uma emite cerca de treze vezes mais luz do que a outra. Uma linha de tokens com todos os matizes fixados em 50% lê-se como uma mistura desordenada de pesos.

Escalonar a luminosidade em HSL com o matiz e a saturação fixos falha pela mesma razão, mais outra: a saturação é medida em relação ao espaço disponível nessa luminosidade, por isso o passo dos 50% é o mais colorido e tudo o que fica de cada lado desmaia. O ângulo de matiz também se mantém enquanto o croma percebido se desmorona, e é isso que dá aos passos intermédios aquele aspeto turvo. Não são a cor base escurecida, são a cor base acinzentada numa quantidade que ninguém escolheu.

O amarelo mostra o pior disto. Baixar a luminosidade em HSL leva um amarelo direito ao azeitona, porque um amarelo escuro nesse ângulo de matiz é simplesmente azeitona, e mantê-lo amarelo obriga a rodar o matiz para o laranja à medida que se desce. O azul tem o problema oposto: fica tão baixo em luminância que toda a sua variação utilizável vive acima dos 50%.

LCH e OKLCH

O LCH é a forma polar do CIE Lab, construído a partir de experiências de correspondência de cores: luminosidade de 0 a 100, croma (intensidade cromática, sem máximo fixo) e um ângulo de matiz. O OKLCH tem a mesma forma, mas assenta no Oklab, que corrige os desvios de matiz pelos quais o Lab é conhecido, sobretudo os azuis a puxar para o roxo à medida que clareiam.

.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 */

Uniformidade percetual significa que uma distância numérica igual corresponde aproximadamente a uma distância percebida igual. Uma escala em passos de 0,08 de luminosidade parece ter passos regulares, uma fila de matizes fixados numa só luminosidade lê-se como uma fila única, e alterar a luminosidade deixa o matiz onde o pôs. A interpolação também pode usar o espaço, como em linear-gradient(in oklch, #0000FF, #FFFF00). Uma advertência: a luminosidade do OKLCH está muito mais perto do brilho aparente do que a do HSL, mas não é a fórmula de luminância da WCAG, por isso os pares de texto continuam a ter de passar por um verificador de contraste.

Gamute e recorte

Um gamute é o conjunto de cores que um espaço consegue representar. O sRGB é a opção por omissão há muito tempo; o Display P3 cobre cerca de um quarto mais da cor visível, com quase todo esse ganho nos vermelhos e verdes profundos. O croma em OKLCH não tem limite superior, por isso é fácil escrever uma cor que nenhum ecrã consegue mostrar.

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

O recorte canal a canal traz uma cor fora do gamute de volta fixando cada canal no seu limite de forma independente, o que desloca o matiz e achata sombras vizinhas num só valor, pelo que os gradientes ganham bandas e o detalhe desaparece. O CSS Color 4 faz antes um mapeamento: mantém a luminosidade e o matiz e reduz o croma em OKLCH até a cor caber.

É por isto que uma captura de ecrã de uma fotografia viva pode parecer apagada. A fotografia traz um perfil P3 e o monitor mostra-a por inteiro; uma exportação convertida para sRGB espreme esses píxeis saturados para um volume mais pequeno, e são os vermelhos e os verdes que têm mais caminho a percorrer.

Composição alfa

Uma cor semitransparente sobre um fundo produz uma terceira cor, opaca, canal a canal:

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

Preto a 50% sobre branco dá (127.5, 127.5, 127.5), ou seja #808080. É desta coincidência que vem a ideia de que "preto com 50% de opacidade é cinzento", e ela só se verifica sobre branco: o mesmo preto sobre #3B82F6 dá cerca de #1E417B, um azul escuro, e sobre uma fotografia dá uma cor diferente em cada píxel. O preto semitransparente é um operador de escurecimento, não um cinzento.

A composição corre sobre os valores codificados, por isso esse #808080 tem uma luminância relativa de cerca de 0,216 e não de 0,5. Meça o hexadecimal composto, nunca a cor declarada. O alfa também não se acumula como se esperaria: duas camadas a 50% cobrem 75%, porque a segunda só atua sobre o que a primeira deixou passar.

Tintas, sombras, tons e harmonias

TermoReceita tradicionalEm OKLCH
Tintabase mais brancosubir o L, baixar ligeiramente o C
Sombrabase mais pretobaixar o L
Tombase mais cinzentobaixar o C mantendo o L

As harmonias são desvios de matiz. Escolhem um destaque; não constroem um sistema.

HarmoniaDesviosBoa para
Complementar+180°um destaque contra um matiz dominante, embora vibre quando ambos estão saturados
Análoga±30°agrupamentos calmos, fundos, estados dentro de uma família
Tríade+120°, +240°conjuntos categóricos, como as séries de um gráfico

Os desvios na roda RGB não são percetualmente regulares, já que essa roda gasta um arco desproporcionado no verde, por isso trate uma harmonia calculada como um primeiro rascunho e ajuste-a a olho. Um gerador de paletas que constrói tintas, sombras e harmonias a partir de uma base coloca esse rascunho no ecrã depressa.

A escala de neutros exige uma decisão própria. Um cinzento puro, com vermelho, verde e azul iguais, ao lado de uma interface colorida parece morto e vagamente sujo. Dê aos neutros um croma pequeno, aproximadamente 0,005 a 0,02 em OKLCH, perto do matiz da marca: cinzentos quentes por volta dos 60° a 90° leem-se como papel, cinzentos frios por volta dos 250° leem-se como técnicos. Vá reduzindo esse croma à medida que sobe para os passos mais claros, para que os fundos não pareçam tingidos.

Daltonismo

Cerca de 1 em cada 12 homens e 1 em cada 200 mulheres tem uma deficiência de visão cromática. Os tipos vermelho-verde representam quase tudo, com a deuteranomalia à frente por larga margem; os tipos tritan, azul-amarelo, afetam menos de 1 em cada 10 000.

As confusões que contam numa interface são vermelho contra verde com luminosidade semelhante, verde contra castanho e laranja, roxo contra azul e rosa contra cinzento. A visão protan também escurece os vermelhos, por isso um vermelho escolhido para transmitir urgência pode acabar por ser a coisa mais discreta do ecrã.

A cor nunca pode, portanto, ser o único portador de significado. Acompanhe cada estado com uma etiqueta, um ícone, uma forma ou uma posição, e faça com que estados emparelhados difiram na luminosidade além do matiz, porque um vermelho e um verde com a mesma luminosidade em OKLCH são a pior combinação possível. Converter uma captura de ecrã para escala de cinzentos apanha essas colisões em segundos.