这篇文章主要介绍如果用canvas实现固定坐标数组的飞线动画(折线),或者可以说是电流效果吧。
之前在网上有看到过流星托尾的效果,那种效果的主要方式是通过不断添加“蒙板”层的方式逐渐淡化前几秒的位置,而绘制的线其实就是一个小球,一直在移动。看起来是线,代码层面上其实只画了一个点。主要的代码为
ctx.fillStyle = 'rgba(0,0,0,0.1)';//这里颜色要与背景图的颜色一样,但是透明度要小
ctx.rect(0,0,800,600);//整个绘制区域的大小
但是,该方法有一个很严重的bug,托尾的轨迹无法完全清楚,虽然一开始颜色较淡,但是久而久之就会越来越深。具体的实现代码这里不赘述,可自行百度。
而今天要讲述的方法,是不断绘制每一帧的“线”。没错,是正儿八经的线。由于会涉及到拐角转弯,所以会有一定的计算和比较,这是难点。托尾的效果自然可以通过对线设置渐变颜色来实现。
constructor(color, length, width, speed, route, step = 0) {
this.startx = route[0].x;//设置起始点坐标
this.starty = route[0].y;
this.route = route;//轨迹点,数组格式
this.speed = speed;//速度
this.width = width;//线的宽度
this.color = color;//飞线的颜色
this.length = length;//飞线的总体长度
this.step = step;//表示以第几个轨迹点作为起始点绘制飞线动画,默认是第一个
this.drawTimes = 0;
this.colorStep = [0, 0.6, 1];//渐变比例
}
参数的设置可以根据实际情况做调整,下面主要讲一下绘制的方法:
draw(ctx) {
this.drawTimes++;//帧数自增
ctx.beginPath();
let { route, startx, starty, color, speed } = this;
ctx.moveTo(this.startx, this.starty);//首先要绘制起始点的位置
let length = this.drawTimes * speed <= this.length ? this.drawTimes * speed : this.length;//计算每一帧需要绘制的长度
let endx = startx;
let endy = starty;
for (let i = this.step + 1, len = route.length; i < len; i++) {
let next_x = route[i].x;//获取第二个轨迹点的坐标
let next_y = route[i].y;
let tempLength = Math.sqrt(Math.pow((next_x - startx), 2) + Math.pow((next_y - starty), 2));//这一步是关键,主要是计算当前起始点到下一个轨迹点的直线长度,计算方法就是勾股定理
if (tempLength > length) {
// 当上述长度大于这一帧的线长时,说明飞线在这一帧还未运动至下一个轨迹点,因此这一帧飞线为直线的状态,不存在拐角
let dx = length * (next_x - startx) / tempLength;//按照线段比例计算出x的增量
let dy = length * (next_y - starty) / tempLength;
endx = startx + dx;//得出这一帧的终点坐标
endy = starty + dy;
ctx.lineTo(endx, endy);//连线完成这一帧的飞线,并退出循环
break;
} else {
// 如果存在拐角,则飞线必定经过下一个轨迹点
ctx.lineTo(next_x, next_y);//连接下一个轨迹点
startx = next_x;//将下一个轨迹点的坐标设置为起始坐标
starty = next_y;
endx = next_x;
endy = next_y;
length = length - tempLength;//计算剩余未绘制的飞线长度,开始下一轮的计算
}
}
let lg = ctx.createLinearGradient(this.startx, this.starty, endx, endy);
lg.addColorStop(this.colorStep[0], `rgba(${color[0]},${color[1]},${color[2]})`);
lg.addColorStop(this.colorStep[1], `rgba(${color[0]},${color[1]},${color[2]})`);
lg.addColorStop(this.colorStep[2], `rgba(${color[0]},${color[1]},${color[2]})`);
ctx.strokeStyle = lg;
ctx.lineWidth = this.width;
ctx.stroke();
}
这一步的主要作用是计算下一帧时,飞线起始点的坐标,绘制下一帧的飞线,将飞线动画串联。由于每一帧的长度间隔为一个speed的大小,下一帧的起始点有可能还在当前直线上运动,也有可能已经拐弯。
next() {
let _this = this;//在绘制下一帧飞线前,线获取当前帧的状态
let endStep = this.step;//获取起始轨迹点的index,默认为轨迹数组的第一个点
let { route, startx, starty } = this;
let length = this.speed;//与上一个方法不同,这个方法中length的值表示每一帧运动的长度,及一个speed的长度
for (let i = endStep + 1, len = route.length; i < len; i++) {
let next_x = route[i].x;//获取下一个轨迹点的坐标
let next_y = route[i].y;
let tempLength = Math.sqrt(Math.pow((next_x - _this.startx), 2) + Math.pow((route[i].y - _this.starty), 2));//与上一个方法相同,通过一个勾股定理计算每一帧的运动是否经过一个拐点
let dx = length * (next_x - _this.startx) / tempLength;
let dy = length * (next_y - _this.starty) / tempLength;
if (tempLength > length) {
//如果下一帧运动完后,起始点不经过拐点,则计算下一帧的起始点并赋值,结束循环
this.startx += dx;
this.starty += dy;
break;
} else {
// 如果经过拐点,则将起始点设置为拐点的坐标,并做下一轮的循环
_this.startx = next_x;
_this.starty = next_y;
_this.step++;
length = length - tempLength;//计算剩余的长度
}
}
if (this.step >= route.length - 1) {
// 如果已经运行到最后一个轨迹点,则表示一次飞线动画完成
return 'finish';
} else {
return 'go';
}
}