A List Apart

Menu

Illustration by

Finessing `feColorMatrix`

Have you seen Spotify’s end-of-year campaign? They’ve created a compelling visual aesthetic through image-color manipulation.

Screenshot of Spotify’s end-of-year campaign
Article Continues Below

Image manipulation is a powerful mechanism for making a project stand out from the crowd, or just adding a little sparkle—and web filters offer a dynamic and cascadable way of doing it in the browser.

CSS vs. SVG

Earlier this year, I launched CSSgram, a pure CSS library that uses filters and blend modes to recreate Instagram filters.

Image grid from Una Kravets’ CSSGram showing a variety of filters and blend modes that recreate Instagram filters

Now, this could be done with tinkering and blend modes—but one key feature CSS filters lack is the ability to do per-channel manipulation. This is a huge downside. While CSS filters are convenient, they are merely shortcuts derived from SVG and therefore provide no control over RGBA channels. SVG (particularly the feColorMatrix map) gives us much more power and lets us take CSS filters to the next level, granting significantly more control over image manipulation and special effects.

SVG filters

In the SVG world, filter effects are prefixed with fe-. (Get it? For “filter effect.”) They can produce a wide variety of color effects, ranging from blur to generated 3-D textures. The term fe- is a bit loose, though; see the end of this article for a summary of each of the SVG filter effects’ methods.

SVG filters are currently supported in the following browsers:

Screenshot from caniuse.com

Screenshot from caniuse.com.

So yeah, you should be good to go for the most part, unless you need to support IE9 or older. SVG filter support is relatively stable, and is more widespread than CSS filters and blend modes. There are also few major weird bugs, unlike with CSS blend modes (where only Chrome 46 has issues rendering the multiply, difference, and exclusion blend modes).

Note: Some of the 3-D filters, such as feConvolveMatrix, do have known bugs in certain browsers, though feColorMatrix, which this article focuses on, does not. Also, keep in mind that performance will inevitably take a tiny hit when it comes to applying any action in-browser (as opposed to rendering a pre-edited image on the page).

Using filters

The basic layout of an SVG filter goes like this:


<svg>
  <filter id="filterName">
    // filter definition here can include
    // multiple of the above items
  </filter>
</svg>

Within an SVG, you can declare a filter. Most of the time, you’ll want to declare filters within defs of an SVG and can apply them in CSS like so:


.filter-me {
  filter: url('#filterName');
}

The filter URL is relative, so both filter: url('../img/filter.svg#filterName') and filter: url('http://una.im/filters.svg#filterName') are valid.

feColorMatrix

When it comes to color manipulation, feColorMatrix is your best option. feColorMatrix is a filter type that uses a matrix to affect color values on a per-channel (RGBA) basis. Think of it like editing the channels in Photoshop.

This is what the feColorMatrix looks like (with each RGBA value as 1 by default in the original image):


<filter id="linear">
    <feColorMatrix
      type="matrix"
      values="R 0 0 0 0
              0 G 0 0 0
              0 0 B 0 0
              0 0 0 A 0 "/>
  </filter>
</feColorMatrix>

The matrix here is actually calculating a final RGBA value in its rows, giving each RGBA channel its own RGBA channel. The last number is a multiplier. The final RGBA value can be read from top to bottom like a column:


/* R G B A 1 */
1 0 0 0 0 // R = 1*R + 0*G + 0*B + 0*A + 0
0 1 0 0 0 // G = 0*R + 1*G + 0*B + 0*A + 0
0 0 1 0 0 // B = 0*R + 0*G + 1*B + 0*A + 0
0 0 0 1 0 // A = 0*R + 0*G + 0*B + 1*A + 0

Here’s a better visualization:

Hand-drawn sketch showing a schematic visualization of the fecolormatrix

RGB values

Colorizing

You can colorize images by omitting and mixing color channels like so:


<!-- lacking the B & G channels (only R at 1) -->
<filter id="red">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the R & G channels (only B at 1) -->
<filter id="blue">
 <feColorMatrix
    type="matrix"
    values="0   0   0   0   0
            0   0   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the R & B channels (only G at 1) -->
<filter id="green">
  <feColorMatrix
    type="matrix"
    values="0   0   0   0   0
            0   1   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

Here’s what adding the “green” filter to an image looks like:

Photo showing what the addition of the “green” filter would look like

Channel mixing

You can also mix RGB channels to get solid colorizing results:


<!-- lacking the B channel (mix of R & G)
Red + Green = Yellow
This is saying there is no yellow channel
-->
<filter id="yellow">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the G channels (mix of R & B)
Red + Blue = Magenta
-->
<filter id="magenta">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- lacking the R channel (mix of G & B)
Green + Blue = Cyan
-->
<filter id="cyan">
  <feColorMatrix
    type="matrix"
    values="0   0   0   0   0
            0   1   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

In each of the previous examples, we mixed colors in CMYK mode, so removing the red channel would mean that green and blue remain. When green and blue mix, they create cyan. Red and blue make magenta. We still retain some of the red and blue values where they are most prominent, but in areas that lack the two (light areas of white, where all colors are present in the RGB schema, or areas of green), the RGBA values of the other two channels replace them.

Justin McDowell has written an excellent article that explains HSL (hue, saturation, lightness) color theory. With SVG, the lightness value is the luminosity, which we also need to keep in mind. Here, each luminosity level is retained in each channel, so for magenta, we get an image that looks like this:

Photo showing how a magenta effect is produced when each luminosity level is retained in each channel

Why is there so much magenta in the clouds and lightest values? Consider the RGB chart:

RGB chart

When one value is missing, the other two take its place. So now, without the green channel, there is no white, cyan, or yellow. These colors don’t actually disappear, however, because their luminosity (or alpha) values have not yet been touched. Let’s see what happens when we manipulate those alpha channels next.

Alpha values

We can play with the shadow and highlight tones via the alpha channels (fourth column). The fourth row affects overall alpha channels, while the fourth column affects luminosity on a per-channel basis.


<!-- Acts like an opacity filter at .5 -->
<filter id="alpha">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   0   0
            0   0   1   0   0
            0   0   0   .5  0 "/>
</filter>

<!-- increases green opacity to be
     on the same level as overall opacity -->
<filter id="hard-green">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   1   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<filter id="hard-yellow">
  <feColorMatrix
    type="matrix"
    values="1   0   0   1   0
            0   1   0   1   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

In the following example, we’re reusing the matrix from the magenta example and adding a 100% alpha channel on the blue level. We retain the red values, yet override any red in the shadows so the shadow colors all become blue, while the lightest values that have red in them become a mix of blue and red (magenta).


<filter id="blue-shadow-magenta-highlight">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1   1   0
            0   0   0   1   0 "/>
</filter>

Image showing what happens when we reuse the matrix from the magenta example and add a 100% alpha channel on the blue level

If this last value was less than 0 (up to -1), the opposite would happen. The shadows would turn red instead of blue. At -1, these create identical effects:


<filter id="red-overlay">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1  -1   0
            0   0   0   1   0 "/>
</filter>

<filter id="identical-red-overlay">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   0   0   0
            0   0   0   1   0 "/>
</filter>

Image showing a red overlay, making the shadows red instead of blue

Making this value .5 instead of -1, however, allows us to see the mixture of color in the shadow:


<filter id="blue-magenta-2">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   0   0   0   0
            0   0   1  .5   0
            0   0   0   1   0 "/>
</filter>

Image showing a mixture of colors in the shadows

Blowing out channels

We can affect the overall alpha of individual channels via the fourth row. Since our example has a blue sky, we can get rid of the sky and the blue values by converting blue values to white, like this:


<filter id="elim-blue">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   1   0   0   0
            0   0   1   0   0
            0   0   -2   1   0 "/>
</filter>

Image showing an example of blowing out a channel. We can get rid of the sky and the blue values by  converting blue values to white

Here are a few more examples of channel mixing:


<!-- No G channel, Red is at 100% on the G Channel, so the G channel looks Red (luminosity of G channel lost) -->
<filter id="no-g-red">
  <feColorMatrix
    type="matrix"
    values="1   1   0   0   0
            0   0   0   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- No G channel, Red and Green is at 100% on the G Channel, so the G Channel looks Magenta (luminosity of G channel lost) -->
<filter id="no-g-magenta">
  <feColorMatrix
    type="matrix"
    values="1   1   0   0   0
            0   0   0   0   0
            0   1   1   0   0
            0   0   0   1   0 "/>
</filter>

<!-- G channel being shared by red and blue values. This is a colorized magenta effect (luminosity maintained) -->
<filter id="yes-g-colorized-magenta">
  <feColorMatrix
    type="matrix"
    values="1   1   0   0   0
            0   1   0   0   0
            0   1   1   0   0
            0   0   0   1   0 "/>
</filter>

Lighten and darken

You can create a darken effect by setting the RGB values at each channel to a value less than 1 (which is the full natural strength). To lighten, increase the values to greater than 1. You can think of this as expanding or decreasing the RGB color circle shown earlier. The wider the radius of the circle, the lighter the tones created and the more white is “blown out”. The opposite happens when the radius is decreased.

Diagram showing how you can create a darken effect by setting the RGB values at each channel to a a value less than 1; to lighten, increase the values to greater than 1

Here’s what the matrix looks like:


<filter id="darken">
  <feColorMatrix
    type="matrix"
    values=".5   0   0   0   0
             0  .5   0   0   0
             0   0  .5   0   0
             0   0   0   1   0 "/>
</filter>

Image with a darken filter applied

<filter id="lighten">
  <feColorMatrix
    type="matrix"
    values="1.5   0   0   0   0
            0   1.5   0   0   0
            0   0   1.5   0   0
            0   0   0   1   0 "/>
</filter>

Image with a lighten filter applied

Grayscale

You can create a grayscale effect by accepting only one shade’s pixel values in a column. There are different grayscale effects, however, based on which active levels one applies. Here we’re doing a channel manipulation, since we’re grayscaling the image. Consider these examples:


<filter id="gray-on-light">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            1   0   0   0   0
            1   0   0   0   0
            0   0   0   1   0 "/>
</filter>

Image showing a 'gray on light' effect

<filter id="gray-on-mid">
  <feColorMatrix
    type="matrix"
    values="0   1   0   0   0
            0   1   0   0   0
            0   1   0   0   0
            0   0   0   1   0 "/>
</filter>

Image showing a 'gray on mid' effect

<filter id="gray-on-dark">
  <feColorMatrix
    type="matrix"
    values="0   0   1   0   0
            0   0   1   0   0
            0   0   1   0   0
            0   0   0   1   0 "/>
</filter>

Image showing a 'gray on dark' effect

Pulling it all together

The real power of feColorMatrix lies in its ability to mix channels and combine many of these concepts into new image effects. Can you read what’s going on in this filter?


<filter id="peachy">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0  .5   0   0   0
            0   0   0  .5   0
            0   0   0   1   0 "/>
</filter>

We’re using the red channel at its normal alpha channel, applying green at half strength, and applying blue on the darker alpha channels but not at its original color location. The effect gives us dark blue in the shadows, and a mix of red and half-green for the highlights and midtones. If we recall red + green = yellow, red + (green/2) would be more of a coral color:

Image showing what happens when we use the red channel at its normal alpha channel, apply green at half strength, and apply blue on the darker alpha channels but not at its original color location

Here’s another example:


<filter id="lime">
  <feColorMatrix
    type="matrix"
    values="1   0   0   0   0
            0   2   0   0   0
            0   0   0  .5   0
            0   0   0   1   0 "/>
</filter>

In that segment, we’re using the normal pixel hue of red, a blown-out green, and blue devoid of its original hue pixels, but applied in the shadows. Again, we see that dark blue in the shadows, and since red + green = yellow, red + (green*2) would be more of a yellow-green in the highlights:

Image showing what happens when we use the normal pixel hue of red, a blown-out green, and blue devoid of its original hue pixels, but applied in the shadows. Again, we see that dark blue in the shadows, and since red + green = yellow, red + (green*2) would be more of a yellow-green in the highlights

So much can be explored by playing with these values. An excellent example of such exploration is Rachel NaborsDev Tools Challenger, where she filters out the longer wavelengths (i.e., the red and orange channels) from the fish in the sea, explaining why “Orange Roughy” actually appears black in the water. (Note: requires Firefox.)

How cool! Science! And color filters! Now that you have a basic grasp of the situation, you, too, have the tools you need to create your own effects.

For some of those really rad Spotify duotone effects, I recommend you check out an article by Amelia Bellamy-Royds, who goes into even more detail about feColorMatrix. Sara Soueidan also wrote an excellent post on image effects where she recreates CSS blend modes with SVG.

Filter effects reference

Once you understand what’s going on with the feColorMatrix, you have the basic tools to create detailed filters within a single contained filter definition, but there are other options out there that will let you take it even further. Here’s a handy guide to all of the fe-* options currently out there for further exploration:

  • feBlend: similar to CSS blend modes, this function describes how images interact via a blend mode
  • feComponentTransfer: an umbrella term for a function that alters individual RGBA channels (i.e. , feFuncG)
  • feComposite: a filter primitive that defines pixel-level image interactions
  • feConvolveMatrix: this filter dictates how pixels interact with their close neighbors (i.e., blurring or sharpening)
  • feDiffuseLighting: defines a light source
  • feDisplacementMap: displaces an image (in) using the pixel values of another input (in2)
  • feFlood: complete fill of the filter subregion with a specified color and alpha level
  • feGaussianBlur: blurs input pixels using an input standard deviation
  • feImage: for use within other filters (like feBlend or feComposite)
  • feMerge: allows for asynchronous application of filter effects, instead of layering them
  • feMorphology: erodes or dilates lines of source graphic (think strokes on text)
  • feOffset: used for creating drop shadows
  • feSpecularLighting: source for the alpha component as a bump map, a.k.a. the “specular” portion of the Phong Reflection Model
  • feTile: refers to how an image is repeated to fill a space
  • feTurbulence: allows the creation of synthetic textures using Perlin Noise

Additional resources

5 Reader Comments

Load Comments