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
).
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.
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
.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>
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>
Check out this article on CSS Masks: http://thenittygritty.co/css-masking
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).