在JavaScript中自然地混合两种颜色

45
问题: 我想在Javascript中混合两种颜色,并得到混合后的颜色。虽然stackoverflow上有很多类似的问题,但我没有找到任何实际有效的解决方法。我知道混合两种不同颜色的颜料和光线会产生非常不同的结果(http://en.wikipedia.org/wiki/Color_mixing)。
以下是我已经看到并尝试实现的问题和建议的解决方案: 1: Mixing two RGB color vectors to get resultant
因此,在RGB中混合颜色。我已经实现了它,在某些情况下它有效,在某些情况下则无效。 工作示例: 混合红色黄色 -> 橙色。太棒了!
http://jsbin.com/afomim/1/edit

不起作用的示例:混合蓝色黄色 -> 灰色。 不是很好! :) http://jsbin.com/afomim/5/edit
我知道在RGB混合中,混合蓝色黄色永远不会得到绿色,我也知道为什么。

我们在这里找不到答案,让我们继续前进。

2:像油漆一样将颜色相加(蓝色+黄色=绿色等)

让我们尝试按照这个讨论建议的CMYK值来工作。将青色与黄色混合会产生绿色:
http://jsbin.com/igaveg/1/edit
但是将蓝色与黄色混合会产生黑色.
http://jsbin.com/igaveg/2/edit ->不起作用! 3:如何使用C#“自然”混合颜色?
一个非常类似的问题。最受欢迎的答案建议将颜色转换为LAB,这个解决方案似乎很有前途。
所以我将我的颜色转换为LAB。转换算法是正确的,我已经测试过了!

http://jsbin.com/oxefox/1/edit

现在我有两种LAB颜色,但如何混合它们呢?
注意:我知道可能找不到一个算法将蓝色和黄色混合并得到完美的绿色,但我希望能生成类似于绿色的东西 :)

我只是好奇:为什么蓝色和黄色混合会变成绿色(或类似的颜色)? - Martin R
我所说的蓝色并不完全是#0000ff,黄色也不一定是#ffff00。但在现实世界中,如果你混合一些蓝色和一些黄色,大多数时候你得到的是绿色。这个应用程序:http://www.fiftythree.com/paper(由苹果颁发的奖项)具有一些很棒的颜色混合功能,他们说他们已经研发了一年以上了:)如果你查看他们的页面,你会看到他们对于蓝色和黄色应用了同样的理论。 - Tamás Pap
2
除了死灵术之外,为什么不使用HSL / HSV而不是RGB / CMYK来混合颜色呢?我的意思是,如果可见的颜色光谱只是一个刻度(以nm为单位),那么合并两种色调就是取平均值,饱和度和亮度/价值也是如此。我错了吗? - NemoStein
12个回答

49
我花了3-4天的时间来解决这个问题。这是一个非常复杂的问题。
如果你想要“自然地”混合两种颜色,可以采取以下方法:
  1. CMYK混合:这不是完美的解决方案,但如果你需要一个现在就能用的解决方案,而且不想花几个月的时间学习这个主题、做实验和编程,你可以查看这个网址:https://github.com/AndreasSoiron/Color_mixer

  2. 实现Kubelka-Munk理论。我花了很多时间阅读它并试图理解它。如果您想要一种专业的解决方案,这应该是正确的方法,但每个您想要混合的颜色都需要6个参数(如反射率、吸收等)。只有R、G、B是不够的。实现这个理论并不难,但获取每个颜色所需的参数似乎是缺失的一部分。如果您想出如何做到这一点,请告诉我 :)

  3. 实验性的:您可以像ipad应用程序Paper的开发人员所做的那样做些事情。他们手动选择了100对受欢迎的颜色,并通过肉眼测试确定了它们应该如何混合。了解更多信息here

目前我个人将实现CMYK混合,如果有时间,也许以后会尝试像Fiftythree的人一样做些什么。我们会看的 :)


我会研究CIE XYZ、CIE Lab等等,以获取“真实”的颜色。 - Mr. Developerdude

17

当我尝试将两种RGB颜色混合在一起时,我实际上遇到了相同的问题。以下两个函数对我有用:

//colorChannelA and colorChannelB are ints ranging from 0 to 255
function colorChannelMixer(colorChannelA, colorChannelB, amountToMix){
    var channelA = colorChannelA*amountToMix;
    var channelB = colorChannelB*(1-amountToMix);
    return parseInt(channelA+channelB);
}
//rgbA and rgbB are arrays, amountToMix ranges from 0.0 to 1.0
//example (red): rgbA = [255,0,0]
function colorMixer(rgbA, rgbB, amountToMix){
    var r = colorChannelMixer(rgbA[0],rgbB[0],amountToMix);
    var g = colorChannelMixer(rgbA[1],rgbB[1],amountToMix);
    var b = colorChannelMixer(rgbA[2],rgbB[2],amountToMix);
    return "rgb("+r+","+g+","+b+")";
}

要将红色([255,0,0])和蓝色([0,0,255])均匀混合,您可以调用

colorMixer([255,0,0], [0,0,255], 0.5);//returns "rgb(127,0,127)" (purple)

这可能会有所帮助,但您需要首先将每个颜色值转换为数组。如果您使用Fabric.js来处理画布元素,则变得非常容易。只需调用

var rgbA = new fabric.Color(yourColor);
var rgbB = new fabric.Color(yourSecondColor);

然后调用

colorMixer(rgbA.getSource(),rgbB.getSource(),0.5);
希望这些函数能够帮到您。

2
这个确实帮了不少。谢谢! - KCK

12

RYB颜色模型可能是进行颜色混合计算的合适选择。 根据维基百科,它主要用于艺术和设计教育,特别是绘画。

要混合两种颜色,需要将两种颜色从RGB转换为RYB,通过添加每个颜色分量混合颜色,并将结果颜色从RYB转换回RGB。

我尝试使用在线颜色混合工具,结果如下:

  • 0000FF(蓝色)与#FFFF00(黄色)混合产生#008000(暗绿色

  • FF0000(红色)与#FFFF00(黄色)混合产生#FFA000(橙色

因此,该方法产生了您所预期的结果。

不幸的是,我无法找到一个带有“即用公式”的参考资料,可以直接将RGB转换为RYB,然后再转回RGB。

文章“定量可视化中的颜色混合和组合——Gossett和Chen”在“2.实现直观的颜色混合”一节中描述了RYB颜色模型的一般思想。

根据该论文,RYB到RGB的转换是通过三线性插值完成的。

困难的部分是将RGB转换为RYB,因为需要反转三线性插值。 请参见RGB和RYB颜色空间之间的转换

更多信息请参考。

即使这个答案没有提供完整的计算公式,我希望它能给您一些处理的思路。


谢谢您详细的回答,非常有帮助。我尝试了很多方法,在HSL和LAB空间中取得了一些成功,但我仍在寻找更好的解决方案。我明天会检查RYB模型,并带着我的结果回来。 - Tamás Pap
我仍处于学习和实验阶段,但我会带着结果回来的。 :) - Tamás Pap
我现在可以将RYB转换为RGB,并且正在研究RGB到RYB的转换。在您的第一个示例中:“#0000FF(蓝色)与#FFFF00(黄色)混合得到#008000(深绿色)”,您能否解释一下如何从添加0000FF和FFFF00得到008000?因为如果我简单地相加各组分,我得到FFFFFF,如果我计算它们的平均值,我得到7A7A7A。那么,在将这两种颜色转换为RYB之后,我该如何“混合”它们呢?希望我们可以找到答案 :) - Tamás Pap
@TamasPap:我只使用了[在线混合颜色工具](http://knowpapa.com/cmt/)来完成。据我了解:RYB中的蓝色是(1.0,0.0,0.0),RYB中的黄色是(0.0,1.0,0.0)。如果在RYB中添加这两个值,则可以得到(1.0,1.0,0.0)。现在,您需要将此值转换回RGB,这应该会得到一些深绿色。- 在我看来,“在线混合颜色工具”使用的转换与Gossett和Chen的论文中的值略有不同。该工具使用JavaScript,因此可以进行检查。 - Martin R

6
当前被接受的答案链接指向 这个代码库,它有一个已过期的演示页面并使用冗长、陈旧的代码。
因此,我基于相同的代码编写了一个原生 JavaScript 颜色混合器:
console.log(mix_hexes('#3890b9', '#f6ff00')); // #8cc46f

function hex2dec(hex) {
  return hex.replace('#', '').match(/.{2}/g).map(n => parseInt(n, 16));
}

function rgb2hex(r, g, b) {
  r = Math.round(r);
  g = Math.round(g);
  b = Math.round(b);
  r = Math.min(r, 255);
  g = Math.min(g, 255);
  b = Math.min(b, 255);
  return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
}

function rgb2cmyk(r, g, b) {
  let c = 1 - (r / 255);
  let m = 1 - (g / 255);
  let y = 1 - (b / 255);
  let k = Math.min(c, m, y);
  c = (c - k) / (1 - k);
  m = (m - k) / (1 - k);
  y = (y - k) / (1 - k);
  return [c, m, y, k];
}

function cmyk2rgb(c, m, y, k) {
  let r = c * (1 - k) + k;
  let g = m * (1 - k) + k;
  let b = y * (1 - k) + k;
  r = (1 - r) * 255 + .5;
  g = (1 - g) * 255 + .5;
  b = (1 - b) * 255 + .5;
  return [r, g, b];
}


function mix_cmyks(...cmyks) {
  let c = cmyks.map(cmyk => cmyk[0]).reduce((a, b) => a + b, 0) / cmyks.length;
  let m = cmyks.map(cmyk => cmyk[1]).reduce((a, b) => a + b, 0) / cmyks.length;
  let y = cmyks.map(cmyk => cmyk[2]).reduce((a, b) => a + b, 0) / cmyks.length;
  let k = cmyks.map(cmyk => cmyk[3]).reduce((a, b) => a + b, 0) / cmyks.length;
  return [c, m, y, k];
}

function mix_hexes(...hexes) {
  let rgbs = hexes.map(hex => hex2dec(hex)); 
  let cmyks = rgbs.map(rgb => rgb2cmyk(...rgb));
  let mixture_cmyk = mix_cmyks(...cmyks);
  let mixture_rgb = cmyk2rgb(...mixture_cmyk);
  let mixture_hex = rgb2hex(...mixture_rgb);
  return mixture_hex;
}

hex2dec 可以稍微优化一下:function hex2dec(hex) { return hex .match(/[^#]{2}/g) .map((n) => parseInt(n, 16)); } - Chris Webb
这对我来说非常有用,但是我不得不在rgb2cmyk函数中添加一个空控制: c = isNaN(c) ? 0 : c; m = isNaN(m) ? 0 : m; y = isNaN(y) ? 0 : y; k = isNaN(k) ? 0 : k; - Natalia

4
这个项目对我有很大的帮助: https://github.com/ProfJski/ArtColors 我将其代码转换为Objective-C并验证其按照描述的工作。 请参阅下面引用的“原则5”部分: “原则5”

ArtColors should provide a simple function call that subtractively mixes two colors in a realistic way with a minimum of code, taking only two RGB inputs and a blending ratio, like this:

Return Color=SubtractiveMix(Color a, Color b, percentage)

ArtColors uses an algorithm which (I think) gives pretty good results with a fraction of the code of the other methods, and no need for calculating or storing reflectance data or computationally complex formulas. The goal is 80% realism with only 20% of the code.

The basic approach was inspired by considering how paints actually mix. Examine this close-up of paints mixing:

Paints Mixing

If you look carefully, you can see that in some areas, the two paints are completely blended, and the result is subtractive: yellow and blue are making a much darker green. Red and blue are making a very dark purple. Yet in other areas, where the blending is not so thorough, fine lines of yellow and blue exist side-by-side. These paints are reflecting yellow and blue light. At a distance, these colors are additively blended by the eye when the distict swirls are too small to be seen.

Consider further that mixing paints is a mixture in the Chemistry sense: no chemical change happens. The red and blue molecules are still there in the thorough blend, doing exactly what they were doing when separate: reflecting red and blue light. There's just a lot of subsurface physical effects going on as light bounces around in the medium. Incident light is absorbed and reflected by one molecule, and then by another, and eventually the result reflects out to the eye.

How does this help solve our problem?

Strictly subtractive approaches start with White, and then subtract the RGB values of Color A and Color B from White, and return what is left. This approach is often too dark. Why? Some of each pigment is still reflecting its distinctive color on a tiny scale. If we take an approach that is part additive, part subtractive, we get a more realistic result!

Moreover, if Color A = Color B, our function should return that same color. Mixing the same color with the same color should equal the same color! Using a strictly subtractive algorithm, the result is a darker version of the original hue (because the input color values are subtracted from White twice). The closer the two input colors, the less change should be seen in the blend.

The ArtColor code for subtractive mixing is:

Color ColorMixSub(Color a, Color b, float blend) {
    Color out;
    Color c,d,f;

    c=ColorInv(a);
    d=ColorInv(b);

    f.r=max(0,255-c.r-d.r);
    f.g=max(0,255-c.g-d.g);
    f.b=max(0,255-c.b-d.b);

    float cd=ColorDistance(a,b);
    cd=4.0*blend*(1.0-blend)*cd;
    out=ColorMixLin(ColorMixLin(a,b,blend),f,cd);

    out.a=255;
return out;
}

Explanation of Code: Color a and Color b are the input colors. blend specifies how much of each color to blend, from 0 to 1.0, like a linear interpolation (LERP). 0.0 = All color A, 1.0 = All color B. 0.5 = 50%-50% mix of A and B.

First we find the RGB inverses of Color a and b, and assign them to new colors c and d.

    c=ColorInv(a);
    d=ColorInv(b);

Then we subtract both c and d from pure RGB White, clamping the result to zero, and assign the result to color f.

    f.r=max(0,255-c.r-d.r);
    f.g=max(0,255-c.g-d.g);
    f.b=max(0,255-c.b-d.b);

So far, f is the purely subtractive result, which suffers from the problems mentioned above.

Next, we calculate the "Color Distance" between Color a and Color b, which is just the vector distance between them in RGB space, normalized between 0.0 (identical colors) and 1.0 (completely opposite, like white and black).

float cd=ColorDistance(a,b);

This value will help solve the problem that mixing two similar hues should not change the result very much. The color distance factor cd is then tranformed by a quadratic transfer function, which regulates how much subtractive and additive mixing we do:

cd=4.0*blend*(1.0-blend)*cd;

ArtColor Blend

The endpoints ensure that blend percentages near 0% or 100% look very close to the original input colors. The quadratic curve gives a good color gamut for the mix that comes next. The peak of the curve is determined by color distance. The output of this function determines the amount of additive vs. subtractive blending in our result. More distant colors will blend with a more subtractive dynamic (fully subtractive at y=1.0). Similar colors blend with a more additive dynamic (a flatter curve) that still has a subtractive factor. The maximum of the quadratic transfer function is the normalized color distance, so colors diametrically opposed in the color space will blend fully subtractively.

The last line does all the work:

out=ColorMixLin(ColorMixLin(a,b,blend),f,cd);`

First, we additively mix Color A and Color B in the specified blend ratio, which is accomplished by ColorMixLin(a,b,blend). This represents the additive blending effect of those fine swirls of color in the image above and subsurface interaction. Absence of this factor may be where a strictly subtractive approach yields odd results. This additive result is then blended with our purely subtractive result color f, according to the transfer function mentioned above, which is based on the color distance between Color a and Color b.

Voila! A pretty good result occurs for a wide range of input colors.


4
使用CIELAB颜色,您可以在LAB颜色空间中为每个颜色拥有三个坐标。 (顺便说一句,您已经做得非常出色了)。最好且最容易实现的方法是找到连接两个点的虚拟线段的三维中点。您只需平均每个颜色的各个组件即可轻松完成此操作:平均L,平均a和平均b。然后通过反转转换将此颜色转换回RGB空间(确保两种方式都保持相同的照明空间)。
如果您的新颜色超出了RGB颜色空间,那么您可能需要将其裁剪到最接近的可见颜色。(蓝色尤其容易出现这种情况)。

现在会尝试一下,并会带着结果回来!谢谢! - Tamás Pap
Jsbin挂了,所以我在codepen上实现了它:http://codepen.io/anon/pen/pscJr,但是没有得到预期的结果:(还有其他想法吗?:) - Tamás Pap

3

现在,您已经有了两个以LAB(或L*a*b*)格式表示的颜色,您可以将它们平均在一起。

L(result) = L(first color) + L(second color) / 2
A(result) = A(first color) + A(second color) / 2
B(result) = B(first color) + B(second color) / 2

你已经知道这个了,对吗?因为这就是你用原始的RGB颜色进行平均处理的方法。

尝试实现它!谢谢! - Tamás Pap
这是一个旧的帖子,但我在另一个 SO 的问题中找到了它。有一个问题:LAB 不是加色彩空间,而颜料(和 CMYK)是减色彩空间,对吗?我不会期望在任何加色模型中使黄色 + 蓝色产生绿色。 - Duncan C

1
这是我关于CIE-LCh色彩空间中颜色混合的一篇好文章,它产生的混合物以与您眼睛感知一致的方式保留色相、饱和度和亮度。 改进的颜色混合

1
请注意,仅链接答案是不被鼓励的,SO答案应该是寻找解决方案的终点(而不是另一个参考站点,随着时间的推移往往会变得陈旧)。请考虑在此处添加独立的摘要,将链接作为参考。 - kleopatra
谢谢分享。我会尽快仔细查看! - Tamás Pap
我认为我已经包含了一个作为概要的答案:在CIE-LCh颜色空间中混合颜色。如果链接失效,可以轻松地在Google上搜索它。 - Stu
3
你的链接已损坏。 - Martin R

1
使用this将RGB转换为CMYK,然后怎么做呢?
// CMYK colors
colorA = [2, 90, 94, 0];
colorB = [4, 0, 80, 0]; 

colorMixC = (colorA[0] + colorB[0]) / 2;
colorMixM = (colorA[1] + colorB[1]) / 2;
colorMixY = (colorA[2] + colorB[2]) / 2;
colorMixK = (colorA[3] + colorB[3]) / 2;

最后,使用 this将CMYK转换为RGB。

0
const getHexChannel = (hex, index) => {
  if (hex.length <= 5) {
    const channel = hex.substr(1 + index, 1);
    return `${channel}${channel}`;
  }
  return hex.substr(1 + index * 2, 2);
};

function hexToRGB(hex) {
  if (typeof hex === 'string' && hex[0] === '#') {
    return [0, 1, 2].map(i => parseInt(getHexChannel(hex, i), 16));
  }
  return hex;
}

function channelMixer(channelA, channelB, amount) {
  const a = channelA * (1 - amount);
  const b = channelB * amount;
  return parseInt(a + b, 10);
}

export function blendColors(colorA, colorB, amount = 0.5) {
  const rgbA = hexToRGB(colorA);
  const rgbB = hexToRGB(colorB);
  return [0, 1, 2].map(i => channelMixer(rgbA[i], rgbB[i], amount));
}

export const lighten = (color, amount) => blendColors(color, '#fff', amount);
export const darken = (color, amount) => blendColors(color, '#000', amount);

3
你好,能否添加一些你的工作内容?仅提供代码并不能帮助我们理解。 - Nicolas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接