How to animate the viewport (viewbox)

3,010 views
Skip to first unread message

Tobias Eriksson

unread,
Nov 5, 2011, 7:19:41 PM11/5/11
to Raphaël
Hi
I would like to pan left and right in my Raphael "image" and do this
using an animation / easing, so that it smoothly moves to the right
(or left).
But I have google;ed with no real result
Seems to me like an obvious thing to do, so I am a bit surprised I
have not found any posts on it yet (well one or two but they realy did
not say much)

Anyone ?

/Tobias

Will Morgan

unread,
Nov 7, 2011, 12:06:38 PM11/7/11
to Raphaël
Hi Tobias,

I have had the exact same issue as you. Whilst it's not pretty, this
does the job. I'm actually working on it at the moment to make it a
little bit smoother...

You have to take in to consideration that since you can't get viewBox
attributes (they are abstracted by Raphael for compatibility with VML,
I suppose), you will need to store these attributes locally and do a
setViewBox after the canvas has been created for concurrency. This is
what getViewBox is for. My attributes would be updated in my function
passed in as a callback.

Raphael.fn.animateViewBox = function(viewX, viewY, width, height,
duration, callback) {

duration = duration || 250;

var originals = ChartManager.getViewBox(), // IMPORTANT: you need to
write this yourself
differences = {
x: viewX - originals.x,
y: viewY - originals.y,
width: width - originals.width,
height: height - originals.height
},
delay = 13,
stepsNum = Math.ceil(duration / delay),
stepped = {
x: differences.x / stepsNum,
y: differences.y / stepsNum,
width: differences.width / stepsNum,
height: differences.height / stepsNum
}, i,
canvas = this;

/**
* Using a lambda to protect a variable with its own scope.
* Otherwise, the variable would be incremented inside the loop, but
its
* final value would be read at run time in the future.
*/
function timerFn(iterator) {
return function() {
canvas.setViewBox(
originals.x + (stepped.x * iterator),
originals.y + (stepped.y * iterator),
originals.width + (stepped.width * iterator),
originals.height + (stepped.height * iterator)
);
// Run the callback as soon as possible, in sync with the last step
if(iterator == stepsNum && callback) {
callback(viewX, viewY, width, height);
}
}
}

// Schedule each animation step in to the future
// Todo: use some nice easing
for(i = 1; i <= stepsNum; ++i) {
setTimeout(timerFn(i), i * delay);
}

}

I know, not pretty right? :-P Ideally this would leverage Raphael's
pretty extensive animation library, but the viewBox attribute is four
attributes made up in to one, and I don't have the time to do that as
this is for a project on a tight deadline :-/ sorry, but I hope it
helps!

Regards,

Will




On Nov 5, 11:19 pm, Tobias Eriksson <tobias.eriks...@softhouse.se>
wrote:

AS

unread,
Nov 14, 2011, 1:23:24 PM11/14/11
to Raphaël
Hi Will,

this really looks promising. Could you provide a basic example, how to
call your animateViewBox function with the drag event?
I guess this must be very basic, but I just recently learned to love
javascript and try to dive down a little deeper.

Help would be appreciated


On Nov 7, 6:06 pm, Will Morgan <aven...@gmail.com> wrote:
> Hi Tobias,
>
> I have had the exact same issue as you. Whilst it's not pretty, this
> does the job. I'm actually working on it at the moment to make it a
> little bit smoother...
>
> You have to take in to consideration that since you can't get viewBox
> attributes (they are abstracted by Raphael for compatibility with VML,
> I suppose), you will need to store these attributes locally and do asetViewBoxafter the canvas has been created for concurrency. This is

Will Morgan

unread,
Nov 14, 2011, 7:51:25 PM11/14/11
to raph...@googlegroups.com
Hey AS,

Sure, I can help. However, you wouldn't call animateViewBox when you're
dragging. When you think about it, you'll see why:

1. dragMove (the Raphael eve event) happens pretty much on mousemove,
which is every pixel of movement of your mouse. Therefore dragMove would
happen every time your cursor moves a pixel, which is maybe every 10ms.
2. animateViewBox would have a duration of 250ms, for example.
3. By moving your mouse you would send hundreds of dragMove events, and
the handlers of those, one might erroneously propose, would call the
animateViewBox method. The result would be a giant queue of
animateViewBox executions, which would leave you with an ugly animation.

So you just need to do a setViewBox call with the coordinates from
ev.pageX and ev.pageY.

However, binding events to the canvas itself with Raphael is kind of
hard, as drag/drop methods are only available on the Element object. I
worked around this by binding directly to the HTML element that is the
parent of the SVG canvas, i.e. the same element you call Raphael on.

I implemented this loosely earlier today, hopefully it's of some use to you:

var dragActive = false,
startCoords = {
x: 0,
y: 0
},
targetCoords = {
x: 0,
y: 0
};

function getDelta(pageX, pageY) {
var trueDelta = getTrueDelta(pageX, pageY);

targetCoords.x += trueDelta.x;
targetCoords.y += trueDelta.y;

return targetCoords;
}
function getTrueDelta(pageX, pageY) {
return {
x: startCoords.x - pageX,
y: startCoords.y - pageY
}
}
function dragEnd(pageX, pageY) {
dragActive = false;
}
function dragStart(pageX, pageY) {
dragActive = true;
startCoords = { x: pageX, y: pageY }
targetCoords = ChartManager.getViewBox();
}
function drag(pageX, pageY) {
if(dragActive) {
var scale = ChartManager.getViewBox()['scale'],
delta = getDelta(pageX, pageY);

delta.x /= scale * 1.75;
delta.y /= scale * 1.75;

//console.log(delta);

ChartManager.setView(delta, false, false);
}
}

// Controls
$('#Canvas')
.bind('mousemove', function(ev) {
drag(ev.pageX, ev.pageY);
})
.bind('mousedown', function(ev) {
if(ev.target.tagName == 'svg' || $.browser.msie && $.browser.version < 9
&& ev.target.tagName != 'shape') {
dragStart(ev.pageX, ev.pageY);
}
});

$(window).bind('mouseup', function(ev) {
dragEnd(ev.pageX, ev.pageY);
});

>> if(iterator == stepsNum&& callback) {

AS

unread,
Nov 22, 2011, 9:42:19 AM11/22/11
to raph...@googlegroups.com
Thanks. You already helped out a lot ;)

Really impressive how solid and yet simple your solution is. 
I played around with it for a while. You already implemented an accelerator scaling, which is perfect for the usual mobile screen limitations.

I tried to code the "setView()" function of the missing ChartManager() and had some trouble with it (because i had to "guess" what to do with the parameters delta, false, false). After a while my viewport got saved by the char manager, but not with the right coordinates. 
So I ended up to use a non accelerated function, that works fine but feels a little bit limiting.

If you don't mind I would like to kindly ask you for your CharManager solution.

There is no other example solution out there afaik. At least not so clear and flexible.
In the meantime I will give it another try.

Help is again very appreciated. 

Will Morgan

unread,
Nov 22, 2011, 10:18:36 AM11/22/11
to raph...@googlegroups.com
setView just modifies a few variables privately held in the ChartManager's closure which represent the Raphael canvas's viewBox attributes, so X, Y, W, H - held in the view and dimensions object (view.x/y, dimensions.width/height etc).

Still in development so some things are commented out, here's the copy and paste though.

setView: function(coords, relative, animate) {
var newX, newY;

animate = typeof(animate) != 'undefined' ? animate : true;

if(relative) {
newX = view.x + coords.x;
newY = view.y + coords.y;
}
else {
newX = coords.x;
newY = coords.y;
}

// Adjust values so they're not out of bounds
//newX = Math.min(Math.max(dimensions.width / -3, newX), dimensions.width / 3),
//newY = Math.min(Math.max(dimensions.height / -3, newY), dimensions.height / 3);

if(supportsRichEffects && animate) {
svgElement.animateViewBox(newX, newY, dimensions.width, dimensions.height, 150, updateViewBoxAttributes);
}
else {
svgElement.setViewBox(newX, newY, dimensions.width, dimensions.height);
updateViewBoxAttributes(newX, newY, dimensions.width, dimensions.height);
}
this.dispatchEvent('chartChangedPosition');
},

Here is updateViewBoxAttributes which basically sets all the stuff in the closure, it's a private function of ChartManager...

function updateViewBoxAttributes(x, y, width, height) {
view.x = x;
view.y = y;
dimensions.width = width;
dimensions.height = height;
scale = oldDimensions.width / width;
}

I run setView back to its original dimensions every time I do a full redraw of the chart to ensure all the values are congruent. Scale is kept as I enforce upper and lower zooming bounds.

Btw I've extended my animation code slightly, to include a queue. Only downside is that its framerate has seemingly reduced in any browser that uses VML, so IE8 and below. I suspect this is because I'm now using setInterval instead of setTimeout. I don't think I'm going to implement easing functions, because IMHO linear is optimal for moving charts around. My colleagues seem to agree - silently anyway ;-)

Hope that helps.

Regards,

Will


--
You received this message because you are subscribed to the Google Groups "Raphaël" group.
To view this discussion on the web, visit https://groups.google.com/d/msg/raphaeljs/-/fu7a_j-1S3cJ.

To post to this group, send an email to raph...@googlegroups.com.
To unsubscribe from this group, send email to raphaeljs+...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/raphaeljs?hl=en-GB.



--
-----------------------
Will Morgan
Web Developer
-----------------------
m: m...@willmorgan.co.uk
t: +447935077670

AS

unread,
Nov 23, 2011, 5:32:39 AM11/23/11
to raph...@googlegroups.com
Will, you are my true hero these days ;)
Hopefully I can come up with some improvements now. 


RaphaelJS really should add some more love here in the future. 
something like "myRaphaelElement.panning(viewportWidth, viewportHeight, ...)" that does all the event handling for you.

In the meantime I discovered more issues.
If you apply the drag element to your svg element, it seems it only works at the whitespace. As soon as I try to dragStart from one of my raphael elements, the drag event isnt delegated to the parentSVG element. 
So I thought, maybe I put another (empty) SVG with the same dimensions above my real SVG. From there I am able to start from every coordinate and simply apply it to the raphaelElement of the lower SVG
But there is the next problem: I wouldn't be able to use touch events for my raphael elements directly.

Did you encounter the same thing? Or is there just something wrong with my code?

Will Morgan

unread,
Nov 23, 2011, 6:40:04 AM11/23/11
to raph...@googlegroups.com
Hah, you're welcome!

I think the weight of this code so far warrants it to be put in a plugin, but I suspect if it were properly integrated in to Raphael's animation library the size would diminish. I can sort of see Dmitry's motivations in not adding this to the core - maybe could be put in a g.raphael or ui.raphael plugin or subpackage in the future with improvements.

I'm not 100% certain on how events work in Raphael but I would expect dragStart/dragMove etc not to bubble up.

Can you post a link to your code so I can have a look? If it's any use, here's mine:


Please note it's a development environment at the moment - as such things may fall over for which I cannot be held responsible. Hopefully the code is expressive enough for you to grasp how everything works, so you can incorporate those concepts in to your project.

Regards,

Will

--
You received this message because you are subscribed to the Google Groups "Raphaël" group.
To view this discussion on the web, visit https://groups.google.com/d/msg/raphaeljs/-/bOSLBHQUxGkJ.

To post to this group, send an email to raph...@googlegroups.com.
To unsubscribe from this group, send email to raphaeljs+...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/raphaeljs?hl=en-GB.

AS

unread,
Dec 2, 2011, 8:30:23 AM12/2/11
to Raphaël
Hey,

even if this is over a week old, I wanted to thank you once again for
all the effort.
The whole chart manager in its concept gave me some more insight and
raised an even bigger horizont for future plans. Lets hope in a good
way ;)

Hope you enjoyed LondonJS ;>
If it wouldn't be so far away, I ve had defenitly gave it a shot

Axel

On 23 Nov., 12:40, Will Morgan <m...@willmorgan.co.uk> wrote:
> Hah, you're welcome!
>
> I think the weight of this code so far warrants it to be put in a plugin,
> but I suspect if it were properly integrated in to Raphael's animation
> library the size would diminish. I can sort of see Dmitry's motivations in
> not adding this to the core - maybe could be put in a g.raphael or
> ui.raphael plugin or subpackage in the future with improvements.
>
> I'm not 100% certain on how events work in Raphael but I would expect
> dragStart/dragMove etc not to bubble up.
>
> Can you post a link to your code so I can have a look? If it's any use,
> here's mine:
>
> http://simplify.dev.betterbrief.co.uk/My-Organisation_Chart.html
>
> Please note it's a development environment at the moment - as such things
> may fall over for which I cannot be held responsible. Hopefully the code is
> expressive enough for you to grasp how everything works, so you can
> incorporate those concepts in to your project.
>
> Regards,
>
> Will
>

Will Morgan

unread,
Dec 7, 2011, 6:27:24 PM12/7/11
to Raphaël
You're welcome.

As a FYI, I've modified the code to increase performance and response
times, especially in IE. I did this by minimizing calls to the DOM, so
I basically reduced my calls to .attr and so on. Also I found that
modifying the transform SVG attribute on elements is faster than the X/
Y coordinates, at least to my perception anyway.

I've also put in hardware acceleration via requestAnimationFrame. The
animation code could do with a bit of a refactor as it's quite
misleading, but it works.

There's a known issue in my code where dragging on top of any element
other than the box causes the drop operation to fail. The app needs a
bit of a polish and then it'll be finished.

Message has been deleted
Message has been deleted

Will Morgan

unread,
May 11, 2012, 4:16:24 AM5/11/12
to raph...@googlegroups.com
It is all there in the JS file source.

--
Will Morgan

On 11 May 2012, at 01:31, Gerry <gerr...@gmail.com> wrote:

Hi Will,

Nice job on the UI (http://simplify.dev.betterbrief.co.uk/My-Organisation_Chart.html).  I'm working on a similar project (a diagram viewer), and I'd love to view the code relating to the animated pan/scroll/zoom. I couldn't see it from your link?

Thanks,
Gerry


> > > For more options, visit this group at
> > >http://groups.google.com/group/raphaeljs?hl=en-GB.
>
> > --
> > -----------------------
> > Will Morgan
> > Web Developer
> > -----------------------
> > m: m...@willmorgan.co.uk
> > t: +447935077670

--
You received this message because you are subscribed to the Google Groups "Raphaël" group.
To view this discussion on the web, visit https://groups.google.com/d/msg/raphaeljs/-/W9-Ca2ktii4J.
Reply all
Reply to author
Forward
Message has been deleted
0 new messages