最近做了埋点方案XTracker的轨迹回放功能,大致效果就是,在指定几个顺序的点之间形成轨迹,来模拟用户在页面上的先后行为(比如一个用户先点了啥,后点了啥)。效果图如下:
在这篇文章中,我们来聊聊轨迹回放的一些技术细节。
注意,本文只关注轨迹的绘制,并不讨论轨迹的各种生成算法。
绘制红点坐标
在绘制轨迹前,需要先绘制轨迹经过的红点坐标。使用SVG绘制红点非常简单:
1 | <svg width="500" height="500"> |
然后根据需要多画几个红点就可以了,也可以通过js批量生成:
1 | function createCircles() { |
两点之间的轨迹
红点坐标画完了,我们来画轨迹。在画多点的轨迹之前,我们先来学习两点之间的轨迹,也就是两点之间曲线的画法。
二次贝塞尔曲线、三次贝塞尔曲线还是圆弧?
SVG通过path可以画多种曲线主要包括:
- 二次贝塞尔曲线:需要一个控制点,用来确定起点和终点的曲线斜率。
- 三次贝塞尔曲线:需要两个控制点,用来确定起点和终点的曲线斜率。
- 圆弧:需要两个半径、旋转角度、逆时针还是顺时针、大圆弧还是小圆弧等多个属性。
显然,二次贝塞尔曲线最为简单,所以我们决定用二次贝塞尔曲线来画两点之间的弧线。在SVG的path中,二次贝塞曲线的参数是:
1 | M x1 y1 Q x2 y2 x3 y3 |
其中x1 y1
是起点,x2 y2
是控制点,x3 y3
是终点。来个demo吧!
1 | <svg width="320px" height="320px"> |
效果:
确定控制点
确定了使用二次贝塞尔曲线,那么问题又来了,如何确定控制点呢?控制点决定了曲线的斜率和方向,我们期望曲线:
- 对称。
- 接近直线,稍微弯曲即可,太弯可能会超出画布范围。
- 曲线永远顺时针,这样可以保证,A点到B点的曲线和B点到A点的曲线不重合。
要想做到这三点,我们只需要让控制点:
- 在两点的中垂线上。
- 距离两点的中点等于某个较小的固定值。
- 在起点和终点的顺时针区域。
画个图吧!
- 在顺时针区域画中垂线。中垂线和垂直线的角度为
angle
- 规定
offset
为某个定值(比如40,或者其他比较小的定值)。 - 那么控制点相对于中点的偏移值就确定了:
offsetX = Math.sin(angle) * offset;
offsetY = -Math.cos(angle) * offset;
完整算法:
1 | function getCtlPoint(startX, startY, endX, endY, offset) { |
起点终点相同的情况
如果起点终点相同,我们就不能使用二次贝塞尔曲线了,而是应该在该点右侧画一个小圆弧,就像这样:
在Path中圆弧的参数格式为:
1 | 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时,终点稍微比起点靠下一点,这样才能保证圆弧在右边。
示例代码:
1 | <svg width="320px" height="320px"> |
效果截图:
将两种情况封装成获取d属性的函数:
1 | function getD(startX, startY, endX, endY) { |
完整demo:
https://codepen.io/lewis617/pen/JrWMBy/
多点之间的轨迹
两点之间弧线确定了,那么如何确定多点之间的轨迹呢?其实很简单,只需要在命令后面加上新的控制点和终点即可:
1 | M x1 y1 Q x2 y2 x3 y3 Q x4 y4 x5 y5 |
所以只需要简单更新一下之前封装的函数即可:
1 | function getD(pointList){ |
如果pointList
为:
1 | var pointList = [ |
那么效果图:
完整demo:
https://codepen.io/lewis617/pen/wrJpGY/
让轨迹回放起来
轨迹画完了,如何让它回放呢?这里需要用到这两个属性:
stroke-dasharray:控制用来描边的点划线的图案范式。
stroke-dashoffset:指定了dash模式到路径开始的距离。
- 先设置
stroke-dasharray
为"length length"
,来让曲线颜色和空白的长度均为曲线长度。 - 然后设置
stroke-dashoffset
初始状态为曲线长度,来保证整个曲线”看起来”都是空白。 - 最后渐变
stroke-dashoffset
属性为0,来模拟画线。
如何渐变呢?使用SVG SMIL animation。
关键代码:
1 | var length = path.getTotalLength(); |
完整demo:
https://codepen.io/lewis617/pen/vexjyp/
给轨迹加上“圆头”
马上就可以看见胜利的曙光了,最后我们来做轨迹的“圆头”:
- 圆头就是个圆点(circle)
- 圆点需要跟着轨迹一起移动
画一个圆点很简单,那么如何画一个按照轨迹移动的圆点呢?答案是:animateMotion元素。
关键代码:
1 | function createPathHead(pathObj, d){ |
至此,轨迹回放的关键技术点就讲完了,再次欣赏下最终的效果:
完整的demo在这里: