RGB 到 HSL 转换
RGB to HSL conversion
我正在创建一个拾色器工具,对于 HSL 滑块,我需要能够将 RGB 转换为 HSL。当我搜索 SO 进行转换的方法时,我发现了这个问题 HSL to RGB color conversion.
虽然它提供了从 RGB 到 HSL 的转换功能,但我看不到任何关于计算中真正发生的事情的解释。为了更好地理解它,我阅读了维基百科上的 HSL and HSV。
后来,我使用 "HSL and HSV" 页面中的计算重写了 "HSL to RGB color conversion" 中的函数。
如果 R 是最大值,我会卡在色相的计算上。请参阅 "HSL and HSV" 页面中的计算:
这是来自另一个 wiki page 的荷兰语:
这是从 answers 到 "HSL to RGB color conversion":
case r: h = (g - b) / d + (g < b ? 6 : 0); break; // d = max-min = c
我用几个 RGB 值测试了所有三个,它们似乎产生了相似(如果不完全)的结果。我想知道的是他们在做同样的事情吗?对于某些特定的 RGB 值,我会得到不同的结果吗?我应该使用哪一个?
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];
}
继续我的评论,英文版看起来是正确的,但我不确定荷兰语版中发生了什么,因为我不理解 WIKI 页面。
这是我从英文 WIKI 页面制作的 ES6 版本,以及一些看起来与 WIKI 示例匹配的示例数据(提供或采用 Javascript 的数字准确性)。希望它在创建您自己的函数时可能有用。
// 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>
HSL 中的色调就像圆圈中的一个角。这种角度的相关值位于 0..360 区间内。但是,计算中可能会出现负值。这就是为什么这三个公式不同的原因。他们最后做同样的事情,他们只是以不同的方式处理 0..360 区间之外的值。或者,更准确的说,0..6的区间最终乘以60得到0..360
hue = (g - b) / c; // dutch wiki
不对负值执行任何操作,并假定后续代码可以处理负 H 值。
hue = ((g - b) / c) % 6; // eng wiki
使用 %
运算符来拟合 0..6 区间内的值
hue = (g - b) / c + (g < b ? 6 : 0); // SO answer
通过添加 +6 使负值变为正值来处理负值
你看,这些只是表面上的差异。第二个或第三个公式都适合您。
我一直在阅读几个 wiki 页面并检查不同的计算,并创建 RGB 立方体投影到六边形的可视化效果。我想 post 我对这种转换的理解。由于我发现这种转换(使用几何形状表示颜色模型)很有趣,所以我会尽量做到彻底。首先,让我们从 RGB 开始。
RGB
好吧,这真的不需要太多解释。在最简单的形式中,您有 3 个值,R、G 和 B,范围 [0,255]。例如,51,153,204
。我们可以用条形图表示它:
RGB 立方体
我们还可以在 3D 中表示颜色 space。我们有三个值 R
、G
、B
,对应于 X
、Y
和 Z
。所有三个值都在 [0,255]
范围内,这会产生一个多维数据集。但在创建 RGB 立方体之前,让我们先处理 2D space。 R、G、B 的两种组合给我们:RG、RB、GB。如果我们将这些绘制在平面上,我们将得到以下结果:
这些是 RGB 立方体的前三个边。如果我们将它们放在 3D space 上,它会产生半个立方体:
如果您查看上图,通过混合两种颜色,我们在 (255,255) 处得到一种新颜色,它们是黄色、品红色和青色。同样,这些的两个组合给了我们:YM、YC 和 MC。这些是立方体的缺失面。一旦我们添加它们,我们就会得到一个完整的立方体:
而51,153,204
在这个立方体中的位置:
将 RGB 立方体投影到六边形上
现在我们有了 RGB 立方体,让我们把它投影到一个六边形上。首先,我们在 x
上将立方体倾斜 45°,然后在 y
上倾斜 35.264°。第二次倾斜后,黑角在下,白角在上,都穿过z
轴。
如您所见,当我们从顶部看立方体时,我们以正确的色调顺序得到了我们想要的六边形外观。但是我们需要将其投影到一个真正的六边形上。我们所做的是绘制一个与立方体顶视图大小相同的六边形。六边形的所有角对应于立方体的角和颜色,白色立方体的顶角投影到六边形的中心。黑色被省略。如果我们将每种颜色映射到六边形上,我们就会看到正确的外观。
51,153,204
在六边形上的位置为:
计算色调
在我们进行计算之前,让我们定义一下色调是什么。
Hue is roughly the angle of the vector to a point in the projection, with red at 0°.
... hue is how far around that hexagon’s edge the point lies.
这是来自 HSL and HSV 维基页面的计算。我们将在本说明中使用它。
检查六边形和51,153,204
在其上的位置。
首先,我们缩放 R、G、B 值以填充 [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
接下来,找到R, G, B
的max
和min
值
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
然后,计算C
(色度)。色度定义为:
... chroma is roughly the distance of the point from the origin.
Chroma is the relative size of the hexagon passing through a point ...
C = OP / OP'
C = M - m
C = 0.8- 0.2 = 0.6
现在,我们有 R
、G
、B
和 C
值。如果我们检查条件,if M = B
returns 对 51,153,204
为真。所以,我们将使用 H'= (R - G) / C + 4
。
我们再检查一下六边形。 (R - G) / C
给出了 BP
段的长度。
segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
我们将把这个线段放在内六边形上。六边形的起点是 0° 处的 R(红色)。如果段长度为正,则应在 RY
上,如果为负,则应在 RM
上。在这种情况下,它是负数 -0.6666666666666666
,并且在 RM
边缘。
接下来,我们需要移动段的位置,或者更确切地说 P₁
向 B
移动(因为 M = B
)。蓝色位于 240°
。六边形有 6 个边。每边对应60°
。 240 / 60 = 4
。我们需要将 P₁
移动(增加)4
(即 240°)。移位后,P₁
将在 P
处,我们将得到 RYGCP
.
的长度
segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
RYGCP = segment + 4 = 3.3333333333333335
六边形的周长6
对应360°
。 53,151,204
到 0°
的距离是 3.3333333333333335
。如果我们将 3.3333333333333335
乘以 60
,我们将得到它的位置(以度为单位)。
H' = 3.3333333333333335
H = H' * 60 = 200°
在if M = R
的情况下,由于我们将线段的一端放在R(0°),如果线段长度为正,则不需要将线段移至R。 P₁
的位置将为正数。但是如果线段长度为负数,我们需要将它平移6,因为负值意味着angular位置大于180°,我们需要做一个完整的旋转。
因此,荷兰语 wiki 解决方案 hue = (g - b) / c;
和英语 wiki 解决方案 hue = ((g - b) / c) % 6;
都不适用于负段长度。只有 SO 答案 hue = (g - b) / c + (g < b ? 6 : 0);
适用于负值和正值。
JSFiddle: Test all three methods for rgb(255,71,99)
JSFiddle: Find a color's position in RGB Cube and hue hexagon visually
工作色调计算
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
}
This page提供颜色空间之间的转换功能,包括RGB到HSL。
function RGBToHSL(r,g,b) {
// Make r, g, and b fractions of 1
r /= 255;
g /= 255;
b /= 255;
// Find greatest and smallest channel values
let cmin = Math.min(r,g,b),
cmax = Math.max(r,g,b),
delta = cmax - cmin,
h = 0,
s = 0,
l = 0;
// Calculate hue
// No difference
if (delta == 0)
h = 0;
// Red is max
else if (cmax == r)
h = ((g - b) / delta) % 6;
// Green is max
else if (cmax == g)
h = (b - r) / delta + 2;
// Blue is max
else
h = (r - g) / delta + 4;
h = Math.round(h * 60);
// Make negative hues positive behind 360°
if (h < 0)
h += 360;
// Calculate lightness
l = (cmax + cmin) / 2;
// Calculate saturation
s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
// Multiply l and s by 100
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
return "hsl(" + h + "," + s + "%," + l + "%)";
}
我正在创建一个拾色器工具,对于 HSL 滑块,我需要能够将 RGB 转换为 HSL。当我搜索 SO 进行转换的方法时,我发现了这个问题 HSL to RGB color conversion.
虽然它提供了从 RGB 到 HSL 的转换功能,但我看不到任何关于计算中真正发生的事情的解释。为了更好地理解它,我阅读了维基百科上的 HSL and HSV。
后来,我使用 "HSL and HSV" 页面中的计算重写了 "HSL to RGB color conversion" 中的函数。
如果 R 是最大值,我会卡在色相的计算上。请参阅 "HSL and HSV" 页面中的计算:
这是来自另一个 wiki page 的荷兰语:
这是从 answers 到 "HSL to RGB color conversion":
case r: h = (g - b) / d + (g < b ? 6 : 0); break; // d = max-min = c
我用几个 RGB 值测试了所有三个,它们似乎产生了相似(如果不完全)的结果。我想知道的是他们在做同样的事情吗?对于某些特定的 RGB 值,我会得到不同的结果吗?我应该使用哪一个?
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];
}
继续我的评论,英文版看起来是正确的,但我不确定荷兰语版中发生了什么,因为我不理解 WIKI 页面。
这是我从英文 WIKI 页面制作的 ES6 版本,以及一些看起来与 WIKI 示例匹配的示例数据(提供或采用 Javascript 的数字准确性)。希望它在创建您自己的函数时可能有用。
// 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>
HSL 中的色调就像圆圈中的一个角。这种角度的相关值位于 0..360 区间内。但是,计算中可能会出现负值。这就是为什么这三个公式不同的原因。他们最后做同样的事情,他们只是以不同的方式处理 0..360 区间之外的值。或者,更准确的说,0..6的区间最终乘以60得到0..360
hue = (g - b) / c; // dutch wiki
不对负值执行任何操作,并假定后续代码可以处理负 H 值。
hue = ((g - b) / c) % 6; // eng wiki
使用 %
运算符来拟合 0..6 区间内的值
hue = (g - b) / c + (g < b ? 6 : 0); // SO answer
通过添加 +6 使负值变为正值来处理负值
你看,这些只是表面上的差异。第二个或第三个公式都适合您。
我一直在阅读几个 wiki 页面并检查不同的计算,并创建 RGB 立方体投影到六边形的可视化效果。我想 post 我对这种转换的理解。由于我发现这种转换(使用几何形状表示颜色模型)很有趣,所以我会尽量做到彻底。首先,让我们从 RGB 开始。
RGB
好吧,这真的不需要太多解释。在最简单的形式中,您有 3 个值,R、G 和 B,范围 [0,255]。例如,51,153,204
。我们可以用条形图表示它:
RGB 立方体
我们还可以在 3D 中表示颜色 space。我们有三个值 R
、G
、B
,对应于 X
、Y
和 Z
。所有三个值都在 [0,255]
范围内,这会产生一个多维数据集。但在创建 RGB 立方体之前,让我们先处理 2D space。 R、G、B 的两种组合给我们:RG、RB、GB。如果我们将这些绘制在平面上,我们将得到以下结果:
这些是 RGB 立方体的前三个边。如果我们将它们放在 3D space 上,它会产生半个立方体:
如果您查看上图,通过混合两种颜色,我们在 (255,255) 处得到一种新颜色,它们是黄色、品红色和青色。同样,这些的两个组合给了我们:YM、YC 和 MC。这些是立方体的缺失面。一旦我们添加它们,我们就会得到一个完整的立方体:
而51,153,204
在这个立方体中的位置:
将 RGB 立方体投影到六边形上
现在我们有了 RGB 立方体,让我们把它投影到一个六边形上。首先,我们在 x
上将立方体倾斜 45°,然后在 y
上倾斜 35.264°。第二次倾斜后,黑角在下,白角在上,都穿过z
轴。
如您所见,当我们从顶部看立方体时,我们以正确的色调顺序得到了我们想要的六边形外观。但是我们需要将其投影到一个真正的六边形上。我们所做的是绘制一个与立方体顶视图大小相同的六边形。六边形的所有角对应于立方体的角和颜色,白色立方体的顶角投影到六边形的中心。黑色被省略。如果我们将每种颜色映射到六边形上,我们就会看到正确的外观。
51,153,204
在六边形上的位置为:
计算色调
在我们进行计算之前,让我们定义一下色调是什么。
Hue is roughly the angle of the vector to a point in the projection, with red at 0°.
... hue is how far around that hexagon’s edge the point lies.
这是来自 HSL and HSV 维基页面的计算。我们将在本说明中使用它。
检查六边形和51,153,204
在其上的位置。
首先,我们缩放 R、G、B 值以填充 [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
接下来,找到R, G, B
max
和min
值
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
然后,计算C
(色度)。色度定义为:
... chroma is roughly the distance of the point from the origin.
Chroma is the relative size of the hexagon passing through a point ...
C = OP / OP'
C = M - m
C = 0.8- 0.2 = 0.6
现在,我们有 R
、G
、B
和 C
值。如果我们检查条件,if M = B
returns 对 51,153,204
为真。所以,我们将使用 H'= (R - G) / C + 4
。
我们再检查一下六边形。 (R - G) / C
给出了 BP
段的长度。
segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
我们将把这个线段放在内六边形上。六边形的起点是 0° 处的 R(红色)。如果段长度为正,则应在 RY
上,如果为负,则应在 RM
上。在这种情况下,它是负数 -0.6666666666666666
,并且在 RM
边缘。
接下来,我们需要移动段的位置,或者更确切地说 P₁
向 B
移动(因为 M = B
)。蓝色位于 240°
。六边形有 6 个边。每边对应60°
。 240 / 60 = 4
。我们需要将 P₁
移动(增加)4
(即 240°)。移位后,P₁
将在 P
处,我们将得到 RYGCP
.
segment = (R - G) / C = (0.2 - 0.6) / 0.6 = -0.6666666666666666
RYGCP = segment + 4 = 3.3333333333333335
六边形的周长6
对应360°
。 53,151,204
到 0°
的距离是 3.3333333333333335
。如果我们将 3.3333333333333335
乘以 60
,我们将得到它的位置(以度为单位)。
H' = 3.3333333333333335
H = H' * 60 = 200°
在if M = R
的情况下,由于我们将线段的一端放在R(0°),如果线段长度为正,则不需要将线段移至R。 P₁
的位置将为正数。但是如果线段长度为负数,我们需要将它平移6,因为负值意味着angular位置大于180°,我们需要做一个完整的旋转。
因此,荷兰语 wiki 解决方案 hue = (g - b) / c;
和英语 wiki 解决方案 hue = ((g - b) / c) % 6;
都不适用于负段长度。只有 SO 答案 hue = (g - b) / c + (g < b ? 6 : 0);
适用于负值和正值。
JSFiddle: Test all three methods for rgb(255,71,99)
JSFiddle: Find a color's position in RGB Cube and hue hexagon visually
工作色调计算
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
}
This page提供颜色空间之间的转换功能,包括RGB到HSL。
function RGBToHSL(r,g,b) {
// Make r, g, and b fractions of 1
r /= 255;
g /= 255;
b /= 255;
// Find greatest and smallest channel values
let cmin = Math.min(r,g,b),
cmax = Math.max(r,g,b),
delta = cmax - cmin,
h = 0,
s = 0,
l = 0;
// Calculate hue
// No difference
if (delta == 0)
h = 0;
// Red is max
else if (cmax == r)
h = ((g - b) / delta) % 6;
// Green is max
else if (cmax == g)
h = (b - r) / delta + 2;
// Blue is max
else
h = (r - g) / delta + 4;
h = Math.round(h * 60);
// Make negative hues positive behind 360°
if (h < 0)
h += 360;
// Calculate lightness
l = (cmax + cmin) / 2;
// Calculate saturation
s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
// Multiply l and s by 100
s = +(s * 100).toFixed(1);
l = +(l * 100).toFixed(1);
return "hsl(" + h + "," + s + "%," + l + "%)";
}