Contrasting color generator

I’m not sure I’m entirely ready to get rid of color ramps, but I am very ready to explore programmatic color systems. This is a little helper script to generate a color value of a specific hue, saturation and desired contrast ratio. This method allows color pairings to be generated relative to each other as opposed to picking from a static ramp.

Note: I’m not a huge fan of how I’ve structured the parameters for the main function in this code snippet. I’ll likely adjust in the future.

Instructions

  1. Copy the code below into a file on your machine and name it contrast.js
  2. Install Node.js
  3. Edit BASE_COLOR, RATIO constants to fit your needs
  4. Fire up a Terminal window and cd to the directory where you saved the contrast.js file
  5. Run node contrast.js in your terminal
  6. Read out the results returned in the terminal
            
/* 
 * 
 * Full disclosure on a few points:
 * 1) I pulled functions from _numerous_ sources, so big thanks for folks for sharing code. 
 *    I will try to retrace my history and provide credits where credit is due. 
 * 
 * 2) The code is still a mess and not fully tested. Some of the lines of code I still don't 
 *    fully understand _why_ it works - which is always a "good" sign. This is only intended 
 *    to be a proof of concept. A lot more work needs to happen to get this in a place that 
 *    I'm confident with.
 * 
 * 3) I'm still trying to work out how this kind of color develop flow would actually work. 
 *    I am not a color expert - by a long shot - and so more vetting/thinking needs to occur.
 *    Some of the things I'm doing could very well be poor practices in relation to color 
 *    theory. I'm learning as I go.
 */

const RED = 0.2126;
const GREEN = 0.7152;
const BLUE = 0.0722;

const GAMMA = 2.4;

function contrast(rgb1, rgb2) {
  var lum1 = luminance(rgb1);
  var lum2 = luminance(rgb2);
  var brightest = Math.max(lum1, lum2);
  var darkest = Math.min(lum1, lum2);
  return (brightest + 0.05) / (darkest + 0.05);
}

function luminance(rgb) {
  var a = rgb.map((v) => {
    return v <= 0.03928
      ? v / 12.92
      : Math.pow((v + 0.055) / 1.055, GAMMA);
  });
  return a[0] * RED + a[1] * GREEN + a[2] * BLUE;
}

function hslToRgb(hsl){

  let h = hsl[0]
  let s = hsl[1]
  let l = hsl[2]
  var r, g, b;
  if(s == 0){
      r = g = b = l; 
  } else{
    var hue2rgb = function hue2rgb(p, q, t){
      if(t < 0) t += 1;
      if(t > 1) t -= 1;
      if(t < 1/6) return p + (q - p) * 6 * t;
      if(t < 1/2) return q;
      if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
      return p;
    }

    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    var p = 2 * l - q;
    r = hue2rgb(p, q, h + 1/3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1/3);
  }

  return [r,g,b];
}

function rgbToHsl(rgb) {
  let r = rgb[0];
  let g = rgb[1];
  let b = rgb[2];
  var max = Math.max(r, g, b), min = Math.min(r, g, b);
  var h, s, l = (max + min) / 2;

  if(max == min){
    h = s = 0; // achromatic
  } else{
    var d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    switch(max){
      case r: h = (g - b) / d + (g < b ? 6 : 0); break;
      case g: h = (b - r) / d + 2; break;
      case b: h = (r - g) / d + 4; break;
    }
    h /= 6;
  }

  return [h, s, l];
}

function getContrastingColor(h, s, rgb1, ratio, type = "auto") {
  var inc = -0.001;
  var l1 = luminance(rgb1);
  let l2 = l1;

  if(type === 'lighter' || (type === 'auto' && contrast(rgb1, [1,1,1]) > contrast(rgb1, [0,0,0]))) {
    inc = -inc
  } 

  while ((contrast(rgb1, hslToRgb([h, s, l2])) < ratio || Math.round( contrast(rgb1, hslToRgb([h, s, l2])) * 10) / 10 > ratio)) {
    l2 += inc

    if(l2 >= 1 || l2 <= 0) {
      return null
    } 
  }
  return hslToRgb([h, s, l2]);
}

function rgbToHex(rgb) {
  return "#" + ((1 << 24) + (Math.round(rgb[0]*255) << 16) + (Math.round(rgb[1]*255) << 8) + Math.round(rgb[2]*255)).toString(16).slice(1);
}

function hexToRgb(hex) {
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? [
    parseInt(result[1], 16)/255,
    parseInt(result[2], 16)/255,
    parseInt(result[3], 16)/255
   ] : null;
}


const BASE_COLOR = '#F7D5B3';
const RATIO = 4.5

let baseRgb = hexToRgb(BASE_COLOR);
let baseHSL = rgbToHsl(baseRgb)

/* 
 * Generate a new color from `baseColor` to be lighter at a `RATIO` contrast ratio.
 */
let newRGB = getContrastingColor(baseHSL[0], baseHSL[1], baseRgb, RATIO, "auto")

/* 
 * Then convert to hex.
 */
if(newRGB) {
  console.log(rgbToHex(newRGB))
} else {
  console.error("No color available at this contrast")
}