Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[filter-effects-2] Add recolor() shorthand #334

Open
gitmullany opened this issue Mar 6, 2019 · 16 comments
Open

[filter-effects-2] Add recolor() shorthand #334

gitmullany opened this issue Mar 6, 2019 · 16 comments

Comments

@gitmullany
Copy link

A popular request on StackOverflow in the css-filters tag is to use filters to recolor an input to a target color. This is trivial in SVG filters using the fifth column of feColorMatrix. In the absence of a recolor() shorthand in CSS, people are chaining together hue-rotate(), sepia(), invert() and saturate() filters to try to get to an approximation. It's both inefficient and inaccurate given hue-rotate's unfortunate tendency to clip result values.

I would like to propose adding a new CSS filter shorthand - recolor() - that takes a single color value as its argument - to this spec section: https://www.w3.org/TR/filter-effects-1/#ShorthandEquivalents

13.1.11 recolor

<filter id="recolor">
  <feColorMatrix type="matrix"
             values="0 0 0 0 [unitized red channel value]
                           0 0 0 0 [unitized green channel value]
                           0 0 0 0 [unitized blue channel value]
                           0 0 0 1 0"/>
</filter>
@karip
Copy link

karip commented Mar 31, 2019

I like this idea! But, there's actually many ways to do recolorization. I have summarized the most used ones below. I also created a CodePen example page, which shows how they affect images. The example page works best with Firefox.

I've added a parameter to control the proportion of the conversion. It is called amount.

sepia(amount)

This is the existing colorization method in CSS shorthands. It preserves black and converts white to yellowish. The amount parameter controls the proportion of the conversion.

CSS shorthand tricks

This is a colorization hack made using the existing CSS shorthand filters. Typically, invert(), sepia(), hue-rotate() and saturate() are used.

References:

recolor(amount, color)

This is the function proposed by the original poster. It colorizes all pixels to the same color.

tint(amount, color)

Preserves black and converts white to the desired color. This is a bit like sepia(), but uses a user defined color.

Alternatively, it could convert black to the desired color and preserve white.

References:

duotone(amount, color1, color2)

This takes two colors. One color for black and one color for white. Intermediate colors get colors between those two.

Setting both colors to the same color will give the same result as recolor.

References:

tritone(amount, color1, color2, color3)

This takes three colors. One for highlights (white), one for midtones and one for shadows (black).

Setting all colors to the same color will give the same result as recolor.
Setting the midtone color to be the average of the two other colors will give the same result as duotone.

Note: For some reason, midtones affect the #c0c0c0 gray instead of #808080 gray in Firefox. Ideally, #808080 gray should be affected.

Note: It is possible to extend tritone for any number of colors. Tritone can be implemented with feComponentTransfer in the table mode and additional colors could just be added to the color table. I don't know if that would be useful..

After Effects has a tritone effect. It seems to affect #808080 gray and gives better results than Firefox.

References:

Apple Motion: tint

Apple Motion has a tint filter, which affects only midtones. It seems to be the same as tritone with black, a user defined color and white.

Android LightingColorFilter

Android has a filter to colorize images. It takes two colors. The first one is used for multiplication and the second one for addition, like this:

R' = R * colorMultiply.R + colorAdd.R
G' = G * colorMultiply.G + colorAdd.G
B' = B * colorMultiply.B + colorAdd.B

If colorMultiply is black, then this gives the same result as recolor.

This isn't as intuitive as duotone, so I wouldn't like to see this in CSS.

References:

Layer Color Blend

A usual method to colorize images in Photoshop and other image editors is to create a solid color layer in the desired color, place it over the image and then blend the color layer with the image using the "Color" blend mode.

The downside is that it also colorizes transparent pixels, so I wouldn't like to see this in CSS.

References:

Summary

There's probably also other colorization methods. All other suggestions are welcome!

I don't expect to see all these implemented in CSS. I think duotone and tritone would be most useful. They are easy to use and they cover most use cases (recoloring text, icons, photos). Duotone works great for simple icons. Tritone is similar to highlight-midtone-shadow filters in photo editors.

@dirkschulze dirkschulze changed the title [filter-effects] Add recolor() shorthand [filter-effects-2] Add recolor() shorthand Apr 19, 2019
@tabatkins
Copy link
Member

tabatkins commented May 9, 2019

I like this a lot!

So, duotone() and tritone() seem great directly as stated. Useful, easy to understand, good names.

For the single-color ones, functionality seems good, but naming is harder.

So, tint() technically means "adding white to a base color", lightening it. (And its opposite, "shade", means to darken a color by adding black.) So saying tint(red) to mean "turn all the white into red" is somewhat wrong; it's the reverse of what "tinting red" should mean.

(People do casually use "tint" in these sorts of ways, but it's extremely varied; if we're relying on casual definitions, we can't rely on a meaningful distinction between "tint" and "shade" to distinguish our "shift black towards the color" from "shift white towards the color" functions.)

I suggest we avoid the issue entirely by just not providing the single-color functions. People can just write duotone(<color>, white) if they want a sepia-like effect, or duotone(black, <color>) if they want to do the opposite.

(Unlike the "you can implement duotone() with tritone()" argument, where you need to do some color math to make it happen, this requires no math. You just fill in one of the arguments with "white" or "black". That seems easy enough to make it reasonable to skip on providing a convenience function.)


I agree that Apple's "tint" doesn't need a convenience function, as tritone() seems to handle it just fine.

I'm not even sure what the Android Lighting thing is doing, semantically. I always look askance at anything trying to add/multiply individual channels; it's smuggling in masking data via color channels in a way that doesn't feel very kosher.

I'm not sure what "color blending" is, mathematically, in Photoshop. But that article's examples seems moderately useful? What do you mean by "colorizes transparent pixels" - do they stay transparent, just change their (undetectable) hue?

@AmeliaBR
Copy link

AmeliaBR commented May 9, 2019

Some thoughts:

In general, I'm very supportive of adding color effects beyond sepia() in the shorthand function. The sepia-saturate-hue-rotate sequence is painful & imprecise, but I've seen it recommended in multiple tutorials and demos. So let's make it easier to do better.

Thinking of the use cases, I'd recommend a single recolor() or colorize() function that accepts 1 to 3 color values:

  • 1 color value sets the target value for black pixels in the input. (White pixels stay white, and all other colors get scaled up by the change in the black point.) This addresses the common case of applying a “fill” color to a solid black icon.

  • 2 color values set the target black and white points (duotone effect).

  • 3 color values set the black, midtone, and white points (tritone effect)

The function could also accept a percentage (or number 0-1) for reducing the overall effect (blending with the original colors), equivalent to the parameter to the sepia() function.

@tabatkins
Copy link
Member

That sounds pretty good to me. I like recolor().

@gitmullany
Copy link
Author

gitmullany commented May 9, 2019 via email

@tabatkins
Copy link
Member

What's the use-case for "set all pixels to this color"? If it's just recoloring icons, as long as they start out black the behavior Amelia proposed will work.

@AmeliaBR
Copy link

I can see use cases for @gitmullany's proposal: grey-ing out a multi-color icon for a disabled function or otherwise forcing a multi-color icon to match a theme color.

It could also help with progressive enhancement if people are currently using red icons + hue-rotate() to control the colour (which gives better results than hacking with sepia() but it not as good as directly setting the color).

Basically, this would be equivalent to expanding recolor(blue) to recolor(blue, blue) instead of to recolor(blue, white).

@BigBadaboom
Copy link

BigBadaboom commented May 10, 2019

Having an icon of one colour (not necessarily black), and wanting to change it to something else, seems to be a common use case. At least based on Stack Overflow questions.

They can always use saturate(), I guess. But the name recolor() suggests that it works on images that are already coloured. What about having an implicit saturate() as part of recolor()? If not, then perhaps colorize() is the more accurate name.

@svgeesus
Copy link
Contributor

But the name recolor() suggests that it works on images that are already coloured. What about having an implicit saturate() as part of recolor()? If not, then perhaps colorize() is the more accurate name.

Saturate increases saturation (multiplies it by a scaling factor). For a grayscale image, the saturation is zero; multiplying it by anything still results in zero. On a hue wheel, all pixels are on the central achromatic axis - there is no hue direction that can be amplified.

I don't have an opinion on recolor vs. colorize except that the latter implies there is no existing color while the former expresses no opinion regarding any existing colors which are first discarded, then the grayscale image is colorized.

@BigBadaboom
Copy link

I meant saturate(0) of course. That was just a typo.

except that the latter implies there is no existing color

Yes. That was my point also. If you are expected to grayscale the image first, then recolor() seems like the wrong verb.

@karip
Copy link

karip commented May 12, 2019

I like the idea of having only one shorthand function.

Authors should not be expected to grayscale the image first. It should be part of the shorthand's functionality. Authors don't necessarily know which grayscale values to use for amount values less than 1, such as this: colorize(0.2, red).

I don't have a strong opinion about the name recolor() vs. colorize(). Photoshop and Gimp have an effect called colorize, but it works like colorize(black, <color>), which is useful for adjusting photos (it's like sepia with a custom color).

I think that the single color case should create an image with only one color. I'm not a color expert, but it feels more logical:

1 color -> all pixels get converted to the same color (not two colors, where the other one is black or white)
2 colors -> two colors
3 colors -> three colors

I found out the reason why my CodePen example didn't produce exactly correct results for tritones. I had misspelled the color-interpolation-filters property. I'll update the example when I have more time.

@gitmullany
Copy link
Author

gitmullany commented May 14, 2019

Based on the preceding comments, can I suggest something along the following lines (I'm not an experienced spec writer - so please treat with appropriate patience/forebearance)

Using https://www.w3.org/TR/filter-effects-1/ as the base document:

add to Section 6.1


recolor() = recolor( colorA, colorB, colorC )

Applies a monotone, duotone or tritone effect on the input image based on the number of input colors. The monotone effect discards the input image's color channels replacing them with ColorA. The duotone and tritone effects map the luminosity of the input image to new range(s) in each color channel based on the red, green and blue values specified in colorA and colorB (and colorC if present). The markup equivalent of this function is given below.

Default value when omitted is opaque black
The initial value for interpolation is opaque black.


And added to Section 13.1.11

recolor(colorA) [where colorA-r is the unitized value of the red color channel etc.]

<filter id="recolor-1color">
  <feColorMatrix type="matrix"
             values="0 0 0 0 colorA-r
                           0 0 0 0 colorA-g
                           0 0 0 0 colorA-b
                           0 0 0 1 0"/>
</filter>

recolor(colorA, colorB [, colorC])
<filter id="recolor-2pluscolor">
  <feColorMatrix type="matrix"
             values="0.2126  0.7152 0.0722  0 0 
                          0.2126  0.7152 0.0722  0 0 
                          0.2126  0.7152 0.0722  0 0 
                          0 0 0 1 0"/>
<feComponentTransfer>
    <feFuncR type="table" tableValues="colorA-r colorB-r [colorC-r]"/>
    <feFuncG type="table" tableValues="colorA-g colorB-g [colorC-g]"/>
    <feFuncB type="table" tableValues="colorA-b colorB-b [colorC-b]"/>
</feComponentTransfer>
</filter>

We could express all of this using feComponentTransfer - but since feColorMatrix has been made very fast in existing browsers and feComponentTransfer is generally quite slow - I'm hoping that the single color case can be supported with high performance with minimal work. This might be a misunderstanding of how the implementors approach spec implementation - but I thought I'd suggest it.

As this stands, this preserves the alpha channel of the input image. There may be cases where people want to adjust the alpha channel or apply a % effect - is it ok not to expose these capabilities in a shorthand (vs making users write a SVG filter.)

@karip
Copy link

karip commented May 19, 2019

Is there a reason why you don't want to have amount (to apply a % effect) in the shorthand function?

User interfaces often use an animated color fading to change their states between active and inactive. That effect could be created with amount, which can be animated with CSS animations and transitions. SVG filters can't be animated with CSS. Also, the amount parameter would make recolor() similar to the sepia(amount) colorization function.

Here are the matrices to calculate the three versions of recolor() with amount to control the proportion of the conversion. If amount is 0, then the original image is shown. If amount is 1, then the recolored version is shown.

recolor() = recolor( amount, colorA, colorB, colorC )

The one color case (monotone) is as performant as the version without the amount value:

<filter id="recolor-1color" color-interpolation-filters="sRGB">
  <feColorMatrix type="matrix"
             values="
    [1 - amount] 0 0 0 ([amount] * [colorA_r])
    0 [1 - amount] 0 0 ([amount] * [colorA_g])
    0 0 [1 - amount] 0 ([amount] * [colorA_b])
    0 0 0 1 0"/>
</filter>

The two color case (duotone) can be written using feColorMatrix, feComponentTransfer and feComposite. feComposite is used to blend the original image and the colorized image.

<filter id="recolor-2color" color-interpolation-filters="sRGB">
    <feColorMatrix type="matrix"
                   values="0.2126 0.7152 0.0722 0 0
                           0.2126 0.7152 0.0722 0 0
                           0.2126 0.7152 0.0722 0 0
                           0 0 0 1 0" />
    <feComponentTransfer>
      <feFuncR type="table" tableValues="[colorA_r] [colorB_r]"/>
      <feFuncG type="table" tableValues="[colorA_g] [colorB_g]"/>
      <feFuncB type="table" tableValues="[colorA_b] [colorB_b]"/>
    </feComponentTransfer>
    <feComposite operator="arithmetic" k1="0" k2="[amount]" k3="[1 - amount]" k4="0"
                 in2="SourceGraphic"/>
</filter>

It can be optimized to use only feColorMatrix, so it is as performant as the one color case. It requires pre-calculation of the color matrix and then the values are set to feColorMatrix:

// grayscale matrix
G = [ 0.2126, 0.7152, 0.0722, 0, 0,
      0.2126, 0.7152, 0.0722, 0, 0,
      0.2126, 0.7152, 0.0722, 0, 0,
      0, 0, 0, 1, 0,
      0, 0, 0, 0, 1 ];

// duotone effect matrix controlled by amount
D = [ ((colorB_r - colorA_r) * amount), 0, 0, 0, (colorA_r * amount),
      ((colorB_g - colorA_g) * amount), 0, 0, 0, (colorA_g * amount),
      ((colorB_b - colorA_b) * amount), 0, 0, 0, (colorA_b * amount),
      0, 0, 0, 1, 0,
      0, 0, 0, 0, 1 ];

// multiply duotone and grayscale matrices so that the original image is affected by both
M = mat_mult(D, G);

// add in the original image
M[0, 0] += (1 - amount);
M[1, 1] += (1 - amount);
M[2, 2] += (1 - amount);

<filter id="recolor-2color-optimized" color-interpolation-filters="sRGB">
    <feColorMatrix type="matrix"
                   values="[values-of-M]"/>
</filter>

The three color case (tritone) can only be written using feColorMatrix, feComponentTransfer and feComposite. It is the same as the non-optimized two color case except feComponentTransfer has three colors:

<filter id="recolor-3color" color-interpolation-filters="sRGB">
    <feColorMatrix type="matrix"
                   values="0.2126 0.7152 0.0722 0 0
                           0.2126 0.7152 0.0722 0 0
                           0.2126 0.7152 0.0722 0 0
                           0 0 0 1 0" />
    <feComponentTransfer>
      <feFuncR type="table" tableValues="[colorA_r] [colorB_r] [colorC_r]"/>
      <feFuncG type="table" tableValues="[colorA_g] [colorB_g] [colorC_g]"/>
      <feFuncB type="table" tableValues="[colorA_b] [colorB_b] [colorC_b]"/>
    </feComponentTransfer>
    <feComposite operator="arithmetic" k1="0" k2="[amount]" k3="[1 - amount]" k4="0"
                 in2="SourceGraphic"/>
</filter>

Here's a new CodePen example with those matrices. The amount parameter has been implemented for all these cases. The example works the best with Firefox and Chrome.

All these recolor() shorthand filters operate in the sRGB colorspace, just like the other existing shorthand filters.

@gitmullany
Copy link
Author

I hadn't put in an amount because these shorthands are transitionable without an amount (I didn't add the spec language for that, but it's in another section). But having an amount seems useful & I think what you're proposing is more flexible. Also, I can't imagine it being much more of an implementation effort. So +1 from me.

@tabatkins
Copy link
Member

@AmeliaBR Yeah, and looking into some other uses of color-mixing, it looks like things like "just mix a little purple into all of these images, to make them visually match a little more" is a useful thing people do. So yeah, recolor(purple) meaning the same as recolor(purple, purple) works for me, and means our definition doesn't have a special-case for single colors. ^_^

@michaelmcleodnz
Copy link

Hi, I'm not sure if this is still in the works, but if it helps expedite this, the single-argument version can be accomplished without a matrix:

<filter id="colorize"><feFlood flood-color="[color]"/><feComposite in2="SourceAlpha" operator="in"/></filter>

or, including an amount parameter:

<filter id="colorize">
  <feFlood flood-color="[color]"/>
  <feComposite in2="SourceAlpha" operator="in"/>
  <feComposite operator="arithmetic" k1="0" k2="[amount]" k3="[1 - amount]" k4="0" in2="SourceGraphic"/>
</filter>

I would like this because the SVG-filter version can't respond to changes in color.
Here is a minimal example where the CSS-filter would be useful for using with currentColor:

<html>
<head>
  <style>
    a[target=_blank]::after {
      content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="10" height="10" viewBox="0 0 20 20"><path d="M7 3h-6v16h16v-6M9,11 18,2M13 1h6v6" fill="none" stroke="black" stroke-linecap="square" stroke-width="2"/></svg>');
      filter: colorize(currentColor);
      margin-left: 0.5ch;
    }
    
    /* Closest fallback is to define a separate filter for each anticipated colour: */
    a[target=_blank]::after { filter: url(#colorize-blue); }
    a[target=_blank]:active::after { filter: url(#colorize-red); }
    /* Doesn't work because of privacy restrictions on :visited */
    a[target=_blank]:visited::after { filter: url(#colorize-purple); }
  </style>
</head>
<body>
  <a href="https://example.org" target="_blank">This link opens in a new window</a>
  
  <svg width="0" height="0">
    <filter id="colorize-blue"><feFlood flood-color="blue"/><feComposite in2="SourceAlpha" operator="in"/></filter>
    <filter id="colorize-red"><feFlood flood-color="red"/><feComposite in2="SourceAlpha" operator="in"/></filter>
    <filter id="colorize-purple"><feFlood flood-color="purple"/><feComposite in2="SourceAlpha" operator="in"/></filter>
  </svg>
</body>
</html>

This adds the following image to external links, and allows it to respond to changes in text color:
This link opens in a new window

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants