SVG allows you to do quite a bit in terms of graphics in the browser that was not possible without a special plugin like Flash. This post describes how I enabled dragging of entities around the screen.

Introduction

If you are unfamiliar with SVG I would recommend checking out my two Kickstart tutorials here: One and Two. Furthermore, you can see many useful examples at SVGBasics. This should give you a good start if you've never seen SVG or XML before. However, this entry also assumes you are moderately familiar with web development concepts like the DOM and JavaScript.

My goal was to create an SVG document that allowed the user to drag entities around the screen as they do with existing windowing environments today. The final results of my work can be seen HERE.

SVG Events

In SVG 1.1, you can hook up event handlers to SVG entities by techniques that should be familiar to most HTML/JS coders nowadays:

<script><![CDATA[
function buttonClick() {
  alert('Hello, world!');
}
]]><script>

<rect id='button' onclick='buttonClick()' ... />

When the rectangle is clicked on, the JavaScript function buttonClick() will be called which lets you do whatever you want. In SVG 1.2 this will change as it uses only DOM Level 3 Events (introducing handlers and listeners), but since there will be some time before SVG 1.2 becomes the defacto standard to code for, SVG 1.1 is it.

Dragging

Now that you understand how to hook up event handlers, the basics of a dragging operation are:

  • User presses the mouse button while the cursor is on top of an object
  • User moves the mouse while holding the button down (dragging), the object should follow the mouse cursor
  • User lifts the mouse button to "release" the object

Each of these is handled by three different events: mousedown, mousemove and mouseup that you are able to capture and handle with arbitrary JavaScript.

SVG Mouse Coordinates

The tricky part of handling mouse events in SVG comes with the fact that the client coordinates (i.e. the browser's window) does not necessarily equal the SVG user coordinates. This is because in SVG you can change coordinate systems within your SVG document by specifying a viewBox attribute on the top-level <svg> element and you can transform SVG entities by scaling, translating, skewing, etc. These are very convenient features to have, yet they do cause some hassle with mapping mouse coordinates to SVG coordinates.

In my investigations, I started with this post by Jonathan Watt on the Yahoo! svg-developers mailing list where he specifies:

var m = El.getScreenCTM();
var p = document.documentElement.createSVGPoint();
p.x = evt.clientX;
p.y = evt.clientY;
p = p.matrixTransform(m.inverse());

This takes the clientX and clientY values within the event object (familiar to most HTML/JavaScript coders nowadays) and transforms them using the inverse of the SVG element's Current Transformation Matrix (CTM). What does that mean? Well, it translates my mouse coordinates into SVG user coordinates for a given SVG element. Exactly what I want. With that we can determine how the mouse cursor is offset from a given anchor point within the SVG element and properly drag it around.

I used the above technique in a very simple demo here: Draggable SVG Demo One. Supported platforms are Internet Explorer + Adobe SVG Viewer 6.0 Alpha and Firefox 1.5. Unfortunately it looks like Opera 9 TP1 and ASV 3.0 do not yet support getScreenTCM() on its SVG elements. Too bad, hopefully they will before Opera 9 is finally released. Anyway, if you have one of the above two configurations, you should be able to drag around the circle to your heart's content. What a blast...

Making It Useful

The problem came when I decided that I wanted to take this out of the 'simple demo phase' and into the 'practical use' phase. The first demo (dragtest.svg) only works with one SVG entity ("ball") and that entity has to be a <circle> element, since the JavaScript modifies the cx and cy attributes when the mouse moves. To make this useful outside of the laboratory, so to speak, requires that I generalize it for multiple entities and entity types.

Since I might want to use dragging on arbitrarily shaped entities, I cannot use a technique that modifies cx/cy attributes (this wouldn't work for <rect> or <g> entities). I already briefly mentioned that SVG allows you to arbitrarily transform your entities via the transform attribute. This is the general mechanism I decided to use to designate an entity's position:

<circle id='ball' transform='translate(400,300)' cx='0' cy='0' r='200' .../>
<rect id='box' transform='translate(500,500)' x='0' y='0' width='300' height='300' ... />
<g id='arbitrary_shape' transform='translate(1000,1000)' ... > ... </g>

I use a fixed shape (the "background" entity) to get the SVG document coordinate system and properly determine the mouse coordinates and offset within the shape, then I use the transform attribute to move the entities around. The transform attribute allows me to do this to any shape without using entity-specific attributes like cx or x. When I want to move the entity around I just do this:

element.setAttribute('transform', 'translate(' + p.x + ',' + p.y + ')');

DOM Sticking

But one issue did come up: How to efficiently get the element's current position? Looking at the above SVG code, it seems that I would have to get the value of the transform attribute (using the DOM getAttribute() method), then scan it looking for the two strings within the parentheses separated by a comma (or space), then parse those values into integers. What a pain.

So instead I decided to use a technique that I'm really growing to love in SVG DOM programming that I'll dub "DOM Sticking". It involves embedding arbitrary bits of information into the SVG entities themselves. It's best illustrated by modifying the above example SVG code:

<circle id='ball' transform='translate(400,300)' dragx='400' dragy='300' cx='0' cy='0' r='200' .../>
<rect id='box' transform='translate(500,500)' dragx='500' dragy='500' x='0' y='0' width='300' height='300' ... />
<g id='arbitrary_shape' transform='translate(1000,1000)' dragx='1000' dragy='1000' ... > ... </g>

In the above, I simply added the dragx and dragy attributes which are attributes invented by me that mean nothing to an SVG renderer. In other words, the software drawing the picture completely ignores these attributes, but I can use them in my DOM to ease matters. When I introduced these attributes, I made sure the dragx/dragy attributes have the same initial values as the transform translation.

Now whenever I move the elements, I need to update the dragx/dragy attributes at the same time:

element.setAttribute('transform', 'translate(' + p.x + ',' + p.y + ')');
element.setAttribute('dragx', p.x);
element.setAttribute('dragy', p.y);

But the benefit is that, since the dragx/dragy attributes are always in sync with the element's current translation, when I want to determine the SVG entity's current position I can just use:

var x = parseInt(element.getAttribute('dragx'));
var y = parseInt(element.getAttribute('dragy'));

No complicated and expensive scanning of the transform attribute, no delving into the element's CTM, just pick them up from the DOM and use them.

In using the above technique, it may be more proper to stick the dragx/dragy attributes into their own namespace (in which case I'd have to use getAttributeNS and setAttributeNS methods), but for the purposes of this demo it seems to work just fine. The reason for this is maybe because the SVG elements are in the SVG namespace but the attributes on those elements are in the null namespace.

Enabling ASV3

Subsequent to my first publishing this entry, I was informed that ASV 3 doesn't support getScreenCTM() and someone had already posted a function that would calculate this manually. Luckily I was able to find this function from Holger Will and with a small correction to his function to handle percentages in width/height - the function now works beautifully in ASV3.

Conclusion

You can see the final demo which allows you to drag around four objects here: Draggable SVG Demo Two. It supports IE+ASV3, IE+ASV6, and Firefox 1.5. Feel free to view the source and learn whatever you can. By the way, I came across this blog entry today so I thought I'd pick up his cool SVG feed icon and use it in my demo. Since Firefox doesn't allow me to specify an external SVG document, I embedded his SVG into my own code. Thanks, Marcus!

To make this demo perfectly reusable, I would probably change the following:

  • Use a class attribute to specify that an SVG entity is draggable. i.e. <circle id='ball' class='draggable' ...>
  • Then, remove the dragx/dragy attributes from my SVG code.
  • Then, in my JavaScript init() function, scan for all SVG entities of the class 'draggable' and in addition to adding the event listener, I would parse their transform attributes to get their initial position, then add the dragx/dragy attributes from within the JavaScript at that stage.

The above changes would have the effect of allowing you to easily turn entities into draggable entities by simply adding class='draggable' to the SVG description - no other changes would be required but you still gain the convenience and speed of DOM Sticking.

§194 · December 21, 2005 · JavaScript, Software, SVG, Technology, Web, XML · · [Print]

13 Comments to “How To Enable Dragging in SVG”

  1. Rob Banfield says:

    Great article! – I didn’t realise that one could arbitrarily add attributes to elements.

    Maybe you could extend the article to show the dragx/dragy attributes being defined in their own namespace?

    Rob

  2. Jeff Schiller says:

    Thanks, Rob. You can see this technique at http://www.codedread.com/svgnav.svg. Look at oldxform, zoom and targetzoom attributes on the buttons…

  3. jastarafie says:

    Hey there, great article… I used it for displaying the x and y position of the mouse when hovering over a .svg file.
    The only thing that’s not working fine, is the performance. It takes up to 5 seconds to update the coordinates, and not smootly like your example.
    I was wondering if you maybe know what the problem is?
    I putted the .js code in seperate file, which is used by a .php file to generate the xml/svg.
    Running apache2 + php5

    Thanx in advance and sorry for the bad language…

  4. Craig says:

    Is it possible to define additional attributes in a seperate or extendede schema?

    Quote:

    In using the above technique, it may be more proper to stick the dragx/dragy attributes into their own namespace (in which case I’d have to use getAttributeNS and setAttributeNS methods), but for the purposes of this demo it seems to work just fine. The reason for this is maybe because the SVG elements are in the SVG namespace but the attributes on those elements are in the null namespace.

  5. Jeff Schiller says:

    As I mentioned, the SVG DOM allows you to define your own attributes as you like on SVG entities. As for a separate schema, well I guess you’d have to start with the SVG DTD and add your stuff on top. I’m not familiar enough with DTD to say how easy this is though…

  6. Jeff Schiller says:

    Ruud,

    I don’t have a lot of time right now but I might get around to this. If you have some time, you can take the version at http://www.codedread.com/dragsvg.js (search for getScreenCTM function). This was tweaked by Johan Sundstrom and myself. Free to use and modify as long as you credit, no warranties, blah blah blah 😉

    Regards,
    Jeff

  7. Alex says:

    Thank you for a great tutorial. I’m just learning JavaScript, and this was very helpful. One question I have about these examples is, how do you make the dragged shapes follow the mouse pointer without “falling off?” For example, I have modified the code from your first example (which used a circle) to use a semi-opaque rect. When I drag the rect around, inevitably the mouse pointer leaves its perimeter, caused I assume by the SVG renderer being too slow to keep up with fast drags. The mouse pointer then “loses” the rect and I have to start the drag again, making sure to drag slowly or risk losing my “grip” on it. The shapes in your examples, by comparison, act like they are attached to the mouse pointer with rubber bands – which is ideal.

  8. Hi Alex,

    I’m glad you liked this tutorial – it’s a little old now, but I’m happy someone is lookign at it.

    You pose an interesting question and I suppose the glib response would be that the SVG renderers need to get much faster. What I did here though was upon mousedown is track the target (i.e. the entity that was clicked upon and then only clear that when mouseup occurs. Then, during mousemove, if we’ve been dragging an entity, we know exactly which one. See the script that deals wtih the variable “draggingElement” in my code and where it gets set (the mousedown/mouseup event handlers).

  9. Alex,

    Thinking about it a little more – I may have updated the JavaScript dragging library many months after posting this article to use the technique I describe above of “remembering” the entity at the start of the drag…

    Jeff

  10. Alex says:

    I found out what I did wrong. I changed “if (ball)” in the init() function to “if (evt.target == ball)”… It’s not the target anymore once the mouse pointer leaves it… got it 🙂

  11. Helder Magalhães says:

    I’ve just had a look and the implementation for getScreenCTM in ASV3 seems to work OK. 🙂 Kind of slow though… :-|

    Just a tip: that part of the code has no semicolons ending JS instructions. This may become an issue if someone runs a JS minifier on the script. 😉

  12. Marko Matic Cipi says:

    DAAAAAMN!!! xD Works with jQuery SVG perfectly!!! THANX MAN! 😀