Adobe

decor

Web Platform Team Blog

Making the web awesome

gradientmaps.js – Gradient Maps for the Web

In Photoshop, you can manipulate images in all kinds of ways using gradient map adjustment layers. You can tint or tone images, easily convert them to black and white, adjust the midtones, or remap all the colors of an image into your own custom colormap for some great creative effects.

blue_eggs_cropped blue_eggs_cropped_grayscale blue_eggs_cropped_colored blue_eggs_cropped_inverse_sepia

Now what if you could do that to HTML content as well?

Not simply images.  I mean any HTML content: text, video, images, you name it.

gradientmaps.js, a new open source library recently released on GitHub lets you do just that, very simply.  The library depends on the ability to apply SVG filters to HTML content.  That support is currently present only in Chrome and Firefox, but hopefully the other browsers should be following suit with support in the near term.  I wrote a support matrix that details which browsers support SVG filters on HTML content, which will help you know which browsers this library can be used on.

What are Gradient Maps?

The best way to understand gradient maps is to think in terms of images and linear gradients.  The darker parts of the image get mapped to the left side of the gradient, while the brighter parts get mapped to the right side of the gradient.

Screen Shot 2013-08-03 at 3.58.05 PM

But remember, we’re not just talking about images, we’re talking about HTML content.  The image in this case is what actually gets rendered on the screen, be it text, images, video, etc.

gradientmaps.js Example

Let’s look at an example so you’ll see what I’m talking about.  Make sure you are using Chrome or Firefox to view this, so you can see the gradient maps in action.

Check out this Pen!

On the right you should see an embedded iFrame.  You can change the page you want to display by typing in your own URL and clicking the “Reload IFrame” button.

On the left you’ll see a list of gradient map presets, and below that you can enter your own custom gradient maps and apply them to the iFrame on the right.

If you click on some of the presets, you’ll see that the gradients look an awful lot like CSS linear gradients, and that is intentional.  You can copy/paste directly from a CSS linear gradient.  Everything is the same except there is no initial angle, sides or corners.

Due to security constraints with iFrames, the Codepen above will not show embedded Flash video.  Instead, if you want to try applying gradient maps to sites like Adobe TV or YouTube, then you can run the same demo here: http://blattchat.com/gradient-map-on-an-iframe/.  Make sure, if you want to view a YouTube video, that you use the embed URL.  Or, if you want to see a wacky example of animated gradient maps, try this out.

Using gradientmaps.js

The API is really easy to use.  First, include the gradientmaps library on your page:

<script src="gradientmaps.min.js"></script>

Now, you can apply a gradient map to any element using the following Javascript:
GradientMaps.applyGradientMap(targetElement, gradientString);

gradientString is specified as a comma separated list of color stops.  Each color stop is a color (specified in RGB, RGBA, HSL, HSLA, a named color or #hex format) followed by an optional position (specified as either a fraction from 0.0 to 1.0 or a percentage).

For example, here’s a gradientString that would convert your content to black and white:

"black, white"

If you want the gradient to start at black, gradually turning to blue at 10%, and then to full white at 100%, you could do the following:
"black, rgb(0, 0, 255) 10%, white"

There are a few assumptions made when no position is specified.  These are the same assumptions as for a CSS linear gradient:

  • If the initial color stop has no position, it is assumed to be 0%
  • If the final color stop has no position, it is assumed to be 100%
  • If a color stop has no position, and it is not the first or last color stop, it is positioned half way between the previous and next color stop
  • If a color stop’s position is less than the previous color stop, it is repositioned to that of the previously positioned color stop

If you want to remove a gradient map from an element, the following method is available:

GradientMaps.removeGradientMap(targetElement);

That’s it.  Pretty simple and straightforward.

How it Works

Behind the scenes, gradientmaps.js is making use of SVG filters, specifically the color matrix and component transfer filter primitives.  When applying a gradient map, first an SVG element is added to the DOM, and a filter with a dynamically generated ID is added to the SVG:

<svg version="1.1" width="0" height="0">
   <filter id="filter-1375725609202"/>
</svg>

A color matrix filter primitive is then used to convert the HTML rendered image to grayscale, with the darkest colors mapped to black, and the brightest colors mapped to white.
<svg version="1.1" width="0" height="0">
   <filter id="filter-1375725609202">
      <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"/>
   </filter>
</svg>

A component transfer filter primitive then takes the output from the previous filter primitive to convert the grayscale intensities to the requested gradient map.
<svg version="1.1" width="0" height="0">
   <filter id="filter-1375725609202">
      <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 color-interpolation-filters="sRGB">
         <feFuncR type="table" tableValues="1 0 0"/>
         <feFuncG type="table" tableValues="0 1 0"/>
         <feFuncB type="table" tableValues="0 0 1"/>
         <feFuncA type="table" tableValues="1 1 1"/>
      </feComponentTransfer>
   </filter>
</svg>

Calculating the actual values for the component transfer primitive’s individual channel transfer functions is where the meat of the gradientmap library lies.

The component transfer filter primitive allows you to remap the individual color channels using transfer functions (feFuncR, feFuncG, feFuncB and feFuncA for RGBA respectively).  In our case we are using table type transfer functions.  There are different types of transfer functions that you can read about in the filter effects specification.  With table type transfer functions, the function is defined through linear interpolation of the values specified in the tableValues attribute.

In the example above,the red transfer function has its tableValues set to:

1 0 0

The table values will be evenly distributed, meaning the first value will map reds of value 0.0, the last value will map reds of 1.0 and any stops in between will be evenly distributed.  In the case above, the middle 0 will thus map reds of value 0.5.  In this example, the darkest reds will get mapped to full red, and any reds with a value of 0.5 or more will get mapped so they have no red.

Screen Shot 2013-08-05 at 2.31.58 PM

The problem of course is that we want to be able to add values at arbitrary positions in the gradient, not just at evenly distributed positions.  The library takes care of that for you, figuring out what set of evenly distributed table values can be used with the individual channel transfer functions to achieve the desired effect.

For example, if you wanted to start at black, go to blue at 10% and then end on white:

Screen Shot 2013-08-05 at 2.38.14 PM

the library would calculate table values for the transfer function table values at 0%, 10%, 20%, 30%, 40%, 50%, 60%, 70%, 80%, 90% and 100%:

<svg version="1.1" width="0" height="0">
   <filter id="filter-1375725609202">
      <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 color-interpolation-filters="sRGB">
         <feFuncR type="table" 
            tableValues="0 0 0.1111111111111111 0.2222222222222222 0.3333333333333333 
                         0.4444444444444444 0.5555555555555555 0.6666666666666666 
                         0.7777777777777778 0.888888888888889 1"/>
         <feFuncG type="table" 
            tableValues="0 0 0.1111111111111111 0.2222222222222222 0.3333333333333333 
                         0.4444444444444444 0.5555555555555555 0.6666666666666666 
                         0.7777777777777778 0.888888888888889 1"/>
         <feFuncB type="table" tableValues="0 1 1 1 1 1 1 1 1 1 1"/>
         <feFuncA type="table" tableValues="1 1 1 1 1 1 1 1 1 1 1"/>
      </feComponentTransfer>
   </filter>
</svg>

Finally, the target element has its CSS modified, setting -webkit-filter and filter to point to the dynamically created SVG filter:
-webkit-filter: url(#1375725609202); 
        filter: url(#1375725609202);

When modifying an existing gradient map, the library will look for the existing filter that is applied to that element, and modify it in place so you don’t end up with zombie filters in your DOM.  When removing a filter, the CSS filter attributes will be reset and the filter will be removed from the SVG element.  If the SVG has no other filter children, it will be removed as well, ideally keeping everything nice and tidy.

Summary

I’d like to the thank the Adobe Web engine team for originally pointing me down this path and for their support, both technically and legally.

You can find the all of the code and examples on my GitHub repository:

https://github.com/awgreenblatt/gradientmaps

By all means, let me know if you find problems or think there are some features to add.  Even better, make the changes yourself and do a pull request.  I’d love to get other’s input on making this better.

I can best be reached on Twitter at @agreenblatt. That’s also a great way to keep up with any major updates or improvements to the library.  You can also read about some of my other technical ramblings at http://blattchat.com.  Please do let me know if you do anything interesting with the library.

Enjoy!

One Comment

  1. August 06, 2013 at 6:02 pm, Kevin Lozandier said:

    This library is very interesting. I appreciate the thorough explanation and the hard work Mr. Greenblatt and rest of the Adobe Web Platform Team involved with this exerted to realize this library.