RSS

SVG image zooming and panning with RaphaelJs

13 Apr


raphaeljs image zoom

View the demo here.

Easy way to do image zooming and panning without sending images to the server to be processed and trimmed with RaphaelJS and a few simple steps.

1. Set initial zoom, image window width and height:

var mapZoom = 1;

var mapWidth = 400;
var mapHeight = 400;
$('#mapHolder').css('width', mapWidth);
$('#mapHolder').css('height', mapHeight);

2. Create a RaphaelJS object and add a full-size image to it.

var paper = Raphael("mapHolder", mapWidth, mapHeight);
var mapImage = paper.image("https://dashasalo.files.wordpress.com/2011/09/figure1-worldatlas-large.jpg", 0, 0, mapWidth, mapHeight);

You should see your image resized to specified mapWidth and mapHeight.

3. Next step is to draw a menu of controls. I used RaphaelJS to draw the controls but you can also use images if you like.

var mapMenuHolderWidth = 40;
var mapMenuHolderHeight = 216;
var mapMenuHolderX = mapWidth - mapMenuHolderWidth - 6; // menu right margin
var mapMenuHolderY = 5; // menu top margin

// draw menu panel
var mapMenuHolder = paper.rect(mapMenuHolderX, mapMenuHolderY, mapMenuHolderWidth, mapMenuHolderHeight, 12).attr({fill: 'black', stroke: '#aaa', 'stroke-width': 2, opacity: 0.7});

Now controls themselves. You can find very nice icons and paths used to draw them on RaphaelJs website. SVG paths for icons were quite long and made code unreadable so I removed them from this code snippet. If you need paths have a look at the demo source code.

var maxIcon = paper.path("M22...051z").attr({fill: "#bbb", stroke: "#000", translation: (mapMenuHolderX+5) + ', '+(mapMenuHolderY + 10)});

var minIcon = paper.path("M22...884z").attr({fill: "#bbb", stroke: "#000", translation: (mapMenuHolderX+5) + ', '+(mapMenuHolderY + 7+ 34)});
  		
var arrowTop = paper.path("M239...73z").attr({fill: "#bbb", stroke: "#000", scale: 0.10, translation: '245, -25', rotation: 180});
  		
var arrowLeft = paper.path("M239...73z").attr({fill: "#bbb", stroke: "#000", scale: 0.10, translation: '245, 4', rotation: 90});
  		
var arrowRight = paper.path("M239...73z").attr({fill: "#bbb", stroke: "#000", scale: 0.10, translation: '245, 33', rotation: -90});
  		
var arrowBottom = paper.path("M239...73z").attr({fill: "#bbb", stroke: "#000", scale: 0.10, translation: '245, 63'});

I used one path for all the arrows and just rotated it to make it point different direction.

4. Now we add a bunch of handlers to the controls to catch the click event.

// maximize icon clicked 
maxIcon.click(function(e){
  	mapZoom++;
  	mapImage.scale(mapZoom, mapZoom);
  	e.stopPropagation();
  	e.preventDefault();
  }); 

// minimize icon clicked 
  minIcon.click(function(e){
  	if (mapZoom == 1) return;
  	mapZoom--;
  	mapImage.scale(mapZoom, mapZoom);
  	adjustMapEdge();
  	e.stopPropagation();
  	e.preventDefault();
  }); 
  
// top arrow clicked		
arrowTop.click(function(e){
  	mapImage.translate(0, 20);
  	adjustMapEdge();
  	e.stopPropagation();
  	e.preventDefault();
}); 
  
// bottom arrow clicked		
arrowBottom.click(function(e){
  	mapImage.translate(0, -20);
  	adjustMapEdge();
  	e.stopPropagation();
  	e.preventDefault();
}); 
  
// left arrow clicked		
arrowLeft.click(function(e){
  	mapImage.translate(20, 0);
  	adjustMapEdge();
  	e.stopPropagation();
  	e.preventDefault();
}); 
  		
// right arrow clicked
arrowRight.click(function(e){
  	mapImage.translate(-20, 0);
  	adjustMapEdge();
  	e.stopPropagation();
  	e.preventDefault();
});  	

We will have a look at adjustMapEdge function in a second.

You can also add nice hover effects to the controls with mouseover and mousedown events (look at my demo).

5. AdjustMapEdge function makes sure that image doesn’t move out of its window boundaries. For instance, if user moves up for long enough eventually he/she will hit the image top edge. When this happens we shouldn’t allow the image move any further up and just use its current location even if the user drags the image up again. This way from this position the user can only move down, left or right.

var adjustMapEdge = function()
{
    if (mapImage.attr('x') > 0) mapImage.attr({'x': 0});
    if (mapImage.attr('x') < (mapWidth - mapImage.attr('width'))) mapImage.attr({'x': (mapWidth - mapImage.attr('width'))});
    		
    if (mapImage.attr('y') > 0) mapImage.attr({'y': 0});
    if (mapImage.attr('y') < (mapHeight - mapImage.attr('height') )) mapImage.attr({'y': (mapHeight - mapImage.attr('height') )});
};

6. Panning the image. Panning the image can be split into 3 basic steps – grab the image, hold the mouse button and drag the image, release the mouse button and drop the image.

As there is no reliable way to say whether mouse button is being hold when the mouse moves, we use a javascript variable to store the click state. In the code snippet below when mouse button is clicked I set clicking variable to true. Once the mouse button is released set clicking to false. And while mouse moves check if clicking is still true (hasn’t been released).

// coordinates of the last drag position
var lastX = 0, lastY = 0;

// record the click event on the image for drag and drop
var clicking = false;

// set clicking to true to record start of drag and drop
// and update the last coordinates
$('#mapHolder').mousedown(function(e){
    	clicking = true;
    	lastX = e.pageX;
    	lastY = e.pageY;
    	$('#mapHolder').css('cursor', 'move');
    	e.stopPropagation();
    	e.preventDefault();
});

// when the mouse button is released set clicking to false
// to stop drag and drop		
$(document).mouseup(function(e){
    	clicking = false;
    	$('#mapHolder').css('cursor', 'default');
    	e.stopPropagation();
    	e.preventDefault();
});

$('#mapHolder').mousemove(function(e){
    	// when mouse if moved check if user is also holding a mouse button
    	if (clicking == false) return;
    		
    	var currentMapPosX = 0, currentMapPosY = 0;
    	
    	// get difference between current and previous mouse position	
    	if ((mapImage.attr('x') <= 0) && (mapImage.attr('x') >= (mapWidth - mapImage.attr('width'))))
    	{
    		currentMapPosX = e.pageX - lastX;
    	}
    		
    	if ((mapImage.attr('y') <= 0) && (mapImage.attr('y') >= (mapHeight - mapImage.attr('height'))))
    	{
    		currentMapPosY = e.pageY - lastY;
    	}
    	
    	// move the image	
    	mapImage.translate(currentMapPosX, currentMapPosY);
    	
    	// record previous position
    	lastX = e.pageX;
    	lastY = e.pageY;
    	
    	// check for image edge	
    	adjustMapEdge();
});		
Advertisements
 
39 Comments

Posted by on April 13, 2011 in JavaScript, RaphaelJS

 

Tags: , , , ,

39 responses to “SVG image zooming and panning with RaphaelJs

  1. Charles

    April 13, 2011 at 8:05 pm

    It’s lovely.
    Linked to it on my site.

     
    • Dasha Salo

      April 13, 2011 at 8:29 pm

      Thanks 🙂

      I know your website has lots of useful info on raphaeljs and svg. Helped me several times!

       
  2. Charles

    April 14, 2011 at 12:52 pm

    Would like to put your developer/designer profile up here along with the the other folks.

    http://www.irunmywebsite.com/raphael/developers.php?q=10

     
    • Dasha Salo

      April 14, 2011 at 1:00 pm

      Would be great 🙂

       
  3. Charles

    April 14, 2011 at 1:12 pm

    Just spend a few minutes copying Shakthi’s and I will do the rest:

    charlesavt@yahoo.com

     
  4. Ilkka

    June 6, 2011 at 10:56 am

    Is it possible to use a vector image which is drawn by paths in a separate .js file? The paper.image command only accepts image files. I know it also accepts .svg which would be fine if I didnt need clickable elements and animation to this vector image which I’ve done using raphael in this separate .js file.

     
    • Dasha Salo

      June 6, 2011 at 11:38 am

      I am not sure what you are trying to achieve. You said you have an image paths in the separate file and you need to draw it? If so why not just use path() method in Raphael?

       
      • Ilkka

        June 6, 2011 at 12:03 pm

        Sorry I’m a bit of a rookie when it comes to coding.

        Basically I have a vector image I’ve made in Illustrator. From the svg file I got the paths, circles etc. which I then converted manually in to a file.js using Raphael. I further added animations when mouse hovers over some elements and clickable links to these elements.

        I guess what I want to do is get this whole file.js to load as the variable ‘mapImage’ however I don’t know how to do this as I couldn’t find a command which would allow me to load an external .js on to a paper.

        so instead of

        var mapImage = paper.image(image.jpg, 0, 0, 500, 500);

        I need something which allows me to load this file.js instead of image.jpg, so that I can keep all the animation effects etc. working when you zoom in and pan around.

        Sorry again for being novice, hope the explanation was clear.

         
      • Dasha Salo

        June 6, 2011 at 1:49 pm

        Maybe you could use a set?

        So you define a set in your file.js with all the paths, include file.js into main html page and then use this set as one object.

        For example,

        // file.js
        var imageSet = paper.set();
        imageSet.push(
            paper.path("..."),
            paper.path("..."),
            ...
        );
        

        If you would want to get a particular path from the set you just do it as with an array:

        var secondPath = imageSet[1];
        

        Hope this helps 🙂

         
  5. Ilkka

    June 7, 2011 at 7:07 am

    Hi, I managed to get the image to view properly by using set. However, as soon as I zoom in all the attributes I’ve given to the different elements get messy. Also the way I’ve organised circles to points on the paper makes them stay at absolute positions and panning doesn’t work at all.

    Thanks for your help tho! Maybe this isn’t the right script for what I’m trying to achieve but good work anyhow!

     
  6. Ilkka

    June 7, 2011 at 10:26 am

    Hey again,

    I’m learning more and more about javascript here and I see the zoom works by scaling the image. The problem I’m experiencing is caused by this. While scale = 1, set draws my image correctly. However, when zooming in and changing the scale, each different path and circle on my drawing gets scaled as individual objects and thus the relationship between each other is broken resulting in a distorted image. For some reason the panning doesn’t work either.

     
    • Dasha Salo

      June 7, 2011 at 2:16 pm

      Maybe you could join your paths into one?

       
  7. Michael Blix

    June 20, 2011 at 6:51 am

    Works great.

    It looks like this will be easier with the 2.0 branch which has a setViewBox function.
    http://raphaeljs.com/2.0/#Paper.setViewBox

     
    • Dasha Salo

      June 20, 2011 at 8:47 am

      Oh definitely! Haven’t played with v2 yet but looks promising.

       
      • Michael Blix

        June 20, 2011 at 11:49 pm

        Playing around with it a little more one thing I did notice was that the center of the image does not stay the same while zooming (if have previously panned away from the default image center). It seems to do a slight effective pan as well.

         
  8. Dasha Salo

    July 30, 2011 at 8:06 am

    I think you are right. It must be the way I calculate zooming position.

     
  9. selasie

    August 4, 2011 at 5:37 am

    Nice post. I wanted to know your thoughts on zooming regarding text. I’m working on an editor of some sort. Scaling the text (in this case just font-size) actually moves it away from the center of focus (ie. the middle of the screen). Upon discussing with a friend, we agreed I should do a translation after to the desired position. Whats are your thoughts?

     
    • Dasha Salo

      August 4, 2011 at 9:57 am

      Selasie, are you using “print” to output text?

       
      • selasie

        August 4, 2011 at 1:46 pm

        No. Am using paper.text(…..), whats the difference?

         
      • Dasha Salo

        August 4, 2011 at 2:11 pm

        Did you try setting ‘text-anchor’?

        For example textObj.attr({‘text-anchor’: ‘start’}) to align to the left.

         
  10. selasie

    August 4, 2011 at 2:14 pm

    nope. should I? and whats the effect going to be if i do.
    the only attributes I’m currently using are font-weight, size, family and fill.

     
  11. Charles

    August 4, 2011 at 2:32 pm

    I thought that the default was scaling from the centre. It sounds like the text is not scaling from the centre.

    Find the centre of the text (xy) using getBBox and then use the xy coordinates as 2nd and 3rd arguments in your call to scale.
    bbox=textObj.getBBox();
    x=bbox.x + (bbox.width/2);
    y=bbox.y + (bbox.height/2);
    scale(2, 2, x, y);

     
    • selasie

      August 4, 2011 at 2:50 pm

      thanks Charles. will try it out and let u know what happens

       
      • Dasha Salo

        August 4, 2011 at 3:11 pm

        Selasie, could you please try the following:

        define the text:
        var txt = R.text(350, 410, “Raphaël kicks butt!”).attr({‘text-anchor’: ‘start’});

        then scale the text:
        txt.attr({‘font-size’: 30, ‘text-anchor’: ‘start’});

        This should scale it from the left starting point.

        As you said centered text didn’t work. I tried to do the same with:

        define the text:
        var txt = R.text(350, 410, “Raphaël kicks butt!”);

        then scale the text:
        txt.attr({‘font-size’: 30});

        which scales from centre point by default and it all worked ok.

        Could you please try and let me know if these are working as expected.

         
  12. selasie

    August 4, 2011 at 3:40 pm

    @Dasha: your code is perfectly fine. Its the same piece of code I’m using. The scaling is fine. The issue is with the final positioning of the text.
    Let me clarify a bit. I have a grid and texts. Its seems that my grid is not position well. The grid anchors to the bottom right whiles the text anchors to the top left at zoom < 1. The desired effect is that both should move inwards (center of canvas) at zooms 1. As far as size is concerned their final output is correct. As far as positioning is concerned , the final output is wrong. An expansion and contraction on the canvas. I hope this explains it.

    @chales: using your code anchors every thing to the top left at zoom < 1. seems the top left of the canvas has become the focal point . I used it on the grid. Its has the same effect on the text as in case of just using scale without using bbox to find to the center of the text.

    btw: I'm using a path for my grid.

    I could send a screenshot if possible.

     
    • Dasha Salo

      August 4, 2011 at 3:44 pm

      Please send the screenshots to darushechka@gmail.com

       
      • selasie

        August 4, 2011 at 4:06 pm

        k

         
      • Dasha Salo

        August 4, 2011 at 5:28 pm

        So you want to not only scale the text but also move it with the grid? Right?

         
  13. selasie

    August 4, 2011 at 4:19 pm

    sent u sample shots

     
    • selasie

      August 4, 2011 at 5:32 pm

      yep exactly.
      The effect is supposed to be that of an expansion and contraction

       
      • Dasha Salo

        August 5, 2011 at 10:22 am

        Selasie, I don’t think you can achieve this by any default font method.

        You will have to calculate position of font relative to grid and do all the maths yourself i am afraid…

        I guess I would first calculate the offset of text relative to grid at 100% zoom. Then when zoom changes apply this change to offset and move the text to the new offset.

         
  14. selasie

    August 5, 2011 at 4:43 pm

    k. Thanks for the help.

     
  15. fischmar

    August 29, 2011 at 12:28 pm

    Hey,

    i’m getting a 404 for the example http://dashasalo.com/svgImageZoom/imageZoomRaphael.html

    trying to realize a zoom and pan with raphael 2.0 on multiple svg imported paths/texts etc…

     
    • Dasha Salo

      August 30, 2011 at 9:26 am

      Oh sorry! Have been moving things around lately. Will get it fixed asap.

      Thank you for letting me know!

       
      • fischmar

        August 30, 2011 at 10:12 am

        Thx in advance. The examples for the worldmap articles are also unavailable. I’ll post a link to an example jsfiddle, as soon as it works… 😉

         
      • Dasha Salo

        September 1, 2011 at 6:47 pm

        All woking now! Sorry for inconvenience again!

         
  16. martin (@gabel)

    September 5, 2011 at 2:47 pm

    Thank you! It’s your blog, no need 2b sry… 🙂

    I’ve put an example in a fork of your fiddle, just if you’re interested in.. http://jsfiddle.net/gabel/kKJLB/

    Features: Pan & Zoom through mouse(-wheel) and buttons with raphael v2 viewbox

    My todos: Structure code in closure/jquery plugin way…

     
  17. martin (@gabel)

    September 6, 2011 at 10:09 am

    That was the wrong link… 😛

    http://jsfiddle.net/gabel/cXW3x/

     
    • Dasha Salo

      September 6, 2011 at 11:35 am

      Happens 🙂

       

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: