I was wondering if it is possible to perform a hue/saturation color transformation, using css/svg filters, in the same way that Photoshop Hue/Saturation works.
Essentially, from what I've read, Photoshop internally converts all pixels from an RGB representation to a HSL representation, and basically increase the hue (H), saturation (S) or lightness (L) values according to what the user defines using the sliders, as seen in the image below:
Here, I chose the default range (Master
), which means that all hue values will be considered.
Using CSS hue-rotate
filter, we can have an approximate result to this:
Some colors are not exacly the same, due to the issues pointed out in this answer from another question: https://stackoverflow.com/a/19325417/4561405. (That's ok for me, I don't need it to be as accurate as Photoshop.)
So, essentially, the internal procedure for both approaches seems to be roughly the same.
Now, Photoshop also allows me to define a range of colors to be considered for adjustment, as seen in the picture below:
Essentially, what this range of values means is that any color with a hue
vaue that falls off the range limits will be ignored. Hence, with my example, colors #1, #2 and #5 are left untouched.
I am trying to do the same thing using CSS os SVG filters, but I can't find a way to do it. I'm reading the filter effect documentation (https://drafts.fxtf.org/filter-effects/), to see if there's anything there that I could use to define the ranges, but I can't find anything.
Does anyone knows if there's a way to do what I intend to? Any valid alternative to CSS filters, perhaps?
EDIT: this snippet show what I am getting with filter: hue-rotate(45deg)
, and the result that I want to obtain.
.block-wrapper {
width: 100%;
height: 50px;
display: flex;
margin-bottom: 10px;
}
.block {
width: 20%;
height: 100%;
}
.b1 {
background-color: rgb(29 85 34);
}
.b1-goal {
background-color: rgb(29 85 34);
}
.b2 {
background-color: rgb(32 53 79);
}
.b2-goal {
background-color: rgb(32 53 79);
}
.b3 {
background-color: rgb(175 43 52);
}
.b3-goal {
background-color: rgb(173 75 51);
}
.b4 {
background-color: rgb(172 94 50);
}
.b4-goal {
background-color: rgb(166 160 44);
}
.b5 {
background-color: rgb(96 230 33);
}
.b5-goal {
background-color: rgb(96 230 33);
}
.hue-45 {
filter: hue-rotate(45deg);
}
<h3>Original</h3>
<div class="block-wrapper">
<div class="block b1"></div>
<div class="block b2"></div>
<div class="block b3"></div>
<div class="block b4"></div>
<div class="block b5"></div>
</div>
<h3>With <code>hue-rotate: 45deg;</code></h3>
<div class="block-wrapper hue-45">
<div class="block b1"></div>
<div class="block b2"></div>
<div class="block b3"></div>
<div class="block b4"></div>
<div class="block b5"></div>
</div>
<h3>What I want: update hue only for red colors</h3>
<div class="block-wrapper">
<div class="block b1-goal"></div>
<div class="block b2-goal"></div>
<div class="block b3-goal"></div>
<div class="block b4-goal"></div>
<div class="block b5-goal"></div>
</div>
This is actually quite difficult to do straight away, since the feColorMatrix
handles colors in RGB and there's not yet a way to do that in HSL. (Correct me if I'm wrong.)
So I found a solution that comes close to what you might want. The idea is to first mask away the colors you don't want to hue-rotate. Then hue-rotate the remainder and paste that on top of the original.
The code for the SVG with filter looks something like:
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<filter id="partial-hue-rotation">
<!--
1) Mask away the colors that shouldn't be hue-rotated.
This is done based on the R-channel value only.
The R-channel value comes in at [0-1],
so multiply it by 255 to get the original value (as in rgb()).
Then subtract (lowest R-channel value of color range - 1)
to leave all color with a R-channel value higher than that.
-->
<feColorMatrix
color-interpolation-filters="sRGB"
type="matrix"
in="SourceGraphic"
result="only-red-visible"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
255 0 0 0 -171"
/><!-- Colors with R-channel > 171 will be left (and thus effected). -->
<!--
2) Apply hue rotation to remaining colors.
-->
<feColorMatrix
type="hueRotate"
values="45"
in="only-red-visible"
result="rotated-part"
/>
<!--
3) Now paste the rotated part on top of the original.
-->
<feMerge>
<feMergeNode in="SourceGraphic" />
<feMergeNode in="rotated-part" />
</feMerge>
</filter>
<!--
This filter is to check if the right range is hue-rotated.
All white areas will be rotated.
The bottom row of values can be copied over the bottom row
of the filter above.
-->
<filter id="test-partial-hue-rotation">
<feColorMatrix
color-interpolation-filters="sRGB"
type="matrix"
in="SourceGraphic"
result="marked-range"
values="0 0 0 0 1
0 0 0 0 1
0 0 0 0 1
255 0 0 0 -171"
/><!-- Colors with R-channel > 171 will be white. -->
<feMerge>
<feMergeNode in="marked-range" />
</feMerge>
</filter>
</svg>
To apply the filter, just add filter: url(#partial-hue-rotation)
to an element's CSS.
To test to see if you are effecting the right colors/parts, you can add filter: url(#test-partial-hue-rotation);
to an element's CSS. All white parts will be hue-rotated. (You might want to set the background color of the parent to black to see it.)
Notes and limitations:
255
in the first column and -X
in the last column of the alpha row in the matrix. (255
in second column for B-value selection, etc)hue-rotate
and feColorMatix
's hueRotate
is calculated (source). This might be eliminated by adding color-interpolation-filters="sRGB"
to the hueRotate
feColorMatrix
tag (not sure).Anyway, it is a first attempt at this and maybe this approach can help you on your way. :)
More information:
For more information on how the color matrix for hue-rotate is calculated, see the C++ implementation of the Chrome browser.
See also matrix equivalents of shorthand filters.
And this post.
So after some reading and thinking, I came up with the idea to use the blend mode difference
to provide the filter with the information about which colors are 'in range' and should be effected.
This works as follows:
<feBlend>
in difference mode with the original and the flood. (The darkest parts have the most overlap with the mid-color, e.g. are closest to it on the color wheel.)feColorMatrix
we now translate this greyscale to alpha values and at the same time map these to have the lowest 2/3 be transparent (will be removed).feComposite
to mask the original image and apply the effect (hue rotation) to this part only.The mid-point and width of the to-be-effected color range can be chosen:
flood-color
of the feFlood
. (Use fully saturated and 50% lightness colors for best effect, so #ff0000
, #00ff00
, etc.)feColorMatrix
with result="alpha-mask"
. Example: keep 1/3 of the color wheel gives an offset value of (2/3) * -255
.Updated working JSFiddle here. (The bottom one, filter #partial-hue-rotation.)
Note:
The hue rotation effect does a horrible job, so not sure what goes wrong there, but the resulting colors are the same with CSS's hue-rotate()
filter.. so, yeah..
Unfortunately, the filter above does not work correctly for all colors. For a SVG filter that correctly converts the SourceGraphic
to greyscale hue values (where 0deg = black
and 360deg = white
), have a look at the #hue-values
filter I made in this JSFiddle.
If you want to only apply filter effects to all reds/greens/blues/cyans/magentas/yellows, the #tonegroup-select
filter in the same JSFiddle can be used.
The code of this filter is:
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="tonegroup-select"
x="0%" y="0%"
width="100%" height="100%"
primitiveUnits="objectBoundingBox"
color-interpolation-filters="sRGB"
>
<!-- Compare RGB channel values -->
<feColorMatrix type="matrix" in="SourceGraphic" result="test-r-gte-g"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 -255 0 0 1"
/>
<feColorMatrix type="matrix" in="SourceGraphic" result="test-r-gte-b"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 0 -255 0 1"
/>
<feColorMatrix type="matrix" in="SourceGraphic" result="test-g-gte-r"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -255 255 0 0 1"
/>
<feColorMatrix type="matrix" in="SourceGraphic" result="test-g-gte-b"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 -255 0 1"
/>
<feColorMatrix type="matrix" in="SourceGraphic" result="test-b-gte-r"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -255 0 255 0 1"
/>
<feColorMatrix type="matrix" in="SourceGraphic" result="test-b-gte-g"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -255 255 0 1"
/>
<!-- Logic masks for tone groups -->
<!-- For example: all red colors have red channel values greater than or equal to the green and blue values -->
<feComposite operator="in" in="test-r-gte-g" in2="test-r-gte-b" result="red-mask" />
<feComposite operator="in" in="test-g-gte-r" in2="test-g-gte-b" result="green-mask" />
<feComposite operator="in" in="test-b-gte-r" in2="test-b-gte-g" result="blue-mask" />
<feComposite operator="in" in="test-g-gte-r" in2="test-b-gte-r" result="cyan-mask" />
<feComposite operator="in" in="test-b-gte-g" in2="test-r-gte-g" result="magenta-mask" />
<feComposite operator="in" in="test-r-gte-b" in2="test-g-gte-b" result="yellow-mask" />
<!-- Select all colors in tone group -->
<!-- Note: uncomment the right tone group selection here -->
<!-- Note: greyscale colors will always be selected -->
<feComposite operator="in" in="SourceGraphic" in2="red-mask" result="selection" />
<!-- <feComposite operator="in" in="SourceGraphic" in2="green-mask" result="selection" /> -->
<!-- <feComposite operator="in" in="SourceGraphic" in2="blue-mask" result="selection" /> -->
<!-- <feComposite operator="in" in="SourceGraphic" in2="cyan-mask" result="selection" /> -->
<!-- <feComposite operator="in" in="SourceGraphic" in2="magenta-mask" result="selection" /> -->
<!-- <feComposite operator="in" in="SourceGraphic" in2="yellow-mask" result="selection" /> -->
<!-- Cut selection from original image -->
<!-- Note: use same mask for `in2` attribute as with selection -->
<feComposite operator="out" in="SourceGraphic" in2="red-mask" result="not-selected-source" />
<!-- Apply effects to `selection` only -->
<feColorMatrix
type="saturate"
values="0"
in="selection"
result="edited-selection"
/>
<!-- After all effects, adjustments, etc -->
<!-- the last `result` output name should be "edited-selection" -->
<!-- Bring it all together -->
<feMerge>
<!-- <feMergeNode in="selection" /> --><!-- Uncomment to check selection -->
<feMergeNode in="not-selected-source" />
<feMergeNode in="edited-selection" />
</feMerge>
</filter>
</defs>
</svg>
In the comments inside the code, you find further information on the working and instructions on how to use it.
For more information and reference, have a look at: