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

Rotating SVG path points for better morphing

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

我已经设置了一个演示 here on Codepen 以更好地说明问题。基本上变形形状从简单到复杂,反之亦然,Javascript 功能正常工作,但是,视觉效果不是很令人愉悦。

第一个形状变形看起来很糟糕,第二个看起来好一点,因为我 changed/rotated 它有点点,但最后一个例子很完美。

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

没有关于此主题的任何文档,因此非常感谢来自 RaphaelJS / SnapSVG / d3.js / three/js 开发人员的任何 suggestion/input。

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

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

您可以修改代码片段以提供您想要的任何开始和结束形状。限制如下:

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

顺便说一句,为了回应您的一些评论,虽然我觉得 Snap.svg 很有趣,但我也发现它的文档有些欠缺。

更新下面的代码片段适用于 Firefox(Mac 或 Windows)和 Safari。但是,Chrome 似乎无法从其外部网站访问 Snap.svg 库()。 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);
}
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>