SVG Evolution 2: Our First Steps Into SVG

By Jeff Schiller

Article update, 16th June 2010: Updates to support information for SVG text selection, and declarative animation. Opera and Safari now support both of these, although Firefox is still behind in this area.

In Case You Are Just Tuning In

This is the second in a short series of articles in which we explore how to integrate Scalable Vector Graphics (SVG) into web applications. In our first article, we gave an introduction to SVG language and some of its basic capabilities. We introduced the concept that SVG images are XML documents with DOMs and that web developers have the ability to integrate SVG documents with other (X)HTML and SVG documents such that scripted communication between document DOMs can take place.

To illustrate the concepts, we took a simple image gallery and rewrote the application using SVG documents for the raster images. The end result is an HTML+SVG image gallery that provides equivalent functionality while presenting us with a lot of interesting possibilities for enhancements. We will start to explore these possibilities in this and the next article.

Immediate Benefits

SVG, as a graphical format, has many benefits over its raster counterparts. But it's interesting to observe that, even in our simple SVG Image Gallery application we created to be at parity with the HTML version that user agents (browsers) can allow some immediate benefits by default.

Opera SVG context menu
Figure 1 - Opera's SVG context menu

Remember: The 'S' in SVG stands for "scalable". As a result, user agents can allow the user to zoom, pan and otherwise manipulate SVG images in ways that might not make sense for other graphic formats. In Opera you can right-click on any SVG image to zoom in/out. Try this in the SVG version of the photo gallery: Click on a thumbnail, then once the image loads in the main panel, right-click the large image and choose Zoom In from the context menu. Now hold down the mouse button on the image and move the mouse around - this allows you to pan around the image. Because the format is SVG, the user gets Image Zoom and Pan for free. The Adobe SVG Viewer also allows similar manipulations. NOTE: At this time, Firefox does not support SVG zooming/panning out of the box, though there is an extension that allows this.

It is also worth noting that, because SVG is plain text XML, the user can also look at the source of an SVG image using the context menu. Looking at the "source" of an image is probably a new concept to most folks, but it can be extremely useful in certain situations (when learning about the SVG format, when trying to discover copyright/contact information, etc). In Opera, this is achieved by choosing the "Source" option from the context menu. In Firefox, this is achieved by choosing "This Frame -> View Frame Source".

For most other intents and purposes, an SVG image in a web page functions very much like a raster image in a web page. For instance, you can copy an SVG image via the context menu and paste it into a different application as a raster bitmap.

The Painters Model and Transformations

Next, let's modify our simple example. We're going to add a simple visible watermark to the photos to illustrate how SVG content is rendered as well as the concept of transformations. If you're lucky, you might also learn something about SVG text and opacity.

The Painters Model

SVG uses something called the Painters Model to dictate how SVG content should be rendered. What this means is that conceptually the SVG document is rendered in the order in which the SVG entities appear in the XML. In other words, if I have a simple SVG document like:

<svg>
  <circle fill="blue" cx="50" cy="50" r="40"/>
  <rect fill="red" x="0" y="0" width="50" height="50"/>
</svg>

The blue circle will be rendered and then the red rectangle will be rendered "on top" of the circle, partially obscuring it (like this). If we had flipped the order (put the <rect> element before the <circle> element), then the circle would partially obscure the rectangle (like this).

We use our knowledge of the painter's model concept to draw a simple text caption on top of the main image like so:

<g id="the_preview">
  <image id="thePreviewImage" class="preview" x="0" y="0" width="640" height="480"/>
  <text fill="white" fill-opacity="0.4" x="10" y="20" 
  font-size="20" font-family="Times">SVG Image Gallery</text>
</g>

Because the <text> element appears after the <image> element in the SVG document, the text will be drawn on top of the raster image.

Note that we are filling the text with the colour white, but we are making it partially transparent (fill-opacity="0.4"). SVG supports a set of Fill and Stroke Properties that can be applied to any SVG element (i.e. shapes, rasters, text). In the text example, we are not stroking (outlining) the text, only filling it. Read more about the SVG text element here.

Transformations

SVG supports the ability to transform any element in a variety of ways via the transform attribute. The value of the transform attribute can take a whitespace/comma-separated list of the following values:

translate( tx, [ty] )
Shifts the element by tx and ty units. ty is optional and defaults to the value of 0.
scale( sx, [sy] )
Scales the element by sx in the x-direction and sy in the y-direction. sy is optional and defaults to the value of sx.
rotate( rotate-angle [cx cy] )
Rotates the element by rotate-angle degrees around the given point (cx,cy). cx and cy are optional and default to the point (0,0).

The transform attribute supports additional transformations (skewing and general-purpose matrix transformation), see the SVG spec for further details.

We're going to use the rotate value to put our digital watermark along the left side of the image:

<g id="the_preview">
  <image id="thePreviewImage" class="preview" x="0" y="0" width="640" height="480"/>
  <text transform="rotate(90, 10, 20)" fill="white" fill-opacity="0.4" x="10" y="20" 
  font-size="20" font-family="Times">SVG Image Gallery</text>
</g>

Note that SVG coordinates start at the top-left corner of the image. The y-axis goes from the top of the image to the bottom and the x-axis goes from the left of the image to the right. Thus, we need to rotate our text +90 degrees (clockwise) around the anchor point of the text (10,20).

Click here to see the SVG gallery with the digital watermark added.

SVG text selection and copy
Figure 2 - SVG Text selection

By now, I hope you are realizing that the SVG format offers some very exciting possibilities. Each SVG image is a living, breathing document that can be modified, transformed, re-arranged, scripted, and styled to your heart's content. Now let's move on to some eye candy!

Breathing Life Into the UI

As far as web applications go, our photo gallery user interface is very simple and not very interactive. There is nothing in the UI that responds organically or reacts to mouse movements to indicate the application is alive and well. Modern applications sport these interfaces not just because they look good but offer the user a sense of satisfaction that they are operating a well-oiled machine and not a dated, rusty antique. It tells the user something is happening and makes the application seem responsive.

We are going to make our preview SVG document respond to mouse-over/mouse-out events by highlighting the moused-over image. Such techniques have been used for a long time in web applications by some combination of HTML, CSS and/or JavaScript. For a touch of class, we're going to animate the highlighting such that it smoothly transitions from dim to bright. And we're going to do it without any JavaScript or CSS using SVG Declarative Animation instead.

Dropping A Veil

Our first step is to add a "veil" on top of the thumbnail raster image to darken it. The veil will be a semi-transparent black and will completely cover the image:

<g id="thumbnail" onclick="showPreviewFromSVG(this, event); return false;" cursor="pointer">
  <rect id="blueborder" x="0" y="0" width="104" height="79" fill="blue"/>
  <image id="theImage" class="thumb" x="2" y="2" width="100" height="75"/>
  <rect id="veil" fill="black" x="2" y="2" width="100" height="75" fill-opacity="0.3">
  </rect>
</g>

Note that the above "veil" element has an opacity of 0.3, which has the effect of darkening all the thumbnail images slightly.

Animation

Next, we add some animation to the veil. We are only going to touch briefly on the topic of animation, since there have already been two excellent articles on SVG animation (here and here). For this animation, our needs are simple:

  • when the mouse enters the thumbnail, animate the fill-opacity of the veil from 0.3 to 0.0 over 250ms
  • when the mouse leaves the thumbnail, animate the fill-opacity of the veil from 0.0 to 0.3 over 250ms

Since the subject of the animation is the veil, we add two <animate> elements as children to the "veil" <rect> element. The attribute of the "veil" element that we want to animate is the fill-opacity attribute. Here is the same code as above with the animation bits added:

<g id="thumbnail" onclick="showPreviewFromSVG(this, event); return false;" cursor="pointer">
  <rect id="blueborder" x="0" y="0" width="104" height="79" fill="blue"/>
  <image id="theImage" class="thumb" x="2" y="2" width="100" height="75"/>
  <rect id="veil" fill="black" x="2" y="2" width="100" height="75" fill-opacity="0.3">
    <animate attributeName="fill-opacity" attributeType="XML" 
      begin="mouseover" dur="0.25s" fill="freeze" to="0.0" />
    <animate attributeName="fill-opacity" attributeType="XML" 
      begin="mouseout" dur="0.25s" fill="freeze" to="0.3" />
  </rect>
</g>

The to attribute is the final value that the attribute should have. The dur attribute is how long the animation should take. The begin attribute defines when that animation will start (in this case, the animations begin upon a mouseover/mouseout event). The fill attribute defines what will happen when the animation is over: in this case we want to freeze the values once the animation is finished (the other option is "remove" which would remove the animated effect once the animation is complete, obviously we do not want this for our example) .

Click Here to play with the animation by waving your mouse over the thumbnail images.

Conditional Processing: The Cold Hard Reality

Declarative animation is really cool, and as of 2010 it is supported in Opera 9+ and Safari 4+, so the effect (although subtle) in the example above is visible in those browsers. However, what happens to Firefox? In Firefox the <animate> elements are ignored, thus all thumbnails will be slightly darkened and remain that way with no other visible effect. But at least it is still usable.

Now, darkening all the thumbnail images isn't the end of the world, but for users of browsers that don't support animation, we are darkening the images for no apparent reason. We'll briefly address how to handle this situation next.

The SVG spec is huge, packed with many features. The spec authors realized this, and consequently realized that expecting user agent developers to support all features of the SVG language during the early phases of adoption would be unrealistic. As a result, SVG supports conditional processing, which allows content developers and web authors to produce fall-back content when a particular SVG feature is not supported. A list of SVG feature strings is available in the spec here.

The key to conditional processing is the <switch> element and the requiredFeatures attribute. As per the spec: The 'switch' element evaluates the requiredFeatures, requiredExtensions and systemLanguage attributes on its direct child elements in order, and then processes and renders the first child for which these attributes evaluate to true. In other words, we can wrap the "veil" <rect> element of our thumbnail image in a <switch> element and then add a requiredFeatures attribute to the <rect> elements. In this way, if the user agent supports declarative animation, the "veil" <rect> will be rendered (and its <animate> children will be processed) and if declarative animation is not supported, then the "veil" <rect> element is not processed or rendered.

Here is the final code for the thumbnail SVG image:

<g id="thumbnail" onclick="showPreviewFromSVG(this, event); return false;" cursor="pointer">
  <rect id="blueborder" x="0" y="0" width="104" height="79" fill="blue"/>
  <image id="theImage" class="thumb" x="2" y="2" width="100" height="75"/>
  <switch>
  <rect id="veil" fill="black" x="2" y="2" width="100" height="75" fill-opacity="0.3"
  requiredFeatures="http://www.w3.org/TR/SVG11/feature#Animation">
    <animate attributeName="fill-opacity" attributeType="XML" 
      begin="mouseover" dur="0.25s" fill="freeze" to="0.0" />
    <animate attributeName="fill-opacity" attributeType="XML" 
      begin="mouseout" dur="0.25s" fill="freeze" to="0.3" />
  </rect>
  </switch>
</g>

Click Here to see this in action.

Alternative Solutions

I should point out a couple other possible solutions that may have occurred to the astute reader:

  • We could have scripted the animation here for Firefox users by creating event listeners and then using setTimeout to adjust the value of fill-opacity in the SVG DOM, or we could have used the excellent smilscript until such time as Firefox supports declarative animation.
  • Even easier, instead of using the veil to darken all thumbnails and removing it when hovered over, we could have used the veil to lighten the hovered thumbnail (by filling the veil with white) and then having the fill-opacity attribute animate from 0.0 to 0.3 when the thumbnail was moused over. In the real world, this would be a preferred approach, but to be honest, we were using this somewhat contrived example to illustrate the conditional processing functionality within SVG. We leave this as an exercise for the reader.

Are you still with me? Ok, enough playing around with this glitzy animation, let's get into some actually useful SVG functionality on the next page.

Adding Some Functionality

While eye candy is always good to get some wow, let's start to work on some practical uses of SVG in the area of "image gallery". We'll start slow by adding a toolbar with some buttons to the main image (the preview image) in the photo gallery. This will allow us to get our hands dirty with some SVG scripting. We'll also get a chance to see how CSS can be used to style SVG elements.

Styling The Toolbar

The toolbar should be semi-transparent and appear right on top of the main image. First, here is the code for the toolbar:

<g id="options_panel" display="none" transform="translate(50,5)">
  <rect id="options_panel_bkgnd" x="0" y="0" width="120" height="30" />
  <polyline class="topleftborder" points="0,30 0,0 120,0"/>
  <polyline class="botrightborder" points="120,0 120,30 0,30"/>
</g>

We will add the "options_panel" group (<g>) element to the SVG main image group ("the_preview"). At this point, the options_panel simply contains a rectangle ("options_panel_bkgnd") with some polylines serving as borders to give the panel a 3D feel. We're going to use CSS to style the SVG elements by updating our external CSS file:

g#options_panel {
  fill-opacity: 0.5;
  stroke-opacity: 0.5;
}
rect#options_panel_bkgnd {
  fill: grey;
}
polyline.topleftborder {
  fill: none;
  stroke: white;
  stroke-width: 2;
}
polyline.botrightborder {
  fill: none;
  stroke: grey;
  stroke-width: 2;
}

In the CSS, we set the entire "options_panel" group's opacity (both fill and stroke) to 0.5. This means that all children will, by default cascade rules, inherit the opacity of their parent, meaning the entire panel will be semi-opaque. The fill colour of the background rectangle and the stroke of the polylines are also styled.

Note that to associate this external CSS file with the SVG content, we must add the following line to the XML prolog:

<?xml-stylesheet href="ex6-gallery.css" type="text/css"?>

Displaying The Toolbar

You may notice in the above SVG code that the options_panel group is initially not displayed (the display attribute is set to "none"). This is because we only want to make the toolbar visible when we mouse-over the main image. To do this, we add a JavaScript function and assign two event listeners to call it. Here is the SVG code with the JavaScript added:

<g id="the_preview" onmouseover="showOptions(true)" onmouseout="showOptions(false)">
  <image id="thePreviewImage" class="preview" x="0" y="0" width="512" height="384"/>
  <text transform="rotate(90, 10, 20)" fill="white" fill-opacity="0.4"
    x="10" y="20" font-size="30" font-family="Times">SVG Image Gallery</text>
  <g id="options_panel" display="none" transform="translate(50,5)">
    <rect id="options_panel_bkgnd" x="0" y="0" width="120" height="30" />
    <polyline class="topleftborder" points="0,30 0,0 120,0"/>
    <polyline class="botrightborder" points="120,0 120,30 0,30"/>

    <script><![CDATA[
      function showOptions(bShow) {
        var optionsPanel = document.getElementById("options_panel");
        if(optionsPanel) {
          optionsPanel.setAttributeNS(null, "display", (bShow ? "inline" : "none"));
        }
      }
    ]]></script>
  </g>
</g>
The empty toolbar
Figure 3 - The empty toolbar

The event handlers for the mouse-over and mouse-out events are set at "the_preview" group element. These event handlers call the showOptions() function which accepts a true/false argument. The argument dictates whether the "options_panel" element will be visible or not. Click here to observe this in action (you will have to move your mouse onto the main image to see the empty toolbar).

Note that I've put the JavaScript <script> element as a child of the "options_panel" element. This may give the false impression that the script statements belong to the "options_panel" element or only has scope to the "options_panel" element. This is not the case. Scripts in SVG operate just like scripts in HTML: Script statements are simply executed when they are first encountered in document order. The showOptions() function does not belong to the DOM object but exists in the global (or "window") scope. Placing <script> elements nested within other SVG elements might make sense for small applications to keep code local to the SVG elements they impact, but as your application grows, it will eventually make sense to move all JavaScript into a separate .js file and reference that file once within the SVG document.

Some Simple Buttons

Now we'll add our first buttons to the toolbar. Nothing fancy to begin with, we're going to use the power of SVG transformations to do some very basic things: Flipping the image vertically and horizontally.

Buttons on the control panel will just be <rect> elements that contain simple graphics, listen for mouse click events and pass control to JavaScript functions. We leave it as exercises to the reader for something fancier (including improved button graphics, mouseout/mouseover event listening with some visual effect, animated button images, etc). Here is the code for a basic button:

<g id="flipv_button" cursor="pointer" onclick="flipVert()" pointer-events="all">
  <title>Flip Image vertically</title>
  <-- Button graphics go here in a 25x25 coordinate space -->
  <script><![CDATA[
    function flipVert() {
      // ...
    }
  ]]></script>
</g>

We will use the template above and copy it for each button, then transform the <g> element by doing a translation for each button. Note that the <title> element is used to provide some description of what the button does. User agents should make this information available (i.e. in tooltip or in the status bar) when moused over.

For flipping the image, our functions will simply keep track of a global JavaScript variable, toggle it from 1 to -1 and then set the transform attribute with an appropriate value of scale():

<script><![CDATA[
  // Global variables
  var gnFlippedHoriz = 1;
  var gnFlippedVert = 1;
  function flipImage() {
    var scaleStr = "translate(256,192) scale(";
    scaleStr += gnFlippedHoriz;
    scaleStr += ",";
    scaleStr += gnFlippedVert;
    scaleStr += ") translate(-256,-192)";
    var img = document.getElementById("thePreviewImage");
    if(img) { img.setAttributeNS(null, "transform", scaleStr); }
  }
  function flipHoriz() {
    gnFlippedHoriz *= -1;
    flipImage();
  }
  function flipVert() {
    gnFlippedVert *= -1;
    flipImage();
  }
]]></script>

Look at what the flipImage() function does: A horizontal flip is accomplished by scale(-1,1), while a vertical flip is scale(1,-1). Since all transformations take place with respect to the origin (0,0) of the coordinate system, we need to first move the image so that its center point is at the origin (translate(-256,-192)), then scale the image, then move the image back so that its top-left point is at the origin (translate(256,192)). To flip the image horizontally (but not vertically), the transform attribute value should be "translate(256,192) scale(-1,1) translate(256,-192)".

Click Here to play with the final version of the code.

I hope this article has helped convinced you that SVG is a technology that can be used to provide both elegant interactivity and useful functionality to Rich Internet Applications in modern web browsers. In the next (and final) article in this series, we'll buff off the rough edges of our sample SVG+XHTML+CSS+JS web application, provide yet more useful functionality to the Image Gallery, and illustrate a couple more complex features of SVG.

Jeff Schiller is a software developer and freelance writer interested in web and game development living in the Chicago area. He maintains a blog at http://blog.codedread.com/


This article is licensed under a Creative Commons Attribution, Non Commercial - No Derivs 2.5 license.

Comments

The forum archive of this article is still available on My Opera.

No new comments accepted.