SVG Essentials/Animating and Scripting SVG

From WikiContent

< SVG Essentials(Difference between revisions)
Jump to: navigation, search
(Initial conversion from Docbook)
Current revision (13:40, 7 March 2008) (edit) (undo)
(Initial conversion from Docbook)
 
(2 intermediate revisions not shown.)

Current revision

SVG Essentials

Up to this point we have produced static images; once constructed, they never change. In this chapter, we will examine two methods of making graphic images move. The first method, animation, is movement that is controlled by you, the author. The second method, scripting, lets the person viewing the graphic interact with and modify the image.

In Chapter 10 we suggested that filters should be used as a means to enhance a graphic's message, not as an end in themselves. This suggestion is even more crucial with animation. Drunk with the power of animation, you will be tempted to turn your every graphic into an all-dancing, all-singing, Broadway spectacular. As long as your goal is experimentation, this is fine. If your goal is to convey a message, however, nothing is worse than gratuitous use or overuse of animation. Let me state this clearly: nobody except the company CEO is interested in repeated viewing of a spinning, flashing, color-changing, strobe-lit version of the company logo.

In this chapter, our message is the animation, so most of our examples will be remarkably free of any content. We will, of course, avoid gratuitous and overwrought animation as much as possible.

Contents

Animation Basics

The animation features of SVG are based on the World Wide Web Consortium's Synchronized Multimedia Integration Language Level 2 (SMIL2) specification http://www.w3.org/TR/smil20/. In this system, you specify the starting and ending values of the attribute, color, motion, or transformation that you wish to animate; the time at which the animation should begin; and the duration of the animation. Example 11-1 shows this in action.

Example 11-1. The incredible shrinking rectangle

<rect x="10" y="10" width="200" height="20" stroke="black" fill="none">     [1]
    <animate     [2]
        attributeName="width"     [3]
        attributeType="XML"     [4]
        from="200" to="20"     [5]
        begin="0s" dur="5s"     [6]
        fill="freeze" />     [7]
</rect>     [8]
            

[1] A <rect> element without the ending />. The animation will be contained within the element.

[2] Begin specification of animation

[3] Specify the attribute whose value should change over time.

[4] Width is an XML attribute in the <rect> element. The other common value of attributeType is CSS, indicating that the property we want to change is a CSS property. If you leave this off, the default value of auto is used; it searches through CSS properties first and then XML attributes.

[5] The starting and ending values for the attribute. In this example, the starting value is 200 and the ending value is 20.[1]

[6] The beginning and duration times for the animation. In this example, we measure time in seconds, specified by the s after the number. For more details, see Section 11.2.

[7] After the five-second duration, keep the attribute at its end value. If you remove this line, the attribute will return to its original value of 200 after the five-second animation has finished. This is the SMIL fill attribute, which tells the animation engine how to fill up the remaining time. Don't confuse it with SVG's fill attribute, which tells SVG how to paint an object.

[8] We have to close the <rect> element, since it is now a container element.

Figure 11-1 and Figure 11-2 show the beginning and ending stages of the animation. They can't do justice to the actual effect, so we strongly recommend that you download the Adobe SVG Viewer plugin and try the example. Type it into a file, enclosed in the appropriate <?xml?> and <svg> tags, and open it within your browser.

Figure 11-1. Beginning of animation

Beginning of animation

Figure 11-2. Ending of animation

Ending of animation

Let's move on to a more ambitious example. In Example 11-2 we'll start with a 20-by-20 green square that will grow to 250-by-200 over the space of 8 seconds. For the first three seconds, the opacity of the green will increase, then decrease for the next three seconds. Note that fill-opacity is referred to with attributeType="CSS" since it was set in a style.

Example 11-2. Multiple animations on a single object

<rect x="10" y="10" width="20" height="20"
    style="stroke: black; fill: green; style: fill-opacity: 0.25;">
<animate attributeName="width" attributeType="XML"
    from="20" to="250" begin="0s" dur="8s" fill="freeze"/>
<animate attributeName="height" attributeType="XML"
    from="20" to="200" begin="0s" dur="8s" fill="freeze"/>
<animate attributeName="fill-opacity" attributeType="CSS"
    from="0.25" to="1" begin="0s" dur="3s" fill="freeze"/>
<animate attributeName="fill-opacity" attributeType="CSS"
    from="1" to="0.25" begin="3s" dur="3s" fill="freeze"/>
</rect>

Our last simple example, Example 11-3, animates a square and a circle. The square will expand from 20-by-20 to 120-by-120 over the space of eight seconds. Two seconds after the beginning of the animation, the circle's radius will start expanding from 20 to 50 over the space of four seconds. Figure 11-3 shows a combined screenshot of the animation at four times: zero seconds, when the animation begins; two seconds, when the circle starts to grow; six seconds, when the circle finishes growing; and eight seconds, when the animation is finished.

Example 11-3. Simple animation of multiple objects

<rect x="10" y="10" width="20" height="20"
    style="stroke: black; fill: #cfc;">
    <animate attributeName="width" attributeType="XML"
         begin="0s" dur="8s" from="20" to="120" fill="freeze"/>
    <animate attributeName="height" attributeType="XML"
         begin="0s" dur="8s" from="20" to="120" fill="freeze"/>
</rect>

<circle cx="70" cy="70" r="20"
    style="fill: #ccf; stroke: black;">
    <animate attributeName="r" attributeType="XML"
        begin="2s" dur="4s" from="20" to="50" fill="freeze"/>
</circle>

Figure 11-3. Stages of multi-object animation

Stages of multi-object animation

How Time Is Measured

SVG's animation clock starts ticking when the SVG has finished loading, and it stops ticking when the user leaves the page. You may specify a beginning or duration for a particular animation segment as a numeric value in one of these ways:

  • A full clock value in hours, minutes, and seconds (1:20:23).
  • A partial clock value in minutes and seconds (2:15).
  • A time value followed by a "metric," which is one of h (hours), min (minutes), s (seconds), or ms (milliseconds), for example dur="3.5s" begin="1min". If no metric is specified, the default is seconds. You may not put any whitespace between the value and the metric.

You may also tie an animation's beginning time to the beginning or end of another animation. Example 11-4 shows two circles; the second one will start expanding as soon as the first one has stopped shrinking. Figure 11-4 shows the important stages of the animation.

Example 11-4. Synchronization of animations

<circle cx="60" cy="60" r="30" style="fill: #f9f; stroke: gray;">
    <animate id="c1" attributeName="r" attributeType="XML"
        begin="0s" dur="4s" from="30" to="10" fill="freeze"/>
</circle>

<circle cx="120" cy="60" r="10" style="fill: #9f9; stroke: gray;">
    <animate attributeName="r" attributeType="XML"
        begin="c1.end" dur="4s" from="10" to="30" fill="freeze"/>
</circle>

Figure 11-4. Stages of synchronized animations

Stages of synchronized animations

It is also possible to add an offset to this synchronization. To make an animation start two seconds after another animation, you would use a construction of the form begin="otherAnim.end+2s". (You may add whitespace around the plus sign.) The offset must be positive; to make an animation's start point begin="otherAnim.end-2s" would require the computer to look into the future, and there is no such thing as Psychic Vector Graphics. In Example 11-5, the second circle begins to grow one and a fourth seconds after the first circle begins shrinking.

Example 11-5. Synchronization of animations with offsets

<circle cx="60" cy="60" r="30" style="fill: #f9f; stroke: gray;">
    <animate id="c1" attributeName="r" attributeType="XML"
        begin="0s" dur="4s" from="30" to="10" fill="freeze"/>
</circle>

<circle cx="120" cy="60" r="10" style="fill: #9f9; stroke: gray;">
    <animate attributeName="r" attributeType="XML"
        begin="c1.begin+1.25s" dur="4s" from="10" to="30" fill="freeze"/>
</circle>

Now that we know about synchronizing animations, we can introduce the end attribute, which sets an end time for an animation. This is not a substitute for the dur attribute! The following animation will start six seconds after the page loads, and will last for twelve seconds or until an animation named otherAnim ends; whichever comes first.

<animate attributeName="width" attributeType="XML"
    begin="6s" dur="12s" end="otherAnim.end"
    from="10" to="100" fill="freeze"/>

You can, of course, set the value of end to a number; this is useful for halting an animation partway through so that you can see if everything is in the right place. This is how we were able to create Figure 11-3. The following animation starts at six seconds, and should last for twelve seconds, but is halted at nine seconds.

<animate attributeName="width" attributeType="XML"
    begin="6s" dur="12s" end="9s"
    from="10" to="100" fill="freeze"/>

Repeated Action

The animations we've produced so far occur exactly once; we set fill to freeze to keep the final stage of the animation. If we want to have the object return to its pre-animation state, we omit the attribute. (This is equivalent to setting fill to the default value of remove.)

Two other attributes allow you to repeat an animation. The first of them, repeatCount, is set to an integer value telling how many times you want a particular animation to repeat. The second, repeatDur, is set to a time telling how long the repetition should last. If you want an animation to repeat until the user leaves the page, set either repeatCount or repeatDur to the value indefinite. The animation in Example 11-6 shows two circles. The upper circle moves from left to right in two repetitions of five seconds each. The second circle moves from right to left for a total of eight seconds.

Example 11-6. Example of repeated animation

<circle cx="60" cy="60" r="30" style="fill: none; stroke: red;">
    <animate attributeName="cx" attributeType="XML"
        begin="0s" dur="5s" repeatCount="2"
        from="60" to="260" fill="freeze"/>
</circle>

<circle cx="260" cy="130" r="30" style="fill: #ccf; stroke: black;">
    <animate attributeName="cx" attributeType="XML"
        begin="0s" dur="5s" repeatDur="8s"
        from="260" to="60" fill="freeze"/>
</circle>

Just as it was possible to synchronize an animation with the beginning or ending of another animation, we can tie the start of one animation to the start of a specific repetition of another animation. You give the first animation an id, then set the begin of the second animation to id .repeat( count ), where count is a number beginning at zero for the first repetition. In Example 11-7, we have an upper circle moving from left to right three times, requiring five seconds for each repetition. The lower square will go right to left only once, and will not begin until halfway through the second repetition. (We use an offset to achieve this effect.)

Example 11-7. Synchronizing an animation with a repetition

<circle cx="60" cy="60" r="30"
    style="fill: none; stroke: red;">
    <animate id="circle-anim" attributeName="cx" attributeType="XML"
        begin="0s" dur="5s" repeatCount="3"
        from="60" to="260" fill="freeze"/>
</circle>

<rect x="230" y="100" width="60" height="60"
    style="fill: #ccf; stroke: black;">
    <animate attributeName="x" attributeType="XML"
        begin="circle-anim.repeat(1) + 2.5s" dur="5s"
        from="230" to="30" fill="freeze"/>
</rect>

The set Element

All of these animations have modified numeric values over time. You may want to set a non-numeric attribute or property though. For example, you might want an initially invisible text item to become visible at a certain time; there's no real need for both a from and to. Thus, we have the convenient shorthand of the <set> element, which needs only a to attribute and the proper timing information. Example 11-8 shrinks a circle down to zero, then reveals text one-half second after the circle is gone.

Example 11-8. Example of set element

<circle cx="60" cy="60" r="30" style="fill: #ff9; stroke: gray;">
    <animate id="c1" attributeName="r" attributeType="XML"
        begin="0s" dur="4s" from="30" to="0" fill="freeze"/>
</circle>

<text text-anchor="middle" x="60" y="60" style="visibility: hidden;">
    <set attributeName="visibility" attributeType="CSS"
        to="visible" begin="4.5s" dur="1s" fill="freeze"/>  
    All gone!
</text>

The animateColor Element

The <animate> element doesn't work with colors, since a color is not represented as a simple numeric value. Instead, the special <animateColor> element fills that purpose. Its from and to attributes are color values, as described in Chapter 3, in Section 3.2.2. In Example 11-9 we animate the fill and stroke colors of a circle, changing the fill from light yellow to red and the gray outline to blue. Both animations start two seconds after the page loads; this gives you time to see the original colors.

Example 11-9. Example of animateColor

<circle cx="60" cy="60" r="30"
	style="fill: #ff9; stroke: gray; stroke-width: 10;">
    <animateColor attributeName="fill"
        begin="2s" dur="4s" from="#ff9" to="red" fill="freeze"/>
    <animateColor attributeName="stroke"
        begin="2s" dur="4s" from="gray" to="blue" fill="freeze"/>
</circle>

Note

If you have several animations for a particular object, one animation can refer to the previous one with the keyword prev. We could rewrite the preceding example as Example 11-10. Tying two related animations together with prev lets you change them both by editing just the first one.

Example 11-10. Use of the prev keyword in animation

<circle cx="60" cy="60" r="30"
	style="fill: #ff9; stroke: gray; stroke-width: 10;">
    <animateColor attributeName="fill"
        begin="2s" dur="4s" from="#ff9" to="red" fill="freeze"/>
    <animateColor attributeName="stroke"
        begin="prev.begin" dur="4s" from="gray" to="blue" fill="freeze"/>
</circle>

The animateTransform Element

Just as <animate> doesn't work with colors, it doesn't work with rotate, translate, scale, or skew transformations either, since they're all "wrapped up" inside the transform attribute. The <animateTransform> element comes to the rescue. You set its attributeName to transform. The type attribute's value then specifies the transformation whose values should change (one of translate, scale, rotate, skewX, or skewY). The from and to values are specified as appropriate for the transform that you're animating.

Example 11-11 stretches a rectangle from normal scale to a scale of four times in the horizontal direction and two times in the vertical direction. Note that we've centered the rectangle around the origin so it doesn't move as it scales; we place it inside a <g> so it can be translated to a more convenient location. Figure 11-5 shows the beginning and end of the animation.

Example 11-11. Example of animateTransform

<g transform="translate(120,60)">
<rect x="-10" y="-10" width="20" height="20"
    style="fill: #ff9; stroke: black;">
    <animateTransform attributeType="XML"
        attributeName="transform" type="scale"
        from="1" to="4 2"
        begin="0s" dur="4s" fill="freeze"/>
    Stretch
</rect>
</g>

Figure 11-5. animateTransform -- before and after

animateTransform -- before and after

If you intend to animate more than one transformation, you must use the additive attribute. The default value of additive is replace, which replaces the specified transformation in the object being animated. This won't work in a series of animations, since the second animation would override the first one. By setting additive to sum, SVG will accumulate the transformations. Example 11-12 stretches and rotates the rectangle; the before and after pictures are in Figure 11-6.

Example 11-12. Example of multiple animateTransform elements

<rect x="-10" y="-10" width="20" height="20"
    style="fill: #ff9; stroke: black;">
    <animateTransform attributeName="transform" attributeType="XML"
        type="scale" from="1" to="4 2"
        additive="sum" begin="0s" dur="4s" fill="freeze"/>
    <animateTransform attributeName="transform" attributeType="XML"
        type="rotate" from="0" to="45"
        additive="sum" begin="0s" dur="4s" fill="freeze"/>
</rect>

Figure 11-6. Multiple animateTransforms -- before and after

Multiple animateTransforms -- before and after

The animateMotion Element

By using translate with the <animateTransform> element, you can cause an object to animate along a straight-line path. The <animateMotion> element lets you do this as well; additionally, it allows you to animate an object along an arbitrary path.

If you insist on using <animateMotion> for straight-line motion, you simply set the from and to attributes, assigning them each a pair of (x, y) coordinates. Example 11-13 moves a grouped circle and rectangle from (0,0) to (60,30).

Example 11-13. Animation along a linear path

<g>
    <rect x="0" y="0" width="30" height="30" style="fill: #ccc;"/>
    <circle cx="30" cy="30" r="15" style="fill: #cfc; stroke: green;"/>
    <animateMotion from="0,0" to="60,30" dur="4s" fill="freeze"/>
</g>

If you want a more complex path to follow, use the path attribute instead; its value is in the same format as the d attribute in the <path> element. Example 11-14, adapted from the SVG specification, animates a triangle along a cubic Bézier curve path.

Example 11-14. Animation along a complex path

<!-- show the path along which the triangle will move -->
<path d="M50,125 C 100,25 150,225, 200, 125"
        style="fill: none; stroke: blue;"/>

<!-- Triangle to be moved along the motion path.
   It is defined with an upright orientation with the base of
   the triangle centered horizontally just above the origin. -->
<path d="M-10,-3 L10,-3 L0,-25z" style="fill: yellow; stroke: red;">
    <animateMotion
        path="M50,125 C 100,25 150,225, 200, 125"
        dur="6s" fill="freeze"/>
</path>

As you can see in Figure 11-7, the triangle stays upright throughout its entire path.

Figure 11-7. animateMotion along a complex path

animateMotion along a complex path

If you would prefer that the object tilt so its x-axis is always parallel to the slope of the path, just add the rotate attribute with a value of auto to the <animateMotion> element. Example 11-15 shows the SVG and Figure 11-8 shows screenshots taken at various stages of the animation.

Example 11-15. Animation along a complex path with auto-rotation

<!-- show the path along which the triangle will move -->
<path d="M50,125 C 100,25 150,225, 200, 125"
        style="fill: none; stroke: blue;"/>

<!-- Triangle to be moved along the motion path.
   It is defined with an upright orientation with the base of
   the triangle centered horizontally just above the origin. -->
<path d="M-10,-3 L10,-3 L0,-25z" style="fill: yellow; stroke: red;" >
    <animateMotion
        path="M50,125 C 100,25 150,225, 200, 125"
               rotate="auto"
        dur="6s" fill="freeze"/>
</path>

Figure 11-8. animateMotion along a complex path with auto-rotation

animateMotion along a complex path with auto-rotation

Put simply, when you leave off the rotate attribute, you get the default value of zero, and the object acts like a hot-air balloon floating along the path. If you set rotate to auto, the object acts like a car on a roller coaster, tilting up and down as the path does.

You can also set rotate to a numeric value, which will set the rotation of the object throughout the animation. Thus, if you wanted an object rotated 45 degrees no matter what direction the path took, you'd use rotate="45".

In Example 11-15, we drew the path in blue so that it was visible, and then duplicated the path in the <animateMotion> element. You can avoid this duplication by adding an <mpath> element within the <animateMotion> element. The <mpath> will contain an xlink:href attribute that references the path you want to use. This also comes in handy when you have one path you wish to use to animate multiple objects. Here's the preceding example, rewritten as Example 11-16, using <mpath>.

Example 11-16. Motion along a complex path using mpath

<path id="cubicCurve" d="M50,125 C 100,25 150,225, 200, 125"
        style="fill: none; stroke: blue;"/>

<path d="M-10,-3 L10,-3 L0,-25z" style="fill: yellow; stroke: red;" >
    <animateMotion dur="6s" rotate="auto" fill="freeze">
        <mpath xlink:href="#cubicCurve"/>
    </animateMotion>
</path>

Using Links in SVG

To this point, you, the author of the SVG document, have made all the decisions about a graphic. You decide what a static image should look like, and if there are any animations, you decide when they start and stop. In this section, we will see how to hand some of that control over to the person who is viewing your document.

The easiest sort of interactivity to provide is linking, accomplished with the <a> element. By enclosing a graphic in this element, it becomes active; when clicked, you go to the URL specified in the xlink:href attribute. You can link to another SVG file or, depending upon your environment, a web page. In Example 11-17, clicking the word "Cat" will link to an SVG drawing of a cat; clicking the red, green, and blue shapes will link to the World Wide Web Consortium's SVG page. All the items within the second link are individually linked to the same destination, not the entire bounding box. When you test this example and move the cursor between the shapes, you will see that those areas are not linked.

Example 11-17. Links in SVG

<a xlink:href="cat.svg">
    <text x="100" y="30" style="font-size: 12pt;">Cat</text>
</a>

<a xlink:href="http://www.w3.org/SVG/">
    <circle cx="50" cy="70" r="20" style="fill: red;"/>
    <rect x="75" y="50" width="40" height="40" style="fill: green;"/>
    <path d="M120 90, 140 50, 160 90 Z" style="fill: blue;"/>
</a>

It's worth noting that xlink:href has a namespace prefix; though this attribute is duplicated in the SVG specification, it belongs to the XLink specification. The SVG DTD handles the namespace declaration.

Scripting SVG

The next step up from linking — and it's a big step — is scripting. You can write a program in ECMA Script (the European Computer Manufacturer's Association standard version of what is commonly called JavaScript) to interact with an SVG graphic. Interaction occurs when graphic objects respond to events.

Objects can respond to mouse events associated with clicking the mouse button: click, mousedown, and mouseup;[2] events associated with moving the mouse: mouseover (the mouse pointer is within the object), mouseout (the mouse pointer leaves the object), and mousemove; events associated with an object's status: load (the object has been fully parsed and is ready to render); and the non-standardized events associated with pressing keys: keydown and keyup.

Note

The names listed here are the names of the events. We'll be using attributes to specify the functions which should handle the events. These event handler attributes begin with the word on. Thus, onclick is an attribute whose value specifies a function that handles a click event.

To allow an object to respond to an event, you add an on eventName attribute to the element in question. The value of the attribute will be an ECMA Script statement, usually a function call. This function usually takes the reserved word evt as one of its parameters. evt has properties and methods that describe the event that has occurred. The three methods you will use most often are getTarget(), which returns a reference to the graphic object that is responding to this event, and getClientX() and getClientY(), which return the x- and y- coordinates of the mouse when the event occurred. A value of (0,0) indicates the upper left corner of the SVG viewport. These functions return the position in the viewer window regardless of any zoom or pan that the user may have done.

Changing Attributes of a Single Object

The simplest type of event handling is where an event occurring on object X modifies some attribute of that object. Let's look at Example 11-18, which makes a circle respond to the mouseover event by making the circle's radius larger. The circle will respond to the mouseout event by making the radius smaller.

Example 11-18. Basic scripting -- changing a single object

<script type="text/ecmascript">     [1]
<![CDATA[     [2]
function enlarge_circle(evt)     [3]
{
    var circle = evt.getTarget();     [4]

    circle.setAttribute("r", 50);     [5]
}

function shrink_circle(evt)     [6]
{
    var circle = evt.getTarget();

    circle.setAttribute("r", 25);
}
// ]]>     [7]
</script>
 
<circle cx="150" cy="100" r="25" fill="red"     [8]
    onmouseover="enlarge_circle(evt)"     [9]
    onmouseout="shrink_circle(evt)"/>
  
<text x="150" y="175" style="text-anchor: middle;">
    Mouse over the circle to change its size.
</text>

[1] The beginning <script> tag indicates that you are preparing to leave the world of SVG/XML and enter the ECMA Script environment. The type attribute tells which scripting language you are using.

[2] In XML, the less than sign introduces a tag, and the ampersand symbol is used for escaping characters (see Appendix A in Section A.2.5). Since ECMA Script isn't XML, we want to turn off this special behavior. The <![CDATA[ tells XML to stop treating the less than and ampersand symbols as special; they become ordinary content, which is the way ECMA Script likes it. This completes our exit from the SVG/XML world and immerses us in the ECMA Script environment.

[3] The first function, enlarge_circle, takes one parameter; the event that triggered the call.

[4] We use the evt.getTarget to return a reference to the graphic object that triggered the event, and store it in variable circle

[5] To change an attribute of a graphic object, call the object's setAttribute function with two parameters: the name of the attribute you wish to change, and its new value. setAttribute is a void function; it returns no value. There is a corresponding getAttribute function which takes as its parameter the name of the attribute; it returns a string representation of the attribute's current value. These functions are part of the DOM, or Document Object Model. The DOM is a standard API for accessing, modifying, and rearranging the elements in an XML document.

[6] This function, patterned exactly like the previous one, will set the r attribute of the event's target to 25.

[7] The ]]> ends the <![CDATA[ on the second line of the script. It returns the less than and ampersand to their special XML status, just in time for </script> on the next line, which leaves ECMA Script and returns you to the SVG/XML world. The leading slashes on line 16 introduce an ECMA Script comment, ensuring that the remainder of the line is not interpreted as part of the script. (This is not necessary with version 2.0 Adobe plugin, but other XML applications may require the slashes.)

[8] Since we will be setting an attribute to change the color, we specify the fill color with a presentation attribute. We can't use style, because its setting overrides the value of a presentation attribute.

[9] Here is our bridge between the SVG world and the ECMA Script world. The onmouseover event handler will call enlarge_circle, and the onmouseout event handler will call shrink_circle. In this context, evt is provided by the SVG viewer environment.

Changing Attributes of Multiple Objects

Sometimes you will want an event that occurs on object A to affect attributes of both object A and some other object B. Example 11-19 shows possibly the world's crudest example of SVGcommerce. Figure 11-9 shows a T-shirt whose size changes as the user clicks each labeled button. The currently selected size button is highlighted in light yellow.

Figure 11-9. Screenshots of different selections

Screenshots of different selections

Example 11-19. Changing multiple objects in a script

<svg width="400" height="250"  viewBox="0 0 400 250"
    onload="init(evt)">     [1]

<script type="text/ecmascript">
<![CDATA[

var scaleChoice = 1;     [2]
var scaleFactor = new Array( 1.25, 1.5, 1.75 );

function init( evt )
{
    transformShirt();
}

function setScale( n )
{
    obj = svgDocument.getElementById( "scale" + scaleChoice );      [3]
    obj.setAttribute( "fill", "white" );
    scaleChoice = n;
    obj = svgDocument.getElementById( "scale" + scaleChoice );
    obj.setAttribute( "fill", "#ffc" );
    transformShirt();
}

function transformShirt( )
{
    var obj = svgDocument.getElementById( "shirt" );     [4]
    obj.setAttribute( "transform",
        "scale(" + scaleFactor[scaleChoice] + ")"
    );
    obj.setAttribute( "stroke-width",
        1 / scaleFactor[scaleChoice] );
}

// ]]>
</script>
 
<defs>
    <path id="shirt"      [5]
        d="M -6 -30, -32 -19, -25.5 -13, -22 -14, -22 30, 23 30,
            23 -14, 26.5 -13, 33 -19, 7 -30
            A 6.5 6 0 0 1 -6 -30"
        fill="white" stroke="black"/>     [6]
</defs>

<use xlink:href="#shirt" x="150" y="150"/>

<g onclick="setScale(0)">     [7]
<rect id="scale0" x="100" y="10" width="30" height="30"
    fill="white" stroke="black"/>
<text x="115" y="30" text-anchor="middle">S</text>
</g>

<g onclick="setScale(1)">
<rect id="scale1" x="140" y="10" width="30" height="30"
    fill="#ffc" stroke="black"/>      [8]
<text x="155" y="30" text-anchor="middle">M</text>
</g>

<g onclick="setScale(2)">
<rect id="scale2" x="180" y="10" width="30" height="30"
    fill="white" stroke="black"/>
<text x="195" y="30" text-anchor="middle">L</text>
</g>

</svg>

[1] As soon as the document finishes loading, the load event occurs, and the onload handler will call the init function, passing it the event information. Most scripts will use this event handler to make sure that all their variables are set up properly.

[2] This script works by keeping track of which button (S, M, or L) has been chosen, and indexing into the corresponding entry in the scaleFactor array. The default is index number one, medium.

[3] svgDocument is a reference to the entire SVG document, and we'll be using its properties and methods to access the parts of the document that we need. This entire script hinges on the use of the document's getElementById function. getElementById takes a string as its parameter, and returns the object which has that string as its id. The setScale function finds the currently chosen button and turns its fill color to white. It then updates the current choice, and changes that button's fill color to light yellow. Finally, it updates the image of the shirt.

[4] In addition, the transformShirt function calls on the document's getElementById to access the graphic object with id of shirt, and changes its transform attribute to the proper scale. It also sets the stroke-width to the reciprocal of the scaling factor so the stroke width of the shirt's border always equals one.

[5] Here's the shirt; it's centered around (0,0) so it will appear to expand around its center.

[6] Since we're changing the attributes in the script, we have to use presentation attributes here instead of styles. (If we used styles, they would override any attribute settings.)

[7] Each of the buttons will call the setScale function when clicked; the parameter gives the index for the scaling factor, and each button's <rect> element is named with the corresponding number at the end.

[8] The default button has a background set to light yellow when the document first loads.

Dragging Objects

Let us expand this example by adding "sliders" that can be dragged to set the color of the shirt, as shown in Figure 11-10.

Figure 11-10. Screenshot of color sliders

Screenshot of color sliders

We'll need a few more global variables in the script. The first of these, slideChoice, tells which slider (0, 1, or 2) is currently being dragged; its initial value is -1, meaning that no slider is active. We'll also use an array called rgb to hold the percent of red, green, and blue; the initial values are all 100, since the shirt is initially white.

var slideChoice = -1;
var rgb = new Array( 100, 100, 100);

We now draw the sliders themselves. The color bar and the slide indicator are drawn on a white background, and they are grouped together. The id attribute goes on the indicator <line> element, since its y-coordinate will be changing. The event handlers will be attached to the enclosing <g> element. The group will then capture the mouse events that happen on any of its child elements. (This is why we drew the white background; the mouse will still track even if you drag outside the colored bar.)

<g onmousedown="startColorDrag(0)"
    onmouseup="endColorDrag()"
    onmousemove="doColorDrag(evt,0)"
    transform="translate( 230, 10 )">
    <rect x="-10" y="-5" width="40" height="110" style="fill: white;"/>
    <rect x="5" y="0" width="10" height="100" style="fill: red;"/>
    <line id="slide0" x1="0" y1="0" x2="20" y2="0"
        style="stroke: gray; stroke-width: 2;"/>
</g>

<g onmousedown="startColorDrag(1)"
    onmouseup="endColorDrag()"
    onmousemove="doColorDrag(evt,1)"
    transform="translate( 280, 10 )">
    <rect x="-10" y="-5" width="40" height="110" style="fill: white;"/>
    <rect x="5" y="0" width="10" height="100" style="fill: green;"/>
    <line id="slide1" x1="0" y1="0" x2="20" y2="0"
        style="stroke: gray; stroke-width: 2;"/>
</g>

<g onmousedown="startColorDrag(2)"
    onmouseup="endColorDrag()"
    onmousemove="doColorDrag(evt,2)"
    transform="translate( 330, 10 )">
    <rect x="-10" y="-5" width="40" height="110" style="fill: white;"/>
    <rect x="5" y="0" width="10" height="100" style="fill: blue;"/>
    <line id="slide2" x1="0" y1="0" x2="20" y2="0"
        style="stroke: gray; stroke-width: 2;"/>
</g>

The corresponding functions are as follows:

/*
 * Stop dragging the current slider (if any)
 * and set the current slider to the one specified.
 * (0 = red, 1 = green, 2 = blue)
 */
function startColorDrag( which )
{
    endColorDrag( );
    slideChoice = which;
}

/*
 * Set slider choice to -1, indicating that no
 * slider is begin dragged.
 */
function endColorDrag( )
{
   slideChoice = -1;
}

/*
 * Move the specified slider in response to the
 * mousemove event.
 */
function doColorDrag( evt, which )
{
    /*
     * If no slider is active, or the event is on a
     * slider other than the active one, do nothing
     */
    if (slideChoice < 0 || slideChoice != which)
    {
        return;
    }
    
    /*
     * Get the slider indicator line object, and the
     * mouse position (relative to the top of the color bar)
     */
    var obj = evt.getTarget();
    var pos = evt.getClientY() - 10;

    /* Clamp values to range 0..100 */
    if (pos < 0) { pos = 0; }
    if (pos > 100) { pos = 100; }
    
    /* Move the slider line to the new mouse position */
    obj = svgDocument.getElementById( "slide" + slideChoice );
    obj.setAttribute("y1", pos );
    obj.setAttribute("y2", pos );
    
    /* Calculate the new color value for this slider */
    rgb[slideChoice] = 100-pos;
    
    /*
     * Put together all the color values and
     * change the shirt's color accordingly.
     */
    var colorStr = "rgb(" + rgb[0] + "%," +
        rgb[1] + "%," + rgb[2] + "%)";
    obj = svgDocument.getElementById( "shirt" );
    obj.setAttribute("fill", colorStr );
}

There's only one minor point to take care of — the document will respond to an onmouseup only if it occurs within the slider area. So, if you click the mouse on the red color bar, drag the mouse down to the shirt, then release the mouse button, the document will be unaware of it. When you then move the mouse over the red slider again, it will still follow the mouse. To solve this problem, we insert a transparent rectangle that completely covers the viewport, and have it respond to a mouseup event by calling stopColorDrag. It will be the first, and therefore bottom-most object in the graphic. To make the rectangle as unobtrusive as possible, it will be set to style="fill: none;". "But wait," you interject. "A transparent area cannot respond to an event!" No, ordinarily it can't, but we can set the pointer-events attribute to visible, meaning that an object can respond to events as long as it is visible, no matter what its opacity.[3]

<rect x="0" y="0" width="400" height="300" style="fill: none;"
     onmouseup="endColorDrag()"
     pointer-events="visible"/>

Modifying Text

Although the sliders do give us the interactivity we want, it's hard to judge the percentages by eye. We'd much prefer to show the percentages of red, green, and blue below each slider, as depicted in Figure 11-11.

Figure 11-11. Screenshot of labeled color sliders

Screenshot of labeled color sliders

Here's the SVG for the text underneath the red slider; the ones under the blue and green slider have ids of pct1 and pct2. This element will go into a separate group from the color bar and slider line so that it does not also become clickable.

<text id="pct0" x="10" y="120"
    style="font-size: 9pt; text-anchor: middle;">100%</text>

The number we want to change as we drag the mouse is not an attribute; it's the child of the <text> element, so we can't use setAttribute to modify the text. Instead, we have to create a new text node and insert it into the document tree in place of the old one. Here's the code that we add at the end of the doColorDrag function. The first line retrieves the <text> element. (The obj variable has already been declared, so we don't need to say var again.) The second line asks the document to create a new text node whose content is the percentage of the currently chosen slider. This text node needs to be placed in its proper location within the document tree. That's what the third line does; it replaces the first child of the text element with the newly created node.

    obj = svgDocument.getElementById( "pct" + slideChoice );
    var newText = svgDocument.createTextNode( rgb[slideChoice] + "%" );
    obj.replaceChild( newText, obj.getFirstChild() );

Note

You can do much more than simply modify the text in a document. You can remove elements, change their order, or even create new elements with attributes and add them to the document under script control. A list of the functions that manipulate the Document Object Model are listed in Appendix E of the World Wide Web Consortium's Document Object Model (DOM) Level 1 Specification at http://www.w3.org/TR/1998/REC-DOM-Level-1-19981001. A detailed explanation of what the functions do can be found in the DOM Reference chapter of XML in a Nutshell, by Elliotte Rusty Harold and W. Scott Means. Although the examples in that book use Java rather than ECMA Script, the explanations apply to both.

Interacting with an HTML Page

It is possible to embed an SVG graphic in a web page using the <embed> element. The relevant attributes are the src for the graphic (a URL), the width and height of the graphic, and the type attribute, which will be image/svg+xml. Once embedded, you can add code to the SVG script and the HTML page's script so they can communicate with one another.

Our goal in this example is to embed the preceding SVG example into a web page. This web page will have a form that lets users type in the red, green, and blue percentages. The values they enter will be reflected in the sliders. If they adjust the sliders, the values in the form fields will be updated accordingly.

Here is the HTML document, with references to the (as yet unwritten) updateSVG function. This function will take the input field number and the value currently within the input field.

<html>
<head>
<title>SVG and HTML</title>
</head>

<body bgcolor="white">
<h2>SVG and HTML</h2>


<div align="center">
<embed src="shirt_interact.svg"
    width="400" height="250"
    type="image/svg+xml" />

<form name="rgbForm">
    Red: <input id="fld0" type="text" size="5" value="100"
            onchange="updateSVG(0, this.value)" />% <br /> 
    Green: <input id="fld1" type="text" size="5" value="100"
            onchange="updateSVG(1, this.value)" />% <br />
    Blue: <input id="fld2" type="text" size="5" value="100"
            onchange="updateSVG(2, this.value)" />%    
</form>
</div>
</body>
</html>

Here is the script that goes into the head of the HTML document. Function updateSVG checks to see that the input value is an integer (it will discard any decimal part), and, if so, calls setShirtColor. setShirtColor is actually a reference to a function that exists in the SVG document, and it will be the SVG document's responsibility to connect the function to this HTML reference.

Function updateHTMLField will be called from the SVG document's script. It will receive a form field number and a percent value, which it will display in the appropriate form field.

<script language="Javascript">
<!--
function updateSVG( which, amount )
{
    amount = parseInt( amount );
    if (!isNaN(amount))
    {
        window.setShirtColor( which, amount );
    }
}

function updateHTMLField( which, percent )
{
    document.rgbForm[ "fld" + which ].value = percent;
}
// -->
</script>

Let us now turn our attention to the SVG document. The parent of the document is the browser window in which it is embedded. We use the reserved word parent in the init function to connect the SVG document's svgSetShirtColor function to the HTML page's setShirtColor reference.

function init( evt )
{
    parent.setShirtColor = svgSetShirtColor;
    svgDocument = evt.getTarget().getOwnerDocument();
    transformShirt();
}

Since there are now effectively two ways to set the color values, we'll make things more modular by rewriting doColorDrag to call the new svgSetShirtColor function:

function doColorDrag( evt, which )
{
    /*
     * If no slider is active, or the event is on a
     * slider other than the active one, do nothing
     */
    if (slideChoice < 0 || slideChoice != which)
    {
        return;
    }
    
    /*
     * Get the slider indicator line object, and the
     * mouse position (relative to the top of the color bar)
     */
    var obj = evt.getTarget();
    var pos = evt.getClientY() - 10;
    
    /*
     * Since pos=0 is at the 100% point on the scale,
     * take 100-pos and send that to svgSetShirtColor
     * along with the slider number.
     */
    svgSetShirtColor( which, 100 - pos );
}

Function svgSetShirtColor will do what the remainder of doColorDrag used to do, with two major differences. It uses the slider number that it is given as the first parameter, not the global sliderChoice variable. The second parameter is now a percentage; in the original version it was the y-position of the mouse. These are the sorts of changes you have to make when you decide to modularize simple code that was written for an ad-hoc example.

function svgSetShirtColor( which, percent )
{
    var obj;
    var colorStr;
    var newText;

    /* Clamp values to range 0..100 */
    if (percent < 0) { percent = 0; }
    if (percent > 100) { percent = 100; }

    /* Move the slider line to the new mouse position */
    obj = svgDocument.getElementById( "slide" + which );
    obj.setAttribute("y1", 100-percent );
    obj.setAttribute("y2", 100-percent );
    rgb[which] = percent;
    
    /*
     * Put together all the color values and
     * change the shirt's color accordingly.
     */
    colorStr = "rgb(" + rgb[0] + "%," +
        rgb[1] + "%," + rgb[2] + "%)";
    obj = svgDocument.getElementById( "shirt" );
    obj.setAttribute("fill", colorStr );
    
    /*
     * Change text to match slider position
     */
    obj = svgDocument.getElementById( "pct" + which );
    newText = svgDocument.createTextNode( rgb[which] + "%" );
    obj.replaceChild( newText, obj.getFirstChild());
}

This code accomplishes the HTML to SVG communication. Our last step is to communicate from SVG back to HTML if the user decides to choose colors with the slider. Rather than continuously update the HTML fields, we have made the design decision to update the HTML when dragging stops. We add the boldface code to function endColorDrag. The result is shown in Figure 11-12; the screenshot has been edited to eliminate unnecessary whitespace).

function endColorDrag( )
{
     /*
     * If a slider was being moved, send the slider number
     * and its value back to the updateHTMLField function
     * in the parent web browser window.
     */
    if (slideChoice >= 0)
    {
        parent.updateHTMLField( slideChoice, rgb[slideChoice] );
    }
    
    /* In any case, nobody's being dragged now */
    slideChoice = -1;
}

Figure 11-12. Screenshot of HTML and SVG interaction

Screenshot of HTML and SVG interaction

Scripting and Animation Together

Scripting and animation can work together. You can start an animation in response to an event, and you can use the beginning, end, or repetition of an animation to invoke a function. Example 11-20 shows a trapezoid and a button. When you click the button, a message saying "Animation in progress" appears, and the trapezoid rotates 360 degrees. When it finishes rotating, the message disappears. Relevant screenshots are shown in Figure 11-13.

Figure 11-13. Screenshot of two stages of scripting with animation

Screenshot of two stages of scripting with animation

Note

The numbered callouts in the example are in conceptual order, not text order. I've found it easier to design the SVG first and add the scripting later. One advantage of this method is that I can see if the base drawing looks good before I start making it react to events.

Example 11-20. Scripting and Animation Together

<script type="text/ecmascript">
<![CDATA[

function init( evt )
{
    /* initialization code goes here */
}

function setMessage( visStatus )      [1]
{
    var message = svgDocument.getElementById( "message" );
    message.setAttribute( "visibility", visStatus );
}

// ]]>
</script>

<g id="button">     [2]
    <rect x="10" y="10" width="40" height="20" rx="4" ry="4"
        style="fill: #ddd;"/>
    <text x="30" y="25" style="text-anchor: middle;">Start</text>
</g>

<text id="message" x="60" y="25" visibility="hidden">     [3]
    Animation in progress.
</text>

<g transform="translate(100, 60)">
    <path d="M-25 -15, 0 -15, 25 15, -25 15 Z"
        style="stroke: gray; fill: #699;">
        
        <animateTransform id="trapezoid" attributeName="transform"
            type="rotate" from="0" to="360"
            begin="button.click"     [4]
            dur="6s"
            onbegin="setMessage('visible')"      [5]
            onend="setMessage('hidden')"/> 
    </path>
</g>

[1] Here's the function, which takes the visibility status that was passed to it, and sets the message's visibility property accordingly.

[2] The start button is a simple rounded rectangle with text. The entire group gets the id.

[3] The message, initially hidden. Again, we use a presentation attribute here rather than a style, since we'll be modifying the attribute.

[4] Instead of giving the begin time for the animation in terms of seconds, we begin whenever a click event is detected on the button object. Since we're waiting for the event, we use click rather than the event handler attribute onclick.

[5] The next two lines are event handlers, so they begin with the prefix on. When the animation begins (onbegin), we call setMessage with the argument 'visible'; when it ends (onend) we call the same function with the argument 'hidden'.[4]

Animation via Scripting

Because SMIL2 animation is integrated with SVG, your animations become part of the document structure. Since it's all XML, animated SVG documents remain easy for humans to read and for XML tools to process. The addition of scripting, as we showed in the preceding section, makes it possible to add some user interaction with an animation.

While animation can handle most of the things you'll want to move, there are some things the animation elements can't do easily. For example, it's hard to change a <path> dynamically. We're not talking about making an object move along a path (<animateMotion>); we want the path itself to change over time. The path isn't a transformation, so <animateTransform> won't work; in addition, it's not a simple numeric value, so we can't use <animate>. We'll have to use scripting instead.

Example 11-21 uses scripting alone to repeatedly change the d attribute of the path over time. The key to doing animation via scripting is the setInterval function. setInterval sets an "alarm clock" to go off repeatedly at regular intervals. When the alarm goes off, the scripting engine performs the ECMA Script statements you want. The simplest form is:

               timer_variable =
    setInterval( "ECMA Script function",
        interval time in milliseconds);

This function returns a reference to the timer that it set. You should save that reference in a global variable so that other functions can access it. You call setInterval once, and, the ECMA Script function you've specified will be called every interval milliseconds. The repetition will continue until you call clearInterval(timer_variable).

Example 11-21 shows a cubic Bézier curve. When the user clicks the start button, we'll move the control points every tenth of a second for five seconds (fifty movements in all). The control points will move towards each other in the x direction starting at five pixels per interval. They will move away from each other in the y direction at three-fourths of a pixel per interval. Note especially the boldfaced line and its commentary!

Example 11-21. Animating a path with scripting

<svg width="300" height="200" viewBox="0 0 300 200"
    onload="init(evt)"> 
<script type="text/ecmascript">
<![CDATA[

/* Starting coordinates for both control points */
var cp1 = new Array( 100, 50 );
var cp2 = new Array( 200, 200 );

var moveLimit = 50;     // total number of moves to make
var currentMove = 0;    // current number of moves

var deltaX;             // X movement amount
var deltaY;             // Y movement amount

var delay = 100;        // one-tenth second

var timer;              // store the alarm clock

function init( evt )
{
    /* do any global initialization here */
}

/*
 * Initialize movement; set control points back to their
 * starting positions, set the current number of moves
 * to zero, and set an interval timer to move the
 * control points every "delay" milliseconds.
 */
function setupMove()
{
    var i;

    currentMove = 0;
    deltaX = 5;         /* start X at five pixels per interval */
    deltaY = 0.75;      /* start Y at 3/4 pixels per interval */
    setGraphicInfo();
    timer = setInterval( "moveControlPoints()", delay );
}

function setGraphicInfo()
{  
    var obj;
    var pathString;
    
    /*
     * Construct a path that describes a cubic
     * Bezier curve using the current coordinates
     * for the control points.
     */
     pathString = "M 50 120 C " +
        (cp1[0] + currentMove * deltaX) + " " +
        (cp1[1] - currentMove * deltaY) + ", " +
        (cp2[0] - currentMove * deltaX)  + " " +
        (cp2[1] + currentMove * deltaY) + ", " +
        "250, 120";
    
    obj = svgDocument.getElementById( "cubic" );
    obj.setAttribute( "d", pathString );
}

function moveControlPoints()
{
    currentMove++;      // we've done one more move
    setGraphicInfo();   // adjust the control points and path

    /*
     * If we have finished moving, clear the timer, which
     * will stop the repeated wake-up calls.
     */
    if (currentMove >= moveLimit)
    {
       clearInterval( timer );
    }
}

function stopMovement()
{
    clearInterval( timer );
}


/*
 * setInterval expects its first argument to be found in
 * the main window. We oblige it by creating a reference
 * to this script's moveControlPoints function as a
 * window property
 */
window.moveControlPoints = moveControlPoints;


// ]]>
</script>

<g id="button" onclick="setupMove();">
    <rect x="10" y="10" width="40" height="20" rx="4" ry="4"
        style="fill: #ddd;"/>
    <text x="30" y="25" style="text-anchor: middle;">Start</text>
</g>

<g id="button" onclick="stopMovement()">
    <rect x="70" y="10" width="40" height="20" rx="4" ry="4"
        style="fill: #ddd;"/>
    <text x="90" y="25" style="text-anchor: middle;">Stop</text>
</g>

<path id="cubic" style="stroke: blue; fill: none;"
    d="M 50 120 C 100 50, 200 200, 250 120" />

</svg>

Figure 11-14 shows the path in its initial and final states.

Figure 11-14. Beginning and ending of scripted path change

Beginning and ending of scripted path change

In this example, the moveControlPoints function didn't need any parameters. Let's say the function needed two parameters, pathColor and pathStrokeWidth To ensure that moveControlPoints got those parameters every time it was called, we would add them at the end of our call to set up the interval timer:

timer = setInterval("moveControlPoints()", delay,
    pathColor, pathStrokeWidth );

setInterval [5] is the only safe way to script repeated actions with a delay interval. Using setInterval is like leaving a message with a desk clerk at a hotel to give you a wake-up call every hour; between calls you are free to sleep or do other things. In scripting terms, this kind of code leaves the computer free to do other tasks between actions:

function startGoodAction()
{
    timer = setInterval( "doGoodAction()", delay );
}

function doGoodAction()
{
    obj.setAttribute( "x", value );
    value = value + increment;
    repetitions = repetitions + 1;
    if ( repetitions == limit )
    {
        clearInterval( timer );
    }
}

Beginning programmers will often do something like this:

function doBadAction()
{
    obj.setAttribute( "x", value );
    value = value + increment; 
    if ( repetitions != limit )
    {
        waitCount = 0;
        while (waitCount < waitLimit)
            waitCount++;
        doBadAction();
    }
}

This technique is akin to constantly looking at your watch and asking, "Is it time yet?" This gives you no time to sleep or do anything else. In an SVG script, it gives the computer little chance to attend to other tasks. Additionally, this uses a recursive style, which, if not carefully controlled, can consume your system resources faster than you can say "Error: stack overflow."

Though the preceding example was a simple one, this technique has great power. If you choose to use scripting to do animation, you will then have available all the interactivity features of scripting; you can make animation dependent on mouse position or complex conditions involving multiple variables. If you're doing simple animation, we recommend that you use SVG's animation elements.

Notes

  1. There is also a by attribute, which you may use instead of to; it is an offset that is added to the starting from value; the result is the ending value.
  2. A click event is defined as a mousedown followed by a mouseup; all three are different events, and cannot be used synonymously.
  3. Other values for pointer-events let you respond to an object's events in the filled areas only (fill), outline areas only (stroke), or the fill and outline together (painted), whether visible or not. Corresponding attribute values of visibleFill, visibleStroke, and visiblePainted take the object's visibility into account as well.
  4. The event handler onrepeat is invoked each time an animation repeats after the first iteration.
  5. Or its variant, setTimeout, which sets the timer to go off exactly once. The generic model for calling this function is: timer_variable = setTimeout( "action_function( parameters )", delay ); Use clearTimeout( timer_variable ) to cancel a pending timer event.
Personal tools