Funkcje pomocnicze
Wzory na przekształcenia barw wyglądają prosto, ale w kodzie potykają się o drobiazgi: kąt 360° zamiast 0°, ujemne modulo, wartość 255,5 albo szarość, dla której odcień nie istnieje. Ta strona zbiera funkcje, z których korzystają wszystkie pozostałe przekształcenia w tym dziale.
Obcinanie do zakresu (clamp)
Przekształcenia często dają wyniki minimalnie poza zakresem 0..1, przez błędy zaokrągleń albo dlatego, że barwa jest poza gamą (zob. CIE XYZ i RGB).
const clamp = (x, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, x));
Obcinanie trzeba stosować świadomie. Przy błędzie zaokrąglenia (np. 1,0000001) jest właściwe. Przy barwie spoza gamy (np. R = -0,17) zmienia odcień i warto przynajmniej o tym wiedzieć.
Modulo dla kątów
W JavaScripcie, C i Javie operator % zachowuje znak dzielnej: -30 % 360 daje -30, a nie 330. Kąt barwy trzeba normalizować tak:
const wrapHue = (h) => ((h % 360) + 360) % 360;
wrapHue(-30) // 330
wrapHue(370) // 10
wrapHue(360) // 0
W Pythonie % działa od razu poprawnie dla dodatniego dzielnika.
Max, min i chroma
Prawie każde przekształcenie z RGB zaczyna od tych trzech wartości:
Gdy d = 0, barwa jest szarością i odcień nie jest określony. W kodzie trzeba to sprawdzić przed dzieleniem przez d. Przy liczbach zmiennoprzecinkowych lepiej porównywać z małą tolerancją (np. d < 1e-9) niż z dokładnym zerem.
Klasyczna funkcja hue2rgb
W wielu bibliotekach przekształcenie HSL na RGB wygląda tak, z pomocniczymi wartościami p i q:
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
function hslToRgb(h, s, l) { // h w zakresie 0..1
if (s === 0) return { r: l, g: l, b: l };
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return { r: hue2rgb(p, q, h + 1 / 3), g: hue2rgb(p, q, h), b: hue2rgb(p, q, h - 1 / 3) };
}
q to największa składowa wyniku, p najmniejsza. Funkcja hue2rgb rysuje "trapez" między nimi: przez 1/6 koła wartość rośnie od p do q, przez 1/3 jest równa q, przez 1/6 maleje, a przez resztę jest równa p. Każda składowa to ten sam trapez przesunięty o 1/3 koła. Dla HSL (15°, 66,7%, 47,1%) wychodzi p = 0,1569 i q = 0,7843, a wynik to (200, 80, 40).
Ten sam wynik daje krótszy wzór z wartością bezwzględną ze strony kąt barwy na RGB. Oba są poprawne. Warto je rozpoznawać, bo w cudzym kodzie spotyka się oba.
Zaokrąglanie do bajtów
const toByte = (c) => Math.round(clamp(c) * 255);
const fromByte = (n) => n / 255;
Mnożymy przez 255, nie przez 256, i zaokrąglamy, a nie obcinamy w dół. Math.floor(c * 256) też się spotyka, ale daje 256 dla c = 1 i przesuwa wszystkie wartości. Po przeliczeniu w obie strony (RGB → HSV → RGB) wynik może się różnić od wejścia o 1 na jednej składowej, jeśli HSV zapisano z małą dokładnością. To normalne.
Zapis HEX
const toHex = (r, g, b) =>
'#' + [r, g, b].map((c) => toByte(c).toString(16).padStart(2, '0')).join('');
function parseHex(s) {
let h = s.replace('#', '');
if (h.length === 3) h = [...h].map((c) => c + c).join(''); // #c52 → #cc5522
const n = parseInt(h, 16);
return { r: ((n >> 16) & 255) / 255, g: ((n >> 8) & 255) / 255, b: (n & 255) / 255 };
}
Funkcja przejścia sRGB
Przed obliczeniami fizycznymi (mieszanie, XYZ, luminancja) wartości trzeba zlinearyzować. Wzory i uzasadnienie są w artykule o korekcji gamma:
const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const toGamma = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
Wszystkie te funkcje, razem z przekształceniami do HSV, HSL, HSI, CMYK, XYZ, Lab i Luv, są w skrypcie kalkulatora tej strony: /assets/color.js.