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:
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).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:
#AC9977
and the color we want, and use hue-rotate
.saturate
.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?
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
).
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.
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.
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.
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.
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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With