Saturday 29 March 2014

Filled Under:

SVG Magnify/Zoom Effect With JavaScript

11:23:00

SVG is such a great web technology with so much potential for all kinds of things. Did you know you can run “inline” JavaScript within SVG markup? I’ve been experimenting a lot with SVG over the past few weeks with the aim of trying to expand its uses beyond just icons.

Today I’ll be running through another experiment using inline JavaScript within SVG for some simple DOM manipulations. The demo uses SVG clipping masks along with some JS to create the effect. The SVG files are stored externally and imported into the HTML with an <object> tag.
Rather than go through the complexities of the demo itself, I’ll run through a simplified version of the principle and how everything works (that seemed to work quite well with the previous demo). If you haven’t read it already, it’s worth taking a look over the previous demo on SVG clipping masks as this one uses similar techniques.
You can check out the demo below or download the source files if you want to take a look at the specifics.

How the effect works

Before we get started, let’s take a brief look at how things work. Fundamentally, the idea is to create an object that’s going to be “magnified”. Then we create a clipping mask. Finally we duplicate the element that’s being magnified inside a “helper” object via an xlink, clip it with the mask (which is the same size as the helper) and scale its contents. Gee that sounds really complicated. Fear not, let’s go through this.

We have a square and a circle

Pretty basic, we’ve got two objects, a pink square and a green stroked circle.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="414.88px" height="268.316px" xml:space="preserve" >

<rect id="pinksquare" x="126.225" y="57.365" fill="#EC008C" width="139.056" height="139.055"/>

<circle id="greencircle" style="fill:none;stroke:#39B54A;stroke-width:3;" cx="156.875" cy="135.95" r="27.095"/>

</svg>
 
 
pink-square-svg

We turn the circle into a clipping mask

We’ve turned the circle into an SVG <clipPath>. We’ll use this circle to clip the duplicated content that gets “magnified“.


<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="414.88px" height="268.316px" xml:space="preserve" >

<rect id="pinksquare" x="126.225" y="57.365" fill="#EC008C" width="139.056" height="139.055"/>

<clipPath id="clipgroup">
<circle id="clipcircle" cx="156.875" cy="135.95" r="27.095" />
</clipPath>

</svg>
 
 

Duplicate the circle

Because the circle is now a clipping mask, it won’t be visible. We need something to be visible that the user can see. Let’s duplicate the circle. Now we have two circles ontop of each other, one is a clipping mask, the other is a circle with a green stroke.


<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="414.88px" height="268.316px" xml:space="preserve" >

<rect id="pinksquare" x="126.225" y="57.365" fill="#EC008C" width="139.056" height="139.055"/>

<clipPath id="clipgroup">
<circle id="clipcircle" cx="156.875" cy="135.95" r="27.095" />
</clipPath>

<circle id="greencircle" style="fill:none;stroke:#39B54A;stroke-width:3;stroke-miterlimit:10;" cx="156.875" cy="135.95" r="27.095"/>

</svg>
 
 

Duplicate the square inside the second circle

This is really how the effect works. We use a <use> xlink with the #id of the square and place it inside a group with the circle ontop of it. We then add the id of the clipping mask to clip the <use> element. When it gets scaled via JavaScipt, it will give the impression of being “magnified” within the circle.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="414.88px" height="268.316px" xml:space="preserve" >

<rect id="pinksquare" x="126.225" y="57.365" fill="#EC008C" width="139.056" height="139.055"/>

<clipPath id="clipgroup">
<circle id="clipcircle" cx="156.875" cy="135.95" r="27.095" />
</clipPath>

<g id="clippedI">
<use xlink:href="#pinksqaure" clip-path="url(#clipgroup)"/>
<circle id="greencircle" style="fill:none;stroke:#39B54A;stroke-width:3;stroke-miterlimit:10;" cx="156.875" cy="135.95" r="27.095"/>
</g>

</svg>
 

Add JavaScript event attributes

We need to move circle with the cursor. So we need to detect when that happens. We can ad event attributes to detect it.


<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="414.88px"
height="268.316px" onload="startup(evt)" onmousemove="mouseMove(evt)" onmouseup="mouseUp(evt)" xml:space="preserve" >

<g id="pinksqaure" onmousedown="mouseDown(evt)" onmousemove="mouseMove(evt)" onmouseup="mouseUp(evt)">
<rect x="126.225" y="57.365" fill="#EC008C" width="139.056" height="139.055"/>
</g>

<clipPath id="clipgroup">
<circle id="clipcircle" cx="156.875" cy="135.95" r="27.095" />
</clipPath>

<g xmlns="http://www.w3.org/2000/svg" id="clippedI" onmouseup="mouseUp(evt)" onmousedown="mouseDown(evt)" transform="translate(-246,-249),scale(2.5,2.5)">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#pinksqaure" clip-path="url(#clipgroup)" />
<circle id="greencircle" style="fill:none;stroke:#39B54A;stroke-width:3;stroke-miterlimit:10;" cx="156.875" cy="135.95" r="27.095"/>
</g>

</svg>
 
 

Set our event functions

We can now add our JavaScipt directly in the SVG markup itself to handle those events.

//Cache Vairables
var clippedImage;
var cp;
var mycircle;
var greencircle;
var gMouseDown = 0;
var scaleFactor = 2.5;

//Start Up Event and get Ids
function startup(evt)
{
clippedImage = document.getElementById("clippedI");
cp = document.getElementById("clipgroup");
mycircle = document.getElementById("clipcircle");
greencircle = document.getElementById("greencircle");
}

//Set the eveent when the mouse moves over the element
// Detect whether the mouse is down or not
// Get the attributes of the clip circle and the green circle and change them.
// Call the resize function
function mouseMove(evt)
{
if(!gMouseDown) return;

var x = evt.clientX;
var y = evt.clientY;

mycircle.setAttributeNS(null,"cx", x);
mycircle.setAttributeNS(null,"cy", y);

greencircle.setAttributeNS(null,"cx", x);
greencircle.setAttributeNS(null,"cy", y);

resizeImage(x,y);
}

// Mouse is down
function mouseDown(evt)
{
gMouseDown = 1;
}

// Mouse is not down
function mouseUp(evt)
{
gMouseDown = 0;
}

// Resize and transform the content
function resizeImage(x,y)
{
var newx = x - scaleFactor*x;
var newy = y - scaleFactor*y;

var tx = "translate(" + newx + "," + newy
+ "),scale(" + scaleFactor + "," + scaleFactor +")";
clippedImage.setAttribute("transform", tx);
}
 
 
As you can see, things are pretty straight forward. I’ve annotated the code for explanation.

Final Markup

Our final markup looks like this, note how the JavaScript is kept within the SVG itself.


<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="414.88px"
height="268.316px" onload="startup(evt)" onmousemove="mouseMove(evt)" onmouseup="mouseUp(evt)" xml:space="preserve" >
<script>

<![CDATA[

//Cache Vairables
var clippedImage;
var cp;
var mycircle;
var greencircle;
var gMouseDown = 0;
var scaleFactor = 2.5;

//Start Up Event and get Ids
function startup(evt)
{
clippedImage = document.getElementById("clippedI");
cp = document.getElementById("clipgroup");
mycircle = document.getElementById("clipcircle");
greencircle = document.getElementById("greencircle");
}

//Set the event when the mouse moves over the element
// Detect whether the mouse is down or not
// Get the attributes of the clip circle and the green circle attach them to mouse position
// Call an resize function
function mouseMove(evt)
{
if(!gMouseDown) return;

var x = evt.clientX;
var y = evt.clientY;

mycircle.setAttributeNS(null,"cx", x);
mycircle.setAttributeNS(null,"cy", y);

greencircle.setAttributeNS(null,"cx", x);
greencircle.setAttributeNS(null,"cy", y);

resizeImage(x,y);
}

// Mouse is down
function mouseDown(evt)
{
gMouseDown = 1;
}

// Mouse is not down
function mouseUp(evt)
{
gMouseDown = 0;
}

// Resize and transform the content
function resizeImage(x,y)
{
var newx = x - scaleFactor*x;
var newy = y - scaleFactor*y;

var tx = "translate(" + newx + "," + newy
+ "),scale(" + scaleFactor + "," + scaleFactor +")";
clippedImage.setAttribute("transform", tx);
}

//]]>
</script>
<g id="pinksqaure" onmousedown="mouseDown(evt)" onmousemove="mouseMove(evt)" onmouseup="mouseUp(evt)">
<rect x="126.225" y="57.365" fill="#EC008C" width="139.056" height="139.055"/>
</g>

<clipPath id="clipgroup">
<circle id="clipcircle" cx="156.875" cy="135.95" r="27.095" />
</clipPath>

<g xmlns="http://www.w3.org/2000/svg" id="clippedI" onmouseup="mouseUp(evt)" onmousedown="mouseDown(evt)" transform="translate(-246,-249),scale(2.5,2.5)">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#pinksqaure" clip-path="url(#clipgroup)" />
<circle id="greencircle" style="fill:none;stroke:#39B54A;stroke-width:3;stroke-miterlimit:10;" cx="156.875" cy="135.95" r="27.095"/>
</g>

</svg>
 
 

Working Demo


Mobile Support

We still need to add mobile support right? It’s pretty straight forward actually. We can add event listeners for ’touchmove’ like this:


// Detect when the window has loaded
window.addEventListener('load', function() {

var b = document.getElementById('clippedI'),

// Offsets touch to center of the object
xbox = b.offsetWidth / 2, // half the box width

ybox = b.offsetHeight / 2, // half the box height

bstyle = b.style; // cached access to the style object

// Add event listener to check if the object is being touched and moved
b.addEventListener('touchmove', function(event) {

// cache variables of x and y position of finger
var x = event.targetTouches[0].pageX;
var y = event.targetTouches[0].pageY;

// the default behaviour is scrolling.
event.preventDefault();

// Set object parameters to center of finger
bstyle.left = event.targetTouches[0].pageX - xbox + 'px';
bstyle.top = event.targetTouches[0].pageY - ybox + 'px';

// Set values to finger position
mycircle.setAttributeNS(null,"cx", x);
mycircle.setAttributeNS(null,"cy", y);

greencircle.setAttributeNS(null,"cx", x);
greencircle.setAttributeNS(null,"cy", y);

resizeImage(x,y);

}, false);

}, false);
 
 

Final Thoughts

This could be a great way of handling SVG animation and manipulation, it definitely opens the doors as to what’s possible with inline JavaScript within the SVG itself. Furthermore, from a deployment point of view, it has some benefits as you only need to load the SVG and script when needed. Although caching might be an issue for some people.

One Last Thing

Finally, a little easter egg. Did you know you can do all your scripting directly in Illustrator? If you go to Window -> SVG Interactivity, you can write all your scripting directly in there including setting variables and other stuff. If you’re working on complex SVG files, it might come in handy as you don’t need to trawl through heaps code just to find IDs and classes.
svg-interactivity

I hope you’ve enjoyed this tutorial and demo! As always if you have any questions drop them in the comments.

The app screen design is based off a free PSD by Olia Gozha, you can find it here on Dribbble.

0 comments:

Post a Comment