December 22, 2005

Simple color effects

RGB is the most widespread method for representing colors. HTML, CSS, Java's java.awt.Color constructors, and countless more, all expect color to be described in terms of their RGB components.

But RGB is not the best choice for doing common operations such as desaturation and certain types of gradients (e.g., shades of a color and fade to black or white). There's a much more natural way of manipulating colors for that purpose without going crazy with interpolation in the RGB color space. The trick is to work with the HSV representation, instead. In the HSV space, a color is defined by it's hue (the "rainbow" of colors), it's saturation and it's brightness or value.

For any given color, change the brightness to get a darker or lighter shade. If you want to desaturate it (without changing it's brightness), simply reduce it's saturation value.

Here's a simple example, and the relevant Javascript code:


function RGB(r, g, b)
{
   this.r = r
   this.g = g
   this.b = b
}

RGB.prototype = {
   toHSV: function()
   {
     var max = Math.max(this.r, this.g, this.b)
     var min = Math.min(this.r, this.g, this.b)

     var s = (max - min) / max

     switch (max) {
       case this.r: return new HSV(60 * (this.g - this.b) / (max - min), s, max)
       case this.g: return new HSV(60 * (this.b - this.r) / (max - min) + 120, s, max)
       case this.b: return new HSV(60 * (this.r - this.g) / (max - min) + 240, s, max)
     }
   },

   toHex: function()
   {
     var r = Math.floor(this.r * 255).toString(16)
     var g = Math.floor(this.g * 255).toString(16)
     var b = Math.floor(this.b * 255).toString(16)

     return "#" + (r.length == 1? "0" : "") + r
       + (g.length == 1? "0" : "") + g
       + (b.length == 1? "0" : "") + b
   }
}


function HSV(h, s, v)
{
   this.h = h
   this.s = s
   this.v = v
}

HSV.prototype = {
   toRGB: function()
   {
     var sextant = Math.floor(this.h / 60) % 6

     var f = this.h / 60 - sextant
     var p = this.v * (1 - this.s)
     q = this.v * (1 - f * this.s)
     t = this.v * (1 - (1 - f) * this.s)

     switch (sextant) {
       case 0: return new RGB(this.v, t, p)
       case 1: return new RGB(q, this.v, p)
       case 2: return new RGB(p, this.v, t)
       case 3: return new RGB(p, q, this.v)
       case 4: return new RGB(t, p, this.v)
       case 5: return new RGB(this.v, p, q)
     }
   },

   toHex: function()
   {
     return this.toRGB().toHex()
   }
}


function desaturate(box, color)
{
   box.style.backgroundColor = color.toHex()

   color.s -= .01
   if (color.s >= 0) {
     window.setTimeout(function() { desaturate(box, color), 50 })
   }
}

function fadeToBlack(box, color)
{
   box.style.backgroundColor = color.toHex()

   color.v -= .01
   if (color.v >= 0) {
     window.setTimeout(function() { fadeToBlack(box, color), 50 })
   }
}

function fadeToWhite(box, color)
{
   box.style.backgroundColor = color.toHex()

   var v = 1 - color.v
   var distance = Math.sqrt(color.s * color.s + v * v)

   color.s -= 0.01 * color.s / distance
   color.v += 0.01 * v / distance

   if (color.v <= 1 && color.s >= 0) {
     window.setTimeout(function() { fadeToWhite(box, color), 50 })
   }
}