Doing That Drag Thang
February 27, 2002
Introduction
In last month's article, we took a wee trip in the exciting lands of SMIL-powered SVG animation. In that article, we used XML elements to achieve our goals. Today I will show you around a place that might sound a little scary, but that's just as much fun when you take the time to imagine how many possibilities it offers: it is time to take a look at scripting SVG, for all the nifty interactions that declarative SVG Animation could not handle.
As an XML application, SVG benefits from the Document Object Model. The DOM is an object-oriented API for reading from and writing to an XML document. Even if you've never heard of the DOM, you might have had some unfortunate experience with its wayward sibling, DHTML. DHTML really was the combination of HTML, CSS, JavaScript, and a DOM. What made DHTML such a headache is that the two main browser vendors had different DOMs, neither being compliant with the DOM as specified by the W3C. Recent versions of major browsers now support the W3C DOM Level 2, just like the Adobe SVG Viewer, which also offers support for the SVG DOM. If you need more formal introductions to the DOM, I would strongly suggest a bit of preparatory reading before delving into this article. The O'Reilly Network has some great articles by Scott Andrew LePera on scripting DOM Level 2 in an XHTML context (see parts one and two), and the W3C is the official source for the various DOM specifications.
What's in it for us?
Scripting SVG opens up many new possibilities. While client-side scripting is a well-established practice in different environments (especially DHTML and Flash ActionScript), I believe the SVG scripting environment offers a more comprehensive and standards-based approach. Adobe's SVG Viewer version 3.0 offers stable and powerful tools for us to work with in a way that has never been possible before.
First, the basic API is the W3C DOM Level 2 and offers a generic approach to XML scripting. We have the possibility to script every element and attribute in a consistent way rather than deal with specific features that have been made available by the custom API of a (legacy) browser or that of Flash 5 ActionScript. For instance, while Flash 5 ActionScript allows for duplication of existing movie clips and symbols, it does not allow for on-the-fly creation of graphic objects or completely generic graphic properties access (say, updating a stop-color of a gradient fill). Also, Adobe's Viewer comes with the Mozilla JavaScript engine -- making it immune from the pecularities of the host web browser's JavaScript support -- and on top of that offers the most advanced features of JavaScript.
The DOM Level 2 surely is a great basis for scripting SVG, but being a generic API, it does not handle any of the graphics-oriented needs of SVG scripting. You might well want to have a DOM method for computing an object's bounding box or applying a matrix transform to a given coordinate. Well, SVG has its own extension of the core DOM that does, among other things, just that -- the SVG DOM. In this article we take a short stroll through the SVG DOM.
I will show you how to build a simple and common graphics-oriented interaction, a drag. A word of warning before we get started though: this is an introductory example and does not handle zooming, panning, viewBox, or any other subtleties. I promise we will get back to the dragging in a more generic and powerful way later when we get more familiar with the SVG DOM. Still, this example will show you how to handle events, get mouse pointer coordinates, update elements' CSS properties, and work with two of the basic classes of the SVG DOM, SVGPoint and SVGMatrix. Download the source code for the example, and then let's get started.
The concepts of dragging
Take a look at the SVG example before we proceed (requires SVG plug-in)
Being able to drag an object around the screen is quite a powerful thing. And it is quite simple to achieve. In fact, interactivities are often simple enough to code as long as you have a firm understanding of what needs to happen when you click here and there. The basic idea is that once you have clicked on a shape, it should follow your mouse around the screen until you release your mouse click. A first approach would be to simply apply the mouse coordinates to the shape, but that would result in always dragging by the upper left corner of the shape (the SVG coordinate system being left to right and y-down). So what you want to do when starting the drag is to compute the distance from the top-left corner of your shape to the position of your mouse cursor. Then, when you want to update the shape's coordinates on moving your mouse around, you'll just have to take the mouse coordinates and subtract that distance from it.
Catching events in SVG
In order to have certain pieces of code executed when we click or move the mouse around,
we need to have a mechanism so that our code could be automatically informed of mouse
activities. Luckily, SVG provides event listeners. You might have heard of these before
and probably even used these in DHTML with attributes like onmouseover
. An
event listener is always be focused on what's going on in your SVG, and when something
noteworthy happens it tells you. In our case, we need to listen to three different
events
(all-mouse related): mousedown
(when pushing a mouse button),
mousemove
(when moving the mouse), and mouseup
(when releasing
a mouse click). These are the events, and the event listeners are attributes with
an "on"
prefix. If we want to execute an initialization script when we encounter a
mousedown
event on our shape, we could just write
onmousedown="some_function()"
. In our demo, it is reflected in this bit of
SVG:
<g id="target" onmousedown="initDrag()">
Then all we have to do is implement all we need done for initialization purposes in
the
initDrag()
function (we'll see how it looks later on). Similarly you can
see how we handle mousemove
and mouseup
events:
<g id="background" onmousemove="drag()" onmouseup="endDrag()" style="pointer-events: none;">
Adding script to SVG
Now that we have established a bridge from SVG to JavaScript code, it is time to take
a
look at the code itself. How do we actually get to write JavaScript code with SVG?
SVG has
a <script>
element that allows for either inline coding within a
CDATA
section (to delimit non-XML portions of the document) or a link to a
separate JavaScript library. In our case we xlink:href
to a library called
drag.es
, which keeps the SVG code cleaner:
<script a3:scriptImplementation="Adobe" type="text/ecmascript" xlink:href="drag.es" />
What about that a3:scriptImplementation="Adobe"
bit? The
scriptImplementation
attribute is an SVG extension provided by Adobe (and
cleanly introduced as part of their namespace) to allow us to tell the Adobe SVG Viewer
to
use its own scripting engine rather than the hosting browser's (and believe me, you
really
want to do that). Now open up the drag.es
file and pay close attention.
Wading through the code
I will not go through every single line of this (short) script. However, I believe the file is clearly commented and that comments and the articles I have recommended at the beginning should fill in neatly. Let's concentrate on code specific to the SVG DOM. Before we actually get into the event handler functions, there is one bit of code that is executed when loading the file that's worth taking a look:
var offset = root.createSVGPoint();
The root
variable is a global pointer to the root <svg>
element of our document. As the root element, this element has special powers and
has a
method called createSVGPoint()
that we make use of here. This method, quite
simply, creates an SVGPoint
and returns it, making our global
offset
variable an SVGPoint
itself. But what's an
SVGPoint
? It is one of the few "datatype" objects featured in the SVG DOM
and is a representation for a point. As to the role of the offset
variable,
it will be used later in the script to keep track of the offset of the dragging
session.
We said before that the initDrag()
function was called when clicking on our
draggable shape. The use of this function is to compute the dragging offset
and apply a few style changes to our composition. We start off with an interesting
line of
code:
var matrix = target.getCTM();
The getCTM()
method is a neat function that returns the "current
transformation matrix", as an SVGMatrix
datatype object, of the node we call
it on. As you probably know, most SVG elements feature the transform
attribute in which you can specify a matrix or pre-set types of transformations (like
a
translation). In our example, the position of the "target" group is defined with such
an
attribute:
<g id="target" transform="translate(80,70)" style="pointer-events: all">
The SVGMatrix
returned by getCTM()
helps handling data stored
in the transform
attribute. In this case, it would have been easy to just
parse the string to find out the translation, but there are cases when you have inline
matrix multiplications that would require a lot more work. So we'll use our
SVGMatrix
here. Remember what an SVG matrix looks like: (a, b, c, d,
e, f)
with e
and f
being the fields relative to x and y
translations. Thus, if we want to read 80 and 70 from the attribute, we can simply
use
matrix.e
and matrix.f
now that we have stored the matrix in
the matrix
variable.
Now that we have the original position of the draggable object before any dragging
is
done, we need to find out the position of the mouse so that we can compute the dragging
offset. For this, we have created another function called getMouse()
. Here we
call it:
var mouse = getMouse(evt);
You will probably notice that the getMouse()
function takes an argument
evt
that we have not used or declared before. This is because the DOM
offers a mechanism for inspecting the event that got sent to our function.
evt
is a name commonly given for the events object that is implicitly and
automatically passed to all event handling functions. The events object really is
quite
helpful and holds information like a reference to the node that received the event,
mouse
coordinates, and other neat things. Looking at the code for getMouse()
we see
that kind of thing:
var position = root.createSVGPoint(); position.x = evt.clientX; position.y = evt.clientY; return position;
clientX
and clientY
are two fields that the SVG DOM provides
for us to be able to track mouse positions. The position these fields give us are
computed
relative to the top-left of the SVG rendering area (the Adobe SVG Viewer within your
browser) and do not take into account zooming or panning. getMouse()
returns
an SVGPoint
storing the mouse coordinate for the event provided as a
parameter. Now that we have both the mouse coordinates, we can go back to our event
handling initDrag()
function and compute the offset:
offset.x = matrix.e - mouse.x; offset.y = matrix.f - mouse.y;
There we are. To finish things off we will make a crucial adjustment to the CSS
properties of the background
and the target
layers so that the
dragging goes smoothly. Now that our draggable element has received the initiating
event
(mousedown
), it is important that we make sure that it will not receive any
more events that could conflict with the ones our background
layer expects.
To prevent an SVG element (and its children) from receiving events, one has to set
its CSS
pointer-events
property to "none". But why do we have to do that?
We have set our SVG so that the background
layer handles the
mousemove
event. If the draggable shape still receives events, it will
prevent graphics underneath (our background
layer) from receiving events.
Then why did we not let the draggable shape handle mousemove
itself? Well, if
the same layer receives both mousedown
and mousemove
events, our
SVG Viewer might not have enough time to go through all the code in the
mousedown
event handler function (here, initDrag()
) before
processing the mousemove
event handler function (here, drag()
).
It happens really often that you click and start moving your mouse around in the same
millisecond. In that case, our precious offset will not have had enough time to be
computed and our much-coveted effect will be ruined, sacrebleu! If JavaScript
offered anything similar to Java's synchronized
, life would have been easier.
Hence we have to do this:
target.style.setProperty('pointer-events', 'none'); background.style.setProperty('pointer-events', 'all');
If you've made it thus far, then you're a courageous SVGer; this last bit really was
tricky. Now that the offset is computed, and we're sure event handling was being taken
care of as expected, we can do the easy, i.e., the drag itself. Our appropriately-named
drag()
does this quite well by getting the mouse coordinates every time we
move our mouse, computing the new position of our draggable shape taking into account
our
pre-computed offset, and finally writing to the SVG transform
attribute in
order to have the graphics updated. Here's how it goes:
// gets the pointer position var mouse = getMouse(evt); var x = mouse.x + offset.x; var y = mouse.y + offset.y; // updating the matrix target.setAttribute('transform', 'translate(' + x + ',' + y + ')');
That was easy. The last thing left for us to do is to handle the mouseup
event with our endDrag()
function. All we need is to reset the
pointer-events
values to what they were originally. So this really is only
the inverse of what we have done in initDrag()
, updating the CSS values of
the target
and background
layers.
Wrapping it all up
I hope this simple dragging interaction has uncovered before your starry-eyed faces the power and simplicity of the SVG DOM. It's only the beginning, though. SVG DOM scripting will be a recurring theme in this column since it is an unending gold source. In the next few columns, we will make this code generic, handle zoom and pan (for that "scalable" part), increase performances, and bridge SVG and JavaScript in a more elegant way. Until then, take it easy and à bientôt!