SVG Tips and Tricks, Part One
March 27, 2002
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?
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();
Related Reading |
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.