Budowa palety kolorów przy pomocy OKLCH

Budowa palety kolorów przy pomocy OKLCH
Budowa palety kolorów przy pomocy OKLCH

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.

Dokumentacja w MDM

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

Wielki Słownik Języka Polskiego

(monochrome) using different shades of one colour

Oxford Learner’s Dictionaries

Mmm…
Mmm…

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:

4 odcienie plus bazowy kolor (w środku)
4 odcienie plus bazowy kolor (w środku)
  • 100 ── najjaśniejszy odcień
  • 200 ── jasny odcień
  • 300 ── bazowy / referencyjny
  • 400 ── ciemny odcień
  • 500 ── najciemniejszy odcień

Czyli:

  • color-pomegranate-100
  • color-pomegranate-200
  • color-pomegranate-300
  • color-pomegranate-400
  • color-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

100
200
300
400
500
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

Odcienie
100
200
300
400
500
Chroma
#a65a54
#c93434
#f20000
#ff0000
#ff0000

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

100
200
300
400
500
100
200
300
400
500

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:

Kolor kontrastujący po dodaniu 180°
Kolor kontrastujący po dodaniu 180°

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

  1. Dla uproszczenia, bo nie chodzi tutaj o decyzje projektowe; patrz Od design tokenów do zmiennych…