Dev.Opera - Follow the standards, break the rulesDev.Opera - Follow the standards, break the rules

Login

Lost password?

SVG Evolution 3: Applying Polish

Grey Scale

For our next trick, we'll see how we can use an SVG filter to turn any colour image into a greyscale one. This is something that is not possible in SVG without using filters. First, let's define the theory of what we want to do:

Theory

Without getting too far into colour theory, digital representation and mathematics, very briefly - each pixel in an image has a Red, Green, and Blue "channel", which is a fancy name for value. We often see the abbreviation RGB to describe this. The reason RGB terminology is used is because each actual physical pixel on the screen is made up of 3 different phosphors that emit red, green and blue light in various combinations to create all the colours you can possibly see. For instance, a pure red pixel would have a value of zero for Green (G=0) and Blue (B=0) channels and a non-zero value in Red (the brighter the red, the higher the value of the R component).

We also speak of an Alpha channel per pixel that defines that pixel's transparency, so a completely opaque pixel has the highest Alpha value of 1.0 (meaning this element completely obscures elements "behind" it at that pixel), while a completely transparent pixel has an A value of 0 (meaning other entities can be seen "behind" that pixel).

If each RGBA channel can vary between 0.0 and 1.0, one way to convert a coloured pixel into a grey pixel is to first determine how "bright" (or luminous) the pixel is by doing the following:

Brightness = (Red Value + Green Value + Blue Value) / 3.0
           = (0.3333*R + 0.3333*G + 0.3333*B)

Thus, a completely black pixel (RGB=0,0,0) would result in a "Brightness" of 0 and a completely white pixel (RGB=1,1,1) would result in a "Brightness" of (1+1+1)/3 = 1.0. A completely blue pixel (RGB=0,0,1) would result in a "Brightness" of 0.3333, while a completely yellow pixel (RGB=1,1,0) would result in a "Brightness" of 0.6666.

Now that we know how bright each pixel is, we can simply set each of the Red, Green, and Blue channels to this value. When you set a pixel's RGB values to the same value, you are forcing it to be a grey pixel (that varies from pure black to pure white in proportion to the "Brightness" value).

This is what we want to do. Essentially, we can write 4 equations for the new R'G'B'A' values (grey-scale) based on the old RGBA values (colour):

R' = 0.3333*R + 0.3333*G + 0.3333*B + 0.0*A
G' = 0.3333*R + 0.3333*G + 0.3333*B + 0.0*A
B' = 0.3333*R + 0.3333*G + 0.3333*B + 0.0*A
A' =    0.0*R      0.0*G +    0.0*B + 1.0*A (always use the same alpha)

The Color Matrix

Mathematicians like to use short-cuts in writing equations like those above. Basically if we take all the numbers in the above equations (in bold), they can be grouped into a rectangle of 16 numbers that we call a matrix:

| 0.3333 0.3333 0.3333 0.0 |
| 0.3333 0.3333 0.3333 0.0 |
| 0.3333 0.3333 0.3333 0.0 |
| 0.0    0.0    0.0    1.0 |

SVG provides the feColorMatrix filter primitive. This primitive allows you to specify the exact color matrix that should be applied to the input image. The filter primitive will output the resultant RGBA pixels after multiplying them by the specified color matrix.

The feColorMatrix primitive actually adds a column to the color matrix to allow for offsets to be added to the RGBA values, but for our purposes that column can stay all zeros. Below shows the updated color matrix with the extra column added:

| 0.3333 0.3333 0.3333 0.0  0.0 |
| 0.3333 0.3333 0.3333 0.0  0.0 |
| 0.3333 0.3333 0.3333 0.0  0.0 |
| 0.0    0.0    0.0    1.0  0.0 |

So Where's The Code?

We're finally at a point where we can apply all this theory to some SVG and see what's what. We're going to construct a filter consisting of a feColorMatrix primitive with the above matrix and apply it to an image:


    <svg viewBox="0 0 512 192">
      <defs>
       <filter id="grey-filter" filterUnits="userSpaceOnUse" x="0" y="0" width="512" height="192">
          <feColorMatrix in="SourceGraphic" type="matrix"
              values="0.3333 0.3333 0.3333 0 0
                      0.3333 0.3333 0.3333 0 0 
                      0.3333 0.3333 0.3333 0 0 
                      0      0      0      1 0"/>
       </filter>
      </defs>
      
      <image xlink:href="calif2.jpg" x="0" y="0" width="256" height="192" />
      <image xlink:href="calif2.png" x="256" y="0" width="256" height="192" 
             filter="url(#grey-filter)" />
    </svg>
    

The result is shown below. It is clear that our filter does what we wanted - turns any colour image into a greyscale one.

Figure 6 - Demonstrating a Greyscale Filter

So now back to our SVG Image Gallery web application. First, we'll put in the "grey-filter" filter element into our defs section. Then, we'll create a new button in our toolbar (see last article). In this instance, I want the button to indicate "Switch to Greyscale", and when clicked, it will incidate "Switch to Color". We do this by adding two buttons to the toolbar in the same location and making one of these buttons initially not visible:


        <g id="options_panel" display="none" transform="translate(50,5)">
            ...
            <!-- Flip Horizontal button -->
            ...
            <!-- Flip Vertical button -->
            ...
            <!-- Greyscale button -->
            <g id="grey_button" transform="translate(62.5,2.5)" cursor="pointer" 
                pointer-events="all">
                <title>Switch To Greyscale</title>
                <polyline class="topleftborder" points="0,25 0,0 25,0"/>
                <polyline class="botrightborder" points="25,0 25,25 0,25"/>
                <rect x="1" y="2" width="4" height="20" fill="#222" stroke="none"/>
                <rect x="4" y="2" width="4" height="20" fill="#444" stroke="none"/>
                <rect x="7" y="2" width="4" height="20" fill="#666" stroke="none"/>
                <rect x="10" y="2" width="4" height="20" fill="#888" stroke="none"/>
                <rect x="13" y="2" width="4" height="20" fill="#aaa" stroke="none"/>
                <rect x="16" y="2" width="4" height="20" fill="#ccc" stroke="none"/>
                <rect x="19" y="2" width="4" height="20" fill="#eee" stroke="none"/>
            </g>
            <!-- Color button, initially not displayed -->
            <g id="color_button" transform="translate(62.5,2.5)" cursor="pointer" 
                pointer-events="all" style="display:none" >
                <title>Switch To Color</title>
                <rect x="1" y="2" width="5" height="20" fill="red" stroke="none"/>
                <rect x="6" y="2" width="5" height="20" fill="blue" stroke="none"/>
                <rect x="11" y="2" width="5" height="20" fill="yellow" stroke="none"/>
                <rect x="16" y="2" width="5" height="20" fill="purple" stroke="none"/>
                <rect x="21" y="2" width="5" height="20" fill="green" stroke="none"/>
                <polyline class="topleftborder" points="0,25 0,0 25,0"/>
                <polyline class="botrightborder" points="25,0 25,25 0,25"/>
            </g>
    

This gives us a new button on our toolbar: [html:img style="margin:5px" src="new-button.png" alt="[button]"]

Now we need to hook up some functionality to it. We create a global variable, gnGreyMode, that defines our greyscale mode (initially set to 0). Then we create a simply JavaScript function that will set or unset the filter attribute on the image:

    
        <script>
            function setGrey() {
                var sGreyButtStyle = (gnGreyMode ? "display:none" : "display:default");
                var sColorButtStyle = (gnGreyMode ? "display:default" : "display:none");
                var sImageFilter = (gnGreyMode ? "url(#grey-filter)" : "");
                document.getElementById("grey_button").setAttributeNS(null, "style", sGreyButtStyle);
                document.getElementById("color_button").setAttributeNS(null, "style", sColorButtStyle);
                document.getElementById("thePreviewImage").setAttributeNS(null, "filter", sImageFilter);
            }
            ...
        </script>
    

Then, we hook up a mouse-click handler to each of the buttons like so:


        <g id="grey_button" onclick="gnGreyMode=true; setGrey()" ...</g> 
        <g id="color_button" onclick="gnGreyMode=false; setGrey()" ...</g>
    

And now we're done &mdash; see the updated SVG Image Gallery application with greyscale capabilities.

Side Note: Revisiting the Drop Shadow

Now that we've learned a bit about color theory, I took the opportunity to add a feColorMatrix element into our drop shadow filter. This primitive simply takes the black shadow and outputs a slightly transparent shadow (not fully black) which is then merged to achieve the final result. We do this by applying the following color matrix:

| 1 0 0 0   0 |
| 0 1 0 0   0 |
| 0 0 1 0   0 |
| 0 0 0 0.8 0 |

which translates to the following filter equations:

R' = 1*R
G' = 1*G
B' = 1*B
A' = 0.8*A

In other words, our drop shadow is no longer completely opaque (few shadows are) but slightly transparent, which provides a more realistic effect. This picture shows how we have assembled the Drop Shadow filter.

Conclusion

Our SVG Image Gallery web application has grown in little jets and spurts as we've added various drops of eye candy and functionality to it. Viewing this application in Opera or other advanced renderers of SVG, the app starts to feel very, um, "Flash"-like, especially when you consider where we started. But the application still has lots of room to grow. Off the top of my head, some useful features that could still be added:

  • A Rotate tool
  • A Crop tool
  • A "brightness" slider that adjusts the lightness/darkness of the whole image
  • Red, Green, Blue sliders that affect colour properties of the image
  • A "burn" tool (creates small, mostly transparent circles filled with a radial gradient that can darken specific areas of the image)
  • Ability to save the modified SVG DOM to client's disk as an SVG image
  • Ability to save the modifications to the server side

Implementation notes

The support for SVG filters varies at the time of this writing, April 2007. Opera 9+ has great support for SVG Filters and early Alpha builds are good indications that Firefox 3 may have at least some support for SVG Filters when it is released.

The drop shadows example will only be visible using Opera 9+ or Firefox 3 (Alpha 4). Only Opera 9+ supports the greyscale filter, Firefox 3 may support it when released.

Of course, as with any development project, you have to decide where you draw the line on hacking new features and when you consider "re-factoring" and trying to design something enterprise-level. There are drawbacks at the moment to using SVG, particularly inline with XHTML, since only two major web browsers support this technology at the time of writing. More browsers are due to arrive soon with native SVG support (Safari later this year), so the technology will become increasingly relevant in the future as implementations mature.

digg Digg this article  del.icio.us Add to del.icio.us

Article categories