大屏曲线动画总结

375 阅读4分钟

项目背景:在可视化大屏当中,需要根据实时数据接口来对有数据的区域进行高亮轮播,同时从起始点向有数据的区域绘制线条的流动效果。具体展示效果如下

大屏动画.mp4.gif

需求一

轮播高亮区域,在背景图当中,每个区域的位置固定,依次对有数据的区域进行高亮显示

  • 实现方法:接口请求完成后,维护需要高亮区域列表,包括背景图和背景图位置,依次循环该列表,每个间隔3秒更换一次背景图的src路径和style属性
<img ref="imgElement" :src="imgUrl" class="high-light">
  • 出现问题:由于每次都在更新图像位置以及背景图,惊奇发现在每次轮播过程当中,会在一两个区域闪现一下上个区域的高亮图,然后才显示当前区域。由于每个区域的形状差异较大,会造成明显观感上的跳动。
  • 分析原因:如代码所示,每次更新高亮区域是,会将图片所在路径进行替换更新并同步更新图片位置,然而img需要会根据路径来下载图片,但此时图片位置已更改,若图片未及时下载就会造成图片错位
  • 解决方法:使用图片预加载,等图片预加载完成后再进行高亮图片属性替换

需求二

通过给定区域的起点和终点生成贝塞尔曲线动画及流动效果 image.png

  • 实现方法:通过canvas进行图形绘制,分为两张canvas,一张绘制线条,一张绘制线条上流的巨星。基于requestAnimationFrame实现动画效果,通过时间来控制canvas上图形的位置。

  • 二阶贝塞尔曲线:感觉三个点来确定一条曲线,其中两个点为起始点,另一个为控制点,即曲线的弯曲程度。但我们只有起始点,没有控制点。通过控制弯曲程度来生成控制点。

  • 控制点生成方式:在已知起始两点坐标后,可以得到两点的垂直平分线及中点,将在垂直平分线上任意点和中点的距离比值作为curveness,从而可以得到控制点的坐标。图如下所示

image.png

  • 线条简要代码实现:
function animateLine() {
   // 线条进度
   percent = (percent + 1) 
   // 绘制线条
   drawCurvePath(ctx, start, end, percent)
   requestAnimationFrame(animateLine)
}

function drawCurvePath(ctx, start, end, curveness = 0.2, percent) {
  const cp = [
    (start[0] + end[0]) / 2 - (start[1] - end[1]) * curveness,
    (start[1] + end[1]) / 2 - (end[0] - start[0]) * curveness
  ]
  ctx.moveTo(start[0], start[1])
  const x = quadraticBezier(start[0], cp[0], end[0], percent)
  const y = quadraticBezier(start[1], cp[1], end[1], percent)
  ctx.lineTo(x, y)
  ctx.stroke()
}

function quadraticBezier(p0, p1, p2, t) {
  const k = 1 - t;
  return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2;    // 二次贝赛尔曲线方程
}
  • 出现问题:众所周知,requestAnimationFrame是通过浏览器刷新每帧的空闲时间来执行内部函数。因此,上述我的思路是每一帧让线条的进度增加1%。当页面一直处于可见状态时,完美运行。然后,意外的情况则是页面不可见状态。在浏览器的机制下,页面处于不可见状态(切换到其他标签页或页面不在视图内等情况)时,requestAnimationFrame内部会停止执行,直到页面处于可见状态时,才会再次运行。如果只是简单的动效重新绘制倒也简单,也处于可接受范围内。但离谱的时,动画在此时会飞速般运行,整个动效处于杂乱无章的状态。
  • 分析原因:requestAnimationFrame不是简单的停止执行,而是将其存入栈中,待可见时,存入栈当中的函数会同时执行,进而导致大量的帧在同一刻执行,而不同帧的渲染的线条起始位置不一,造成杂乱无章。
  • 解决方法:通过时间补偿方法来控制线条进度,而不是帧。
  • 线条流动矩形效果:矩形在流动过程当中需要跟随曲线的弯曲方向来调整朝向。通过计算当前移动到的位置和起点位置的tan值来旋转矩形的方向。
  • 简要代码实现:
function drawRect(ctx, start, end, curveness = 0.2, percent) {
  const cp = [
    (start[0] + end[0]) / 2 - (start[1] - end[1]) * curveness,
    (start[1] + end[1]) / 2 - (end[0] - start[0]) * curveness
  ]

  let preX = start[0], preY = start[1]
  const x = quadraticBezier(start[0], cp[0], end[0], t)
  const y = quadraticBezier(start[1], cp[1], end[1], t)

  ctx.translate(x, y)
  ctx.rotate(Math.atan2(y - preY, x - preX) - Math.atan2(0, -1))
  preX = x
  preY = y
  ctx.fillRect(x, y, 18, 4)
  }