The CSS filter property has a number of filter functions.
I'm looking to "wash out" the image I'm applying this to, as if I was overlaying a solid white layer on top of the image and controlling its opacity. It should fade from the original image to a fully white image using a factor custom property (--fade: 0.5;
) that can be animated.
At first glance, the brightness()
filter seems like what I'd want. However this actually multiplies the chosen factor by each pixel, brightening the light pixels but keeping the already-dark pixels dark (when given a value above 1, or darkening the bright pixels if below 1).
I'm surprised by the lack of a filter which overlays a translucent white color over the top of the image. How can I still achieve this with a CSS filter
property?
Original |
---|
CSS filter with brightness(1.5) |
CSS filter with brightness(1.9) |
---|---|
Desired (overlayed 50% opacity white) | Desired (overlayed 90% opacity white) |
---|---|
--factor: 0.5; /* Should range only between 0 and 1 */
filter: invert(calc(var(--factor) / 2)) brightness(calc(1 + var(--factor)));
This is achieved by combining the brightness()
function with the invert()
function as shown below. The trick is to get invert
to reach 0.5
and brightness
to reach 2
when the desired brightness is 100%. The result is a very close approximation of the desired filter which should be sufficient for producing the visually intended result, even when the --factor
parameter is animated between 0 and 1.
Solution at --factor: 0.5 |
Solution at --factor: 0.9 |
---|---|
Observe that invert(0.5)
produces a fully-gray image, where every pixel is halfway along its path towards its inverse, meaning they all meet at 0.5 (middle gray). Run the code snippet below for invert(factor / 2)
. At invert(1 / 2)
, the result of this operation is mapping the blacks, midtones, and whites all to middle gray.
But we actually want to map the blacks, midtones, and whites to white, which is exactly double middle gray. Since brightness()
just multiplies every pixel by its given value, we can double the pixels using brightness(2)
. Run the code snippet below for brightness(1 + factor)
.
Combining both functions together, we first map each pixel along the path to middle gray, then double it to white.
document.addEventListener("DOMContentLoaded", () => {
const img = document.querySelector("img");
const input = document.querySelector("input");
const update = () => document.body.style.setProperty("--factor", input.value);
update();
input.addEventListener("input", update);
});
#invert:checked ~ div img {
filter: invert(calc(var(--factor) / 2));
}
#brightness:checked ~ div img {
filter: brightness(calc(1 + var(--factor)));
}
#combined:checked ~ div img {
filter: invert(calc(var(--factor) / 2)) brightness(calc(1 + var(--factor)));
}
#ground-truth:checked ~ div::after {
content: "";
background: rgba(255, 255, 255, var(--factor));
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
div {
display: inline-block;
position: relative;
}
<label><code>factor</code>:</label>
<input type="range" min="0" max="1" step="any" value="0.5" /><br />
<input type="radio" name="choice" id="invert" value="invert" />
<label for="invert"><code>invert(factor / 2)</code></label><br />
<input type="radio" name="choice" id="brightness" value="brightness" />
<label for="brightness"><code>brightness(1 + factor)</code></label><br />
<input type="radio" name="choice" id="combined" value="combined" checked />
<label for="combined"><code>invert(factor / 2) brightness(1 + factor)</code> (approximate filter)</label><br />
<input type="radio" name="choice" id="ground-truth" value="ground-truth" />
<label for="ground-truth">desired ground truth (correct filter)</label><br />
<div>
<img src="https://i.sstatic.net/k53TSb8R.png" alt="Test image" style="display: block" />
</div>
For some fun math exploration, I've analyzed the error between the desired and approximated filters.
The result is generally correlated with the linear interpolation of white over the image, but the equations are not exactly the same.
The equation for the desired filter is:
f(factor, pixel) = (pixel) - (pixel)(factor) + (factor)
The equation for the approximated filter is:
f(factor, pixel) = (pixel) + (0.5 - pixel)(factor)(factor) + (0.5)(factor)
This approximation does technically have a small region where white values are clipped (shown above the gray ceiling plane below). A comparison of the correct and approximated filters, as functions of the chosen factor and the input pixel channel brightness, can be seen in this 3D graph: https://www.desmos.com/3d/ldxipd3j1n
Z = 1