SVG軌跡回放實踐
最近做了埋點方案XTracker的軌跡回放功能,大致效果就是,在指定幾個順序的點之間形成軌跡,來模擬用戶在頁麵上的先後行為(比如一個用戶先點了啥,後點了啥)。效果圖如下:
在這篇文章中,我們來聊聊軌跡回放的一些技術細節。
注意,本文隻關注軌跡的繪製,並不討論軌跡的各種生成算法。
繪製紅點坐標
在繪製軌跡前,需要先繪製軌跡經過的紅點坐標。使用SVG繪製紅點非常簡單:
<svg width="500" height="500">
<circle r="5" cx="50" cy="55" fill="red"></circle>
</svg>
然後根據需要多畫幾個紅點就可以了,也可以通過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);
});
}
兩點之間的弧線
紅點坐標畫完了,我們來畫軌跡。在畫多點的軌跡之前,我們先來學習兩點之間的軌跡,也就是兩點之間弧線的畫法。
二次貝塞爾曲線、三次貝塞爾曲線還是圓弧?
SVG通過path可以畫多種弧線主要包括:
- 二次貝塞爾曲線:需要一個控製點,用來確定起點和終點的曲線斜率。
- 三次貝塞爾曲線:需要兩個控製點,用來確定起點和終點的曲線斜率。
- 圓弧:需要兩個半徑、兩個圓心,逆時針還是順時針,大圓弧還是小圓弧等多個屬性。
顯然,二次貝塞爾曲線最為簡單,所以我們決定用二次貝塞爾曲線來畫兩點之間的弧線。在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>
效果:
確定控製點
確定了使用二次貝塞爾曲線,那麼問題又來了,如何確定控製點呢?控製點決定了曲線的斜率和方向,我們期望曲線:
- 對稱。
- 接近直線,稍微彎曲即可,太彎可能會超出畫布範圍。
- 曲線永遠順時針,這樣可以保證,A點到B點的曲線和B點到A點的曲線不重合。
要想做到這三點,我們隻需要讓控製點:
- 在兩點的中垂線上。
- 距離兩點的中點小於某個固定值。
- 在兩點順時針區域。
畫個圖吧!
- 在順時針區域畫中垂線。中垂線和垂直線的角度為
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>
效果截圖:
將兩種情況封裝成獲取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],
];
那麼效果圖:
完整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