SVG轨迹回放实践

时间:2022-09-04 16:40:15

最近做了埋点方案XTracker的轨迹回放功能,大致效果就是,在指定几个顺序的点之间形成轨迹,来模拟用户在页面上的先后行为(比如一个用户先点了啥,后点了啥)。效果图如下:

SVG轨迹回放实践

在这篇文章中,我们来聊聊轨迹回放的一些技术细节。

注意,本文只关注轨迹的绘制,并不讨论轨迹的各种生成算法。

绘制红点坐标

在绘制轨迹前,需要先绘制轨迹经过的红点坐标。使用SVG绘制红点非常简单:

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

SVG轨迹回放实践

然后根据需要多画几个红点就可以了,也可以通过js批量生成:

function createCircles() {
  var r = "5",
    fill = "red",
    // circleGroup是红点的容器
    circleGroup = document.querySelector("#circle-group");
  // pointList是红点的坐标集合
  pointList.forEach(function(point) {
    var circle = document.createElementNS(
      "http://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轨迹回放实践

两点之间的轨迹

红点坐标画完了,我们来画轨迹。在画多点的轨迹之前,我们先来学习两点之间的轨迹,也就是两点之间曲线的画法。

二次贝塞尔曲线、三次贝塞尔曲线还是圆弧?

SVG通过path可以画多种曲线主要包括:

  • 二次贝塞尔曲线:需要一个控制点,用来确定起点和终点的曲线斜率。

    SVG轨迹回放实践
  • 三次贝塞尔曲线:需要两个控制点,用来确定起点和终点的曲线斜率。

    SVG轨迹回放实践
  • 圆弧:需要两个半径、旋转角度、逆时针还是顺时针、大圆弧还是小圆弧等多个属性。

    SVG轨迹回放实践

显然,二次贝塞尔曲线最为简单,所以我们决定用二次贝塞尔曲线来画两点之间的弧线。在SVG的path中,二次贝塞曲线的参数是:

M x1 y1 Q x2 y2 x3 y3

其中x1 y1是起点,x2 y2是控制点,x3 y3是终点。来个demo吧!

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

效果:

SVG轨迹回放实践

确定控制点

确定了使用二次贝塞尔曲线,那么问题又来了,如何确定控制点呢?控制点决定了曲线的斜率和方向,我们期望曲线:

  • 对称。
  • 接近直线,稍微弯曲即可,太弯可能会超出画布范围。
  • 曲线永远顺时针,这样可以保证,A点到B点的曲线和B点到A点的曲线不重合。

要想做到这三点,我们只需要让控制点:

  • 在两点的中垂线上。
  • 距离两点的中点等于某个较小的固定值。
  • 在起点和终点的顺时针区域。

画个图吧!

SVG轨迹回放实践

  • 在顺时针区域画中垂线。中垂线和垂直线的角度为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.atan2(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];
}

起点终点相同的情况

如果起点终点相同,我们就不能使用二次贝塞尔曲线了,而是应该在该点右侧画一个小圆弧,就像这样:

SVG轨迹回放实践

在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 id="line1" stroke="black" fill="none" d="M 50 50 A 10 10 0 1 1 50 50.1"/>
</svg>

效果截图:

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:

<iframe height='265' scrolling='no' title='svg:两点间的弧线:非圆弧' src='//codepen.io/lewis617/embed/JrWMBy/?height=265&theme-id=0&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>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],
];

那么效果图:

SVG轨迹回放实践

完整demo:

<iframe height='265' scrolling='no' title='svg:多点间的弧线:非圆弧' src='//codepen.io/lewis617/embed/wrJpGY/?height=265&theme-id=0&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>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:

SVG轨迹回放实践

<iframe height='265' scrolling='no' title='svg:多点间的弧线回放' src='//codepen.io/lewis617/embed/vexjyp/?height=265&theme-id=0&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen svg:多点间的轨迹回放 by lewis liu (@lewis617) on CodePen.

给轨迹加上“圆头”

马上就可以看见胜利的曙光了,最后我们来做轨迹的“圆头”:

  • 圆头就是个圆点(circle)
  • 圆点需要跟着轨迹一起移动

画一个圆点很简单,那么如何画一个按照轨迹移动的圆点呢?答案是:animateMotion元素

关键代码:

function createPathHead(pathObj, d){
  var r = 3;
  var head = document.createElementNS("http://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("http://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;
}

至此,轨迹回放的关键技术点就讲完了,再次欣赏下最终的效果:

SVG轨迹回放实践

完整的demo在这里:

<iframe height='359' scrolling='no' title='xtracker 动画2:其他弧线' src='//codepen.io/lewis617/embed/RLpxPj/?height=559&theme-id=0&default-tab=result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen xtracker 动画2:其他弧线 by lewis liu (@lewis617) on CodePen.

SVG轨迹回放实践的更多相关文章

  1. 物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用&lpar;转载&rpar;

    物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用(转载)   2015年11月14日|    by: nbboy|    Category: 系统设计, 缓存设计, 高性能系统 摘要 ...

  2. 如何实现LBS轨迹回放功能?含多平台实现代码

    本篇文章告诉您,如何实现轨迹回放.并且提供了web端,iOS端,Android端3个平台的轨迹回放代码.拷贝后可以直接使用.另外,文末有小彩蛋,算是开发者的福利. Web端/JavaScript 实现 ...

  3. Google地图轨迹回放模拟

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  4. OpenLayers3的轨迹回放

    OpenLayers3实现轨迹回放需要动画操作,官网上的例子用的是postcompose,但是还可以使用javascript中setInterval和setTime. 我的例子是按官网上来的http: ...

  5. 如何使用JS来开发室内三维地图的轨迹回放功能

     在制作完成室内三维地图的功能后,最经常有的需求就是如何做人员的轨迹回放,一般流程都是从数据库中查询轨迹坐标后,经过后台查询接口返回给前端,接下来的事情都交给JS来完成. 如果想做好一个性能好的轨迹回 ...

  6. GPS&sol;轨迹追踪、轨迹回放、围栏控制

    折腾一个多月终于弄完了这个项目,起初都未曾接触GPS/轨迹追踪.轨迹回放.圈划围栏...等一些在百度地图或者Googel地图操作的一些业务,后端的业务相对来说简单点 cas单点登录,mongdb灵活的 ...

  7. 使用GMap&period;NET类库,实现地图轨迹回放。&lpar;WPF版&rpar;

    前言 实现轨迹回放,GMap.NET有对应的类GMapRoute.这个类函数很少,功能有限,只能实现简单的轨迹回放.要实现更复杂的轨迹回放,就需要自己动手了. 本文介绍一种方法,可以实现复杂的轨迹回放 ...

  8. 使用百度地图API实现轨迹回放

    调用百度地图API实现路线的轨迹回放功能其实很简单,只要搞懂以下几点即可: 1.需要用Polyline方法先绘制好路线图 2.用Marker添加标注点 3.关键一步,通过结合定时器,使用Marker创 ...

  9. Mapbox轨迹回放

        轨迹回放是webgis中的常见功能,是一种被客户喜闻乐见的GIS动画.     动画是一种短时间内不停重绘达到不断运动的效果.本文中轨迹回放就是事先计算好所需要的点,后面再进行播放.      ...

随机推荐

  1. &commat;service中构造方法报错

    因为类首先被Spring实例化的时候,会调用构造函数.只有实例化后,才会注入.你等于没注入就调用了,所以报错.

  2. 【技术宅3】截取文件和url扩展名的N种方法

    //截取文件扩展名的N种方法   //第1种 //strrchr() 函数查找字符在指定字符串中最后一次出现的位置,如果成功,则返回其后面的字符串 //返回带有点的扩展名 function get_e ...

  3. sqlserver系统表操作

    查询表名中包含‘user’的方法Select * From sysobjects Where name like '%user%' 如果知道列名,想查找包含有该列的表名,可加上系统表syscolumn ...

  4. 7&period;纯 CSS 创作一个 3D 文字跑马灯特效

    原文地址:https://segmentfault.com/a/1190000014663038 感想:简单的从右到左动画 HTML代码: <div class="box"& ...

  5. 虚拟机之 搭建discuz论坛

    1.下载 mkdir /data/www cd !$ wget http://download.comsenz.com/DiscuzX/3.2/Discuz_X3.2_SC_GBK.zip 2.解压 ...

  6. nyoj 1023——还是回文——————【区间dp】

    还是回文 时间限制:2000 ms  |  内存限制:65535 KB 难度:3   描述 判断回文串很简单,把字符串变成回文串也不难.现在我们增加点难度,给出一串字符(全部是小写字母),添加或删除一 ...

  7. Hibernate-ORM&colon;11&period;Hibernate中的关联查询

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 本篇博客将讲述Hibernate中的关联查询,及其级联(cascade)操作,以及指定哪一方维护关联关系的(i ...

  8. iOS-技术细节整理

    遇到未使用类,可以看看xcode->help->developer documentation 下面做一下简单的技术细节整理 Auto Layout使用Auto Layout来灵活改变UI ...

  9. &lpar;转&rpar;MongoDB numa系列问题三:overcommit&lowbar;memory和zone&lowbar;reclaim&lowbar;mode

    内核参数overcommit_memory : 它是 内存分配策略 可选值:0.1.2.0:表示内核将检查是否有足够的可用内存供应用进程使用:如果有足够的可用内存,内存申请允许:否则,内存申请失败,并 ...

  10. Codeforces 270E Flawed Flow 网络流问题

    题意:给出一些边,给出边的容量.让你为所有边确定一个方向使得流量最大. 题目不用求最大流, 而是求每条边的流向,这题是考察网络流的基本规律. 若某图有最大,则有与源点相连的边必然都是流出的,与汇点相连 ...