
SVG Tips and Tricks, Part One
Introduction
The previous installments of this
column discussed the two major techniques at the core of interactive
SVG: SVG animation
and DOM scripting. We
saw how the powerful declarative syntax adapted from SMIL
brought life to SVG documents, and how SVG's DOM goes further than
XML's core
DOM. This month's column, rather than a focused exploration of a
particular SVG topic, will examine a few helpful tips and
tricks. First, I'll introduce the viewBox attribute for
zooming purposes, then explain how to use the SMIL DOM interfaces for
remote animation startups, and, last, I'll conclude with a closer look
at DOM Events. At the end, you will understand the tricks used and
abused in the interactive tree map companion
demo.
Before we turn to specifics, I should tell you a bit about the demo. The SVG code of the tree map was actually generated by XSLT from an XML file. The aim was to construct a mechanism to represent an XML file's hierarchy in SVG and be able to browse it in an interactive way. So we are dealing with a nice little tree app here. If you look at the SVG code you'll see that there is still a strong depth structure here too. The idea is that each time we met a node in the XML file, we would create a rectangle and a text label. If the node had a child, then we would process the subtree according to the same rule. By the way, I nicked this idea from Niklas Gustavsson, who's done a fair bit of similar experimentation.
Fiddling With viewBox
One of the obvious advantages of vector graphics is that they can scale for crisp viewing in a variety of situations. You can apply any of SVG's transformations and filters (or any combination) to your graphics and always get a clean rendering. Suppose you have an SVG world map. You might allow viewers to select a country to see it in more detail by displaying it in the largest factor possible. How do you do this?
|
Related Reading
|
A first approach would be to take the group of shapes that represent
the map and apply the correct SVG scale and
translate transformations to it. However, it would be a
bit of a headache to compute the correct transformations. Wouldn't it
be nice to have a mechanism that would allow you to specify an area to
be displayed at maximum zoom factor? Well, there is one, the
viewBox attribute.
A viewBox is, according to the SVG
recommendation, "
a rectangle in user space which should be mapped to the bounds
of the viewport established by the given element". So if
your map had a country with a bounding box of
(100,100,400,200), then to have it fully zoomed
you would set the viewBox attribute to the same
values. The SVG implementation computes, from the rectangle
coordinates you specified, the correct transformations to
apply to the graphics, which saves you a fair bit of
head-scratching. The viewBox can be used on any
shape that defines a new viewport (and in a few more cases;
the SVG recommendation has the details). And it gets better.
What happens when you want to zoom an object whose ratio does not
match your composition's size? The viewBox has a
companion attribute,
preserveAspectRatio, which allows you to specify how the
Viewer should handle zooming of shapes of any type of ratio. But the
best thing about the viewBox is that it can be
animated. If you look at the tree map
demo, you'll see we have used that feature to animate the
top-level (outermost) <svg> element. Animating a
viewBox is not different than the way we animated the
y attribute of the cubes in the first
column. A simple viewBox animation would look like the following.
<svg width="600" height="400" viewBox="0 0 600 400">
<circle cx="100" cy="100" r="90" style="fill: red" />
<rect x="250" y="180" width="300" height="200" style="fill: green" />
<animate attributeName="viewBox" begin="1s" dur="1s"
values="0 0 600 400; 250 180 300 200" fill="freeze" />
</svg>
What we've done is specify the original viewBox at
what it would have been by default (starting at the origin with
composition's width and height). Then we added an
<animate> element that would ask the Viewer to
smoothly transition between the original viewBox and the
one that will fit the green rectangle (250 180 300
200). In our tree map demo, we have a
more subtle usage of the <animate> element since we
script it a fair bit
Scripting Animations
|
| |
Back in the days when we were animating cubes, we saw that SVG, by borrowing from SMIL Animation, allowed us to define animations declaratively using a simple yet powerful XML syntax. Then we saw that when doing drag-and-drop that SVG had full DOM Level 2 capabilities as well as some extensions of its own (the SVG DOM) that opened a new world of possibilities for scripting. Now, how could we mix these two sides of interactive SVG to extend our skills further?
Since the animation in SVG is done through an XML model, we can
actually use the simple DOM scripting techniques to start making funky
things already. In our tree map demo, since
there is never more than one animation at a time, we use a single
<animate> element for every single
animation. However, with bare SVG, we can't make an
<animate> element compute the bounding box of
whatever element has received the mousedown event and pass it to the
values attribute, then launch itself. We need a little
more programmatic power here, which is where DOM scripting comes into
play. Here is what our single <animate> looks like
originally (as placed as the immediate child of our outermost
<svg> element):
<animate id="anim" attributeName="viewBox" begin="undefined"
dur="0.75s" values="" keyTimes="0; 1" keySplines="0 .75 0.25 1"
calcMode="spline" fill="freeze" />
We have also placed event listeners on our squares in the tree
map so that they would call the startAnimation()
method that we have coded. It is within this function that we
access our <animate> element and update its
values. Basically, all we do is get the bounding box of the
object we have just clicked and add it to the
values attribute after the current
viewBox. We actually have a method
printViewBox() that gets an object's bounding box
and returns it as a nicely formatted string. So updating the
values is easy enough, we don't get to use
anything fancier than the DOM 2 Core. But then near the end of
the startAnimation() method we have this funny
line:
anim.beginElement();
SVG animation borrows another bit off SMIL animation: its
ElementTimeControl DOM interface. Yet another DOM
interface, but don't panic, this one is small (only four
methods) and truly essential. It allows you to start and stop
SVG animations from your script without tracking the current
time or anything messy like that. It is fully integrated with
the SVG animation model and respects settings from the
restart and fill attributes. You can
also set timed offsets when you want to start or end an
animation. In order to call these methods on an animation
element, it has to have its begin or
end attributes set to
undefined. Calling beginElement() on
our <animate> element will start the
animated transition right away, with no offset. Using DOM 2
Core and the SVG DOM we update our animation's parameters and
launch it rather easily.
Playing With Events
There are a few remaining subtleties in our tree map. You might have noticed that there are actually two speeds at which transition animations can be played. If you click on a shape with your left button, the animation will last 0.75 seconds. If you right-click on the shape, it will animate twice as fast and not show the Adobe SVG Viewer menu (if you are using the Adobe SVG Viewer). How did we do that? With a little help from the DOM Level 2 Events. We've already played a bit with events when working on drag-and-drop, which included inspecting the events to get the mouse coordinates. Now we can introduce a couple more subtleties. Consider this piece of code:
function getSpeed (evt) {
var speed = 0.75;
if(evt.button == 2) {
evt.preventDefault();
speed /= 2;
}
return speed;
}
|
Also in Sacré SVG |
The getSpeed() function is called when we update the
dur attribute of our animation within our
startAnimation() function. You see that it is passed an
evt event from startAnimation(), and
evt will save the day again by giving us the information
we need. Since our event was originally triggered by clicking on a
square, it is of type MouseEvent. Its
button property informs us of what button the user
pressed, returning 0 for left, 1 for middle
and, 2 for right. All we need to check is for
evt.button to be equal to 2, and in that
case divide the normal speed by 2. We also need to make sure that the
usual context menu will not appear since this would considerably clog
our view and lessen the effect of our transition animation. Then we
just have to call the preventDefault() method as defined
on the
Event interface that MouseEvent inherits from. This
method will make sure that "any default action associated with the
event will not occur".
Wrapping It All Up
That concludes our tips and tricks session. I hope it wasn't too confusing. I take care to read and respond to your comments so I can improve this column; please do not hesitate to drop me a line.
- Stylesheet for Tree demo?
2002-04-02 13:54:05 Jasen Jacobsen
