HEX, RGB, HSL e os espaços de cor mais recentes
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.
| Cor | HSL | Luminância relativa | Luminosidade em OKLCH |
|---|---|---|---|
#FFFF00 | hsl(60 100% 50%) | 0,928 | 0,97 |
#0000FF | hsl(240 100% 50%) | 0,072 | 0,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
| Termo | Receita tradicional | Em OKLCH |
|---|---|---|
| Tinta | base mais branco | subir o L, baixar ligeiramente o C |
| Sombra | base mais preto | baixar o L |
| Tom | base mais cinzento | baixar o C mantendo o L |
As harmonias são desvios de matiz. Escolhem um destaque; não constroem um sistema.
| Harmonia | Desvios | Boa 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.