閱讀903 返回首頁    go 阿裏雲 go 技術社區[雲棲]


SVG軌跡回放實踐

最近做了埋點方案XTracker的軌跡回放功能,大致效果就是,在指定幾個順序的點之間形成軌跡,來模擬用戶在頁麵上的先後行為(比如一個用戶先點了啥,後點了啥)。效果圖如下:

image

在這篇文章中,我們來聊聊軌跡回放的一些技術細節。

注意,本文隻關注軌跡的繪製,並不討論軌跡的各種生成算法。

繪製紅點坐標

在繪製軌跡前,需要先繪製軌跡經過的紅點坐標。使用SVG繪製紅點非常簡單:

<svg width="500" height="500">
  <circle r="5" cx="50" cy="55" fill="red"></circle>
</svg>

image

然後根據需要多畫幾個紅點就可以了,也可以通過js批量生成:

function createCircles() {
  var r = "5",
    fill = "red",
    // circleGroup是紅點的容器
    circleGroup = document.querySelector("#circle-group");
  // pointList是紅點的坐標集合
  pointList.forEach(function(point) {
    var circle = document.createElementNS(
      "https://www.w3.org/2000/svg",
      "circle"
    );
    circle.setAttribute("r", r);
    circle.setAttribute("cx", point[0]);
    circle.setAttribute("cy", point[1]);
    circle.setAttribute("fill", fill);
    circleGroup.appendChild(circle);
  });
}

image

兩點之間的弧線

紅點坐標畫完了,我們來畫軌跡。在畫多點的軌跡之前,我們先來學習兩點之間的軌跡,也就是兩點之間弧線的畫法。

二次貝塞爾曲線、三次貝塞爾曲線還是圓弧?

SVG通過path可以畫多種弧線主要包括:

  • 二次貝塞爾曲線:需要一個控製點,用來確定起點和終點的曲線斜率。image
  • 三次貝塞爾曲線:需要兩個控製點,用來確定起點和終點的曲線斜率。image
  • 圓弧:需要兩個半徑、兩個圓心,逆時針還是順時針,大圓弧還是小圓弧等多個屬性。 image

顯然,二次貝塞爾曲線最為簡單,所以我們決定用二次貝塞爾曲線來畫兩點之間的弧線。在SVG的path中,二次貝塞曲線的參數是:

M x1 y1 Q x2 y2 x3 y3

其中x1 y1是起點,x2 y2是控製點,x3 y3是終點。來個demo吧!

<svg width="320px" height="320px">
  <path  stroke="black" fill="none" d="M 0 50 Q 25 10 50 50"/>
</svg>

效果:

image

確定控製點

確定了使用二次貝塞爾曲線,那麼問題又來了,如何確定控製點呢?控製點決定了曲線的斜率和方向,我們期望曲線:

  • 對稱。
  • 接近直線,稍微彎曲即可,太彎可能會超出畫布範圍。
  • 曲線永遠順時針,這樣可以保證,A點到B點的曲線和B點到A點的曲線不重合。

要想做到這三點,我們隻需要讓控製點:

  • 在兩點的中垂線上。
  • 距離兩點的中點小於某個固定值。
  • 在兩點順時針區域。

畫個圖吧!

image

  • 在順時針區域畫中垂線。中垂線和垂直線的角度為angle
  • 規定offset為某個定值(比如40,或者其他比較小的定值)。
  • 那麼控製點相對於中點的偏移值就確定了:offsetX = Math.sin(angle) * offset;offsetY = -Math.cos(angle) * offset;

完整算法:

function getCtlPoint(startX, startY, endX, endY, offset) {
  var offset = offset || 40;
  var angle = Math.atan((endY - startY) / (endX - startX));
  var offsetX = Math.sin(angle) * offset;
  var offsetY = -Math.cos(angle) * offset;
  var ctlX = (startX + endX) / 2 + offsetX;
  var ctlY = (startY + endY) / 2 + offsetY;
  return [ctlX, ctlY];
}

起點終點相同的情況

如果起點終點相同,我們就不能使用二次貝塞爾曲線了,而是應該在該點右側畫一個小圓弧。在Path中圓弧的參數為:

A rx ry x-axis-rotation large-arc-flag sweep-flag x y
  • 弧形命令A的前兩個參數分別是x軸半徑和y軸半徑。
  • x-axis-rotation表示弧形的旋轉情況。
  • large-arc-flag決定弧線是大於還是小於180度,0表示小角度弧,1表示大角度弧。
  • sweep-flag表示弧線的方向,0表示從起點到終點沿逆時針畫弧,1表示從起點到終點沿順時針畫弧。
  • 最後兩個參數是指定弧形的終點。

弧形命令A的具體用法不屬於本文範疇,請參考:https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths

因為我們要求:

  • 圓弧接近於圓,不是橢圓。
  • 圓弧在右側。
  • 大於180度。

所以,我們的圓弧參數為:

  • x軸和y軸半徑同為某個很小的定值(我們就設為10吧)
  • x-axis-rotation為0,不需要旋轉,既然是圓,轉了也白轉。
  • large-arc-flag為1,顯然大於180度。
  • sweep-flag為1或0都行,不過要保證為1時,終點稍微比起點靠下一點,這樣才能保證圓弧在右邊。

示例代碼:

<svg width="320px" height="320px">
  <path  stroke="black" fill="none" d="M 50 50 A 10 10 0 1 1 50 50.1"/>
</svg>

效果截圖:

image

將兩種情況封裝成獲取d屬性的函數:

function getD(startX, startY, endX, endY) {
  var ctlPoint = getCtlPoint(startX, startY, endX, endY, 40);
  var d = ["M", startX, startY].join(" ");
  if (startX !== endX || startY !== endY) {
    d += [" Q", ctlPoint[0], ctlPoint[1], endX, endY].join(" ");
  } else {
    d += [" A", 10, 10, 0, 1, 1, endX, endY + 0.1].join(" ");
  }
  return d;
}

完整demo:

See the Pen svg:兩點間的弧線:非圓弧 by lewis liu (@lewis617) on CodePen.

多點之間的弧線

兩點之間弧線確定了,那麼如何確定多點之間的弧線呢?其實很簡單,隻需要在命令後麵加上新的控製點和終點即可:

M x1 y1 Q x2 y2 x3 y3 Q x4 y4 x5 y5

所以隻需要簡單更新一下之前封裝的函數即可:

function getD(pointList){
  var offset = offset || 40;
  var d = (['M' ,pointList[0][0], pointList[0][1]]).join(' ');
  pointList.forEach(function(point, i){
    if(i>0){
      var startX = pointList[i-1][0],
          startY = pointList[i-1][1],
          endX = point[0], 
          endY = point[1];

      var ctlPoint = getCtlPoint(startX, startY, endX, endY, offset);

      if(startX !== endX || startY !== endY){
        d+=([' Q', ctlPoint[0], ctlPoint[1], endX, endY]).join(' ');
      }else{
        d+=([' A', 10, 10, 0, 1, 1, endX, endY + 0.1]).join(' ');
      }
    }
  })
  return d;
}

如果pointList為:

var pointList = [
  [0, 50],
  [0, 50],
  [50, 50],
  [100, 50],
  [0, 100],
  [50, 100],
  [100, 100],
];

那麼效果圖:

image

完整demo:

See the Pen svg:多點間的弧線:非圓弧 by lewis liu (@lewis617) on CodePen.

讓軌跡回放起來

軌跡畫完了,如何讓它回放呢?這裏需要用到這兩個屬性:

stroke-dasharray:控製用來描邊的點劃線的圖案範式。

stroke-dashoffset:指定了dash模式到路徑開始的距離。

  • 先設置stroke-dasharray"length length",來讓曲線顏色和空白的長度均為曲線長度。
  • 然後設置stroke-dashoffset初始狀態為曲線長度,來保證整個曲線"看起來"都是空白。
  • 最後漸變stroke-dashoffset屬性為0,來模擬畫線。

如何漸變呢?使用SVG SMIL animation

關鍵代碼:

var length = path.getTotalLength();
path.setAttribute("stroke-dasharray", length + " " + length);
path.setAttribute("stroke-dashoffset", length);
path.innerHTML= '<animate attributeName="stroke-dashoffset" to="0"  dur="7s" begin="0s" fill="freeze" repeatCount="indefinite"/>';

完整demo:

See the Pen svg:多點間的弧線回放 by lewis liu (@lewis617) on CodePen.

給軌跡加上“圓頭”

馬上就可以看見勝利的曙光了,最後我們來做軌跡的“圓頭”:

  • 圓頭就是個圓點(circle)
  • 圓點需要跟著軌跡一起移動

畫一個圓點很簡單,那麼如何畫一個按照軌跡移動的圓點呢?答案是:animateMotion元素

關鍵代碼:

function createPathHead(pathObj, d){
  var r = 3;
  var head = document.createElementNS("https://www.w3.org/2000/svg", "circle");
  head.setAttribute("id", pathObj.id + "-head");
  head.setAttribute("r", r);
  head.setAttribute("fill", pathObj.stroke);

  var animateMotion = document.createElementNS("https://www.w3.org/2000/svg", "animateMotion");
  animateMotion.setAttribute("path", d);
  animateMotion.setAttribute("begin", "indefinite");
  animateMotion.setAttribute("dur", "7s");
  animateMotion.setAttribute("fill", "freeze");
  animateMotion.setAttribute("rotate", "auto");
  head.appendChild(animateMotion);

  return head;
}

至此,軌跡回放的關鍵技術點就講完了,完整的demo在這裏,點擊按鈕進行回放。

See the Pen xtracker 動畫2:其他弧線 by lewis liu (@lewis617) on CodePen.

最後更新:2017-10-17 17:34:51

  上一篇:go  米菲雲倉代發係統後台開發
  下一篇:go  使用 Dawn 構建 React 項目