Design and documentation journal for my interactive fiction (text games); also reviews and other miscellaneous stuff.

Friday, December 10, 2010

Color Manipulation: Multiply, Saturate, Gray, Brighten, Lighten, Convert, Blend

Free time has been scarce the last couple weeks, due to end of the year holidays and real life complications.  What little I have had has gone into gathering some tools for color manipulation.  (I also did a little more work on the map, room descriptions, and other things that don't require active design integration.) 

Preserved for posterity (and because I have the nasty habit of accidentally saving over code that's difficult to replace): Simple Equations for Color Manipulation, and Inform 7 Code for the Same.

The first big issue is that Inform uses decimal numbers to represent colors.  This is essentially worthless for our purposes.  The only realistic way to work with color is to use the hex or RGB value.  I could also, I suppose, have used HSV, but that's even more complicated to derive than hex/RGB.

Inform doesn't handle hex as a value, and it's not something I'm willing to fool with, so RGB it is. 

An RGB is a kind of value.   255-255-255 specifies an RGB with parts rgb-red, rgb-green, and rgb-blue.
On my first coding draft, I used R, G, and B as parts of a RGB.  Not a good idea - it makes them hard to search for in the code in case you need to replace something.  All my non-temporary variables are now getting real names. 

The downside of making RGB one value instead of three is that you have to be pretty careful with calculations.  The parts are interconnected internally, so careless arithmetic can have very unexpected results.  You also can't unilaterally change parts of a RGB value (ie "Now the rgb-blue part of sky blue is 44.").  This can be a little unintuitive.  Also, you need to watch input: this setup does allow for R values of greater than 255.  And you have to use glulx, because of the size the Inform thinks the values are - it doesn't see 255-255-255, but some conglomerate number.

RGB values are essentially hex values in disguise.  The current version of hex color values uses 6 digits of base 16 to represent a color.  White, the highest number, is FFFFFF, with the first two digits being red, the second two digits representing green, and the last green.  RGB is just the decimal-converted values of hex, with dashes between to mark the color separations. 

So converting from RGB to decimal requires, essentially, converting to pseudo-base 16, and then to base 10.

Section - RGB to Decimal

To decide what number is the decimal value of (colorspace - an RGB):   
    let R1 be rgb-red part of colorspace; [pull out ind. parts to prevent Issues]
    let B1 be rgb-blue part of colorspace; [in hex would be R1R2G1G2B1B2]
    let G1 be rgb-green part of colorspace;
    let r2 be the remainder after dividing r1 by 16; [remainder is second digit of hex]
    let G2 be the remainder after dividing G1 by 16;
    let B2 be the remainder after dividing B1 by 16;
    now R1 is R1 divided by 16; [division w/o remainder is first digit of hex]
    now G1 is G1 divided by 16;
    now B1 is B1 divided by 16;
    now B1 is B1 multiplied by 16; [conversion to decimal]
    now G2 is G2 multiplied by 256;
    now G1 is G1 multiplied by 4096;
    now R2 is R2 multiplied by 65536;
    now R1 is R1 multiplied by 1048576;
    decide on (R1 + R2 + G1 + G2 + B1 + B2);  [sum of conversions]
In all this code, I've gone for steps, rather than one giant equation.  Giant equations have a habit of going horribly wrong, and it's easier to tweak like this. 

Converting from decimal to RGB is similar, except now you're converting to pseudo-hex, but without the actual hex part.

Section - Decimal to RGB   
   
To decide what RGB is the RGB value of (colorspace - a number):
    if colorspace is greater than 16777215:
        decide on 255-255-255;
    if colorspace is less than 1:
        decide on 0-0-0;   
    let count be colorspace;
    let b2 be the remainder after dividing count by 16;
    now count is count divided by 16;
    let b1 be the remainder after dividing count by 16;
    now count is count divided by 16;
    let g2 be the remainder after dividing count by 16;
    now count is count divided by 16;
    let g1 be the remainder after dividing count by 16;
    now count is count divided by 16;
    let r2 be the remainder after dividing count by 16;
    now count is count divided by 16;
    let r1 be the remainder after dividing count by 16;
    now count is count divided by 16;
    now r1 is ((r1 * 16) + r2);
    now g1 is ((g1 multiplied by 16) + g2);
    now b1 is ((b1 multiplied by 16) + b2);
    let temp-hue be the RGB with rgb-red part r1 rgb-green part g1 rgb-blue part b1;
    decide on temp-hue;
All this is pretty straightforward, although it is sort of embarrassing how long it took me to figure out base conversions again.  I think the last time I did that was sixth grade, and it did not stick.

If you're going to be fiddling around with changing the color of text or background, keeping things legible is a primary concern.  Black/white is pretty much always the best option, but you can get surprisingly good results by figuring out how "bright" a color appears and then using black on brighter backgrounds or white text on darker backgrounds (or vice versa, if you're modifying text colors).  I find the middle values can still be a little challenging, but acceptable in small spaces (ie the status bar).  This decision is how I decide what color text to use on color backgrounds. The equation was yoinked from Nbd-tech.

Section - Dark or Bright   

To decide if (colorspace - an RGB) is bright:
    let X be the rgb-red part of colorspace;
    now X is X multiplied by 299;
    let Y be the rgb-green part of colorspace;
    now Y is Y multiplied by 587;
    let Z be the rgb-blue part of colorspace;
    now Z is Z multiplied by 114;
    now X is (X plus Y plus Z);
    now X is (X divided by 1000);
    if X is greater than 126:
        decide yes;
    decide no.
 So now we're down to the fun stuff.  Pretty much all of this I had to figure out on my own; there's plenty of talk online *about* color, but the actual equations used to make changes are few, far between, and usually written in a computer language I don't speak, often with calls to library functions that I don't have.

The equations are mine, but spit out answers within a small range of error of Photoshop and Gimp (where those values agree) and Photoshop (when they don't). 

Gray

100% desaturation gives you pure gray, which should be about the same shade, but minus all the color.  You can this by taking the highest value of R, G, or B, and making it the same (R-R-R if red was the highest value). 

Section - Pure Gray
   
To decide what RGB is the pure gray value of (colorspace - a RGB):
    let temp-gray be the rgb-red part of colorspace;
    if the rgb-green part of colorspace is greater than temp-gray:
        now temp-gray is the rgb-green part of colorspace;
    if the rgb-blue part of colorspace is greater than temp-gray:
        now temp-gray is the rgb-blue part of colorspace;   
    let pure-hue be the RGB with rgb-red part temp-gray rgb-green part temp-gray rgb-blue part temp-gray;
    decide on pure-hue;    

If you have a hue you want to make more gray, find the difference between the pure gray value and the color and add it to the color.  This can be modified by using a percentage, letting you pull a little color out or a lot.

Hue + (Gray - Hue)(percentage change) = Grayer Hue

Section - To Gray A Color

To decide what RGB is (colorspace - a RGB) grayed/greyed by (amount - a number) percent:
    let pure-gray be the pure gray value of colorspace;
    let R be (rgb-red part of pure-gray minus rgb-red part of colorspace);
    let G be (rgb-green part of pure-gray minus rgb-green part of colorspace);
    let B be (rgb-blue part of pure-gray minus rgb-blue part of colorspace);
    now R is (R multiplied by amount) divided by 100;
    now G is (G multiplied by amount) divided by 100;
    now B is (B multiplied by amount) divided by 100;   
    now R is R plus rgb-red part of colorspace;
    now G is G plus rgb-green part of colorspace;
    now B is B plus rgb-blue part of colorspace;       
    decide on the rgb with rgb-red part R rgb-green part G rgb-blue part B;

Saturation

This one stumped me for quite a while.  Saturation is intensity of color - the opposite of gray.  Turning "up" the color in terms of numbers isn't intuitive - what do you actually *do*? 

As saturation increases, you're removing gray, so you're going to see the lowest value go to 0.  The highest value, the place where the most pigment is, will stay the same.

Full Saturation Color:
First find the saturation value.  Saturation = (Highest - lowest)/highest. 
Highest value: Highest Value
Middle value (if any):  (Middle - Lowest)/Saturation
Lowest value: changes to zero.

In Inform terms:

Section - Pure Saturation   
   
To decide what RGB is the pure saturated value of (colorspace - a RGB):
    if rgb-red part of colorspace is 0 or rgb-green part of colorspace is 0 or rgb-blue part of colorspace is 0:
        decide on colorspace;
    let rgb_high be rgb-red part of colorspace;
    let rgb_low be rgb-red part of colorspace;
    if rgb-green part of colorspace is greater than rgb_high:
        now rgb_high is rgb-green part of colorspace;
    if rgb-green part of colorspace is less than rgb_low:
        now rgb_low is rgb-green part of colorspace;
    if rgb-blue part of colorspace is greater than rgb_high:
        now rgb_high is rgb-blue part of colorspace;
    if rgb-blue part of colorspace is less than rgb_low:
        now rgb_low is rgb-blue part of colorspace;   
    let saturation be 0;
    if rgb_high is not 0:
        now saturation is 100 multiplied by (rgb_high - rgb_low) divided by rgb_high;
    let R be 0;
    if rgb-red part of colorspace is rgb_high:
        now R is rgb-red part of colorspace;
    otherwise if rgb-red part of colorspace is rgb_low:
        now R is 0;
    otherwise:
        now R is ((rgb-red part of colorspace - rgb_low) multiplied by 100) divided by saturation;
    let G be 0;   
    if rgb-green part of colorspace is rgb_high:
        now G is rgb-green part of colorspace;
    otherwise if rgb-green part of colorspace is rgb_low:
        now G is 0;
    otherwise:
        now G is ((rgb-green part of colorspace - rgb_low) multiplied by 100) divided by saturation;
    let B be 0;   
    if rgb-blue part of colorspace is rgb_high:
        now B is rgb-blue part of colorspace;
    otherwise if rgb-blue part of colorspace is rgb_low:
        now B is 0;
    otherwise:
        now B is ((rgb-blue part of colorspace - rgb_low) multiplied by 100) divided by saturation;   
    let saturated_hue be the RGB with rgb-red part R rgb-green part G rgb-blue part B;
    decide on saturated_hue;
 Saturating colors by percentages is very similar to the process we used with graying colors, but this time, we know that fully saturated colors will never have highers values than the original color, so a few signs get switched around.

To decide what RGB is (colorspace - a RGB) saturated by (amount - a number) percent/--:
    let pure-sat be the pure saturated value of colorspace;
    let R be (rgb-red part of colorspace minus rgb-red part of pure-sat);
    let G be (rgb-green part of colorspace minus rgb-green part of pure-sat);
    let B be (rgb-blue part of colorspace minus rgb-blue part of pure-sat);   
    now R is (R multiplied by amount) divided by 100;
    now G is (G multiplied by amount) divided by 100;
    now B is (B multiplied by amount) divided by 100;   
    now R is rgb-red part of colorspace minus R;
    now G is rgb-green part of colorspace minus G;
    now B is rgb-blue part of colorspace minus B;       
    decide on the rgb with rgb-red part R rgb-green part G rgb-blue part B;   
 Brightness

This isn't directly related to the function earlier to decide whether a color was dark or bright.  Brightness occurs when the color hue stays the same, but it's pushed towards white; a fully bright hue has one or more values at 255. 

To convert to the brightest shade of a particular color:
Low values (anything that isn't the highest value in the RGB): Low(bright) = Low * 255 / Highest
Highest: Highest = 255

Section - Pure Brightness   
   
To decide what RGB is the brightest value of (colorspace - a RGB):
    if rgb-red part of colorspace is 255 or rgb-green part of colorspace is 255 or rgb-blue part of colorspace is 255:
        decide on colorspace;   
    let rgb_high be rgb-red part of colorspace;       
    if rgb-green part of colorspace is greater than rgb_high:
        now rgb_high is rgb-green part of colorspace;
    if rgb-blue part of colorspace is greater than rgb_high:
        now rgb_high is rgb-blue part of colorspace;   
    let R be 0;
    let G be 0;
    let B be 0;
    if rgb-red part of colorspace is rgb_high: [highest brightness requires the high values to be 255]
        now R is 255;
    otherwise:
        now R is (255 multiplied by rgb-red part of colorspace) divided by rgb_high;
    if rgb-green part of colorspace is rgb_high:
        now G is 255;   
    otherwise:
        now G is (255 multiplied by rgb-green part of colorspace) divided by rgb_high;           
    if rgb-blue part of colorspace is rgb_high:
        now B is 255;       
    otherwise:
        now B is (255 multiplied by rgb-blue part of colorspace) divided by rgb_high;   
    decide on the RGB with rgb-red part R rgb-green part G rgb-blue part B;   
 Brightening and darkening use the same machinery as for saturating and graying colors. 

Section - Brightening a Color

To decide what RGB is (colorspace - a RGB) brightened by (amount - a number) percent/--:
    let pure-bright be the brightest value of colorspace;
    let R be (rgb-red part of pure-bright minus rgb-red part of colorspace);
    let G be (rgb-green part of pure-bright minus rgb-green part of colorspace);
    let B be (rgb-blue part of pure-bright minus rgb-blue part of colorspace);
    now R is (R multiplied by amount) divided by 100;
    now G is (G multiplied by amount) divided by 100;
    now B is (B multiplied by amount) divided by 100;   
    now R is R plus rgb-red part of colorspace;
    now G is G plus rgb-green part of colorspace;
    now B is B plus rgb-blue part of colorspace;       
    decide on the rgb with rgb-red part R rgb-green part G rgb-blue part B;   
   
Section - Darkening a Color   

To decide what RGB is (colorspace - a RGB) darkened by (amount - a number) percent/--:
    let pure-dark be 0-0-0;
    let R be (rgb-red part of colorspace);
    let G be (rgb-green part of colorspace);
    let B be (rgb-blue part of colorspace);   
    now R is (R multiplied by amount) divided by 100;
    now G is (G multiplied by amount) divided by 100;
    now B is (B multiplied by amount) divided by 100;   
    now R is rgb-red part of colorspace minus R;
    now G is rgb-green part of colorspace minus G;
    now B is rgb-blue part of colorspace minus B;       
    decide on the rgb with rgb-red part R rgb-green part G rgb-blue part B;   
Additive vs. Subtractive Color 

There's basically two ways to combine color.  Color can be pigments, in the sense that they absorb all but the color that you see, or they can be light, in that they give the color you see.  Pigments are subtractive, because once all of a certain color is absorbed, there's no way to see it again.  If you layer black marker over a sheet of paper, and then use a yellow marker, you're still only going to see black.

However, if you've got a dark room, and you shine a yellow light, you'll get yellow.  Add a blue light, and you'll get yellow + blue light.

 Multiplying

One of the filters I use most in Photoshop/Gimp.  Multiply is a way to simulate subtractive color combination.  The basic formula for each value is to multiply the two corresponding values together and divide by 255.

Red1 * Red2 / 255 = RedMultiplied.

It's a useful function if you're simulating paint, dye, or filtering out light.  (For this scenario, we're pretending white paint doesn't really exist - real life is, as always, more complicated than the model we create.)

Section - Multiplying Colors

[Multiply is used for *subtractive* color mixing - paints, dyes.  Things will grow darker as combined.  The formula is each part of the color, multiplied by its partner, divided by 255.]   
To decide what RGB is (basecolor - a RGB) multiplied by (topcolor - a RGB):
    let R be the rgb-red part of basecolor;
    now R is R multiplied by the rgb-red part of topcolor divided by 255;
    let G be the rgb-green part of basecolor;
    now G is G multiplied by the rgb-green part of topcolor divided by 255;
    let B be the rgb-blue part of basecolor;
    now B is B multiplied by the rgb-blue part of topcolor divided by 255;   
    decide on the RGB with rgb-red part R rgb-green part G rgb-blue part B.
Lightening

For additive color combination, you take the highest value of each part of the RGB value.  Confusingly, there's no actual addition involved.  It sort of makes sense if you think about what happens if you use a flashlight in full sunlight - things aren't brighter unless you shine the light where the sun isn't.  Brightness isn't purely additive.


Section - Adding or Lightening Colors

[Lightening is *additive* color mixing, as used with light.  Lightening takes the highest of each of the RGB values given.]

To decide what RGB is (basecolor - a RGB) lightened/added by/to (topcolor - a RGB):
    let R be the rgb-red part of basecolor;
    if rgb-red part of topcolor is greater than R:
        now R is rgb-red part of topcolor;
    let G be the rgb-green part of basecolor;
    if rgb-green part of topcolor is greater than G:
        now G is rgb-green part of topcolor;
    let B be the rgb-blue part of basecolor;
    if rgb-blue part of topcolor is greater than B:
        now B is rgb-blue part of topcolor;               
    decide on the RGB with rgb-red part R rgb-green part G rgb-blue part B;
Blending 

Finally, I've included a way to blend colors.  If you use a image editor, it's similar to the way opacity works between layers, and is actually a good model for that white paint mixing problem left over from multiply.  It's also a way to fade or transition convincingly between two colors.

Combination = Original - ((Original - additive)*percentage of additive used)

 Section - Blending a Percentage

[This is an all purpose color-blending option, similar to the general "brighten/saturate" commands.  It is equivalent to the opacity layer in photoshop, where the pigment is a layer on top of the colorspace.]

To decide what RGB is (colorspace - a RGB) blended with (amount - a number) percent of/-- (pigment - a RGB):
    let R be 0;
    now R is rgb-red part of colorspace minus rgb-red part of pigment;
    let G be 0;
    now G is rgb-green part of colorspace minus rgb-green part of pigment;
    let B be 0;
    now B is rgb-blue part of colorspace minus rgb-blue part of pigment;
    now R is (R multiplied by amount) divided by 100;      
    now G is (G multiplied by amount) divided by 100;      
    now B is (B multiplied by amount) divided by 100;      
    now R is rgb-red part of colorspace minus R;
    now G is rgb-green part of colorspace minus G;
    now B is rgb-blue part of colorspace minus B;
    decide on the RGB with rgb-red part R rgb-green part G rgb-blue part B;
These tools allow for pretty fine color control.  Inform/glulx aren't highly accessible in terms of color, but it is possible to make some minor changes that aren't too distracting.

I'm hoping to come up with an automatic sunset generator next. 

Caveat: This is all in beta still, but it seems to work as expected.

Because of the way Inform handles small values, there's always going to be a little variation as numbers approach each other.  A few digits one way or another in a RGB is not readily visible to the naked eye, though, and is well within the margin of error of computer monitors.  There are more complicated formulas that would afford greater accuracy, but it's a lot of extra processing power for very little benefit.