项目背景:在可视化大屏当中,需要根据实时数据接口来对有数据的区域进行高亮轮播,同时从起始点向有数据的区域绘制线条的流动效果。具体展示效果如下
需求一
轮播高亮区域,在背景图当中,每个区域的位置固定,依次对有数据的区域进行高亮显示
- 实现方法:接口请求完成后,维护需要高亮区域列表,包括背景图和背景图位置,依次循环该列表,每个间隔3秒更换一次背景图的src路径和style属性
<img ref="imgElement" :src="imgUrl" class="high-light">
- 出现问题:由于每次都在更新图像位置以及背景图,惊奇发现在每次轮播过程当中,会在一两个区域闪现一下上个区域的高亮图,然后才显示当前区域。由于每个区域的形状差异较大,会造成明显观感上的跳动。
- 分析原因:如代码所示,每次更新高亮区域是,会将图片所在路径进行替换更新并同步更新图片位置,然而img需要会根据路径来下载图片,但此时图片位置已更改,若图片未及时下载就会造成图片错位
- 解决方法:使用图片预加载,等图片预加载完成后再进行高亮图片属性替换
需求二
通过给定区域的起点和终点生成贝塞尔曲线动画及流动效果
-
实现方法:通过canvas进行图形绘制,分为两张canvas,一张绘制线条,一张绘制线条上流的巨星。基于requestAnimationFrame实现动画效果,通过时间来控制canvas上图形的位置。
-
二阶贝塞尔曲线:感觉三个点来确定一条曲线,其中两个点为起始点,另一个为控制点,即曲线的弯曲程度。但我们只有起始点,没有控制点。通过控制弯曲程度来生成控制点。
-
控制点生成方式:在已知起始两点坐标后,可以得到两点的垂直平分线及中点,将在垂直平分线上任意点和中点的距离比值作为curveness,从而可以得到控制点的坐标。图如下所示
- 线条简要代码实现:
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)
}