User Drawn Motion Paths with SnapSVG

User Drawn Motion Paths with SnapSVG

This tutorial will animate a snowboarder along a drawn path using SnapSVG. This demo is a simplified version of the code in: custom designed Christmas card.

Let’s set up the starting SVG, which came from: OpenClipart. A useful design pattern is to always set your SVG width/height to 100% and then constrain the dimensions inside a wrapping div element. Here are the important elements to note:

<div id="templatediv"><!– the SCENE –>
 <svg id="templatesvg">
  <g id="snowboard" transform="matrix(.1 0 0 .1 -85.03 50.47)" >
   <g id="snowboardsubnode" >
  <g id="hiddenslot" transform="matrix(.15 0 0 .15 10 -510)">
   <g id="hiddenslotsubnode">
   <g id="handwave" >
<div id="snowboarddiv"><!– snowboard image –>

"snowboard" group is currently empty but it has transforms all set up to hold the snowboarder.

"handwave" is a hidden hand that is going to show the user how to draw the path. It is hidden by placing it out of view. In case you are not familiar with SVG transforms, the matrix(.15 0 0 .15 10 -510) is read like this: matrix(scaleX 0 0 scaleY translateX translateY) when the 2nd and 3rd args are 0. An easier way to write it is: transform="translate(10, -510), scale(.15, .15)"

This is what needs to be done:

  • User clicks the snowboarder (e.g. the idea is to select from many choices)
  • A paintbrush (a blue circle) is added to the scene with a hand indicating the user should draw something.
  • When the user drags the brush, record the points and construct a path with it.
  • Insert the path and insert an animation to run the snowboard along it.

The first one is a bit of simple jQuery:


In makePaintBrush the core code is also simple; we find the main elements with and make shortcut variables; drawParent;clicked and helpinghand. Note we clone the hand and set things up to remove it later. The actual brush gets drawn with,0,55).attr({ fill: "blue" }); and event handlers brush1.drag(doDrag, startDrag, endDrag);

var drawParent =‘#’+eloop.templatediv+’ #’+eloop.activeslot);
var clicked =‘#snowboarddiv g’);
origclicked = clicked;
var helpinghand ="#handwave");
// [draw] draw it in [draw]
vprint("eloop",">>>>> START makePaintBrush");
var brush1 =,0,55).attr({ fill: "blue" });
recordpoints = true;
var pointsA=[];
if(helpinghand !=null){// add drag help pointer
helpinghand = helpinghand.clone();
brush1.drag(doDrag, startDrag, endDrag);

The hard work gets done by the event handlers. startDrag() only empties anything we’ve set up to remove so let’s look at doDrag()

function doDrag(dx,dy,xx,yy,e){ // deltas, current pos, DOM event
thisguy = brush1;
lmatrix = thisguy.transform().localMatrix;
gmatrix = thisguy.transform().globalMatrix;
mparts = gmatrix.split();
// reduce by whatever the global matrix scale is
addon = new Snap.Matrix().translate(newx,newy);
point ={ cx: newx , cy: newy };
//vprint("eloop","recorded point["+newx+","+newy+"]");
// dx/dy appear to be cumulative for the drag, want the dx from *last* func call
//vprint("eloop",">>>>> dragmove current matrix:"+lmatrix.toTransformString()+" adding:"+addon.toTransformString());
thisguy.transform(lmatrix); // move the brush

The important reference elements are transforms and drag. To make our path we need deltas and though the event handler returns deltas, it is a cumulative delta for the whole drag so those are adjusted by subtracting prevdx/y. All the points are added to global pointsA for latter processing. Because we’ve supplied a custom drag handler we are now responsible for actually moving the brush with thisguy.transform(lmatrix).

When the drag stops the endDrag function gets called. The first part smooths out the points AND assembles the path element. We only use the point if it is at least 2 pixels distant from the previous point.

function endDrag(xx,yy,e){
vprint("eloop","======================= END DRAG ==================";
if(recordpoints){// plot the points
pS={ cx: 0 , cy: 0 };
dd="m"","" "; // start of the [PATH]
for(ii=1; ii<pointsA.length; ii++){
dist=Math.sqrt(Math.pow(,2) +Math.pow(,2));
if(dist>25){ // add it on
p1=pS;","" "; // add to the [PATH]
pS={ cx: 0 , cy: 0 };
else {
;// ignore for path smoothing/point reduction
}// end for all points
vprint("eloop","DRAWING PATH:"+dd);
// [ MOTION ] [ MOTION ]
drawParent.add(origclicked); // put the snowboard in the slot
// draw the hidden path
var p = drawParent.path(dd).attr({ // put [PATH] in slot
fill: "none",
stroke: "none",
strokeWidth: 3

svg_frag= SEE BELOW
var f = Snap.parse(svg_frag); // create the animation
origclicked.append(f); // add animateMotion to origclicked g element
eloop.removethis=[origclicked,p]; // flag for remove on any repeat
brush1.remove(); //all done, remove brush
}// end recordpoints
vprint("eloop"," = = end drag done = = =");
} // end X drag end X

SnapSvg has animation functions but no interface for animateMotion so we’ll simply construct the raw SVG and parse it. Note that a scale transform is also added to give the apperance the object is moving toward you.

<animateMotion id="ani1" calcMode="spline" keyTimes="0;.5;1" keySplines="0.5 0 1 1;0 0 0.5 1" keyPoints="0;.5;1" fill="freeze" dur="5s" repeatCount="indefinite" >’;
<mpath xlink:href="#thepath"></mpath>’;
<animateTransform attributeName="transform" type="scale" calcMode="spline" values="1,1;4,4" keySplines="0.8 0 1 1" keyTimes="0;1" dur="5s" repeatCount="indefinite" ></animateTransform>’;

Check out the code pen:

See the Pen LERRvp by Genolve (@genolve) on CodePen.


Comments are closed