3

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: photoshop hue/saturation (default)

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: hue comparison

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: hue adjustment with color range

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>

8
  • Please provide us with a Minimal, Reproducible Example
    – Rojo
    Commented Sep 22, 2021 at 17:16
  • I thought I just did :) I want to be able to change only the red colors in an image, using CSS filters (or some we-based alternative) Commented Sep 22, 2021 at 17:38
  • When people provide a link, you should click on it and read the article. The first sentence of the article states, "When asking a question, people will be better able to provide help if you provide code..."
    – Rojo
    Commented Sep 22, 2021 at 17:56
  • Hi, it would be useful if you could provide code - for example just reading your question and looking at the images I'd assumed you wanted to change blocks of color. I see from a comment that you want to do things within a range in a whole image which would have been obvious from code. I don't think that sort of filtering is possible with CSS BTW though it would be nice to be proved wrong.
    – A Haworth
    Commented Sep 22, 2021 at 19:30
  • @AHaworth: I included a simple snippet showing what I am able to obtain, and what I want to obtain but don't know exactly how. Commented Sep 22, 2021 at 21:59

1 Answer 1

5

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:

  1. This method only works when the colors you want to hue-rotate can be separated by a single RGB-channel. For example: all colors that have R-values > X. Then you put the 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)
  2. This is not a final solution, since all lighter colors (having R, G and B values that are probably higher then the threshold) will also be hue-rotated.
  3. Obviously, since the alpha channel is used for masking, this only works on opaque colors/content.
  4. Also, this filter is hardcoded for a specific color range and rotational value. So, not quite scalable, but perhaps useful for individual instances.
  5. Apparently there's also a difference between how the CSS 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. :)

Working JSFiddle here

More information:

feColorMatrix documentation

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.

UPDATE: version 2

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:

  1. Fill the entire image (area) with the mid-color of your range (e.g. red).
  2. Use <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.)
  3. Invert the differences and convert them to average greyscale. (Since it's a pure numerical average we need, all channels are taken 0.3333 times.)
  4. Using a 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).
  5. Use feComposite to mask the original image and apply the effect (hue rotation) to this part only.
  6. Then paste the effected part on top of the original.
  7. Done!

The mid-point and width of the to-be-effected color range can be chosen:

  • Mid-color is set as the flood-color of the feFlood. (Use fully saturated and 50% lightness colors for best effect, so #ff0000, #00ff00, etc.)
  • Width is chosen by the alpha channel's offset in the 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..

UPDATE: version 3

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:

8
  • This is wild-- I wish I could give you more than one upvote; nicely done. Commented Sep 23, 2021 at 1:07
  • Uau... this is incredibly helpful and enlightening! Bravo! Thank you. I'll run a few tests to see if this works with all my scenarios, but I think it will. Commented Sep 23, 2021 at 10:29
  • 1
    For testing, you can use this JSFiddle to test if picking the mid-color gives the right range.
    – Philip
    Commented Sep 23, 2021 at 14:14
  • 1
    You can also only uncomment a specific <feMergeNode> to see the result up to that step for debugging.
    – Philip
    Commented Sep 23, 2021 at 14:25
  • @Philip, just one question: what would be the correct way to apply the same SVG filter twice? I'm trying to do it with CSS like this: filter: url(#tonegroup-select) url(#tonegroup-select); and I was expecting the middle box to turn yellow, but it comes out green Commented Sep 28, 2021 at 16:15

Not the answer you're looking for? Browse other questions tagged or ask your own question.