Mixing two colors "naturally" in javascript - javascript
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 :)
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":
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
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 :)
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 :)
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.
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.
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:
Github Repository
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;
}
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).
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.
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:
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;
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.
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
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
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 :)
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.
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);
Related
Calculate if a certain color is in the same family of a given color
How to calculate if a certain RGB value is in the same approximation of color for example if we have the set var color = { r : 20, g : 20, b : 80 } then the same color when lit or blacked should produce approx the same ratio maybe something like var colorBrighter = { r : 40, g : 40, g : 100 } The question if anyone can think of a function or formula to check if the colors are in the same range... a way to determain if a color is a brighter or darker variation of a given color... Thanks!
Direct computations in RGB would be very far from what the eye perceives. Don't even attempt to compute distances in this space, they mean nothing. The usual solution when looking for simple computations that match human perception (for example finding a darker, or near color, or looking for a pair of contrasted colors) is to convert your colors to another color space. HSL gives very good results for that. An excerpt from Wikipedia: The HSL model describes colors in terms of hue, saturation, and lightness (also called luminance). (Note: the definition of saturation in HSL is substantially different from HSV, and lightness is not intensity.) The model has two prominent properties: The transition from black to a hue to white is symmetric and is controlled solely by increasing lightness Decreasing saturation transitions to a shade of gray dependent on the lightness, thus keeping the overall intensity relatively constant The properties mentioned above have led to the wide use of HSL, in particular, in the CSS3 color model.3 As in HSV, hue corresponds directly to the concept of hue in the Color Basics section. The advantages of using hue are The angular relationship between tones around the color circle is easily identified Shades, tints, and tones can be generated easily without affecting the hue You'll find many snippets and libraries doing the conversion (one example). Additionally, if your goal is to set colors on DOM elements, you should consider using hsl directly as it's supported by all browsers: color: hsl(120, 100%, 25%);
a formula to check if the colors are in the same range const difference = (one, two) => Math.sqrt( (one.r - two.r) ** 2 + (one.g - two.g) ** 2 + (one.b - two.b) ** 2 ); Usable as: console.log(difference(color, colorBrighter)); This basically calculates the distance in a 3dimensional "color room", which is (afaik) a common way to determine the difference between two colors. a way to determain if a color is a brighter or darker variation of a given color If you imagine the colors as vectors, one vector has to be the multiple of another: color1 * k = color2 So we just have to check that equation: const differOnlyInBrightness = (one, two) => ( k => one.g * k === two.g && one.b * k === two.b )(two.r / one.r);
How do you mix colors in CSS? (and WHAT is yellow?) [duplicate]
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 :)
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": 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 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 :) 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 :)
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.
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.
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: Github Repository 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; }
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).
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.
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: 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; 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.
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
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
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 :)
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.
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);
Subtract colors
I will try to explain my question with an example: Let's say we have a background colored in a shade of red (easy to find out - color picker) There is a semi-transparent element in front. It is colored blue but appears to be purple. Again the resulting purple shade is easy to find but is there a wayt to find/compute the semi transparent blue shade? Given: rgb(100,0,0) + x = rgb(100,0,100) Find: x = rgba(0,0,100,0.5) My actual case: Background:rgb(147,150,72), Element rgb(44,100,62) This is partially solved in this question for the sole case of white backgrounds, I am actually looking for a more general solution. I have tried to adapt the javascript code but I don't really understand how it is done or if a general solution is even possible. I might be able to code it myself is someone understand how it works and tells me. (if solved I will post solution here of course) Links: Subtraction of colours - Need to get the transparent value of a colour Convert RGB to RGBA over white
A general exact solution is not existent. As stated in the answer you linked to C = X * a + Y * (a-1) is the formula to calculate the color channel C out of the colors X and Y, while a is the alpha value of X. So, when you try to find out the color + alpha channel, you have to solve this equation 3 times (once for each color). Each of those equation does have 2 unknowns. So without requiring additional conditions (e.g. the one mentioned in your second link) to apply, there is an infinite amount of different solutions (or in the specific case of one byte per color channel, up to 256). While determining one exact solution is impossible, you can, of course, determine one or more possible solutions, with the following way: X = C/a - Y + Y/a // Formula from above solved for X So the possible solutions are r3 = r2/a - r1 + r1/a g3 = g2/a - g1 + g1/a b3 = b2/a - b1 + b1/a with rgb(r1, g1, b1) being the background color, rgb(r2, g2, b2) being the mixed color and rgba(r3, g3, b3, a) being the color that was painted on the background. If you now select a so, that none of r3, g3 and b3 are below 0 or above 255, you get a valid solution.
Why won't my color mixing function work as expected?
I have this function that takes two arguments. They are arrays in the [r, g, b] format. function mix(color1, color2) { var r = Math.round((color1[0] + color2[0])/2); var g = Math.round((color1[1] + color2[1])/2); var b = Math.round((color1[2] + color2[2])/2); return [r, g, b]; } If I try to mix red (255, 0, 0) and blue (0, 0, 255), tt gives me [128,0,128], which is purple. But if I try mixing blue (0, 0, 255) and yellow (255, 255, 0) console.log(mix([255,0,0], [0,0,255])); console.log(mix([255,255,0], [0,0,255])); it gives me gray [128, 128, 128], instead of green. Why is this happening?
You need to convert your colours into either HSL or HSV color models (plenty of samples here or on Google, e.g. like this page). You then do the averaging with the resulting numbers, and then convert back to RGB. This will ensure that the saturation and brightness remain consistent, and give you the correct hue that's half way between the two original colours. Using the library linked above: function mix(color1, color2, amount) { if (typeof amount === "undefined") { amount = 0.5; } var hsl1 = rgbToHsl.apply(this, color1); var hsl2 = rgbToHsl.apply(this, color2); var h = amount * hsl1[0] + (1 - amount) * hsl2[0]; var s = amount * hsl1[1] + (1 - amount) * hsl2[1]; var l = amount * hsl1[2] + (1 - amount) * hsl2[2]; return hslToRgb(h, s, l); } NB: this is untested code (the algorithm is correct, I just didn't actually throw it at an interpreter yet), and the results may need to be rounded. However for bonus points it also allows you to specify a mix proportion other than 50/50.
Because you are calculating resulting color as the arythmetic average of two base colors. Colors work in a different way depending on what you mix. If you would mix paints of many different colors, the result would be dark, nearly black. But if you would mix lights of different colors, the result would be nearly white. First define the process you want to simulate - I bet it is the one that is similar to mixing paints, correct? Two basic methods of mixing colors are: subtractive mixing, which looks like that: additive mixing, which looks like that: What you need is subtractive color mixing, whose output should be in RGB notation.
2 main issues here: additive versus subtractive mixing - that's easy to read about But I think, the main issue was that blue and yellow paint do not make green. The type of blue you would be thinking of is in fact not blue: more of a cyan or at least a blue with some green shift. A pure "blue" is neither warm (towards red) or cool (towards yellow/green) and in paint would be something like ultramarine red shade - a pure blue but you would consider quite a dark (almost navy) blue. When mixed in paint with yellow you would not get a bright green, more of a dull grey. In paint to get paint colors which are bright from mixes it is essential to use colors which are not across from each other on the color wheel , but in fact as close as possible ideally, so a bright green can in fact be only obtained in paint from mixing a greeny blue with a greeny yellow - ie. they are neighbours. The green will still not be as bright as a pure green. That cannot be done in subtractive (paint mixing) - to combine colors and get new pure colors can only be done with colored lights - ie. additive mixing , like you see in the theatre. However be aware that additive mixing is somewhat counter-intuitive - instead of your subtractive paint blue/yellow mix giving a neutral gray (black in theory) in fact with lights you would get white .... (still not green!). It is a very complex field because there is also some psychology and physiology involved - our brains cheat or mistake color perception on a regular basis. eg. you mix black with yellow you get a dark green - that is related to the sensitivity or the red / green cones as the brightness decreases - it is in fact dark yellow but we see green.
To get what you expect you need to work in CMY space instead of RGB space. Here's a simple RGB to CMY converter: function rgb2cmy (r, g, b) { var c = 255-r; var m = 255-g; var y = 255-b; return [c,m,y]; } And simply reverse the process to convert back to RGB: function cmy2rgb (c, m, y) { var r = 255-c; var g = 255-m; var b = 255-y; return [r,g,b]; } (if you've been paying attention you'll realize that both functions do the same thing)
I think your formula works fine, it is what, say, photoshop would do if you put blue next to yellow and apply a blur...you'd get gray right in the middle. Blue and yellow don't mix to make green. Red, green, and blue are primaries on computer monitors, camera film and the human eye. Yellow, meanwhile, is a mixture of red and green light, that is, the red and green sensitive cones in your eyes are stimulated when you see yellow. It may not seem intuitive, but it's true. Look at a yellow area of your screen under a magnifying glass and you'll see it is composed of red and green dots. This is in contrast to what you may have learned in elementary school, and sometimes mixing paints or inks may end up with blue and yellow making a (typically muddy) green. So, I don't suggest you change your algorithm, that is a standard way of blending two colors together. Just change your expectations. :)
Random Color Deviation with Normal Distribution
I'd like to generate a random color, based on an original color and a normal distribution, so I can create a random texture telling the script "Okay, this is the general color you should be using, but if you differ greatly every now and then, that's fine". I'm writing in JavaScript, using the Box-Muller transform. But if I use the normal distribution independently on every R, G and B value, I get undesired spikes. If my base color is a light grey, for example, I get pixels that are very red, very green or very blue. This is my script so far ("0, 255" being min and max): function randomHex(hex, deviation){ R = HexToR(hex); G = HexToG(hex); B = HexToB(hex); R = randomNormal(R, deviation, 0, 255); G = randomNormal(G, deviation, 0, 255); B = randomNormal(B, deviation, 0, 255); return RGBtoHex(R,G,B); } I think what I'm looking for is to somehow define a point in the 3D space of RGB color and then select a (uniformly distributed) random point on a sphere who's center is the defined point and who's radius is randomly generated with normal distribution. That's the theory, anyway. I just don't have a clue how to make that work in JavaScript. I'd be thankful for any pointers!
I'd try using HSV (or perhaps HSL) instead of RGB. You'll need a function to convert back to RGB (see here for an example). In this case, H is the hue, and your value is some position on a circle of colors. S and V control saturation and value/brightness respectively. I think this will give you more control to get the distributions you're after. More info on wikipedia here too.