Interactive SVG on the iPad
August 30th, 2010 by Gert-Jan van der Wel
It’s difficult to find any decent samples about interactive SVG on the iPad and iPhone. SVG is used more and more these days, but it’s still rather unknown. The combination with touch and gestures on an iDevice is even rarer.
Since I’ve been playing around with it a bit, I’d like to share what I’ve learned. This post describes the steps involved to create a blue rectangle that can be dragged, rotated and scaled.
SVG
To get things started we need to create an SVG document. The shape, in this case a blue rectangle, will be added to the document. Create a file with the .svg extension and add the following lines:
<?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> <rect id="square" width="200" height="200" style="fill:#3399CC" x="20" y="20" /> </svg>
Touch
To make the rectangle interactive we need to listen to touch and gesture events. Gesture events are a special case of touch events. They are created to make it easier to handle multi touch events, like scaling and rotating.
For scripting in SVG you can use the script type “text/ecmascript” inside an SVG doc. Let’s start with dragging the rectangle by adding touch listeners to the rectangle.
<rect id="square" width="200" height="200" style="fill:#3399CC" x="20" y="20" ontouchstart="touchStart(event);" ontouchmove="touchMove(event);" ontouchend="touchEnd(event);" />
Then we add the script (after the rect tag) to handle the touch events and translate the rectangle.
<script type="text/ecmascript"> var x; var y; var touchX; var touchY; var width; var height; function touchStart(event) { event.preventDefault(); // one finger touch => start drag if(event.touches.length == 1) { var touch = event.touches[0]; var node = touch.target; x = parseInt(node.getAttributeNS(null, 'x')); y = parseInt(node.getAttributeNS(null, 'y')); touchX = touch.pageX; touchY = touch.pageY; } } function touchMove(event){ event.preventDefault(); // one finger touch => drag if(event.touches.length == 1) { var touch = event.touches[0]; var node = touch.target; var dx = touch.pageX - touchX; var dy = touch.pageY - touchY; var newX = x + dx; var newY = y + dy; node.setAttributeNS(null, 'x', newX); node.setAttributeNS(null, 'y', newY); } } function touchEnd(event) { } </script>
The functions touchDown and touchMove both check if the touch is done by one finger. On touch down the start coordinates are recorded and on touch move the rectangle is moved to the new position. The function touchEnd is not used.
You can also see that the default browser action, panning the page, is prevented by calling event.preventDefault();
Gestures
Next up are handling the gestures to scale and rotate our rectangle. First add the listeners:
ongesturestart="gestureStart(event);" ongesturechange="gestureChange(event);" ongestureend="gestureEnd(event);"
and then the functions:
function gestureStart(event) { event.preventDefault(); var node = event.target; x = parseInt(node.getAttributeNS(null, 'x')); y = parseInt(node.getAttributeNS(null, 'y')); width = parseInt(node.getAttributeNS(null, 'width')); height = parseInt(node.getAttributeNS(null, 'height')); var transform = (node.getAttributeNS(null, 'transform')); rotation = parseInt(transform.split('rotate(')[1].split(' ')[0]); if(isNaN(rotation)) { rotation = 0; } } function gestureChange(event) { event.preventDefault(); var node = event.target; // scale var newWidth = width * event.scale; var newHeight = height * event.scale; var newX = x - (newWidth - width)/2; var newY = y - (newHeight - height)/2; node.setAttributeNS(null, 'width', newWidth); node.setAttributeNS(null, 'height', newHeight); node.setAttributeNS(null, 'x', newX); node.setAttributeNS(null, 'y', newY); // rotation var newRotation = rotation + event.rotation; var centerX = newX + newWidth/2; var centerY = newY + newHeight/2; setRotation(node, newRotation, centerX, centerY); } function gestureEnd(event) { rotation = rotation + event.rotation; } function setRotation(node, rotation, x, y) { var centerX = x + width/2; var centerY = y + height/2; node.setAttributeNS(null, 'transform', 'rotate('+ rotation +' '+ x +' '+ y +')'); }
In general, these functions behave the same way as the touch functions. The values are recorded when the gesture starts and updated when the gesture changes.
Sample
That’s it. You can find a running version on the Floorplanner Labs and the whole code sample below. I added a toggle handlingGesture make sure the handling of the touch and gesture events don’t interfere.
I also added a bit of a hack to make the drag work when the rectangle is rotated. The rotation is recorded and set to 0 before the movement and re-applied after the movement. You don’t need this hack when you use SVG transformations, but that’s another story.
<?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg xmlns="http://www.w3.org/2000/svg" version="1.1"> <rect id="square" width="200" height="200" style="fill:#3399CC" x="20" y="20" transform="rotate(0 110 110)" ontouchstart="touchStart(event);" ontouchmove="touchMove(event);" ontouchend="touchEnd(event);" ongesturestart="gestureStart(event);" ongesturechange="gestureChange(event);" ongestureend="gestureEnd(event);" /> <script type="text/ecmascript"> var x; var y; var touchX; var touchY; var width; var height; var rotation = 0; var handlingGesture = false; function touchStart(event) { event.preventDefault(); if(!handlingGesture) { // one finger touch => start drag if(event.touches.length == 1) { var touch = event.touches[0]; var node = touch.target; x = parseInt(node.getAttributeNS(null, 'x')); y = parseInt(node.getAttributeNS(null, 'y')); touchX = touch.pageX; touchY = touch.pageY; } } } function touchMove(event){ event.preventDefault(); if(!handlingGesture) { // one finger touch => drag if(event.touches.length == 1) { var touch = event.touches[0]; var node = touch.target; var dx = touch.pageX - touchX; var dy = touch.pageY - touchY; var newX = x + dx; var newY = y + dy; // set rotation to 0 before moving setRotation(node, 0, newX+width/2, newY+height/2); node.setAttributeNS(null, 'x', newX); node.setAttributeNS(null, 'y', newY); // restore rotation setRotation(node, rotation, newX+width/2, newY+height/2); } } } function touchEnd(event) { if(event.touches.length == 0) { handlingGesture = false; } } function gestureStart(event) { event.preventDefault(); handlingGesture = true; var node = event.target; x = parseInt(node.getAttributeNS(null, 'x')); y = parseInt(node.getAttributeNS(null, 'y')); width = parseInt(node.getAttributeNS(null, 'width')); height = parseInt(node.getAttributeNS(null, 'height')); var transform = (node.getAttributeNS(null, 'transform')); rotation = parseInt(transform.split('rotate(')[1].split(' ')[0]); // ouch if(isNaN(rotation)) { rotation = 0; } } function gestureChange(event) { event.preventDefault(); var node = event.target; // scale var newWidth = width * event.scale; var newHeight = height * event.scale; var newX = x - (newWidth - width)/2; var newY = y - (newHeight - height)/2; node.setAttributeNS(null, 'width', newWidth); node.setAttributeNS(null, 'height', newHeight); node.setAttributeNS(null, 'x', newX); node.setAttributeNS(null, 'y', newY); // rotation var newRotation = rotation + event.rotation; var centerX = newX + newWidth/2; var centerY = newY + newHeight/2; setRotation(node, newRotation, centerX, centerY); } function gestureEnd(event) { rotation = rotation + event.rotation; } function setRotation(node, rotation, x, y) { var centerX = x + width/2; var centerY = y + height/2; node.setAttributeNS(null, 'transform', 'rotate('+ rotation +' '+ x +' '+ y +')'); } </script> </svg>






