Mixing two colors "naturally" in javascript
Asked Answered
S

12

47

The problem: I want to mix two colors in javascript, and get the result color. There are a lot of similar question on SO, however I doesn't find anything that actually works correctly. I know that mixing two different colored paints(pigments) and lights will give very different results (http://en.wikipedia.org/wiki/Color_mixing).

Here are the questions and suggested solutions I've already seen, and tried to implement:

1: Mixing two RGB color vectors to get resultant
So, mixing colors in RGB. I implemented it, and in some cases it works in some cases it doesn't.

Working example: Mixing red with yellow -> orange. Great!
http://jsbin.com/afomim/1/edit

Not working example: Mixing blue with yellow -> gray. Not so great! :) http://jsbin.com/afomim/5/edit
I know that in RGB mixing blue with yellow will never make green, and I understand why.

We will not find the answer here, let's go forward.

2: Adding Colours (Colors) Together like Paint (Blue + Yellow = Green, etc)

Let's try to work with CMYK values as suggested in this discussion. Mixing cyan with yellow gives green:
http://jsbin.com/igaveg/1/edit
but mixing blue with yellow results in black.
http://jsbin.com/igaveg/2/edit -> Not working!

3: How to mix colors "naturally" with C#?
A very similar question. The most upvoted answer suggests to convert colors to LAB, and this solution seems promising.
So I converted my colors to LAB. The conversion algo is correct, I tested it!

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

Now I have the two colors in LAB, but how to mix them?

NOTE I know that probably I will not find an algo that mixes blue with yellow and will give the perfect green, but I hope I can generate something similar to green :)

Schwarz answered 11/2, 2013 at 19:4 Comment(3)
I am just curious: Why should blue and yellow mix to green (or something similar)?Bankruptcy
By blue I doesn't meen exactly #0000ff, and or by yellow #ffff00, but in real world, if you mix some blue with some yellow, most of the time you got some green. This app: fiftythree.com/paper (awarded by apple) has some great color mixing functions, and they say they were working on it more than a year :) If you check their page, you see that they apply the same theory about blue and yellow.Sorcery
Necromancy apart, why not use HSL/HSV instead of RGB/CMYK to mix colors? I mean, if the visible color spectrum is nothing more than a scale (in nm), merging two hues is just take the mean, same with saturation and light/value. Am I wrong??Melvin
S
50

I dedicated 3-4 days to this question. It's a really complex problem.

Here is what you can do if you want to mix two colors "naturally":

  1. CMYK mixing: it's not the perfect solution, but if you need a solution now, and you don't want to spend months with learning about the subject, experimenting and coding, you can check this out: https://github.com/AndreasSoiron/Color_mixer

  2. Implementing the Kubelka-Munk theory. I spent a lot of time reading about it, and trying to understand it. This should be the way to go if you want a professional solution, but it needs 6 parameters (like reflectance, absorption, etc.) for each colors you want to mix. Having R, G, B isn't enough. Implementing the theory isn't hard, but getting those parameters you need about each color seems to be the missing part. If you figure it out how to do it, let me know :)

  3. Experimental: you can do something what the developers of the ipad app: Paper have done. They manually selected 100 pairs of popular colors and eyeball-tested how they should blend. Learn more about it here.

I personally will implement the CMYK mixing for the moment, and maybe later, if I have time I'll try to make something like the guys at Fiftythree. Will see :)

Schwarz answered 6/3, 2013 at 10:36 Comment(1)
I would look into CIE XYZ, CIE Lab and so on for "true" color.Laughry
A
18

I actually ran into the same issue when trying to mix 2 RGB colors together. These 2 functions worked for me:

//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+")";
}

To mix red ( [255,0,0] ) with blue ( [0,0,255] ) evenly, you can call

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

This may help, though you have to convert each color value to an array first. If you use Fabric.js to work with canvas elements, this becomes really easy. Just call

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

then call

colorMixer(rgbA.getSource(),rgbB.getSource(),0.5);

Hope these functions help.

Audile answered 23/8, 2015 at 20:15 Comment(1)
This one really helped. Thanks!Sardonyx
B
12

The RYB Color Model could be a suitable choice for the color mixing calculations. According to Wikipedia, it is primarily used in art and design education, particularly painting.

To mix 2 colors, one converts both colors from RGB to RYB, mixes the colors by adding each color component, and converts the resulting color from RYB back to RGB.

I have tried this using the Online Color Mixing Tool, and the results are

  • 0000FF (blue) mixed with #FFFF00 (yellow) gives #008000 (dark green),

  • FF0000 (red) mixed with #FFFF00 (yellow) gives #FFA000 (orange).

So this method produces exactly the results that you expected.

Unfortunately, I was not able to find a reference with "ready-to-use" formula to convert from RGB to RYB and back to RGB.

The paper Paint Inspired Color Mixing and Compositing for Visualisation - Gossett and Chen describes the general idea of the RYB color model in the section "2 ACHIEVING INTUITIVE COLOR MIXING".

According to that paper, the conversion from RYB to RGB is done by Trilinear interpolation.

The difficult part is the conversion from RGB to RYB, because it requires the inversion of the trilinear interpolation. See Conversion between RGB and RYB color spaces for more more information.

Even if this answer does not provide complete formula for the calculation, I hope that it gives some ideas how to proceed.

Bankruptcy answered 12/2, 2013 at 20:7 Comment(4)
Thank you for your detailed answer. It is very helpful. I tried a lot of things, and had some success in HSL and LAB spaces, but I'm still looking for a better solution. I will inspect the RYB model tomorrow, and will come back with my results.Sorcery
I'm still in the learning and experimenting phase, but I will back with the results. :)Sorcery
I can now convert ryb to rgb, and I'm working on rgb to ryb. In your first example: #0000FF (blue) mixed with #FFFF00 (yellow) gives #008000 (dark green), can you explain please how 00800 resulted from adding 0000FF and FFFF00. Because if I simply add components I get FFFFFF, if I calculate the average of them I got 7A7A7A. So, after having the 2 colors in RYB how should I "mix" them? I hope we can figure it out :)Sorcery
@TamasPap: I have done this only with the Online Color Mixing Tool. As I understand it: Blue in RYB is (1.0, 0.0, 0.0), Yellow in RYB is (0.0, 1.0, 0.0). If you add this in RYB you get (1.0, 1.0, 0.0). Now you have to convert this value back to RGB, and this should give some dark green. - It seems to me that the "Online Color Mixing Tool" uses conversions that are slightly different from the values in the paper by Gossett and Chen. The tool uses JavaScript, so one could inspect that.Bankruptcy
B
6

The currently accepted answer links to this repo which has an expired demo page and uses verbose, archaic code.

So I wrote a vanilla JavaScript color mixer based on the same code:

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;
}
Bischoff answered 14/6, 2020 at 6:55 Comment(2)
hex2dec can be optimised slightly: ` function hex2dec(hex) { return hex .match(/[^#]{2}/g) .map((n) => parseInt(n, 16)); } `Squash
This has been very useful for me but I had to add a null control in the rgb2cmyk function c = isNaN(c) ? 0 : c; m = isNaN(m) ? 0 : m; y = isNaN(y) ? 0 : y; k = isNaN(k) ? 0 : k;Fluoride
L
4

This project really helped me: https://github.com/ProfJski/ArtColors

I converted its code to Objective-C and verified it works as described.

See the section on "Principle 5" quoted below:

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.

Louth answered 11/2, 2013 at 19:4 Comment(0)
W
4

With CIELAB colors you have three coordinates for each of your two colors in the LAB color space. (By the way, excellent work in getting this far). What will work best and be easiest to implement for you is to find the three-dimensional midpoint of an imaginary line segment joining the two points in LAB space. You can do this easily by just averaging each of the components of your two colors: the average L, average a and average b. Then convert this color back into RGB space by reversing your transformation (make sure your lighting space stays the same both ways).

Your new color may be outside the RGB color space. You may decide to clip to the nearest visible color in this case. (Blues are especially vulnerable to this).

Wightman answered 11/2, 2013 at 19:20 Comment(2)
Will give it a try now, and will come back with the results! Thank you!Sorcery
Jsbin is down, so I implemented it on codepen: codepen.io/anon/pen/pscJr, but not gives the expected result :( Any other ideas? :)Sorcery
G
3

Now that you have your two colors in LAB (or L*a*b*) format, you can average them together.

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

You already knew this, right? because this is what you were doing with your original RGB colors to average them.

Guddle answered 11/2, 2013 at 19:20 Comment(2)
Trying to implement it! Thank you!Sorcery
This is an old thread, but I found it from a different SO question. A question: Isn't LAB and additive color space, whereas paints (and CMYK) are subtractive color-spaces? I would not expect yellow + blue to yield green in any additive color model.Bitch
V
1

Here's a good article I wrote on color mixing in the CIE-LCh color-space, which produces a mixture that preserves hue, saturation, and luminance in a way that consistent with your eye's perception.

Improved Color Blending

Vital answered 28/2, 2014 at 22:43 Comment(4)
Note that link-only answers are discouraged, SO answers should be the end-point of a search for a solution (vs. yet another stopover of references, which tend to get stale over time). Please consider adding a stand-alone synopsis here, keeping the link as a reference.Corker
Thank you for sharing. I will have a closer look as soon as I can!Sorcery
I thought I did pretty much did include an answer which serves as a synopsis: mix the colors in CIE-LCh color space. It's easy to Google it if the link dries up.Vital
Your link is broken.Bankruptcy
M
1

What about converting RGB to CMYK using this and then:

// 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;

And finaly convert CMYK to RGB using this

Meetly answered 30/7, 2015 at 9:30 Comment(0)
C
0

Create the element you want to paint:

<DIV ID="SWATCH" STYLE="HEIGHT:50PX;WIDTH:50PX;"></DIV>

Place the rgb colours you want to combine into an array (as many as you like):

var colourArray=['#012345','#6789AB','#CDEFED','#CBA987','#654321'];

Next convert any letters to numbers and trap them in sequence:

var tempString=[],convertedColourArray=[];
for(i=0;i<colourArray.length;i++){
    for(x=1;x<=6;x++){
        var oldPigment=colourArray[i].charAt(x);
        if(oldPigment<=9)tempString.push(oldPigment);
        if(oldPigment=='A')tempString.push(10);
        if(oldPigment=='B')tempString.push(11);
        if(oldPigment=='C')tempString.push(12);
        if(oldPigment=='D')tempString.push(13);
        if(oldPigment=='E')tempString.push(14);
        if(oldPigment=='F')tempString.push(15);}
    convertedColourArray.push(tempString);
    tempString=[];}

Then add each of the index position numbers together:

var colourTotal=0,newColour='#';
for(i=0;i<=5;i++){
    for(x=0;x<convertedColourArray.length;x++)colourTotal+=parseFloat(convertedColourArray[x][i]);

Finally take the new number, convert it into the matching character and add it to the newColour variable:

    var newPigment=(Math.floor(colourTotal/colourArray.length));
    if(newPigment<=9)newColour+=newPigment;
    if(newPigment==10)newColour+='A';
    if(newPigment==11)newColour+='B';
    if(newPigment==12)newColour+='C';
    if(newPigment==13)newColour+='D';
    if(newPigment==14)newColour+='E';
    if(newPigment==15)newColour+='F';
    colourTotal=0;}

Now you can paint whatever you want with the new colour:

    document.getElementById('SWATCH').style.backgroundColor=newColour;

I hope this helps and feel free to throw eggs at it :)

Catalonia answered 15/12, 2019 at 10:18 Comment(0)
P
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);
Pub answered 7/2, 2020 at 14:34 Comment(1)
hi, could you add information on what you did there ? only giving code does not help.Unwilled
R
-1

You need to use CMY or RGB color model.

Why Blue + Yellow cannot be Gray?

Blue + Yellow = (Cyan + Magenta) + Yellow => Gray. Why not?

Look at this.

So you can use RGB (CMY) to mix colors.

Reverent answered 10/10, 2013 at 8:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.