Search code examples
htmlcsscolorsrgbhsl

How can I create a color with values for green, blue, orange, and gold only?


If one knows the "weights" of red, green, and blue, one can create a color using a class like

.blaRGB {
    color: rgb(128, 128, 128)
}
.blaHSL {
    color: hsl(33%, 34%, 33%)
}

and use it in HTML like so:

<p class="blaRGB">BLA RGB</p>
<p class="blaHSL">BLA HSL</p>

With the values shown above, blaRGB is dingy gray and blaHSL is white.

But what if one wants to represent combinations of other colors, such as green, blue, orange, and gold?

Is there a way to define such colors with a notation like the following?

.CyndiLauper {
    color: truecolors(24, 13, 11, 12) 
}

Or with color: gbog(24, 13, 11, 12) where the letters in gbog stand for green, blue, orange, and gold respectively?

The intent is that with the CSS definition above, the HTML

<p class="CyndiLauper">These are your True Colors, shining through!</p>

would display the text in a weighted combination of those four colors.

I believe this is possible using some convoluted math using the RGB values of green, blue, orange, and gold, but I don't know how to proceed with that.

UPDATE

I realize that a key component to the question, which I initially omitted, would be, "What exactly do you mean by green, blue, orange, and gold"?

To answer that, according to the official "True Colors" website, and using the ColorZilla browser add-in, the RGB values for these colors are:

green = 15, 167, 73
blue = 0, 152, 215
orange = 243, 123, 38
gold = 255, 230, 101

(They actually look more like forest green, dodger blue, red-orange, and yellow to me.)


Solution

  • There are many ways to mix colors, depending on what color model you're using:

    The following code snippet demonstrates two mixing methods. The leftmost column displays the four original colors. The next column shows the same colors with 0.25 opacity, resulting in translucent colors. The third column layers the translucent colors on top of one another, which is equivalent to alpha compositing.

    The fourth column shows an alternative approach to mixing. Here we see the result of separately averaging the R, G, and B components of each of the four original colors.

    var Colors = {
      names: ['green', 'blue', 'orange', 'gold'],
      values: {
        green: { r: 15, g: 167, b: 73 },
        blue: { r: 0, g: 152, b: 215 },
        orange: { r: 243, g: 123, b: 38 },
        gold: { r: 255, g: 230, b: 101 }
      }
    };
    
    Colors.toHex2 = function (decimal) {
      var hex = decimal.toString(16);
      return (hex.length == 1 ? '0'+hex : hex);
    };
    
    Colors.toColorString = function (value) {
      var g = Colors,
          toHex2 = g.toHex2;
      var parts = [ '#', toHex2(value.r), toHex2(value.g), toHex2(value.b) ];
      return parts.join('');
    };
    
    Colors.load = function () {
      var g = Colors,
          names = g.names,
          values = g.values,
          containers = {
            original: document.getElementById('original'),
            translucent: document.getElementById('translucent'),
            layered: document.getElementById('layered'),
            averaged: document.getElementById('averaged')
          },
          averaged = { r: 0, g: 0, b: 0 };
      document.body.style.paddingTop = 10*(1+names.length) + 'px';
      for (var i = 0; i < names.length; ++i) {
        var name = names[i],
            value = values[name],
            color = g.toColorString(value),
            swatch = document.createElement('div'),
            proportion = 1 / names.length;
        swatch.className = 'swatch';
        swatch.style.backgroundColor = color;
        containers.original.appendChild(swatch);
    
        swatch = swatch.cloneNode();
        swatch.style.opacity = proportion;
        containers.translucent.appendChild(swatch);
    
        swatch = swatch.cloneNode();
        swatch.style.height = 60 + 10*(names.length-1) + 'px';
        swatch.style.top = 10*(1+i-names.length) + 'px';
        containers.layered.appendChild(swatch);
    
        averaged.r += proportion * value.r;
        averaged.g += proportion * value.g;
        averaged.b += proportion * value.b;
      }
    
      swatch = document.createElement('div');
      swatch.className = 'swatch';
      averaged.r = Math.round(averaged.r);
      averaged.g = Math.round(averaged.g);
      averaged.b = Math.round(averaged.b);
      swatch.style.backgroundColor = g.toColorString(averaged);
      containers.averaged.appendChild(swatch);
    };
    
    window.onload = Colors.load;
    body {
      font-family: sans-serif;
      font-size: 24px;
      margin: 0;
      padding-left: 20px;
    }
    .display {
      width: 120px;
      float: left;
    }
    .swatch {
      width: 100px;
      height: 60px;
      margin-bottom: 10px;
    }
    .layered {
      position: relative;
      height: 60px;
    }
    .layered .swatch {
      position: absolute;
    }
    <div id="original" class="display"></div>
    
    <div id="translucent" class="display"></div>
    
    <div id="layered" class="display layered"></div>
    
    <div id="averaged" class="display"></div>

    In the demonstration above, we let the web browser do the alpha compositing for us. We can also do alpha compositing directly. Layer opacity is equivalent to the alpha channel, so we set the alpha channel of each color to the weight of the color. In other words, if the weight of a color is 25%, we set its alpha channel to 0.25.

    Note that the alpha channel ranges from 0 to 1. However, our RGB components range from 0 to 255.

    Suppose we are mixing these two colors:

    • RGB value background with alpha channel alpha.background
    • RGB value foreground with alpha channel alpha.foreground

    We calculate the resulting color's alpha channel alpha.mix like so:

    alpha.mix = 1 - (1-alpha.background)*(1-alpha.foreground);
    

    To compute the R component of the RGB value mix, we do this:

    mix.r = 255 * (foreground.r/255.0 * alpha.foreground/alpha.mix +
            mix.r/255.0 * alpha.background*(1-alpha.foreground)/alpha.mix);
    

    And likewise for the G component mix.g and the B component mix.b.

    The code snippet below is an interactive demonstration that compares alpha compositing with the simpler approach of averaging the RGB components in parallel. Run the code and play around with the sliders to see the difference between these two approaches.

    var Mixer = {
      color: {
        names: ['green', 'blue', 'orange', 'gold'],
        rgb: {
          green: { r: 15, g: 167, b: 73 },
          blue: { r: 0, g: 152, b: 215 },
          orange: { r: 243, g: 123, b: 38 },
          gold: { r: 255, g: 230, b: 101 }
        }
      },
      style: {
        swatch: { width: 150, height: 90, margin: { right: 15 } },
        slider: { height: 20 },
        handle: { width: 20, height: 34 }
      }
    };
    
    Mixer.toHex2 = function (decimal) {
      var hex = decimal.toString(16);
      return (hex.length == 1 ? '0'+hex : hex);
    };
    
    Mixer.toCSS = function (rgb) {
      var g = Mixer,
          toHex2 = g.toHex2;
      var parts = [ '#', toHex2(rgb.r), toHex2(rgb.g), toHex2(rgb.b) ];
      return parts.join('');
    };
    
    Mixer.toString = function (rgb) {
      return 'rgb(' + [rgb.r, rgb.g, rgb.b].join(', ') + ')';
    };
    
    Mixer.makeUnselectable = function (element) {
      element.className += ' unselectable';
      element.ondragstart = element.onselectstart = function (event) {
        event.preventDefault();
      };
    };
    
    Mixer.makeElement = function (tag, className, innerHTML, unselectable) {
      var g = Mixer,
          element = document.createElement(tag);
      element.className = (className ? className : '');
      element.innerHTML = (innerHTML ? innerHTML : '');
      if (unselectable) {
        g.makeUnselectable(element);
      }
      return element;
    };
    
    Mixer.handleDown = function (event) {
      event = event || window.event;
      var g = Mixer;
      g.mouseX = { depart: event.screenX };
      g.activeHandle = this;
      window.onmousemove = Mixer.handleMove;
      window.onmouseup = Mixer.handleUp;
    };
    Mixer.handleMove = function (event) {
      event = event || window.event;
      var g = Mixer,
          handle = g.activeHandle,
          pos = handle.pos,
          handles = g.handles,
          num = g.num,
          slider = g.slider,
          proportion = g.proportion,
          segments = g.segments,
          handleWidth = g.style.handle.width,
          swatches = g.swatches,
          canvas = g.canvas,
          context = g.context,
          mixingFunctions = g.mixingFunctions,
          limit = {
            min: (pos == 0 ? 0 : handles[pos-1].right),
            max: (pos == num-2 ? slider.length.total : handles[pos+1].left) -
                handleWidth
          },
          mouseX = g.mouseX;
      mouseX.current = event.screenX;
      var left = handle.left + mouseX.current - mouseX.depart;
      if (left < limit.min) {
        left = limit.min;
      }
      if (left > limit.max) {
        left = limit.max;
      }
      handle.newLeft = left;
      segments[pos] = left - limit.min;
      context.fillStyle = swatches[pos].css;
      context.fillRect(limit.min, 0, segments[pos], canvas.height);
      segments[pos+1] = limit.max - left;
      context.fillStyle = swatches[pos+1].css;
      context.fillRect(left + handleWidth, 0, segments[pos+1], canvas.height);
      handle.style.left = left + 'px';
      var segmentSpan = segments[pos] + segments[pos+1],
          proportionSpan = proportion[pos] + proportion[pos+1];
      if (segmentSpan != 0) {
        proportion[pos] = Math.round(segments[pos]/segmentSpan * proportionSpan);
        proportion[pos+1] = proportionSpan - proportion[pos];
        swatches[pos].percent.innerHTML = proportion[pos] + '%';
        swatches[pos+1].percent.innerHTML = proportion[pos+1] + '%';
      }
      g.mixColors();
    };
    Mixer.handleUp = function (event) {
      var g = Mixer,
          handle = g.activeHandle;
      window.onmousemove = null;
      window.onmouseup = null;
      handle.left = handle.newLeft;
      handle.right = handle.left + g.style.handle.width;
    };
    
    Mixer.makeFunctionName = function (title) {
      var parts = ['mix'],
          tokens = title.split(' ');
      for (var i = 0; i < tokens.length; ++i) {
        var token = tokens[i];
        parts.push(token[0].toUpperCase() + token.substring(1));
      }
      return parts.join('');
    };
    
    Mixer.mixAlphaCompositing = function (swatch, label) {
      return function () {
        var g = Mixer,
            swatches = g.swatches,
            proportion = g.proportion,
            num = g.num,
            mix = {},
            subs = ['r', 'g', 'b'];
        for (var i = 0; i < subs.length; ++i) {
          var x = subs[i];
          mix[x] = swatches[0].rgb[x];
        }
        var alpha = { back: proportion[0]/100 };
        for (var pos = 1; pos < num; ++pos) {
          var fore = swatches[pos].rgb;
          alpha.fore = proportion[pos]/100,
          alpha.mix = 1 - (1-alpha.back)*(1-alpha.fore);
          if (alpha.mix >= 1.0e-6) {
            for (var i = 0; i < subs.length; ++i) {
              var x = subs[i];
              mix[x] = 255 * (fore[x]/255 * alpha.fore/alpha.mix +
                  mix[x]/255 * alpha.back*(1-alpha.fore)/alpha.mix);
            }
          }
          alpha.back = alpha.mix;
        }
        for (var i = 0; i < subs.length; ++i) {
          var x = subs[i];
          mix[x] = Math.round(mix[x]);
        }
        var css = g.toCSS(mix);
        label.rgb.innerHTML = g.toString(mix);
        label.css.innerHTML = css;
        swatch.style.backgroundColor = css;
        swatch.style.opacity = alpha.mix;
      };
    };
    
    Mixer.mixWeightedAverage = function (swatch, label) {
      return function () {
        var g = Mixer,
            swatches = g.swatches,
            proportion = g.proportion,
            num = g.num,
            mix = { r: 0, g: 0, b: 0 },
            subs = ['r', 'g', 'b'];
        for (var pos = 0; pos < num; ++pos) {
          for (var i = 0; i < subs.length; ++i) {
            var x = subs[i];
            mix[x] += proportion[pos]/100 * swatches[pos].rgb[x];
          }
        }
        for (var i = 0; i < subs.length; ++i) {
          var x = subs[i];
          mix[x] = Math.round(mix[x]);
        }
        var css = g.toCSS(mix);
        label.rgb.innerHTML = g.toString(mix);
        label.css.innerHTML = css;
        swatch.style.backgroundColor = css;
      };
    };
    
    Mixer.mixColors = function () {
      var g = Mixer,
          mixingFunctions = g.mixingFunctions;
      for (var i = 0; i < mixingFunctions.length; ++i) {
        mixingFunctions[i]();
      }
    };
    
    Mixer.load = function () {
      var g = Mixer,
          style = g.style;
    
      // Make color swatches.
      var palette = g.palette = document.getElementById('palette'),
          names = g.color.names,
          swatches = g.swatches = [],
          num = g.num = names.length;
      for (var i = 0; i < num; ++i) {
        var name = names[i],
            rgb = g.color.rgb[name],
            css = g.toCSS(rgb),
            container = g.makeElement('div', 'swatchContainer', '', true),
            percent = g.makeElement('div', 'title', '', true),
            swatch = g.makeElement('div', 'swatch', '', true);
        swatches[i] = { rgb: rgb, css: css, percent: percent };
        container.appendChild(percent);
        swatch.style.backgroundColor = css;
        swatch.style.width = style.swatch.width + 'px';
        swatch.style.height = style.swatch.height + 'px';
        swatch.style.marginRight = style.swatch.margin.right + 'px';
        container.appendChild(swatch);
        container.appendChild(g.makeElement('div', 'label', g.toString(rgb), true));
        container.appendChild(g.makeElement('div', 'label', css, true));
        palette.appendChild(container);
      }
      var totalWidth = num*style.swatch.width + (num-1)*style.swatch.margin.right;
    
      // Initialize proportions.
      var proportion = g.proportion = new Array(num),
          each = Math.floor(100/num);
      for (var i = 0; i < num-1; ++i) {
        proportion[i] = each;
      }
      proportion[num-1] = 100 - (num-1)*each;
      for (var i = 0; i < num; ++i) {
        swatches[i].percent.innerHTML = proportion[i] + '%';
      }
    
      // Prepare the blended swatches.
      var blend = g.blend = { container: document.getElementById('blend') },
          mixers = ['alpha compositing', 'weighted average'],
          between = (totalWidth - mixers.length*style.swatch.width) /
              (mixers.length + 1);
      g.makeUnselectable(blend);
      blend.container.style.width = totalWidth + 'px';
      blend.container.style.height = style.swatch.height + 'px';
      g.mixingFunctions = [];
      for (var i = 0; i < mixers.length; ++i) {
        var mixer = mixers[i],
            container = g.makeElement('div', 'swatchContainer', '', true),
            title = g.makeElement('div', 'title', mixer, true),
            swatch = g.makeElement('div', 'swatch', '', true),
            label = {
              rgb: g.makeElement('div', 'label', '', true),
              css: g.makeElement('div', 'label', '', true)
            };
        swatch.style.width = style.swatch.width + 'px';
        swatch.style.height = style.swatch.height + 'px';
        container.style.left = i*style.swatch.width + (i+1)*between + 'px';
        container.appendChild(title);
        container.appendChild(swatch);
        container.appendChild(label.rgb);
        container.appendChild(label.css);
        blend.container.appendChild(container);
        var functionName = g.makeFunctionName(mixer),
            mixingFunction = g[functionName](swatch, label);
        g.mixingFunctions.push(mixingFunction);
      }
    
      // Assemble the slider widget.
      var slider = g.slider = document.getElementById('slider');
      slider.length = {
        total: totalWidth,
        free: totalWidth - (num-1)*style.handle.width
      };
      var segments = g.segments = new Array(num);
      var tail = slider.length.free;
      for (var i = 0; i < num-1; ++i) {
        var current = Math.round(proportion[i]/100*slider.length.free);
        segments[i] = current;
        tail -= current;
      }
      segments[num-1] = tail;
      slider.style.width = slider.length.total + 'px';
      slider.style.height = style.slider.height + 'px';
      var canvas = g.canvas = g.makeElement('canvas'),
          context = g.context = canvas.getContext('2d');
      g.makeUnselectable(slider);
      g.makeUnselectable(canvas);
      canvas.width = slider.length.total;
      canvas.height = style.slider.height;
      slider.appendChild(canvas);
      var handles = g.handles = new Array(num-1);
      var left = 0;
      for (var i = 0; i < num; ++i) {
        context.fillStyle = swatches[i].css;
        context.fillRect(left, 0, segments[i], canvas.height);
        if (i == num-1) {
          break;
        }
        var handle = handles[i] = g.makeElement('div', 'handle', '', true);
        handle.pos = i;
        handle.style.width = style.handle.width + 'px';
        handle.style.height = style.handle.height + 'px';
        handle.style.top = (style.slider.height - style.handle.height)/2 + 'px';
        handle.left = left + segments[i];
        handle.style.left = handle.left + 'px';
        handle.right = handle.left + style.handle.width;
        left = handle.right;
        handle.onmousedown = g.handleDown;
        slider.appendChild(handle);
      }
    
      g.mixColors();
    };
    
    window.onload = Mixer.load;
    body {
      font-family: sans-serif;
      color: #444;
    }
    
    .unselectable {
      -webkit-user-select: none;
      -khtml-user-drag: none;
      -khtml-user-select: none;
      -moz-user-select: none;
      -moz-user-select: -moz-none;
      -ms-user-select: none;
      user-select: none;
    }
    
    #blend {
      position: relative;
      padding: 10px 0 90px;
    }
    #blend .swatchContainer {
      position: absolute;
    }
    #blend .swatchContainer .title {
      font-size: 17px;
      padding-bottom: 5px;
    }
    
    #slider {
      margin: 20px 0;
      position: relative;
    }
    .handle {
      position: absolute;
      background: #444;
      border-radius: 5px;
      cursor: pointer;
    }
    
    .swatchContainer {
      float: left;
    }
    .swatchContainer .title {
      font-size: 25px;
      text-align: center;
    }
    .swatchContainer .label {
      font-size: 17px;
    }
    <div id="blend"></div>
    
    <div id="slider"></div>
    
    <div id="palette"></div>