RGB na kąt barwy (hue)
Kąt barwy można wyznaczyć z RGB na dwa sposoby: po sześciokącie, jak w HSV i HSL, albo geometrycznie, jak w HSI. Dają prawie to samo, ale nie dokładnie to samo. To odwrotność przekształcenia kąt barwy na RGB.
Wzór sześciokątny
Używany w HSV i HSL. Najpierw ustala się, która składowa jest największa: to wyznacza sektor koła o szerokości 120°. Potem liniowo, wewnątrz sektora:
Dla RGB (200, 80, 40): max = R, więc H = 60° · (0,3137 - 0,1569) / 0,6275 = 60° · 0,25 = 15°.
Wzór geometryczny
Używany w HSI. Barwę rzutuje się na płaszczyznę prostopadłą do osi szarości i mierzy prawdziwy kąt od kierunku czerwieni:
Dla naszego koloru: H = atan2(1,732 · 0,1569; 1,5686 - 0,3137 - 0,1569) = atan2(0,2716; 1,0980) = 13,9°.
Wersja z atan2 jest wygodniejsza w kodzie: nie wymaga osobnego warunku dla B > G i nie dzieli przez zero, gdy mianownik jest mały.
Jak bardzo się różnią
Oba wzory zgadzają się dokładnie co 60°, na wierzchołkach sześciokąta: dla czerwieni, żółci, zieleni, cyjanu, błękitu i magenty. Między nimi wzór sześciokątny jest liniowym przybliżeniem prawdziwego kąta. Największa różnica wynosi około 1,1° (na przykład w okolicy 253° HSV). Nie zależy od nasycenia ani jasności, tylko od odcienia, bo oba wzory zależą wyłącznie od proporcji różnic między składowymi.
Różnica 1° jest niewidoczna, ale ma znaczenie przy przenoszeniu wartości: 15° z programu graficznego to 13,9° w bibliotece analizy obrazu używającej HSI.
Szarości
Gdy R = G = B, barwa nie ma odcienia. Wzór sześciokątny dzieliłby przez zero, a atan2(0, 0) zwraca 0, co tylko udaje wynik. Typowe rozwiązania:
- przyjąć H = 0° (tak robi większość programów),
- zwrócić osobną wartość "brak odcienia" (NaN, null), jak w CSS Color 4, gdzie odcień szarości jest "none",
- przy interpolacji kolorów zachować odcień sąsiedniej barwy, żeby gradient do szarości nie przechodził przez czerwień.
Kod
// wzór sześciokątny (HSV, HSL), r, g, b w zakresie 0..1
function hexHue(r, g, b) {
const max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
if (d < 1e-9) return NaN; // szarość
let h;
if (max === r) h = (g - b) / d;
else if (max === g) h = (b - r) / d + 2;
else h = (r - g) / d + 4;
h *= 60;
return h < 0 ? h + 360 : h;
}
// wzór geometryczny (HSI)
function geoHue(r, g, b) {
const h = Math.atan2(Math.sqrt(3) * (g - b), 2 * r - g - b) * 180 / Math.PI;
return h < 0 ? h + 360 : h;
}