本文翻译自《How to manipulate CSS colors with JavaScript》
颜色模型101
在我们学习如何操作颜色之前,我们需要对CSS如何表示颜色有一个基本的认识。CSS使用两种不同的颜色模型:RGB和HSL。
RGB
RGB为 red green blue
的首字母缩写,由三个数组成,每个数字表示其在最终颜色中对应颜色的色光,范围为0-255之间,使用逗号分隔作为CSS rgb函数的参数。例如:rgb(50, 100, 0)
RGB是一种加色模型,这意味着每个数字越高,最终颜色越亮。如果每个值都相等,则颜色灰阶;如果所有值都是0,则为黑色;如果全为255,则为白色。
或者,你可以使用十六进制表示法来标记RGB颜色,每种颜色的整数从十进制转为十六进制。例如:rgb(50, 100, 0) 转为 #326400。
虽然CSS中习惯使用RGB模型(特别是十六进制),但其难以阅读,特别是难以操作。
HSL
HSL是 hue,saturation,light
的首字母缩写,也包含三个值。
hue
(色调)对应于色环上的位置,并由CSS角度值表示,常使用deg单位。saturation
(饱和度)指颜色的强度,用百分比表示。饱和度越高色彩越纯越浓,饱和度越低则色彩变灰变淡。light
(亮度)也是以百分比表示,指的是颜色有多亮,常规亮度为50%。无论色调和饱和度值如何,100%的亮度都是纯白色,0%的亮度将是纯黑色。
HSL是一个更直观的模型,颜色之间的关系更加明显,颜色的处理往往就像调整其中一个数字一样简单。
颜色模型之间的转换
RGB和HSL颜色模型都将颜色分解为各种属性。要在语法之间进行转换,我们首先需要计算这些属性。
除了色调,每个值都可以由百分比表示,甚至RGB也可以用字节大小的百分比表示。在下面的公式和函数中,这些百分比将由0到1之间的小数表示
也就是 r,g,b 的值需要除以255
在RGB中计算亮度
亮度是HSL三个值中最容易计算的。数学公式如下,其中M是RGB中的最大值,m是最小值:
以下是JavaScript公式:
const rgbToLightness = (r, g, b) => 1 / 2 * (Math.max(r, g, b) + Math.min(r, g, b));
在RGB中计算饱和度
饱和度的计算比亮度稍微复杂一些,如果亮度为0或者1,则饱和度值为0;否则将遵循以下公式,其中L表示亮度:
以下是JavaScript公式
const rgbToSaturation = (r, g, b) => {
const L = rgbToLightness(r, g, b);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
return (L === 0 || L === 1) ?
0 :
(max - min) / (1 - Math.abs(2 * L - 1));
};
在RGB中计算色调
从RGB坐标计算色调角度公式
以下是JavaScript公式:
const rgbToHue = (r, g, b) => Math.round(
Math.atan2(
Math.sqrt(3) * (g - b),
2 * r - g - b,
) * 180 / Math.PI
);
最后 180 / Math.PI 是将结果从弧度转换为度。
计算HSL
将以上函数合并成一个函数
const rgbToHsl = (r, g, b) => {
const lightness = rgbToLightness(r, g, b);
const staturation = rgbToSaturation(r, g, b);
const hue = rgbToHue(r, g, b);
return [hue, staturation, lightness];
};
/**
* rgb = (151, 109, 224);
* hsl = (261, 65%, 65%);
*/
rgbToHsl(151 / 255, 109 / 255, 224 / 255);
// result => [-99, 0.65, 0.65]
从HSL中计算RGB
在计算RGB前,我需要一些先决条件
第一个是 chroma
(浓度)
还需要一个临时的 hue
值,我们需要根据它的范围来决定颜色在色环上的哪个片段
接下来,我们需要一个作为中间值的“x”
一个用于调整RGB每个值的亮度的“m”
根据色调的素值,r,g,b 值会映射到 C
,X
和 0
最后,映射每个值调整亮度
将以上公式合并到JavaScript函数
const hslToRgb = (h, s, l) => {
const C = (1 - Math.abs(2 * l - 1)) * s;
// 处理 hue 为负数的情况
const hPrime = ((h + 360) % 360) / 60;
const X = C * (1 - Math.abs(hPrime % 2 - 1));
const m = l - C / 2;
const withLight = (r, g, b) => [r + m, g + m, b + m];
if (hPrime <= 1) {
return withLight(C, X, 0);
}
if (hPrime <= 2) {
return withLight(X, C, 0);
}
if (hPrime <= 3) {
return withLight(0, C, X);
}
if (hPrime <= 4) {
return withLight(0, X, C);
}
if (hPrime <= 5) {
return withLight(X, 0, C);
}
if (hPrime <= 6) {
return withLight(C, 0, X);
}
};
/**
* hsl = (261, 65%, 65%);
* rgb = (151, 109, 224);
*/
hslToRgb(261, .65, .65).map(color => color * 255);
// result => [151, 109, 224]
创建颜色对象
为了在操作时便于访问它们的属性,我们将处理一下 rbgToHsl
和 hslToRgb
的返回值
const rgbToObject = (red, green, blue) => {
const [hue, saturation, lightness] = rgbToHsl(red, green, blue);
return { red, green, blue, hue, saturation, lightness };
};
const hslToObject = (hue, saturation, lightness) => {
const [red, green, blue] = hslToRgb(hue, saturation, lightness);
return { hue, saturation, lightness, red, green, blue };
};
例子
这个例子中,看看每个属性在调整其他属性时如何相互影响有助于你更深入地理解两个颜色模型如何转换。 See the Pen CSS Colors: Color Conversion by Adam Giese(@AdamGiese) on CodePen.
颜色操作
我们已经掌握如何在这两种颜色模型之间转换了,下面我们看看如何操作这些颜色。
更新属性
上面提到的颜色属性都可以单独操作,然后返回一个颜色对象。例如,我们可以写一个函数来旋转色调角度。
const rotateHue = rotation = ({ hue, ...rest }) => {
const modulo = (x, n) => (x % n + n) % n;
const newHue = modulo(hue + rotation, 360);
return { ...rest, hue: newHue };
};
rotateHue
函数接受一个 rotation
参数并返回一个函数,该函数接受并返回一个对象。这样就可以很容易创建一个用于旋转色调的函数。
const rotate30 = rotateHue(30);
const getComplementary = rotateHue(180);
const getTriadic = color => {
const first = rotateHue(120);
const second = rotateHue(-120);
return [first(color), second(color)];
};
同理,你可以编写函数来“饱和/去饱和”或“变亮/变暗”颜色。
const saturate = x => ({ saturation, ...rest }) => ({
...rest,
saturation: Math.min(1, saturation + x),
});
const desaturate = x => ({ saturation, ...rest }) => ({
...rest,
saturation: Math.max(0, saturation - x),
});
const lighten = x => ({ lightness, ...rest }) => ({
...rest,
lightness: Math.min(1, lightness + x),
});
const darken = x => ({ lightness, ...rest }) => ({
...rest,
lightness: Math.max(0, lightness - x),
});
颜色判断
除了颜色操作,你还可以编写判断颜色状态的函数,返回布尔值。
const isGrayscale = ({ saturation }) => saturation === 0;
const isDark = ({ lightness }) => lightness < .5;
处理颜色数组
筛选
JavaScript函数 Array.prototype.filter()
返回一个新数组,其包含通过所提供函数实现的测试的所有元素。我们可以将上一节编写的判断函数用在这里。
const colors = [/* ... an array of color object ... */];
const isLight = ({ lightness }) => lightness > .5;
const lightColors = colors.filter(isLight);
排序
首先你需要编写一个“比较”函数用于排序一个颜色数组,该函数接受数组的两个元素进行判断。如果函数返回值小于零,则参数1排在参数2前面;如果返回值等于零,则顺序不变;如果大于零,则参数2排在参数1前面。
例如,以下是一个比较两个颜色亮度的函数:
const compareLightness = (a, b) => a.lightness - b.lightness;
为了不重复代码,我们可以编写一个高阶函数来比较任何属性:
const compareAttribute = attribute => (a, b) => a[attribute] - b[attribute];
const compareLightness = compareAttribute('lightness');
const compareSaturation = compareAttribute('saturation');
const compareHue = compareAttribute('hue');
平均属性
你可以通过使用各种JavaScript数组函数来计算特定属性的平均值。例如,通过使用 reduce
计算该属性的总和然后除以数组长度来计算属性的平均值:
const colors = [/* ... an array of color objects ... */];
const toSum = (a, b) => a + b;
const toAttribute = attribute => element => element[attribute];
const averageOfAttribute = attribute => array => array.map(toAttribute(attribute)).reduce(toSum) / array.length;
你可以通过它来规范化一个颜色数组:
/* ... continuing */
const overwriteAttribute = attribute => newValue => array => array.map((color) => ({
...color,
[attribute]: newValue,
}));
const normalizeAttribute = attribute => array => {
const averageValue = averageOfAttribute(attribute)(array);
const normalize = overwriteAttribute(attribute)(averageValue);
return normalize(array);
};
const normalizeSaturation = normalizeAttribute('saturation');
const normalizeLightness = normalizeAttribute('lightness');
const normalizeHue = normalizeAttribute('hue');
总结
颜色是网页不可或缺的一部分,将颜色细分到属性为各种高端操作提供了可能性。