Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Emulate Photoshop's "Color Overlay" using CSS filters?

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.

like image 292
Camilo Martin Avatar asked Apr 05 '15 15:04

Camilo Martin


People also ask

How to overlay an image with color in CSS?

This article will introduce a few methods to overlay an image with color in CSS. We can use the rgba () function to create a color overlay over an image. We can use the function as the value of the background property. The syntax of the rgba () function looks like this.

How to create an opaque overlay on an image?

Here red, green and blue color is set to a value between 0-255 and an opacity ranging from 0-1. If the value of opacity is set to 0, then it is completely transparent, and if the value of opacity is set to 1, it will be completely opaque. We can create an overlay simply by adding a color above an image, decreasing its opacity.

How do I add color to an image in Photoshop?

There are two basic ways to create this effect in Photoshop. Method 1: Add a solid color layer ( Layer > New Fill Layer > Solid Color...) in the color you want, and change its Blend Mode to "Color" or "Hue" -- experiment to see which works best for the particular image.

How do I change the opacity of an image layer?

Create a new layer and fill it with any color you want. Move this layer below the image you want to manipulate and set the layer Blend Mode of the image layer to Multiply. You can then modify the opacity of the image layer if you want, but it isn't necessary. It may also help to desaturate or grayscale the image layer. Show activity on this post.


1 Answers

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).

like image 51
Camilo Martin Avatar answered Oct 10 '22 04:10

Camilo Martin