270

7
+2  Q:

## Mixing colors(adding and subtracting colors) like in Art Class!

Hey all,

I'm building a color class and I'm looking to add operations more(color, percentage) & less(color, percentage). This requires being able to add and subtract colors and I'm having a hard time with the arithmetic. How do I use RGB or HSB(HSV) or HEX to do operations like:

Operation - `echo color('blue')->more('yellow', 100%);`

• blue + yellow = green

or

Operation - `echo color('blue')->more('yellow', 50%);`

• blue + .5 * yellow = dark-green

For subtracting I have a very vague notion of it:

Operation - `echo color('orange-yellow')->less('red', 50%);`

• orange-yellow - .5 * red = yellow

EDIT: Okay thanks for your input so far. I've tried adding CYM to each other unfortunately red (255, 0, 0) ~= (0, 1, 1) in CYM and then if you add that onto blue (0, 0, 255) ~= (1, 1, 0) it will equal (1, 2, 1) or (1, 1, 1) which is black in CYM.

I got the closest using Hue Saturation Brightness (HSB). In fact it works with every color combination except red messes up. I believe this is because red is at the beginning and end of hue (hue uses degrees [0, 360]).

Any more of your thoughts would be greatly appreciated!

EDIT 2:

Okay, so after an evening with messing around, this is a "more" method that I'm really happy with.

It uses the HSB (Hue-Saturation-Brightness) color model, Now don't ask me why I CYM didn't work. I'm a color newbie. It does seem like it would work seeing as thats how printers blend colors. I like the HSB model a lot, plus its what photoshop shows when you use the color picker.

I've added it as an answer, so let me know what you guys think! Thanks again!

Any help would be great!

Thanks, Matt

+3  A:

One solution using the RGB color space is to internally represent the colors as such - red, green, value. When a hex representation is needed, then make one from the current values and send it back.

The `more` and `less` methods then simply manipulate the current red, green, or blue values.

``````public function more(\$color, \$percentage) {
\$this->color[\$color] += \$this->color[\$color] * \$percentage;
}
``````

Convert to a hex string as

``````public function toHex() {
\$red = \$this->color['red'];
\$green = \$this->color['green'];
\$blue = \$this->color['blue'];
return sprintf("%02X%02X%02X", \$red, \$green, \$blue);
}
``````

Converting a string such as `'orange-green'` to it's RGB components is a somewhat different problem.

Unfortunately adding full yellow (#ffff00) to 100%(full) blue (#0000ff) will create white (#ffffff). I'm looking for a green.
Hahaha. That's amazing.
+2  A:

There are a lot of different models for mixing colors. I would suggest the RGBA model.

``````\$colors = array(
'opaque_green' => '00FF00FF',
'transparent_blue' => '0000FF33',
);

public function toHex(\$red, \$green, \$blue, \$alpha) {
return sprintf("%02X%02X%02X%02X", \$red, \$green, \$blue, \$alpha);
}

//Reverted to british spelling :P
public function mixColours(\$colours) {
\$red = 0; \$blue = 0; \$green = 0; \$alpha = 256;
foreach(\$colours as \$colour) {
\$alpha_mod = hexdec(substr(\$colour, 6, 2)) / 256;
\$red   += \$alpha_mod * hexdec(substr(\$colour, 0, 2));
\$green += \$alpha_mod * hexdec(substr(\$colour, 2, 2));
\$blue  += \$alpha_mod * hexdec(substr(\$colour, 4, 2));
}
\$num_colours = count(\$colours);
}
``````

So `mixColours(array(\$colors['opaque_green'], \$colors['transparent_blue']);` should give you some RGBA hex string of an aqua color.

For word conversion you could add up the total light in the color and deduce if the colour is 'dark', 'normal' or 'light'. You could also get the hue as 'green', 'orange' etc and build up a vocabulary to describe the color.

Thanks for your response. I thought this might work but `blue(#0000ff) + yellow(#ffff00) = gray(#808080)` with this model..
Yeah, I didn't actually do real math on that one.Totally changed my answer. You may as well work with a RGBA model if you're doing transparency.I could write a similar function to subtract one colour from another. I really need to get back to work though ;)
Interesting that yellow + blue = grey. I did this in photoshop and it turned out grey. That is just the light model I guess.If you want a pigment model this is the wrong solution.
Yah I think Tom (first comment to question) is on to something.. My only experience with this color theory stuff is my work today. He sounded like he has a clue about the stuff.
Look around for some cmyk color classes. You shouldn't have to reinvent the wheel. My experience with colour theory is openGL, which is of course RGBA. There were plenty of libraries for python and C for color mixing. That was a while ago at uni. This makes me want to read my lecture notes again.
+2  A:

I think the closest you can get is the CMY colour model which, fortunately, is just the inverse of RGB.

``````C = 1 - R
M = 1 - G
Y = 1 - B
``````

Now, if you assume (although CMY is cyan-magenta-yellow) that C = blue, M = red, Y = yellow, you get pretty close to your art colours. For example:

• Blue becomes (1, 0, 0)
• Yellow becomes (0, 0, 1)
• Blue + Yellow becomes (1, 0, 1)
• Converting back to RGB gives (0, 1, 0) which is green

Update:

0 to 1 are just convenient representations for no colour and full colour, equivalent to 0 to 255 of standard RGB.

For those wondering "this is obviously wrong, how is cyan = blue?", you should also realize that CMY != art colour. In fact, art colour doesn't match any primary colour model, so this is just a reasonable assumption to get the kind of colours you expect to get by mixing paints in art.

Ahh okay. I'll try this. One question though.. I'm used to RGB being [0, 255], are the RGB's you're talking about simply R% = R / 255, G% = B / 255, and B% = B / 255?
That does not make any sense. How does (1, 0, 0) + (0, 0, 1) = (1, 0, 1)? What if you have (1, 1, 1) + (0, 0, 0) ? Besides, Cyan != Blue. Blue in CMY is (1, 1, 0)
@Matt: I just represented it as a percentage to make it simple. Just multiply everything by 255 and you'll get standard RGB values. @Aircule: (1, 0, 0) + (0, 0, 1) = (1, 0, 1) obviously, just add the components. Anything beyond 1 is clipped. So (1, 1, 1) + (0, 0, 0) = (1, 1, 1) which is black in RGB - that's what happens when you mix all paints together. I said "assume" even though it's not true, because CMY != art colour.
+1  A:

It seems that converting from RYB to RGB is not so easy, you might want to check this calculator (and javascript behind it).

+1  A:

that was fun!

Examples:

``````// examples
\$c = Color(0x08090a)->more( 0xFF0000 , 100 )->remove(0x110000)->decrease(35)->add(0x002314);
\$c->red = 0xF2;
\$c->red->decrease(25);
``````

You can check the source for all the methods, short version is add,remove,increase,decrease,more,less - full chaining on elements and colors, helper function Color() added to make things easier.

``````<?php

class ColorElement {

private \$value;

public function __construct( \$value = 0 )
{
\$this->setValue( \$value );
}

{
\$value = self::fixValue(\$value);
\$this->value = self::fixValue( \$this->value + \$value );
return \$this;
}

public function remove( \$value )
{
\$value = self::fixValue(\$value);
\$this->value = self::fixValue( \$this->value - \$value );
return \$this;
}

public function increase( \$percentage=100 )
{
\$percentage = self::fixPercentage(\$percentage);
\$this->value = self::fixValue( \$this->value + (int)((\$this->value/100)*\$percentage) );
return \$this;
}

public function decrease( \$percentage=100 )
{
\$percentage = self::fixPercentage(\$percentage);
\$this->value = self::fixValue( \$this->value - (int)((\$this->value/100)*\$percentage) );
return \$this;
}

public function less( \$value , \$percentage=100 )
{
\$percentage = self::fixPercentage(\$percentage);
\$value = self::fixValue(\$value);
\$this->value = self::fixValue( \$this->value - (int)((\$value/100)*\$percentage) );
return \$this;
}

public function more( \$value , \$percentage=100 )
{
\$percentage = self::fixPercentage(\$percentage);
\$value = self::fixValue(\$value);
\$this->value = self::fixValue( \$this->value + (int)((\$value/100)*\$percentage) );
return \$this;
}

public function setValue( \$value )
{
\$this->value = self::fixValue(\$value);
return \$this;
}

public function getValue()
{
return \$this->value;
}

public function __toString()
{
return sprintf('%02X' , \$this->value);
}

public static function fixValue( \$value )
{
return \$value < 0 ? 0 : (\$value > 255 ? 255 : \$value);
}

public static function fixPercentage( \$percentage )
{
return \$percentage < 0 ? 0 : (\$percentage > 100 ? 100 : \$percentage);
}

}

class Color {

private \$_red;
private \$_green;
private \$_blue;

public function __construct( \$hex=0x000000 )
{
\$this->_red = new ColorElement();
\$this->_green = new ColorElement();
\$this->_blue = new ColorElement();
\$this->setColor(\$hex);
}

{
list(\$red, \$green, \$blue) = self::hexRGB(\$hex);
return \$this;
}

public function remove( \$hex )
{
list(\$red, \$green, \$blue) = self::hexRGB(\$hex);
\$this->_red->remove( \$red );
\$this->_green->remove( \$green );
\$this->_blue->remove( \$blue );
return \$this;
}

public function increase( \$percentage=100 )
{
\$this->_red->increase( \$percentage );
\$this->_green->increase( \$percentage );
\$this->_blue->increase( \$percentage );
return \$this;
}

public function decrease( \$percentage=100 )
{
\$this->_red->decrease( \$percentage );
\$this->_green->decrease( \$percentage );
\$this->_blue->decrease( \$percentage );
return \$this;
}

public function less( \$hex , \$percentage=100 )
{
list(\$red, \$green, \$blue) = self::hexRGB(\$hex);
\$this->_red->less( \$red , \$percentage );
\$this->_green->less( \$green , \$percentage );
\$this->_blue->less( \$blue , \$percentage );
return \$this;
}

public function more( \$hex , \$percentage=100 )
{
list(\$red, \$green, \$blue) = self::hexRGB(\$hex);
\$this->_red->more( \$red , \$percentage );
\$this->_green->more( \$green , \$percentage );
\$this->_blue->more( \$blue , \$percentage );
return \$this;
}

public function setColor( \$hex )
{
list(\$this->red, \$this->green, \$this->blue) = self::hexRGB(\$hex);
return \$this;
}

public function __set( \$color , \$value )
{
if( !in_array( \$color, array('red','green','blue') ) ) return;
\$this->{'_'.\$color}->setValue( \$value );
}

public function &__get( \$color )
{
if( !in_array( \$color, array('red','green','blue') ) ) return;
return \$this->{'_'.\$color};
}

public function __toString()
{
return '0x' . \$this->_red . \$this->_green . \$this->_blue;
}

public static function hexRGB( \$hex )
{
return array( \$hex >> 16 & 0xFF , \$hex >> 8 & 0xFF , \$hex & 0xFF );
}

}

function Color( \$hex=0x000000 ) {
return new Color( \$hex );
}
``````

Hope that helps!

edit: just caught up on the thread (after doing this) and see you want 0xFFFF00 + 0x0000FF to make green, not white - sigh this won't do that, it just works with hex rgb colors - apologies!

Oh wow. Thanks for your work! Yah I'd like it to be as intuitive as possible. I'll post the solution once I have it!
+1  A:

The main problem here is understanding the concept of additive/subtractive color :

blue + yellow = green only for pigments (paint, ink and so on) because they are subtractive color

if you're using lights (additive colors) you would get : blue + yellow = white

The solution ? If you want to describe subtractive colors (paint-like combination) you have to apply the rule "sum the complementary colors" :

``````blue(#0000FF) +sub yellow(#FFFF00) = black(#000000)
because
blue_complement(#FFFF00) +add yellow_complement(#0000FF) = #(FFFFFF) --> white which is black complement
``````

(in fact we get some dark brown because pigments are never perfect) So why do we get green in actual life ? because we do not use "blue" but cyan(#00FFFF) and :

``````cyan(#00FFFF) +sub yellow(#FFFF00) = green(#00FF00)
because
cyan_complement(#FF0000) +add yellow_complement(#0000FF) = #(FF00FF) --> magenta
``````

I have to add that this is a very simple way of describing the color interaction which is far more complicated...

A:
``````public function more(\$color, \$percent = 100) {

\$percent = trim(\$percent, '% ');
\$percent /= 100;

// Dictionary lookup that maps 'yellow' to #ffff00 and 'indianred' to #cd5c5c
\$color = \$this->_map_color(\$color);

// Creates a new Color - this will automatically compute RGB, CYM, HEX, and HSB color models
\$color = new Color(\$color);

// \$this->color is current color , \$color is the color to be added to original

// Allows hue to be both 360 degrees and 0 degrees
if(\$this->color->hsb('h') == 0) {
if(\$color->hsb('h') > 180) {
\$h = 360;
} else {
\$h = 0;
}
} else {
\$h = \$this->color->hsb('h');
}

// Computes weights of colors - original:addedColor
// 100% added means perfect blend 50:50
// 50% means 75:25
// 0% means no color added 100:0
\$c2_weight = \$percent / 2;
\$c1_weight = 1 - \$c2_weight;

// Compute the hue, saturation, brightness values using the weights
\$hsb[0] = round(\$c1_weight * \$h + \$c2_weight * \$color->hsb('h'));
\$hsb[1] = round(\$c1_weight * \$this->color->hsb('s') + \$c2_weight * \$color->hsb('s'));
\$hsb[2] = round(\$c1_weight * \$this->color->hsb('b') + \$c2_weight * \$color->hsb('b'));

\$hsb = implode(' ', \$hsb);

// Change current color into the new computed HSB value.
\$this->color = \$this->color->hsb(\$hsb);

return \$this;
}
``````

If the method needs more explanation or if you see something wrong here, let me know! I must say it works beautifully! I'll also note that `less(\$color, \$percent)` is the same except you subtract the color instead. Less still doesn't make intuitive sense to me (yellow-green - green = brown), but I'm pretty sure the computation is correct. Thanks again for all your help!