Budowa palety kolorów przy pomocy OKLCH
- Opublikowano • 5 min
Wprowadzenie
Celem niniejszego wpisu jest praktyczne przybliżenie OKLCH - natywnej funkcji do operacji na kolorach. Dowiesz się, jak wykorzystać OKLCH do budowy palety kolorów. Dla lepszego zrozumienia, zawężę temat do palety monochromatycznej.
Teoria kolorów, przestrzenie kolorów wybiegają poza zakres wpisu.
Lata, lata temu, z zachwytem przeglądałem palety kolorów na Colour Lovers. Było to w dawnych czasach zwanych dla niepoznaki Web 2.0. Palety kolorów, a nawet pojedyncze kolory były inspiracją dla niejednego web mastera. Serwis dziś zdaje się działać jedynie jako archwium a blask dawnej popularności już wygasł.
Mimo upływu lat, jeżeli chcesz wywżeć dobre wrażenie na odwiedzających twoje media, warto zadbać o spójność kolorystyczną. OKLCH daje spore możliwości a jeszcze więcej frajdy. Zapraszam do lektury.
Krótko o OKLCH
The oklch() functional notation expresses a given color in the Oklab color space. oklch() is the cylindrical form of oklab(), using the same L axis, but with polar Chroma (C) and Hue (h) coordinates.
Tyle teorii. Trochę zawiłe ale w praktyce nie jest.
oklch(L C H[ / A])
(L)ightness:
0-1 / 0-100% lub none
(C)hroma:
Ilość koloru w kolorze. 0 do…
Według MDM w praktyce nie przekracza 0.5.
(H)ue:
Barwa: <number> albo <kąt>.
(A)lpha:
Opcjonalne. 0-1 / 0-100%
OKLCH daje jeszcze jedną ciekawą możliwość: konwersję innego koloru na OKLCH:
Uwielbiam granaty (owoce). Kolor granatu skojarzył mi się z #c93434 . Zapisany kolor w Hex mogę odczytać jako:
background-color: oklch(from #c93434 l c h);
Wyliczonym kolorem będzie:
background-color: oklch(0.555161 0.186082 25.6828);
Kanały l, c oraz h w oklch(from #c93434 l c h) nie zostały zmienione. Stąd też kolory zapisane w Hex i po konwersji są tożsame.
#c93434 | |
|---|---|
oklch(0.555161 0.186082 25.6828) |
Zobaczmy co się stanie, kiedy zmienimy wartość L na 0.8. Kolor jest jaśniejszy.
background-color: oklch(0.8 0.186082 25.6828); →
Relatywne wyliczanie koloru
Wiemy już, że możemy wyliczać poszczególne wartości dla L, C i H; wiemy także jakie wartości przyjmują. Zbudujmy więc paletę monochromatyczną. Dla uściślenia dwie definicje.
(paleta kolorów) dobór kolorów lub odcieni
(monochrome) using different shades of one colour
Do zbudowania palety utrzymamy H (barwę) i zmodyfikujemy jedynie L. Dla uproszczenia zostawimy to samo C choć L i C mogą iść w parze.
--color-pomegranate: #c93434; // nasz początkowy kolor w Hex
--color-pomegranate-base: oklch(from var(--color-pomegranate) l c h);
--color-pomegranate-darker: oklch(from #c93434 calc(l - 0.3) c h);
Dla późniejszych operacji wyliczam --color-pomegranate-base z koloru zapisanego w Hex.
Zwróć uwagę na calc(l - 0.3). Właśnie obniżyliśmy L o 0.3.
Tym samym mamy już nakreślony kierunek: możliwość wyliczenia spójnych kolorów:
--color-pomegranate-100: oklch(
from var(--color-pomegranate-base) calc(l + 0.3) c h
);
--color-pomegranate-200: oklch(from var(--color-pomegranate-base) l c h);
--color-pomegranate-300: oklch(
from var(--color-pomegranate-base) calc(l - 0.3) c h
);
Jak wcześniej napisałem, zakres L wynosi 0-1. Jak odczytać aktualne L? Dla ćwiczenia wklej poniższą funkcję w Konsolę (Narzędzia dla programistów) i zbadaj ten kolor: Tak to był ten.
function getOKLCHLightness(el) {
style = getComputedStyle(el);
colorStr = style.backgroundColor;
match = colorStr.match(/oklch\(([\d.]+)/);
return (L = match ? parseFloat(match[1]) : null);
}
getOKLCHLightness($0); // 0.555161
(zaznaczony node jest zawsze dostępny pod zmienną $0). U mnie L wynosi 0.555161.
Koniec ćwiczenia, za chwilę użyjemy czegoś lepszego.
Automatyzacja
Chcę zbudować monochromatyczą paletę zgodnie z konwencją color-pomegranate-level:
- 100 ── najjaśniejszy odcień
- 200 ── jasny odcień
- 300 ── bazowy / referencyjny
- 400 ── ciemny odcień
- 500 ── najciemniejszy odcień
Czyli:
color-pomegranate-100color-pomegranate-200color-pomegranate-300color-pomegranate-400color-pomegranate-500
Zostaje nam jedynie wyliczyć 2 jaśniejsze oraz 2 ciemniejsze odcienie. Do opercji wykorzystam Culori.
import { formatCss, formatHex, oklch, parse } from "culori";
function generateShades(baseColor: string, count: number): string[] {
if (count % 2 === 0) throw new Error("count must be odd");
const base = oklch(parse(baseColor));
if (!base) throw new Error(`Could not parse color: ${baseColor}`);
const half = (count - 1) / 2;
const { l } = base;
const shades: string[] = [];
// Ciemniejsze: równo rozłóż od `0` do bazowego `L`
for (let i = 1; i <= half; i++) {
shades.push(formatCss({ ...base, l: l * (i / (half + 1)) }));
}
// Bazowy kolor tak, jak jest
shades.push(formatCss(base));
// Jaśniejsze: równo rozłóż od bazowego `L` do wartości `1`
for (let i = 1; i <= half; i++) {
shades.push(formatCss({ ...base, l: l + (1 - l) * (i / (half + 1)) }));
}
return shades.reverse();
}
Dla #c93434 otrzymany:
[
oklch(0.8517203692267477 0.186082003984858 25.682719738184133),
oklch(0.7034407384534953 0.186082003984858 25.682719738184133),
oklch(0.5551611076802431 0.186082003984858 25.682719738184133),
oklch(0.37010740512016205 0.186082003984858 25.682719738184133),
oklch(0.18505370256008102 0.186082003984858 25.682719738184133)
]
3 element kolekcji jest naszym bazowym kolorem; Poniższe demo jest interaktywne, zmień kolor i dobrej zabawy!
#c93434
color-pomegranate-100: oklch(0.85 0.19 25.68)color-pomegranate-200: oklch(0.7 0.19 25.68)color-pomegranate-300: oklch(0.56 0.19 25.68)color-pomegranate-400: oklch(0.37 0.19 25.68)color-pomegranate-500: oklch(0.19 0.19 25.68)Zmieniając jedynie wartość samego L utworzyliśmy monochromatyczną paletę z wartościami przechowanymi w formie tokenów1 albo zmiennych HTML.
Znając zakresy dla pozostałych parametrów możemy wyjść poza paletę monochromatyczną. Types of color palettes: Definition, examples, + tips w prosty sposób omawia różne rodzaje pal kolorów. Poniżej dwa dodatki: relacja Chroma do Lightness oraz kolor uzupełniający (także opisany w artykule na blogu Figmy).
Bonus: modyfikacja wartości Chroma
Zobacz jak zachowuje się Chroma w stosunku do Lightness. Paleta zachowuje H i L z wybranego koloru. Wyróżniona próbka to kolor bazowy.
#c93434
Pięciostopniowa paleta została wyliczona przy pomocy Chroma (C), która ma zakres 0-5. Kolor #c93434
został uplasowany w odpowiednim przedziale. Jako ciekawostkę dodam, że tło również generowane przy pomocy OKLCH. Mając na wejściu jeden kolor, na UI uzyskujemy wiele.
// Przykład z komponentu Vue; zaznaczona linia: `background-color` wyliczony z koloru w kolekcji
const styleObject = computed(() => ({
"--data-color-palette-current-color": currentColor.value,
backgroundColor: `oklch(from ${shades.value.at(-1)?.color} l c calc(h + 180) / 0.3)`,
}));
Bonus: kolor kontrastujący
Zgodnie z zasadą 60-30-10, by zachować równowagę w kompozycji, sugeruje się, by kolor dominujący obejmował 60%, kolor uzupełniający 30%, a kolor akcentujący, 10%.
Przy pomocy OKLCH utworzenie koloru uzupełniającego to betka; wystarczy dodać 180° do H:
--color-pomegranate-300: oklch(
from var(--color-pomegranate-base) l c calc(h + 180)
);
#c93434
Zmień kolor i zobacz, że jego kontrastujący odpowiednik znajduje się po drugiej stronie koła kolorów (o ile domyślnie próbnik kolorów w systemie wyświetla koło). U mnie wygląda to tak:
Podsumowanie
Budowanie palety kolorów, a także modyfikowanie ich przy pomocy OKLCH otwiera wiele możliwości oraz ułatwia utrzymywanie spójności. Mam nadzieję, że choć trochę zainteresowałem cię tematem. Pracując jako Frontend Developer nie często mamy możliwość znacznego kształtowania kolorów. Natomiast często pojawia się potrzeba zmodyfikowania kolorów w sytuacji kiedy design system nie przewidział użycia danego elementu w innym kontekście; Znając możliwości OKLCH możemy podbić kontrast, uwypuklić kolor, bazując na dostępnych kolorach.
Footnotes
-
Dla uproszczenia, bo nie chodzi tutaj o decyzje projektowe; patrz Od design tokenów do zmiennych… ↩