1

I am making a react native application and am trying to mix two or more colors together using their hexcodes and then getting the resultant color and its hexcode.

For example, where each color has the same proportion, mixing the colors: #4285F4 + #EA4335 + #FBBC05 = #SomeColorHexcode. Is there an algorithm to use in order to get the resulting color's hexcode?

I was looking through some old posts and there seems to be a solution here: Mixing two colors "naturally" in javascript. But the main solution in this post seems to use jquery, which is not supported by react native. Also, this question was asked about 7 years ago and I was wondering if there was a better algorithm available for mixing colors now.

Thanks!

Questions123
  • 372
  • 2
  • 6
  • 18

3 Answers3

3

Principle

  1. You take the color values
  2. You convert the CSS color values into integer
  3. You take the middle of each RGB value

let color1 = document.getElementById('color1')
let color2 = document.getElementById('color2')
let colormixed = document.getElementById('colormixed')
let button = document.getElementById('mix')

// this function takes an integer and converts it in a 2 digit hexadecimal number
function componentToHex(c) {
  var hex = c.toString(16);
  return hex.length == 1 ? "0" + hex : hex;
}

// this function takes an array of 3 RGB integer values and converts this array into a CSS color, like this: #AAAAA
function rgbToHex([r, g, b]) {
  return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}

// the regex is separating the value into groups of 2 characters, these characters being letters from 'a' to 'f' and digits, that is to say hexadecimal numbers. 
function convert(color) {
  return /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color.value).map(x=>parseInt('0x'+x)).slice(1, 4)
}

button.addEventListener("click", function() {
  // we get the two colors and convert them in integer arrays
  let [c1,c2]= [color1,color2].map(x=>convert(x))

  // this variable will be the array of the mixed color
  let cm = []

  // we take the middle of each RGB value between the two colors
  c1.forEach((c,i) => cm.push(parseInt((c1[i]+c2[i])/2)))

  // we convert the value into a CSS value
  colormixed.value = rgbToHex(cm)
});
<input type="color" id="color1" />
<input type="color" id="color2" />
<input type="button" id="mix" value="Mix" /><br />
<input type="color" id="colormixed"/>

The exec is explained here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec

Short code, several colors

document.getElementById('add').addEventListener("click", function() {
  let newColor = document.createElement("input")
  newColor.type = 'color'
  newColor.className = 'color'
  document.getElementById('colors').appendChild(newColor)
})

document.getElementById('mix').addEventListener("click", function() {
  document.getElementById('colormixed').value = '#'+Array.from(document.getElementsByClassName('color')).map(x=>/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(x.value).map(y=>parseInt('0x'+y)).slice(1, 4)).reduce((a,b)=>a.map( (num, i) => (num + b[i]))).map(x=>parseInt(x/Array.from(document.getElementsByClassName('color')).length).toString(16).padStart(2, '0')).join('')
});
<div id="colors">
  <header>
    <input type="button" id="mix" value="Mix" />
    <input type="button" id="add" value="Add" />
  </header>
  <input type="color" class="color" value="#ffffff" />
  <input type="color" class="color" value="#000000" />
</div>

<label for"colormixed">Result:</label> <input type="color" id="colormixed"/>

Explanation of the one-liner

It takes all the colors:

Array.from(document.getElementsByClassName('color'))

For each element, we take the CSS color code and we separate RGB values:

.map(x=>/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(x.value)

For each element within, we convert every hexadecimal number into an integer:

.map(y=>parseInt('0x'+y))

We take only the RGB triplet:

.slice(1, 4)

We then make the sum of all the RGB arrays:

.reduce((a,b)=>a.map( (num, i) => (num + b[i])))

We divide each RGB summed value by the total number of colors:

.map(x=>parseInt(x/Array.from(document.getElementsByClassName('color')).length)

And we convert back each value to hexadecimal:

.toString(16)

Not forgetting to add a '0' when there is only one digit:

.padStart(2, '0')

We transform the RGB array into a string:

.join('')

And we put the '#' before:

'#' + 

There you have your color mixer.

djcaesar9114
  • 1,880
  • 1
  • 21
  • 41
2

I believe this code produces the same results as the previous answer, but it's hopefully much clearer how it works.

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;
}
GirkovArpa
  • 4,427
  • 4
  • 14
  • 43
  • Thanks! Does this also work if I need to mix 3 colors? I suspect that maybe I can just mix two colors and then mix the resulting color with a third color. Will that produce the same result as mixing the three colors all together at once? – Questions123 Jun 14 '20 at 03:09
  • Yes, your guess is correct. Mixing the mixture of 2 colors with a third color will produce the result you expect. – GirkovArpa Jun 14 '20 at 03:10
  • Thanks! This implementation works just like the one above. – Questions123 Jun 14 '20 at 05:01
  • I was going some more testing with this method, and I am running into an issue where if I keep on adding the color "#FFC6E3" back onto itself, it seems to eventually end up being a dark shade of purple. Is there a reason why this is happening? Also, if I add the color "F8712E" with itself, it turns into "F9722F". – Questions123 Jun 14 '20 at 05:05
  • `F8` `71` `2E` is being rounded to `F9` `72` `2F` because the code involves converting from hex, to decimal, performing floating point operations, then converting back to hex. So precision is lost each time. – GirkovArpa Jun 14 '20 at 05:20
  • 1
    Also something else, if I want to mix three colors, all of which has the same proportion, currently my approach would be to mix two of them together and then mix the resulting color, call it 'colorX', with my last color, call it 'colorY', that I havent mixed yet (as we talked about above). But when I do that, a flaw might be that the algorithm will assume that the colorX is the same proportion as colorY (they are both of size 1/2), which is false because colorX is 2/3 while colorY is 1/3. This will result in an inaccurate mixture. Let me know if my logic is flawed :) – Questions123 Jun 14 '20 at 05:22
  • You're right! My mistake. I just updated the post to support mixing an arbitrary number of colors. – GirkovArpa Jun 14 '20 at 05:55
  • @LeonLi Replacing `Math.round` with `Math.floor` solves the `#F8712E` to `#F9722F` problem you described. Not sure how it affects other numbers. – GirkovArpa Jun 14 '20 at 06:44
0

I like @GirkovArpa's answer but there's no need to go to cmyk coloring, and it doesn't support mixing #000000. Here is a function inspired by that answer that mixes two hexes and can optionally accept a mixing ratio.

For javascript, remove type annotations

function hex2dec(hex: string) {
    const matched = hex.replace('#', '').match(/.{2}/g)
    if (!matched) throw new Error('Invalid hex string');
    return matched.map(n => parseInt(n, 16));
}

function rgb2hex(r: number, g: number, b: number) {
    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('');
}

export function mixHexes(hex1: string, hex2: string, ratio: number = 0.5) {
    if (ratio > 1 || ratio < 0) throw new Error('Invalid ratio');
    const [r1, g1, b1] = hex2dec(hex1);
    const [r2, g2, b2] = hex2dec(hex2);
    const r = Math.round(r1 * ratio + r2 * (1 - ratio));
    const g = Math.round(g1 * ratio + g2 * (1 - ratio));
    const b = Math.round(b1 * ratio + b2 * (1 - ratio));
    return rgb2hex(r, g, b);
}

Usage:

console.log(mixHexes('#3890b9', '#f6ff00', 0.7));
Dallin Romney
  • 2,276
  • 1
  • 6
  • 5