colors - color - rgb to hsl
Conversión de RGB a HSL (3)
Estoy creando una herramienta Selector de color y para el control deslizante HSL, necesito poder convertir RGB a HSL. Cuando busqué en SO la forma de hacer la conversión, encontré esta pregunta de conversión de color HSL a RGB .
Si bien proporciona una función para realizar la conversión de RGB a HSL, no veo una explicación de lo que realmente está sucediendo en el cálculo. Para entenderlo mejor, he leído el HSL y el HSV en Wikipedia.
Más tarde, reescribí la función de "Conversión de color HSL a RGB" usando los cálculos de la página "HSL y HSV".
Estoy atascado en el cálculo del tono si la R es el valor máximo. Vea el cálculo de la página "HSL y HSV":
Esto es de otra página wiki que está en holandés:
y esto es de las answers a "conversión de color HSL a RGB":
case r: h = (g - b) / d + (g < b ? 6 : 0); break; // d = max-min = c
He probado los tres con unos pocos valores RGB y parecen producir resultados similares (si no exactos). Lo que me pregunto es si están haciendo lo mismo? ¿Obtendré diferentes resultados para algunos valores RGB específicos? ¿Cuál debería estar usando?
hue = (g - b) / c; // dutch wiki
hue = ((g - b) / c) % 6; // eng wiki
hue = (g - b) / c + (g < b ? 6 : 0); // SO answer
function rgb2hsl(r, g, b) {
// see https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
// convert r,g,b [0,255] range to [0,1]
r = r / 255,
g = g / 255,
b = b / 255;
// get the min and max of r,g,b
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
// lightness is the average of the largest and smallest color components
var lum = (max + min) / 2;
var hue;
var sat;
if (max == min) { // no saturation
hue = 0;
sat = 0;
} else {
var c = max - min; // chroma
// saturation is simply the chroma scaled to fill
// the interval [0, 1] for every combination of hue and lightness
sat = c / (1 - Math.abs(2 * lum - 1));
switch(max) {
case r:
// hue = (g - b) / c;
// hue = ((g - b) / c) % 6;
// hue = (g - b) / c + (g < b ? 6 : 0);
break;
case g:
hue = (b - r) / c + 2;
break;
case b:
hue = (r - g) / c + 4;
break;
}
}
hue = Math.round(hue * 60); // °
sat = Math.round(sat * 100); // %
lum = Math.round(lum * 100); // %
return [hue, sat, lum];
}
Continuando con mi comentario, la versión en inglés se ve correcta, pero no estoy seguro de lo que está sucediendo en la versión holandesa, ya que no entiendo la página de WIKI.
Aquí hay una versión de ES6 que hice de la página WIKI en inglés, junto con algunos datos de muestra que parecen coincidir con los ejemplos de WIKI (dar o tomar la precisión numérica de Javascript). Esperemos que pueda ser de utilidad al crear su propia función.
// see: https://en.wikipedia.org/wiki/RGB_color_model
// see: https://en.wikipedia.org/wiki/HSL_and_HSV
// expects R, G, B, Cmax and chroma to be in number interval [0, 1]
// returns undefined if chroma is 0, or a number interval [0, 360] degrees
function hue(R, G, B, Cmax, chroma) {
let H;
if (chroma === 0) {
return H;
}
if (Cmax === R) {
H = ((G - B) / chroma) % 6;
} else if (Cmax === G) {
H = ((B - R) / chroma) + 2;
} else if (Cmax === B) {
H = ((R - G) / chroma) + 4;
}
H *= 60;
return H < 0 ? H + 360 : H;
}
// returns the average of the supplied number arguments
function average(...theArgs) {
return theArgs.length ? theArgs.reduce((p, c) => p + c, 0) / theArgs.length : 0;
}
// expects R, G, B, Cmin, Cmax and chroma to be in number interval [0, 1]
// type is by default ''bi-hexcone'' equation
// set ''luma601'' or ''luma709'' for alternatives
// see: https://en.wikipedia.org/wiki/Luma_(video)
// returns a number interval [0, 1]
function lightness(R, G, B, Cmin, Cmax, type = ''bi-hexcone'') {
if (type === ''luma601'') {
return (0.299 * R) + (0.587 * G) + (0.114 * B);
}
if (type === ''luma709'') {
return (0.2126 * R) + (0.7152 * G) + (0.0772 * B);
}
return average(Cmin, Cmax);
}
// expects L and chroma to be in number interval [0, 1]
// returns a number interval [0, 1]
function saturation(L, chroma) {
return chroma === 0 ? 0 : chroma / (1 - Math.abs(2 * L - 1));
}
// returns the value to a fixed number of digits
function toFixed(value, digits) {
return Number.isFinite(value) && Number.isFinite(digits) ? value.toFixed(digits) : value;
}
// expects R, G, and B to be in number interval [0, 1]
// returns a Map of H, S and L in the appropriate interval and digits
function RGB2HSL(R, G, B, fixed = true) {
const Cmin = Math.min(R, G, B);
const Cmax = Math.max(R, G, B);
const chroma = Cmax - Cmin;
// default ''bi-hexcone'' equation
const L = lightness(R, G, B, Cmin, Cmax);
// H in degrees interval [0, 360]
// L and S in interval [0, 1]
return new Map([
[''H'', toFixed(hue(R, G, B, Cmax, chroma), fixed && 1)],
[''S'', toFixed(saturation(L, chroma), fixed && 3)],
[''L'', toFixed(L, fixed && 3)]
]);
}
// expects value to be number in interval [0, 255]
// returns normalised value as a number interval [0, 1]
function colourRange(value) {
return value / 255;
};
// expects R, G, and B to be in number interval [0, 255]
function RGBdec2HSL(R, G, B) {
return RGB2HSL(colourRange(R), colourRange(G), colourRange(B));
}
// converts a hexidecimal string into a decimal number
function hex2dec(value) {
return parseInt(value, 16);
}
// slices a string into an array of paired characters
function pairSlicer(value) {
return value.match(/../g);
}
// prepend ''0''s to the start of a string and make specific length
function prePad(value, count) {
return (''0''.repeat(count) + value).slice(-count);
}
// format hex pair string from value
function hexPair(value) {
return hex2dec(prePad(value, 2));
}
// expects R, G, and B to be hex string in interval [''00'', ''FF'']
// without a leading ''#'' character
function RGBhex2HSL(R, G, B) {
return RGBdec2HSL(hexPair(R), hexPair(G), hexPair(B));
}
// expects RGB to be a hex string in interval [''000000'', ''FFFFFF'']
// with or without a leading ''#'' character
function RGBstr2HSL(RGB) {
const hex = prePad(RGB.charAt(0) === ''#'' ? RGB.slice(1) : RGB, 6);
return RGBhex2HSL(...pairSlicer(hex).slice(0, 3));
}
// expects value to be a Map object
function logIt(value) {
console.log(value);
document.getElementById(''out'').textContent += JSON.stringify([...value]) + ''/n'';
};
logIt(RGBstr2HSL(''000000''));
logIt(RGBstr2HSL(''#808080''));
logIt(RGB2HSL(0, 0, 0));
logIt(RGB2HSL(1, 1, 1));
logIt(RGBdec2HSL(0, 0, 0));
logIt(RGBdec2HSL(255, 255, 254));
logIt(RGBhex2HSL(''BF'', ''BF'', ''00''));
logIt(RGBstr2HSL(''008000''));
logIt(RGBstr2HSL(''80FFFF''));
logIt(RGBstr2HSL(''8080FF''));
logIt(RGBstr2HSL(''BF40BF''));
logIt(RGBstr2HSL(''A0A424''));
logIt(RGBstr2HSL(''411BEA''));
logIt(RGBstr2HSL(''1EAC41''));
logIt(RGBstr2HSL(''F0C80E''));
logIt(RGBstr2HSL(''B430E5''));
logIt(RGBstr2HSL(''ED7651''));
logIt(RGBstr2HSL(''FEF888''));
logIt(RGBstr2HSL(''19CB97''));
logIt(RGBstr2HSL(''362698''));
logIt(RGBstr2HSL(''7E7EB8''));
<pre id="out"></pre>
El tono en HSL es como un ángulo en un círculo. Los valores relevantes para tal ángulo residen en el intervalo 0..360. Sin embargo, los valores negativos pueden salir del cálculo. Y es por eso que esas tres fórmulas son diferentes. Hacen lo mismo al final, simplemente manejan de manera diferente los valores fuera del intervalo 0..360. O, para ser precisos, el intervalo de 0..6 que luego se multiplica por 60 a 0..360
hue = (g - b) / c; // dutch wiki
hue = (g - b) / c; // dutch wiki
no hace nada con valores negativos y presume que el código posterior puede manejar valores H negativos.
hue = ((g - b) / c) % 6; // eng wiki
hue = ((g - b) / c) % 6; // eng wiki
usa el operador %
para ajustar los valores dentro del intervalo 0..6
hue = (g - b) / c + (g < b ? 6 : 0); // SO answer
hue = (g - b) / c + (g < b ? 6 : 0); // SO answer
se ocupa de los valores negativos al agregar +6 para que sean positivos
Ves que estas son solo diferencias cosméticas. La segunda o la tercera fórmula funcionarán bien para ti.
He estado leyendo varias páginas wiki y comprobando diferentes cálculos, y creando visualizaciones de la proyección del cubo RGB en un hexágono. Y me gustaría publicar mi comprensión de esta conversión. Como me parece interesante esta conversión (representaciones de modelos de color que usan formas geométricas), trataré de ser lo más completa posible. Primero, comencemos con RGB.
RGB
Bueno, esto realmente no necesita mucha explicación. En su forma más simple, tiene 3 valores, R, G y B en el rango de [0,255]. Por ejemplo, 51,153,204
. Podemos representarlo utilizando un gráfico de barras:
Cubo RGB
También podemos representar un color en un espacio 3D. Tenemos tres valores R
, G
, B
que corresponden a X
, Y
y Z
Los tres valores están en el rango [0,255]
, lo que da como resultado un cubo. Pero antes de crear el cubo RGB, trabajemos primero en el espacio 2D. Dos combinaciones de R, G, B nos dan: RG, RB, GB. Si tuviéramos que graficarlos en un plano, obtendríamos lo siguiente:
Estos son los tres primeros lados del cubo RGB. Si los colocamos en un espacio 3D, resulta en un medio cubo:
Si verifica el gráfico anterior, al mezclar dos colores, obtenemos un nuevo color en (255,255), y estos son Amarillo, Magenta y Cian. Nuevamente, dos combinaciones de estos nos dan: YM, YC y MC. Estos son los lados que faltan del cubo. Una vez que los agregamos, obtenemos un cubo completo:
Y la posición de 51,153,204
en este cubo:
Proyección de RGB Cube en un hexágono
Ahora que tenemos el Cubo RGB, proyectémoslo en un hexágono. Primero, inclinamos el cubo 45 ° en la x
, y luego 35.264 ° en la y
. Después de la segunda inclinación, la esquina negra está en la parte inferior y la esquina blanca está en la parte superior, y ambas pasan a través del eje z
.
Como puede ver, obtenemos el aspecto hexagonal que queremos con el orden de matiz correcto cuando miramos el cubo desde la parte superior. Pero necesitamos proyectar esto en un hexágono real. Lo que hacemos es dibujar un hexágono que sea del mismo tamaño con la vista superior del cubo. Todas las esquinas del hexágono corresponden a las esquinas del cubo y los colores, y la esquina superior del cubo que es blanca, se proyecta sobre el centro del hexágono. Se omite el negro. Y si mapeamos cada color en el hexágono, tenemos el aspecto correcto.
Y la posición de 51,153,204
en el hexágono sería:
Calculando el tono
Antes de hacer el cálculo, definamos qué matiz es.
El tono es aproximadamente el ángulo del vector con respecto a un punto de la proyección, con rojo a 0 °.
... el tono es la distancia que rodea el borde de ese hexágono.
Este es el cálculo de la página wiki de HSL y HSL . Lo usaremos en esta explicación.
Examine el hexágono y la posición de 51,153,204
en él.
Primero, escalamos los valores R, G, B para completar el intervalo [0,1].
R = R / 255 R = 51 / 255 = 0.2
G = G / 255 G = 153 / 255 = 0.6
B = B / 255 B = 204 / 255 = 0.8
A continuación, encuentre los valores max
y min
de R, G, B
M = max(R, G, B) M = max(0.2, 0.6, 0.8) = 0.8
m = min(R, G, B) m = min(0.2, 0.6, 0.8) = 0.2
Luego, calcule C
(croma). Chroma se define como:
... el croma es aproximadamente la distancia del punto desde el origen.
Chroma es el tamaño relativo del hexágono que pasa por un punto ...
C = OP / OP''
C = M - m
C = 0.8- 0.2 = 0.6
Ahora, tenemos los valores R
, G
, B
y C
Si comprobamos las condiciones, if M = B
devuelve verdadero para 51,153,204
. Entonces, usaremos H''= (R - G) / C + 4
.
Revisemos el hexágono de nuevo. (R - G) / C
nos da la longitud del segmento BP
.
segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
Pondremos este segmento en el hexágono interior. El punto de inicio del hexágono es R (rojo) a 0 °. Si la longitud del segmento es positiva, debe estar en RY
, si es negativa, debe estar en RM
. En este caso, es negativo -0.6666666666666666
, y está en el borde de RM
.
A continuación, necesitamos cambiar la posición del segmento, o más bien P₁
remolca la B
(porque M = B
). El azul está a 240°
. El hexágono tiene 6 lados. Cada lado corresponde a 60°
. 240 / 60 = 4
. Necesitamos cambiar (incrementar) la P₁
en 4
(que es 240 °). Después del turno, P₁
estará en P
y obtendremos la longitud de RYGCP
.
segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
RYGCP = segment + 4 = 3.3333333333333335
La circunferencia del hexágono es 6
que corresponde a 360°
. La distancia de 53,151,204
a 0°
es 3.3333333333333335
. Si multiplicamos 3.3333333333333335
por 60
, obtendremos su posición en grados.
H'' = 3.3333333333333335
H = H'' * 60 = 200°
En el caso de if M = R
, ya que colocamos un extremo del segmento en R (0 °), no necesitamos cambiar el segmento a R si la longitud del segmento es positiva. La posición de P₁
será positiva. Pero si la longitud del segmento es negativa, debemos desplazarla en 6, porque un valor negativo significa que la posición angular es mayor que 180 ° y necesitamos hacer una rotación completa.
Entonces, ni la solución wiki holandesa hue = (g - b) / c;
ni la solución de wiki wiki hue = ((g - b) / c) % 6;
trabajará para la longitud del segmento negativo. Solo la respuesta de SO hue = (g - b) / c + (g < b ? 6 : 0);
Funciona tanto para valores negativos como positivos.
JSFiddle: prueba los tres métodos para rgb (255,71,99)
JSFiddle: Encuentra la posición de un color en el cubo RGB y el hexágono de color visualmente
Cálculo de tono de trabajo:
console.log(rgb2hue(51,153,204));
console.log(rgb2hue(255,71,99));
console.log(rgb2hue(255,0,0));
console.log(rgb2hue(255,128,0));
console.log(rgb2hue(124,252,0));
function rgb2hue(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var c = max - min;
var hue;
if (c == 0) {
hue = 0;
} else {
switch(max) {
case r:
var segment = (g - b) / c;
var shift = 0 / 60; // R° / (360° / hex sides)
if (segment < 0) { // hue > 180, full rotation
shift = 360 / 60; // R° / (360° / hex sides)
}
hue = segment + shift;
break;
case g:
var segment = (b - r) / c;
var shift = 120 / 60; // G° / (360° / hex sides)
hue = segment + shift;
break;
case b:
var segment = (r - g) / c;
var shift = 240 / 60; // B° / (360° / hex sides)
hue = segment + shift;
break;
}
}
return hue * 60; // hue is in [0,6], scale it up
}