skip to Main Content

I have an icon which I’d like to change the color of, using CSS. It is in a data-uri’d optimized SVG inlined in the CSS.

Normally, this wasn’t possible. That’s why icon fonts were invented; their main advantage over SVG is being able to recieve color and text-shadow rules from CSS. Well, CSS filters are now capable of doing both things to arbitrary images, and they now work in all Blink, Webkit and Gecko browsers, and can be expected for future IE/Spartan.

A text-shadow replacement is easy; just use the drop-shadow filter.

Coloring the image into a specific color, however, has proven very tricky, despite all the necessary filters being here. My theory, up until now, is as follows:

  • Use contrast(0) to turn the entire image into solid grey, while keeping the alpha channel (the Mozilla wiki says it’d become black, but in all browsers it becomes grey, must be a typo).
  • Use sepia(1), because we cannot operate on hue/saturation on a grey image. This ensures the entire image is composed of a reference color we can do math on; specifically, #AC9977.

At this point, we should be able to turn the entire image from what is now solid #AC9977 to any color we want using hue-rotate, saturate and brightness.

First, what color coordinates are browsers using? I could not find make sense of the spec to be sure if it’s using HSL (Lightness) or HSV (Value), but since HSB (Brightness) is another name for HSV, I suppose it’s using HSV. Furthermore, using something like brightness(999) saturates colors (instead of making them white), which would happen in HSV but not HSL.

Based on this assumption, we would proceed as follows:

  • Calculate the hue difference between #AC9977 and the color we want, and use hue-rotate.
  • Calculate the saturation difference between both, and use saturate.
  • Calculate the brightness difference between both, and use brightness.

Since this is not the kind of stuff to be done by hand, we’ll use the LESS preprocessor:

.colorize(@color) {
    @sepiaGrey: #AC9977;
    @hOffset: (hsvhue(@color) - hsvhue(@sepiaGrey)) * 1deg;
    @sRatio: unit(hsvsaturation(@color) / hsvsaturation(@sepiaGrey));
    @vRatio: unit(hsvvalue(@color) / hsvvalue(@sepiaGrey));
    -webkit-filter: contrast(0) sepia(1) hue-rotate(@hOffset) saturate(@sRatio) brightness(@vRatio);
    filter: contrast(0) sepia(1) hue-rotate(@hOffset) saturate(@sRatio) brightness(@vRatio);
}

This, in my understanding, should work. But it isn’t. Why, and how to make it work?

Example of what I’m trying to achieve

Consider an icon as an image or an element (background-image, CSS-based shape, etc), with any color, and with a shape defined by transparency (not a rectangular image that could be simply overlaid). I want to make it be entirely composed of a specific color with CSS (presumably, with the use of filters).

                 example

I plan to implement this as a LESS mixin that takes a color argument, but just guidance on the logic behind the HSB functions is enough.

3

Answers


  1. Chosen as BEST ANSWER

    I have made some progress on the maths but they're not pretty; ideally I believe any color could be represented at most in the following CSS filters:

    (-webkit-)filter: contrast(0) sepia(1) hue-rotate(X) saturate(Y) brightness(Z);
    

    In other words, ideally we should be able to express any color as hue, saturation and brightness coordinates relative to sepia grey (#AC9977).

    While I still didn't find a way to do that (nor am sure it's possible), I managed to make an implementation that accepts any shade of pure colors (R, G, B, C, M, Y) or any neutral color (white, black, and greys). A few are optimized (like black is just brightness(0)). Additionally, if the color you specify has transparency, that transparency will be added as an opacity filter.

    This is the code thus far (written in LESS):

    // Filter prefixer.
    .filter(@filters) { -webkit-filter+_: @filters; filter+_: @filters; }
    
    // Helper that conditionally adds opacity filter when color calls for it.
    ._conditional-opacity(@color) when (alpha(@color) < 1) {
      .filter(round(opacity(alpha(@color)), 3));
    }
    
    // Helper that adds a brightness filter when necessary.
    ._conditional-brightness(@channel) when (@channel < 255) {
      .filter(brightness(round(@channel / 255, 3)));
    }
    
    // Special case for pure black.
    .colorize(@color) when (fade(@color, 100%) = #000) {
      .filter(brightness(0));
      ._conditional-opacity(@color);
    }
    
    // Special case for pure grey and off-by-one-grey.
    .colorize(@color) when (fade(@color, 100%) = #7F7F7F),
                           (fade(@color, 100%) = #808080) {
      .filter(contrast(0));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure red.
    .colorize(@color) when (red(@color) > 0)
                       and (green(@color) = 0)
                       and (blue(@color) = 0) {
      .filter(contrast(0) sepia(1) saturate(999));
      ._conditional-brightness(red(@color));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure green.
    .colorize(@color) when (red(@color) = 0)
                       and (green(@color) > 0)
                       and (blue(@color) = 0) {
      .filter(contrast(0) sepia(1) hue-rotate(99deg) saturate(999));
      ._conditional-brightness(green(@color));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure blue.
    .colorize(@color) when (red(@color) = 0)
                       and (green(@color) = 0)
                       and (blue(@color) > 0) {
      .filter(contrast(0) sepia(1) hue-rotate(199deg) saturate(999));
      ._conditional-brightness(blue(@color));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure cyan.
    .colorize(@color) when (red(@color) = 0)
                       and (green(@color) > 0)
                       and (blue(@color) = green(@color)) {
      .filter(contrast(0) sepia(1) invert(1) saturate(999));
      ._conditional-brightness(blue(@color));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure magenta.
    .colorize(@color) when (red(@color) = blue(@color))
                       and (green(@color) = 0)
                       and (blue(@color) > 0) {
      .filter(contrast(0) sepia(1) hue-rotate(-99deg) saturate(999));
      ._conditional-brightness(red(@color));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure yellow.
    .colorize(@color) when (red(@color) > 0)
                       and (green(@color) = red(@color))
                       and (blue(@color) = 0) {
      .filter(contrast(0) sepia(1) hue-rotate(199deg) saturate(999) invert(1));
      ._conditional-brightness(green(@color));
      ._conditional-opacity(@color);
    }
    
    // Special case for shades of pure grey and white.
    .colorize(@color) when (red(@color) = green(@color))
                       and (green(@color) = blue(@color))
                       and not (blue(@color) = 0) // We've optimized these before.
                       and not (blue(@color) = 127)
                       and not (blue(@color) = 128) {
      .filter(contrast(0) brightness(round(blue(@color) / 255 * 2 + .00765, 3)));
      ._conditional-opacity(@color);
    }
    
    .colorize(@color) when (default()) {
      // General case not figured out yet.
    }
    

    If you want to play with it, here's a codepen (it auto-compiles LESS).

    Note that this is not good enough, and if you post an answer which is better (including using a different method to solve the problem), I may accept yours, and will not accept my own unless it can represent any given color (which it currently can't; and I may have given up on it for now).


  2. I have sometimes tried to achieve what you want, and haven’t succeded.

    You have anyway an alternative, using blend modes:

    div {
        background-color: green;
        mix-blend-mode: color;
        position: absolute;
        width: 200px;
        height: 400px;
    }
    <div></div>
        <img src="http://upload.wikimedia.org/wikipedia/commons/thumb/a/a0/Hsl-hsv_models.svg/400px-Hsl-hsv_models.svg.png" height="400">

    I miss the transparency requirement. Let’s try again :-). Drawback: you need to set the image 2 times.

    #test1 {
      background: linear-gradient(red, red), url("http://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png");
      width: 100%;
      height: 500px;
      background-blend-mode: hue;
      -webkit-mask-image: url("http://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png");
    }
    
    body {
      background-color: lightblue;
    }
    <div id="test1">
    </div>

    Ok; let’s say that the wanted result is: you have an image, that will act as a mask. You want to use this mask to set a colored overlay over an existing image, but you want the color to be specified in the CSS styles, so that it is easily editable.

    If it is ok for you to change the images, so that the channel to be used is luminosity instead of alpha, the following example can be your solution
    you need an filter with gray and black colors , like this

    mask file

    .test {
            width: 200px;
        height: 200px;
        display: inline-block;
        background-image: url(http://i.stack.imgur.com/kxKXy.png); 
        background-size: cover;
        background-blend-mode: exclusion;
        mix-blend-mode: hard-light;
    }
    
    .testred {
        background-color: red;
    }
    
    .testblue {
        background-color: blue;
    }
    
    body {
        background: repeating-linear-gradient(45deg, lightblue 0px, lightyellow 50px);
    }
    <div class="test testred"></div>
    <div class="test testblue"></div>
    Login or Signup to reply.
  3. You will have to create an SVG filter referenced through a CSS filter to get close to this. It is not a true overlay blend – that requires blend-mode. But I think it actually gets you what you want. Please be aware that hue-rotation in filters is basically broken – it’s only an approximation in RGB space which gets saturated colors VERY wrong. (It’s actually using the original SVG Filter math under the covers).

    <svg width="800px" height="600px">
    <defs>
      <filter id="fakeOverlay">
        <feColorMatrix type="luminanceToAlpha" result="L2A"/>
        <feFlood flood-color="cyan" result="colorfield"/>
        <feBlend mode="multiply" in="L2A" in2="colorfield"/>
        <feComposite operator="in" in2="SourceGraphic"/>
      </filter>
      </defs>
    <image filter="url(#fakeOverlay)" width="800" height="400" xlink:href="http://i.stack.imgur.com/Mboab.png"/>
      
    </svg>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search