Simplifying Color Selection for ImpactJS Entities

May 12, 2012

The Matlab HSV colormap rendered using 162,000 different colors generated by color-picker.js

The other day I set out to make a quick way to eliminate the need for sprites when rendering very simple entities with ImpactJS. For example the particle effects, paddles and puck in PiSpace. These entities are now rendered using direct canvas methods and constructed on the fly from input verticies. This allows for a more dynamic nature in the game and much better performance with Box2D v2.1a. More on that in a later post.

Along with using raw verticies to render the objects, I wanted to be able to change the color on any entity in the game based on different conditions. Also, I wanted to produce a smooth gradient fade effect between any set of input colors. A direct example of this in PiSpace is the “HEAT” bar that changes from Green -> Yellow -> Red dependent on how long you have used your boost. Previously, in order to obtain the color fading I would like I used the ImpactJS sprite animation engine to select a frame from a supplied asset. This required that I have an art asset that had a frame with the color I wanted to use available for each effect I wanted to create. I am not much an artist, so my asset pipeline is slow moving for these types of things. Fading from green to red is boring and bland, but throw yellow in there and you get a more interesting effect. So, I put together a color-picker.js plugin for ImpactJS that allows me to quickly generate an array of colors in any gradient combination that I want. Click here for a demo. Below is a sample of a few different combinations:

color-select-sample

CW starting in the upper left: 1) 2 color fade with 400 steps. 2) 6 color fade (Jet) with 480 steps. 3) 7 color fade (HSV) with 420 steps. 4) 7 color fade (HSV) with 162,000 steps.

To generate the color maps above I used the JavaScript prototype construct to build a ColorPicker object with the following functions:

genMultiHexArray
Supply an input array with any number of hex colors and the number of steps you desire between two colors. This will return array with ‘steps’ entries between each sequential pair of colors in the input array. Very useful for creating custom multi-color colormaps.

genHexArray
Supply two hex colors and a number of steps. This will return array with ‘steps’ entries fading from color 1 to color 2.

pickHex
Supply two hex colors and a ratio. This will return a color that is equivalent to that fade between those two colors at the ratio point.

A few utility functions that are helpful for browser compatibility and the such:
hexToRGBstr will convert the hex code to a rgb( r, g, b ) string.
hexToRGBA will convert the hex code to a rgb( r, g, b, a) string with a default alpha set to 1.
frontPad will check the length of the hex string to ensure that there are the necessary leading zeros.

Using this method has greatly improved my asset pipeline speed and greatly improved the performance on my games.

The ImpactJS plugin version of this code can be found on GitHub. Here is the complete stand-alone object code:

/*
  genMultiHexArray will take an input array of any number of hex colors
  and the number of steps to create a smooth gradient fade between the
  colors in sequence.
 
  Example: input = [0x00FF00, 0xFFFF00, 0xFF0000] with steps = 10
  will produce a Green to Yellow to Red gradient with 10 steps between
  each source color. Total of 23 color codes.
*/
ColorPicker.prototype.genMultiHexArray = function(input, steps) {
  var multiColor = new Array;

  // Find each sequential pair to compare
  for (var i = 0; i < input.length - 1; i++) {
    // Set hex colors
    var hc1 = input[i];
    var hc2 = input[i + 1];

    // Save first color
    multiColor.push(hc1.toString(16));

    // Break hc1 into RGB components
    var r = hc1 >> 16;
    var g = hc1 >> 8 & 0xFF;
    var b = hc1 & 0xFF;

    // Determine RGB differences between hc1 and hc2
    var rd = (hc2 >> 16) - r;
    var gd = (hc2 >> 8 & 0xFF) - g;
    var bd = (hc2 & 0xFF) - b;

    // Determine new colors
    for (var j = 1; j < steps; j++) {
      // Where are we on the gradient?
      var ratio = j / steps;
      // Calculate new color and add it to the array
      multiColor.push(((r + rd * ratio) << 16 | (g + gd * ratio) << 8 | (b + bd * ratio)).toString(16));
    }
  }

  // Add the last color to the end of the array.
  multiColor.push(input[input.length - 1].toString(16));

  // Test that all hex color codes are properly front loaded
  for (var k = 0; k < multiColor.length; k++) {
    while (multiColor[k].length < 6) {
      multiColor[k] = '0' + multiColor[k];
    }
  }

  // Return the new array of colors.
  return multiColor;
};

// function will return an array with [r,g,b,a] set to
// appropriate values.
ColorPicker.prototype.hexToRGBA = function(hex) {
  // Break hc1 into RGB components
  var d = new Array;
  // R
  d[0] = parseInt(hex.substring(0, 2), 16);
  // G
  d[1] = parseInt(hex.substring(2, 4), 16);
  // B
  d[2] = parseInt(hex.substring(4, 6), 16);
  // Alpha
  d[3] = 255;

  return d;
};

// Function will return a 'rgb( r,g,b )' string set to the
// appropriate values.
ColorPicker.prototype.hexToRGBstr = function(hex) {
  // Break hc1 into RGB components
  return "rgb( " + parseInt(hex.substring(0, 2), 16) + ", " + parseInt(hex.substring(2, 4), 16) + ", " + parseInt(hex.substring(4, 6), 16) + " )";
};

// Useful for checking front padding of a hex color code
ColorPicker.prototype.frontPad = function(hex) {
  while (hex.length < 6) {
    hex = '0' + hex;
  }
  return hex;
};

And if you are interested, here is the code I used to generate the demo images above:

// Initialize ColorPicker object
var picker = new ColorPicker;

// Generate and draw fade between White and Black with 400 steps
var fade = picker.genHexArray(0xFFFFFF, 0x000000, 400);
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
drawBoxes(ctx, fade, "Two Color Fade");

// Generate the Matlab Jet colormap with 80 steps between each color
// then render
var jet = picker.genMultiHexArray([0x0000FF, 0x00FFFF, 0x00FF00, 0xFFFF00, 0xFF0000, 0x000000], 80);
var c2 = document.getElementById("canvas2");
var ctx2 = c2.getContext("2d");
drawBoxes(ctx2, jet, "Matlab Jet");

// Generate the Matlab HSV colormap with 70 steps between each color
// then render
var hsv = picker.genMultiHexArray([0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0x000000], 70);
var c3 = document.getElementById("canvas3");
var ctx3 = c3.getContext("2d");
drawBoxes(ctx3, hsv, "Matlab HSV");

// Regenerate the Matlab HSV colormap in a higher resolution with
// 27,000 steps between each color.
// Use the createImageData method to quickly render the colormap
// with each entry in the array represneted by a single pixel.
var pix = picker.genMultiHexArray([0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0x000000], 27000);
var c4 = document.getElementById("canvas4");
var ctx4 = c4.getContext("2d");
drawPixels(ctx4, pix, picker, "Matlab HSV HighRes");