canvas绘制流光效果

canvas绘制流光效果

前言

最近可能也不是最近(因为22年底时候说过),问这个流光效果能不能不用动图,用代码写出来:


(原图找不到了,找了个类似的)
当时一看这不就是个位移动画嘛,那不分分钟就搞出来了为啥要问下呢,仔细一打听说,这不一定是直线我让它怎么走就怎么走,比如这样:


我一看心想:坏了这是啥啊,怎么还带拐弯的???
但是咱已经答应了那就必须给它整出来对吧,还是动画,这不就对味了。话不多说咱就开整。

1.需求分析

看上面的 图片我们可以发现要实现这段动画需要的元素有:预备好的路线、一条在路线上移动的线段。


路线:既然是不规则的线那普通的盒子边框啊,啥啥的平常的手段肯定是不好或者实现不了了,只能是另辟蹊径了;那都什么好实现这种不规则的路线呢?经过度娘的回答,原来svg和canvas都比较好实现这种线,因为它们都可以根据path路径来绘制出想要的线段。但是根据做好组件后要给平台服务,用svg的话里面可能嵌套的元素可能会太多,平台实现的话可能会很耗时间;用canvas来画的画就最合适不过了,就一个标签,对于谁都省事😀。


路线上移动的线段:说是线段,但实现起来其实是用n个小球来拼成的线段,至于为什么,在下文会为你解惑。


OK,需求分析结束,准备动手开干~

2.实现过程

2.1绘制运动路线

在上面已经提到了canvas可以根据path参数来绘制自定义的线,那么这时就会有两个问题:
path参数从哪来呢?
用什么办法绘制呢?
其实一开始我也不知道,那咋整,抄呗,没啥不好意思的看看人家腾讯,莆田哪个不是呢。通过毅哥给的EasyV的链接,看到人家写好的功能,打开控制台扒它的元素构成,发现原来是svg+canvas组合成的:


(这个截图是LeGo的哈,但是都是这么点元素)

当时就好奇了,这俩任何一个都能实现功能,为啥要组合到一起呢,完全不明白,别着急,继续看下去你会发现貌似确实不得不这么做。
继续上面的问题:看到path元素,啊~原来是用svg这种路径参数来绘制的。第一个问题解决了;


第二个问题,用什么办法绘制呢,那既然都是path参数了,那直接搜关键词吧,canvas path;经mdn一查,马上就有结果了,



用大白话说就是用Path2D对象可以创建出canvas所认识的路径,在用stroke给它绘制出来。
现在问题都解决了,我们要怎么给它放到一起使用呢?

  1. 我们先获取svg中path元素的d属性中的值,保存到本地。

  2. 然后可以把这个参数放到Path2D对象当作参数来创建一个实例出来

  3. 用strokeAPI将创建出来的实例绘制到画布上

1
2
3
4
5
6
7
8
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const pathElement = document.querySelector('path');
const path = pathElement.getAttribute('d');

const strokePath = new Path2D(path);
ctx.stroke(strokePath);

这时我们的路线就出来啦。当然截图里的是给了先的宽度和颜色,这里就不展开说了。

2.2绘制小球

这里就来解释下为什么是绘制小球不是线段,其实答案已经在上一句了,线段只能是直线,没有弯曲的形态,canvas也只有lineTo这个api去绘制直的线段。这个思路不同了我们就只能换个想法了,能不能用某个形状去拼成一条线呢?感觉这个可以有噢。那我们就来试一下:


绘制小球简单,调用现成api就ok了。

1
2
3
4
5
6
7
ctx.arc(x, y, r, startAngle, endAngle, anticlockwise?)
// x:画布中的横坐标
// y:纵坐标
// r:圆的半径
// startAngle: 圆弧的起始点,x 轴方向开始计算,单位以弧度表示。
// endAngle:圆弧的终点,单位以弧度表示。
ctx.stroke(strokePath);

调用完之后我们在给它填充个颜色,就可以在画布中看到这个圆了;

2.3让小球在路线上动起来

现在我们的路线和小球都已经准备好了,那么接下来最关键的就要给它们组合到一起让小球在上面动起来啦。
那首先第一步,怎么才能让小球放到路线里呢,哪怕是随便一个地方都可以。
如果我们把路线看成是由若干个点组成的话,那么这里每个点都应该会有自己的坐标。
经度娘解惑,我们可以利用svg的getTotalLengthgetPointAtLength方法,这应该就是为什么试svg和canvas组合用的原因,



  • getTotalLength的返回值简单来说就是整个路径的总长度;

  • getPointAtLength就是往里面要传一个0-总长度之间的一个浮点数,返回出这个点的x、y坐标;


    利用这两个方法就可以把小球放到路线上了,具体方法:
1
2
3
4
// 1.获取路线总长度
const pathLength = pathElement.getTotalLength();
// 2. 获取路线起点的坐标
const {x, y} = pathElement.getPointAtLength(0);

x,y坐标也有了,直接调用上面绘制小球的方法,把坐标放上去,小球就出现在路线的起点了。

那么现在最重要的,如何让小球动起来呢?

我们知道让画动起来的根本原因其实就是静态图片的更新,只不过是更新的太快给我们的感觉就是动起来了而已。
canvas动画其实就是利用这一原理进行的。

我们可以一直更新小球的x,y坐标,再不断的擦画布,画画来达到动画的效果。

上面已经说过,用getPointAtLength可以知道每个路线上的点的横纵坐标,那我们可以持续的调用这个方法,只让里面的参数变动,是不是就可以让小球位移了呀;真不错;

那么用什么方法让他一直调用getPointAtLength方法呢,很多同学下意识的会想:这还用问?肯定是定时器啊!没错,定时器确实可以,但是定时器是以固定的时间间隔来执行代码,但是动画一般都是一帧一帧的动,用定时器固然不好算出每一帧的时间,结果就是用setInterval会导致动画卡顿。
那我们只能换一种方式了,又经度娘解惑,我发现一直非常契合目前这种需求的api,它就是


requestAnimationFrame


requestAnimationFrame是一种浏览器提供的API,它允许我们在浏览器的下一次重绘之前执行JavaScript代码。这样可以避免浏览器反复重绘,并提供更流畅的动画效果。


当我们使用requestAnimationFrame时,浏览器会在下一次重绘之前调用我们提供的回调函数。回调函数中通常会更新动画的状态,并再次调用requestAnimationFrame以便在下一次重绘时更新动画。


requestAnimationFrame的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。


以下是各个浏览器的兼容问题:


简单来说就是requestAnimationFrame性能好,运行出来的动画流畅,那必须是它了。


那问题来了 怎么用它呢?
别慌,马上就来答案;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let position = 0;
function move() {
// 首先先要把画布擦干净
ctx.clearRect(0, 0, width, height);
// 获取xy坐标
const {x, y} = pathElement.getPointAtLength(position);


// 绘制小球
ctx.arc(x, y, r, startAngle, endAngle, anticlockwise?)
// 更新位移的长度
position++;

// 在这里调用requestAnimationFrame方法,将move函数传进去,就可以实现循环调用了。
requestAnimationFrame(move);
}

2.4绘制若干个小球一起运动

让若干个小球动起来也很简单,只需要让所有小球在画布外面排列生成好之后,再进入轨道行驶就好了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*设置小球起始位置*/
function setCircles() {
for (let i = 0; i < 50; i++) {
circles.push({
x: -1000,
y: -1000,
position: -(i + 1),
t: 0,
});
}
}

function changePosition() {
for (let i of circles) {
i.position++;
// 获取xy坐标
const { x, y } = pathElement.getPointAtLength(i.position);
i.x = x;
i.y = y;
}
}

setCircles();

function move() {
// 首先先要把画布擦干净
ctx.clearRect(0, 0, 1000, 250);

changePosition();
ctx.beginPath();

ctx.strokeStyle = "#000";

ctx.stroke(strokePath);
ctx.save();

for (let i of circles) {
if (i.position < pathLength) {
ctx.beginPath();
// ctx.rotate(0.01);
// ctx.rect(i.x, i.y, 20, 20);
ctx.arc(i.x, i.y, 2, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
}
}

// 在这里调用requestAnimationFrame方法,将move函数传进去,就可以实现循环调用了。
timer = requestAnimationFrame(move);

if (position > pathLength) {
ctx.rect(x + 1000, y + 250, 20, 20);
// ctx.arc(x + 1000, y + 250, 10, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
cancelAnimationFrame(timer);
}
}
move();
  1. 先调用setCircles函数,让小球按照顺序生成在画布外面;
  2. 在move函数中先调用changePosition函数改变每个小球在轨道上的位置,从而生成坐标;
  3. 再遍历存储小球的数组,重新在画布上画出来;
  4. 最后调用move函数;

2.5控制小球的运动速度

上面虽然我们已经让小球们都动起来了,但是貌似还少了点什么。观察了一会儿可能会发现,我们目前好像还控制不了小球的移动速度,因为现在的move函数是根据屏幕刷新率自动调用的,也就是说这个函数我们不能随意控制调用频率,所以只能想办法换一种方式实现了。
那么怎么去控制它们的移动速度呢?

首先我们要去想这个小球动起来的根本原因是啥,是因为一直在根据它位置的改变-获取坐标-重新画画这三步得来的动画,那不妨想想这三步中,哪一步我们能改造改造,好像也只能是第一步了。

1
2
3
4
5
6
7
8
9
function changePosition() {
for (let i of circles) {
i.position++;
// 获取xy坐标
const { x, y } = pathElement.getPointAtLength(i.position);
i.x = x;
i.y = y;
}
}

从上面函数中,可以试着改一下第3行的i.position试试呢,改成+=5会发生什么?

没改之前:


改了之后:


好像真的可以改速度了啊
那现在匀速的完事了,变速是不是也好说了,
在此之前我们要先把初中物理学的匀变速运动公式临时搬出来用一下
V=V0+at;
看到这个公式,有没有什么想法;
V0=初速度, at是在时间间隔t内速度的变化量。


所以,我们可以在每次画画的时候让t自增,来达到变速的效果,试一下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 设置初速度
let speed = 2;
// 设置加速度
let jia = 5;
// 设置时间间隔
let t = 0;

function changePosition() {
const realSpeed = speed + jia*t;
for (let i of circles) {
i.position+=realSpeed;
// 获取xy坐标
const { x, y } = pathElement.getPointAtLength(i.position);
i.x = x;
i.y = y;
}

t+=0.05;
}

看下效果:


结束语

如果你看到了这里我相信你一定也是一个对动画很感兴趣的同学,上面已经介绍了匀速和匀加速运动的实现思路和简要过程,那么匀减速甚至其他的运动曲线是不是也可以举一反三呢?加油!

作者

安建宸

发布于

2023-05-22

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论