A simple animation of a single path is fairly straightforward using some custom attributes:
(function() {
Raphael.fn.addGuides = function() {
this.ca.guide = function(g) {
return {
guide: g
};
};
this.ca.along = function(percent) {
var box = this.getBBox( false );
var g = this.attr("guide");
var len = g.getTotalLength();
var point = g.getPointAtLength(percent * len);
var t = {
transform: "...t" + [point.x - box.x, point.y - box.y]
};
return t;
};
};
})();
var paper = Raphael("container", 600, 600);
paper.addGuides();
var circ1 = paper.circle(50, 50, 40);
var circ2 = paper.circle(150, 50, 40);
var circ3 = paper.circle(250, 50, 40);
var circ4 = paper.circle(350, 50, 40);
var redarc = "M200,100 c 22-7,37,5,38,9";
var redarcpath = paper.path(redarc).attr({'stroke-width': '2', 'stroke': 'red'});
circ3.attr({guide : redarcpath, along : 1})
.animate({along : 0}, 2000, "linear");
see: http://jsfiddle.net/p6Q3x/1/
But when you call this same animate
method on a set, the relative positioning of the elements within the set gets collapsed. The concentric circles of this "target", for example, all get piled on top of each other at the upper left of the set's bounding box:
paper.setStart();
var inner = paper.circle(50, 250, 15);
var middle = paper.circle(50, 250, 30);
var outer = paper.circle(50, 250, 45);
var target = paper.setFinish();
var redarc = "M200,100 c 22-7,37,5,38,9";
var redarcpath = paper.path(redarc).attr({'stroke-width': '2', 'stroke': 'red'});
// basic animation of target on red arc collapses the shapes
target.attr({guide : redarcpath, along : 1})
.animate({along : 0}, 2000, "linear");
see: http://jsfiddle.net/p6Q3x/3/
This is odd, as the lowercase relative "t" in the along
method would suggest that relative positioning was going to be preserved. But no. So I went about cloning the motion path for each element of the set, and applying a different transformation to each of those cloned paths, depending on the relative position of the "child" path within the set's bounding box. But even that is not enough, because transformation matrixes are ignored when paths are used for animation. So each of the transformed motion paths have to be "flattened" back into raw path data. I used Timo's function here for the flattening part:
function animateSet(set, pathToFollow) {
var setBBox = set.getBBox(false);
set.forEach(function (el) {
var pathBBox = el.getBBox( false );
// get the difference between the element's BBox
// and its "parent" set's BBox coordinates
var offset = "...t" + [pathBBox.x - setBBox.x, pathBBox.y - setBBox.y];
// use offset to create a new, unique, displaced motion path
var transformedPath = pathToFollow.clone()
.hide()
.transform(offset);
// flatten that transformation back into raw path data
var flatPathString = flatten_transformations(transformedPath, normalize_path, to_relative, dec);
// and make a new Raphael path array out of that flattened data
var shiftedPath = paper.path(flatPathString).hide();
el.attr({guide : shiftedPath, along : 1})
.animate({along : 0}, 2000, "linear", function() {
transformedPath.remove(); // wipe out temporary motion paths
shiftedPath.remove(); // when animation completes
});
});
}
The good news is that it works! http://jsfiddle.net/p6Q3x/
But this feels really ugly, bloated, and sluggish. I can't quite believe that this needs to be so convoluted. Can anyone suggest a simpler, cleaner, more efficent way to a newcomer? My sense is that the along method needs to be rewritten for sets so that the set's members each get their own bounding box, but I'm at a loss as to how to do that exactly. All help very appreciated.