
Doing That Drag Thang
by Antoine Quint
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">
How are you using SVG animation and JavaScript? Share your tips and tricks in our forums. |
| Post your comments
|
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!
How are you using SVG animation and JavaScript? Share your tips and tricks in our forums.
(* You must be a member of XML.com to use this feature.)
Comment on this Article
- Dragging multiple targets
2003-05-23 10:02:43 Patrick Smith
[Reply]
Hi,
I needed to be able to drag multiple targets, so modified Antoine's script accordingly (full source pasted below).
To drag an element using the newly modified script, the invocation would look like this:
<g id="graphic" onmousedown="setActive('graphic'); initDrag('graphic')" style="pointer-events: all">
If there are any problems with it, I'd love to know.
Thanks,
Patrick
p.s. I also removed references to items that were specific to Antoine's example, e.g. 'frame'.
/***************************************
*
* Author: Antoine Quint
* Article: Doing that drag thang
* http://www.xml.com/lpt/a/2002/02/27/drag.html
* Date: 24/02/2002
*
* Modified by Patrick Smith to support multiple targets
* 05/23/2003
*
***************************************/
// pointer to the root of the document
var root = document.documentElement;
// pointers to SVG elements we will access
var background = root.getElementById("background");
// SVGPoint to keep track of the offset of the dragging session
var offset = root.createSVGPoint();
// what element are we dragging?
var active;
// set our active element
function setActive(name) {
active = name;
}
// called on starting the drag
function initDrag (element) {
// what element are we dragging?
var target = root.getElementById(element);
// track the origin
var matrix = target.getCTM();
// get the relative position
var mouse = getMouse(evt);
offset.x = matrix.e - mouse.x;
offset.y = matrix.f - mouse.y;
// sets the new pointer-events
target.style.setProperty('pointer-events', 'none');
background.style.setProperty('pointer-events', 'all');
}
// called on dragging
function drag () {
// set target to active element
var target = root.getElementById(active);
// 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 + ')');
}
// called on finishing the drag
function endDrag () {
// set target to active element
var target = root.getElementById(active);
// resets the pointer-events
target.style.setProperty('pointer-events', 'all');
background.style.setProperty('pointer-events', 'none');
// reset active element to null
active = '';
}
// returns the mouse coordinates as an SVGPoint
function getMouse (evt) {
var position = root.createSVGPoint();
position.x = evt.clientX;
position.y = evt.clientY;
return position;
}
- Dragging multiple targets
2004-02-21 03:31:46 Rene Mendoza
[Reply]
patrick,
I tested your code with two targets. When I place target1 over target2, drag target2 away from target1, and drag target1 again, target1 doesn't move anymore. Do you have any idea why this is so?
-rene
- When SVG is embedded in HTML...
2003-04-25 03:40:36 Steve Clay
[Reply]
Great article on dragging, thanks a lot. There's a problem when the SVG is embedded in a html page: the coordinates returned from evt.clientX seem to be out by about 50 (strangely the clientY seems to be right!). Any ideas on how to find out dynamically what this offset is so that it can be allowed for in the rest of the code?
Thanks
Steve Clay
- What drags is the text box
2002-12-09 10:11:50 C L
[Reply]
What I can drag in the svg example linked in this article, is ¡the text box!. I expected the other shapes be draggable instead of the text.
I'm using Adobe Viewer 3.0 and IE6.
Thanks for the article, by the way. It addresses a topic I needed.
- broken link
2002-06-25 19:15:19 Bruce Martin
[Reply]
I tried to download the source for the article as suggested, but the link (shown below) is broken. Could you please fix so that I can get the source?
http://www.xml.com/2002/02/27/examples/drag.es.gz
- Stop Drag and Drop when I Zoom
2002-05-15 16:02:54 Gene Pong
[Reply]
Is there a way to disable the drag and drop from this article when someone has zoomed?
- Stop Drag and Drop when I Zoom
2002-05-15 16:33:22 Gene Pong
[Reply]
:) I'm posting a reply to myself. I should have seen this:
Have the onzoom event trigger a function, stopDrag for example, that sets a zoom flag. Then have the functions initDrag and drag check if the zoom flag is not set to proceed with their statements.
I also had to call the endDrag function in the end of the stopDrag function for the text to be displayed after zooming in.
- Drag and Drop with Zoom?
2002-05-15 14:49:26 Gene Pong
[Reply]
How do I scale my movement for dragging whenever I zoom? With the current code from this article, when zoomed in, the element is dragged actually farther than where my mouse is moved.
- Working with .NET
2002-03-21 05:10:21 Mike Berghoff
[Reply]
How do I get the mouse events to work with in Microsoft .NET (ASP.NET)?
I'm new to ASP.NET and would like to be able to change the location of objects on a image through dragging, and then submit the new coordinates back to the server for storage in a database.
It look like SVG would be a good option to use, but how?
Thanks in Advance
Mike B.
- no drag 2
2002-03-03 08:24:59 stephen harlow
[Reply]
Using Adobe SVG Viewer 3.0 with Mac IE 5 - no drag either.
- no drag 2
2002-03-05 02:40:18 Antoine Quint
[Reply]
Hey there,
I have installed MacIE 5.1 and ASV 3.0 on my shiny new iBook, and it works ok. Mind that you do not try to drag from the text and that you wait a second or so before the drag actually happens, the filters make the example a wee bit slow.
Antoine