1

I am using a couple of functions from Snap.SVG, mainly path2curve and the functions around it to build a SVG morph plugin.

I've setup a demo here on Codepen to better illustrate the issue. Basically morphing shapes simple to complex and the other way around is working properly as of Javascript functionality, however, the visual isn't very pleasing.

The first shape morph looks awful, the second looks a little better because I changed/rotated it's points a bit, but the last example is perfect.

So I need either a better path2curve or a function to prepare the path string before the other function builds the curves array. Snap.SVG has a function called getClosest that I think may be useful but it's not documented.

There isn't any documentation available on this topic so I would appreciate any suggestion/input from RaphaelJS / SnapSVG / d3.js / three/js developers.

Limon Monte
  • 52,539
  • 45
  • 182
  • 213
thednp
  • 4,401
  • 4
  • 33
  • 45
  • Usually it's the responsibility of the designer to make sure both shapes have the same number of points/segments, and are orientated suitably with respect to one another. An algorithm to automatically create nice looking vector tweening is non-trivial. Even Flash required [some interaction with the designer to control the tweening behaviour](http://help.adobe.com/en_US/flash/cs/using/WS58E1E1A4-9296-4b75-AB74-D9D545892556.html). – Paul LeBeau Feb 18 '16 at 21:09
  • I was thinking the same thing, but still, more complex shapes makes a very hard life to anyone trying to morph them. – thednp Feb 18 '16 at 21:16
  • Anyways, I am gonna ditch out Snap SVG stuff, it's simply not reliable. – thednp Feb 18 '16 at 23:43
  • Hi @thednp. Just as I posted my answer below I saw your latest comment about ditching Snap.svg. In any case, still take a look at my answer. I think the principles are still very applicable whatever library, if any, you're using. – Andrew Willems Feb 19 '16 at 05:37
  • By the way, if you're dropping Snap and shopping for another SVG library, I'll also had some difficulties with Raphael (bugs in its scale implementation). I'm not sure it's being updated either. – Andrew Willems Feb 19 '16 at 05:38
  • Not aware of any bugs in scale (in Raph or Snap), it may be worth raising a question on SO for it, or bug on github. – Ian Feb 19 '16 at 07:49
  • The best in terms of performance, complexity and scripting size seems D3.js own example of SVG path morphing http://bl.ocks.org/mbostock/3916621 – thednp Feb 19 '16 at 09:58
  • @thednp Your tween examples are very intereseting. Learning D3.js is definitely on my "to do" list. Your work on Kute.js also looks really intriguing! – Andrew Willems Feb 19 '16 at 19:44
  • Thank you @AndrewWillems, why don't you join me on the path to accomplish the best SVG morph there is? – thednp Feb 19 '16 at 19:57
  • Yes, join me on the issue with 1.0.1 update log, I will delete your message shortly if you included some personal info. – thednp Feb 19 '16 at 20:35

1 Answers1

3

I've provided a runnable code snippet below that uses Snap.svg and that I believe demonstrates one solution to your problem. With respect to trying to find the best way to morph a starting shape into an ending shape, this algorithm essentially rotates the points of the starting shape one position at a time, sums the squares of the distances between corresponding points on the (rotated) starting shape and the (unchanged) ending shape, and finds the minimum of all those sums. i.e. It's basically a least squares approach. The minimum value identifies the rotation that, as a first guess, will provide the "shortest" morph trajectories. In spite of these coordinate reassignments, however, all 'rotations' should result in visually identical starting shapes, as required.

This is, of course, a "blind" mathematical approach, but it might help provide you with a starting point before doing manual visual analysis. As a bonus, even if you don't like the rotation that the algorithm chose, it also provides the path 'd' attribute strings for all the other rotations, so some of that work has already been done for you.

You can modify the snippet to provide any starting and ending shapes you want. The limitations are as follows:

  • Each shape should have the same number of points (although the point types, e.g. 'lineto', 'cubic bezier curve', 'horizontal lineto', etc., can completely vary)
  • Each shape should be closed, i.e. end with "Z"
  • The morph desired should involve only translation. If scaling or rotation is desired, those should be applied after calculating the morph based only on translation.

By the way, in response to some of your comments, while I find Snap.svg intriguing, I also find its documentation to be somewhat lacking.

Update: The code snippet below works in Firefox (Mac or Windows) and Safari. However, Chrome seems to have trouble accessing the Snap.svg library from its external web site as written (<script...github...>). Opera and Internet Explorer also have problems. So, try the snippet in the working browsers, or try copying the snippet code as well as the Snap library code to your own computer. (Is this an issue of accessing third party libraries from within the code snippet? And why browser differences? Insightful comments would be appreciated.)

var
  s         = Snap(),
  colors    = ["red", "blue", "green", "orange"], // colour list can be any length
  staPath   = s.path("M25,35 l-15,-25 C35,20 25,0 40,0 L80,40Z"),  // create the "start" shape
  endPath   = s.path("M10,110 h30 l30,20 C30,120 35,135 25,135Z"), // create the "end"   shape
  staSegs   = getSegs(staPath), // convert the paths to absolute values, using only cubic bezier
  endSegs   = getSegs(endPath), //   segments, & extract the pt coordinates & segment strings
  numSegs   = staSegs.length,   // note: the # of pts is one less than the # of path segments
  numPts    = numSegs - 1,      //   b/c the path's initial 'moveto' pt is also the 'close' pt
  linePaths = [],
  minSumLensSqrd = Infinity,
  rotNumOfMin,
  rotNum = 0;

document.querySelector('button').addEventListener('click', function() {
  if (rotNum < numPts) {
    linePaths.forEach(function(linePath) {linePath.remove();}); // erase any previous coloured lines
    var sumLensSqrd = 0;
    for (var ptNum = 0; ptNum < numPts; ptNum += 1) { // draw new lines, point-to-point
      var linePt1 = staSegs[(rotNum + ptNum) % numPts]; // the new line begins on the 'start' shape
      var linePt2 = endSegs[          ptNum  % numPts]; // and finished on the 'end' shape
      var linePathStr = "M" + linePt1.x + "," + linePt1.y + "L" + linePt2.x + "," + linePt2.y;
      var linePath = s.path(linePathStr).attr({stroke: colors[ptNum % colors.length]}); // draw it
      var lineLen = Snap.path.getTotalLength(linePath); // calculate its length
      sumLensSqrd += lineLen * lineLen; // square the length, and add it to the accumulating total
      linePaths[ptNum] = linePath; // remember the path to facilitate erasing it later
    }
    if (sumLensSqrd < minSumLensSqrd) { // keep track of which rotation has the lowest value
      minSumLensSqrd = sumLensSqrd;     //   of the sum of lengths squared (the 'lsq sum')
      rotNumOfMin = rotNum;             //   as well as the corresponding rotation number
    }
    show("ROTATION OF POINTS #" + rotNum + ":"); // display info about this rotation
    var rotInfo = getRotInfo(rotNum);
    show("&nbsp;&nbsp;point coordinates: " + rotInfo.ptsStr); // show point coordinates
    show("&nbsp;&nbsp;path 'd' string: " + rotInfo.dStr); // show 'd' string needed to draw it
    show("&nbsp;&nbsp;sum of (coloured line lengths squared) = " + sumLensSqrd); // the 'lsq sum'
    rotNum += 1; // analyze the next rotation of points
  } else { // once all the rotations have been analyzed individually...
    linePaths.forEach(function(linePath) {linePath.remove();}); // erase any coloured lines
    show("&nbsp;");
    show("BEST ROTATION, i.e. rotation with lowest sum of (lengths squared): #" + rotNumOfMin);
      // show which rotation to use
    show("Use the shape based on this rotation of points for morphing");
    $("button").off("click");
  }
});

function getSegs(path) {
  var absCubDStr = Snap.path.toCubic(Snap.path.toAbsolute(path.attr("d")));
  return Snap.parsePathString(absCubDStr).map(function(seg, segNum) {
    return {x: seg[segNum ? 5 : 1], y: seg[segNum ? 6 : 2], seg: seg.toString()};
  });
}

function getRotInfo(rotNum) {
  var ptsStr = "";
  for (var segNum = 0; segNum < numSegs; segNum += 1) {
    var oldSegNum = rotNum + segNum;
    if (segNum === 0) {
      var dStr = "M" + staSegs[oldSegNum].x + "," + staSegs[oldSegNum].y;
    } else {
      if (oldSegNum >= numSegs) oldSegNum -= numPts;
      dStr += staSegs[oldSegNum].seg;
    }
    if (segNum !== (numSegs - 1)) {
      ptsStr += "(" + staSegs[oldSegNum].x + "," + staSegs[oldSegNum].y + "), ";
    }
  }
  ptsStr = ptsStr.slice(0, ptsStr.length - 2);
  return {ptsStr: ptsStr, dStr: dStr};
}

function show(msg) {
  var m = document.createElement('pre');
  m.innerHTML = msg;
  document.body.appendChild(m);
}
pre {
  margin: 0;
  padding: 0;
}
<script src="//cdn.jsdelivr.net/snap.svg/0.4.1/snap.svg-min.js"></script>
<p>Best viewed on full page</p>
<p>Coloured lines show morph trajectories for the points for that particular rotation of points. The algorithm seeks to optimize those trajectories, essentially trying to find the "shortest" cumulative routes.</p>
<p>The order of points can be seen by following the colour of the lines: red, blue, green, orange (at least when this was originally written), repeating if there are more than 4 points.</p>
<p><button>Click to show rotation of points on top shape</button></p>
Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
  • Thanks for your answer, however there is no way this script execute properly `The XSS Auditor refused to execute a script in 'http://stacksnippets.net/js' because its source code was found within the request. The auditor was enabled as the server sent neither an 'X-XSS-Protection' nor 'Content-Security-Policy' header.` – thednp Feb 19 '16 at 09:55
  • After reading your comment @thednp I checked for browser compatability issues. See the update I've appended to my answer. Simple solution: try it in Firefox. – Andrew Willems Feb 19 '16 at 14:37
  • Thank you so much. Seems to be a wonderful solution. I am now wondering how can I change the path2curve function to better map the curve points with this solution. As I said, I use some functions from Snap.SVG I don't need the entire code. – thednp Feb 19 '16 at 16:14
  • There is really no indication on why you've chosen the 5 and 6 ints in the `getSegs` function. – thednp Feb 19 '16 at 17:57
  • The first 'moveto' command (i.e. the "M") is only followed by two numbers, the x and y coordinates. However, the previous `toCubicBezier` command converts all subsequent drawing instructions to cubic bezier commands, i.e. the svg "C" command. The "C" command (#0) is followed by six numbers, the x and y coordinates for the first (#s 1&2) and second (#s 3&4) control points, followed by the x and y coordinates (#s 5&6) of the following final point That part of the algorithm only cares about that final coordinate, so it chooses the 5th and 6th numbers. – Andrew Willems Feb 19 '16 at 19:34
  • This indeed answers the question. – thednp Feb 20 '16 at 10:34