简单的粒子动画

demo

canvas动画套路

canvas 本意画布,应该就是画油画的那种,因此一次绘画其实只能画出一副画面。但是动画也只不过是多幅画面的连续而已。如果我们创建出连续打多幅画面,就产生了动画。

但通常我们只创建一个 canvas 来进行整个动画,因此,当后一副画面被绘画的时候,前一副画面是需要被擦除的ctx.clearRect()。所以我们需要一些对象imgObj来记录当前画面中一些元素的位置。

所以整的来讲就是

  1. 擦干净画布 ctx.clearRect()
  2. 计算当前绘画元素imgObj的位置
  3. 将元素imgObj画在画布上
  4. 回到步骤1
1
2
3
4
5
6
let loop = () => {
clearCanvas();
calculation();
drawSomething();
window.requestAnimationFrame(loop);
}

使用 requestAnimationFrame 而非 setTimeout 以获得更好的性能。

创建粒子

我们需要一个坐标来表示粒子被画在画布的哪个位置,同时动态的粒子,还需要一个表示其运动的速度。

1
2
3
4
5
6
let particle = {
centerX: 0,
centerY: 0,
speedX: 1,
speedY: 1
}

这样一个粒子的运动信息就可以被记录下来了。

以上的单个粒子还可以加上例如颜色,半径大小等更多的信息。

初始化一组粒子信息

1
2
3
4
5
6
7
8
9
let particleArr = [];
for (let i = 0; i < PARTICLE_NUM; i++) {
particleArr.push({
centerX: random(cWidth),
centerY: random(cHeight),
speedX: random(SPEED) * randomDir(), // randomDir用来随机一个方向
speedY: random(SPEED) * randomDir()
});
}

绘制粒子

循环调用 canvas API 即可。

粒子更新

如果在绘制粒子前不做更新,那我们每次看到的都是同一幅画面,那整个动画就是静止的。

在这一步,之前创建粒子时保存下来的粒子位置以及速度就用上了。

我们可以简单地通过记录的速度来确定当前粒子应该在的位置。

1
2
3
4
let calcParticle = (particle) => {
particle.centerX += particle.speedX;
particle.centerY += particle.speedY;
}

这样就能使整个画面动起来。

但是如果仅仅这样,在不一会儿之后,我们的画布上就不存在粒子了,因为所有粒子沿着初始化的方向不停运动而“逃”到了画布之外。因此,我们在计算的时候再简单地加上一个碰撞检测 (当粒子半径足够小,运动速度不大的时候) ,当判断粒子位置超出画布的时候将速度反向。

1
2
3
4
5
6
7
8
9
10
let calcParticle = (particle) => {
particle.centerX += particle.speedX;
particle.centerY += particle.speedY;
if (particle.centerX < 0 || particle.centerX > cWidth) {
particle.speedX *= -1;
}
if (particle.centerY < 0 || particle.centerY > cHeight) {
particle.speedY *= -1;
}
}

X轴与Y轴各司其职。

绘制网线

逐个判断粒子间的距离,再通过最大距离计算网线的透明度。然后 ctx.lineTo() 两个粒子的中心即可。

END

粒子过多的时候回明显卡顿,而速度过大的时候会感觉动画似乎并不连贯。

javascript 中的链表

链表作为一种基础的数据结构,相较于数组的优势是不需要预先知道存储数据的大小,但也就无法像数组那样通过下标方便读取。

但是在javascript中本身没有链表,只有数组,但是得益于javascript中数组的一些方法,比如 push pop unshift shift splice等,使得数组可以方便的模拟链表,并且可以用下标直接访问。

但我们也可以通过一些方法来创建出更接近链表的结构。

单向链表

1
2
3
4
function Chain(val) {
this.val = val;
this.next = null;
}

以上的结构可以简单模拟单向链表。

比如把数组 ['January', 'February', 'March', 'April'] 转换成单向链表。

1
2
3
4
5
6
7
8
9
10
11
let array = ['January', 'February', 'March', 'April'];
let head = new Chain(array.shift());
let tail = head;
while(array.length > 0) {
let node = new Chain(array.shift());
tail.next = node;
tail = node;
}

// 现在其中的值只能通过头部逐一向后查找了
console.log(head);

循环链表

如果把尾部的next指向头部, 我们就得到了一个循环链表

1
tail.next = head;

双向链表

如果我们修改下 Chain 的结构, 增加一个向前的“指针“

1
2
3
4
5
function Chain(val) {
this.val = val;
this.prev = null;
this.next = null;
}

并在创建链表的时候多做一点,将当前节点的“前指针”指向前一个节点,于是就能得到一个双向链表。

1
2
3
4
5
6
7
8
9
10
let array = ['January', 'February', 'March', 'April'];
let head = new Chain(array.shift());
let tail = head;
while(array.length > 0) {
let node = new Chain(array.shift());
tail.next = node;
node.prev = tail;
tail = node;
}
console.log(head);

双向循环链表

将双向链表的最后一个节点与第一个节点相连

1
2
tail.next = head;
head.prev = tail;