旋转SVG路径点以获得更好的变形效果

时间:2016-02-18 19:53:35

标签: javascript svg snap.svg svg-path svg-morphing

我正在使用Snap.SVG中的一些函数,主要是path2curve及其周围的函数来构建一个SVG变形插件。

我已设置演示版here on Codepen以更好地说明问题。基本上变形形状简单到复杂,反之亦然,在Javascript功能方面正常工作,但是,视觉效果不是很令人满意。

第一个形状变形看起来很糟糕,第二个看起来好一点,因为我稍微改变/旋转它的点,但最后一个例子是完美的。

所以我需要一个更好的path2curve或一个函数来在另一个函数构建curves数组之前准备路径字符串。 Snap.SVG有一个名为getClosest的函数,我认为它可能很有用,但没有记录。

没有关于此主题的任何文档,所以我将非常感谢RaphaelJS / SnapSVG / d3.js / three / js开发人员的任何建议/输入。

1 个答案:

答案 0 :(得分:3)

我在下面提供了一个使用Snap.svg的可运行代码片段,我相信它可以为您的问题提供一个解决方案。关于试图找到将起始形状变形为结束形状的最佳方法,该算法基本上将起始形状的点一次旋转一个位置,对(旋转)起点上的相应点之间的距离的平方求和形状和(未更改的)结束形状​​,并找到所有这些总和的最小值。即它基本上是最小二乘法。最小值标识旋转,作为第一个猜测,将提供"最短"变形轨迹。然而,尽管进行了这些坐标重新分配,但所有的轮换都是如此。根据需要,应在视觉上产生相同的起始形状。

当然,这是一个"盲人"数学方法,但它可能有助于在进行手动视觉分析之前为您提供一个起点。作为奖励,即使您不喜欢算法所选择的轮换,它也会提供路径' d'所有其他轮换的属性字符串,因此已经为您完成了一些工作。

您可以修改代码段以提供所需的任何起始和结束形状。限制如下:

  • 每个形状应具有相同的点数(尽管点类型,例如' lineto',' cubic bezier curve',' horizo​​ntal lineto'等。,可以完全改变)
  • 每个形状都应该关闭,即以" Z"
  • 结束
  • 所需的变形仅涉及翻译。如果需要缩放或旋转,则应在仅基于平移计算变形后应用这些缩放或旋转。

顺便说一句,回应你的一些评论,虽然我发现Snap.svg很有趣,但我发现它的文档有点缺乏。

更新下面的代码段可在Firefox(Mac或Windows)和Safari中使用。但是,Chrome似乎无法从其外部网站访问Snap.svg库(< script ... github ...>)。 Opera和Internet Explorer也存在问题。因此,请尝试使用工作浏览器中的代码段,或尝试将代码段代码和Snap库代码复制到您自己的计算机上。 (这是从代码片段中访问第三方库的问题吗?为什么浏览器存在差异?我们将非常感谢有洞察力的评论。)



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);
}
&#13;
pre {
  margin: 0;
  padding: 0;
}
&#13;
<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>
&#13;
&#13;
&#13;