最近毕设相关的项目制作终于告一段落了,人闲了下来,折腾的心也开始蠢蠢欲动。于是准备着手换掉看腻了的hexo的主题。不过当这篇文章问世时因该会发布在已经实装的新主题里了。

这次也准备往博客里加点coool的特效什么的,考虑了几个方案,最后选择用canvas来做,所以本文就是canvas动画的制作记录。

前言废话,干货在正文

相信诸君已经看到了,这个主题捏他了挺多東风谷早苗的元素,那么特效肯定就从她的符卡里参考啦早苗教你画星星

但是并不能选太张扬、面积过大、太耀眼的,毕竟这是个博客,喧宾夺主就不好了。所以最后选了[一脉单传的弹幕]中的一部分(一颗星),这个符卡弹幕单个看比较朴素,运动起来倒有种特别的美感 而且很难 ,放这里再合适不过了。

原本也有考虑过用svg动画,因为性能消耗更低,不过弹幕类似粒子,canvas更容易适配。最后性能也没有明显的问题,即使在手机上也能正常播放。


正文

这个特效的最后效果可以在博客标题附近看到,当然手机端是不显示的,请使用电脑、平板或电脑模式打开页面。
特效的详细代码可以在这里看到

canvas动画最大的特点就是,它的帧与帧之间是上下文无关的,每一帧都需要自己控制整个画面。这与HTML日常放个标记就好的使用习惯差异较大,不熟悉的话得适应一下。当然,现在也有诸如脏矩形之类的技术,不用重绘全部画面,可以节省了性能,但它们在此意义不大。

canvas的使用

关于canvas的使用教程有很多,这里就简要说明下。canvas标签被渲染后,需要获取一个上下文来控制他的全部状态。

1
var cc = document.getElementById('sanae-canvas');

它就是这个canvas的画笔,可以用它绘制点线面,各种图形,以及图片。

五星路径

既然canvas不维护绘制对象的状态,这就需要我们自己维护了。因为本次特效也涉及倒放鬼畜等情况,所以这里用一个数字控制进度状态,就和css的动画百分比类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
requestAnimationFrame(draw);
function draw() {
...
var points=...//需要绘制的点坐标集合
var progerss+=0.1;
var map = [0, 2, 4, 1, 3, 0];
for (var i = 0; i < 5; i++) {
if (progress > i) {
point = lerp(points[map[i]], points[map[i + 1]], Math.min(progress - i, 1));
cc.lineTo(w + point[0], h - point[1]);
}
}
...
}

上面代码是每一帧中绘制五角星的代码,可以看到,这里的progress随着时间增加,而points中则储存五角星的五个点,map代表画线顺序。可以看到随着progress的增加,我们的五角星也渐渐完整了。

上面的requestAnimationFrame函数是浏览器中自带的动画函数,顾名思义,他会定期调用传入的函数,相比其他的定时函数,它对动画作了优化,会将帧数控制在合理的范围。

旋转与平移

你当然可以自己控制绘制内容来实现canvas中的旋转或平移,但canvas也有一些更加便捷的做法。

1
2
cc.translate(x,y);
cc.rotate(angle);

之前说过canvas的上下文是个画笔,上面两句就是旋转与平移的方法,他们会改编画笔的 坐标系原点 转换的参考系则是之前的上下文状态。

当然画笔的坐标系也能恢复原点,不然操作过多就难以控制。

1
2
cc.save();
cc.restore();

上面两句分别是保存和读取上下文状态,值得注意的是它内部是堆栈结构,可以多层次使用。

粒子效果

终于开始画弹幕了,绘制粒子效果,需要处理的对象瞬间就高了几个数量级,虽然并非不能,但手动一个个画出粒子,他们还在运动,肯定是吃力不讨好的事情。

在游戏行业粒子特效已经非常成熟了,这里我们可以稍稍借鉴下。其实在广义的前端中,这一概念都在被大范围应用,那就是生命周期。

相比前端组件的生命周期,粒子的生命周期更容易理解,生成 -> 运动 -> 消亡。要实现这些功能,独立写一个粒子类是最简单的。

生成和消亡都是一行代码的事,我们的主要工作在于对粒子运动的处理。鉴于粒子运动的代码,与他的运动方式有关,某些时候会相当复杂,这里只给出粒子类的大致结构,详细可以参考我上文给出代码的starObj类实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class starObj{
constuctor(){
//粒子的参数设定,主要涉及运动轨迹相关
}
reset(){
//粒子的初始化设置,便于复用
}
move(){
//粒子的运动处理,会在draw中每帧调用
}
draw(cc){
move();
//在处理完运动之后,传入canvas上下文并绘制这个粒子
}
}

在粒子对象数量膨胀后,生成与消亡产生的gc消耗会很大,从而影响性能。在这里我们可以引入对象池,将消亡的粒子对象保存起来不取绘制,在需要时进行reset初始化,减小gc性能开销。

这里给出简单的对象池实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14

//star factory
var twinklying = [];

function addStar(flag, x, y, angle) {
var star;
if (twinklying.length < 20) {
star = new starObj();
} else {
star = twinklying.shift();
}
star.reset();
twinklying.push(star);
}

twinklying是一个用starObj控制的伪队列,其中"活着"的粒子将被绘制,每当有新的粒子需求,当粒子总数超过上限,会从队列中抽取"死亡"的粒子初始化,重新开始生命周期。

BEFORE EOF

至此这个特效的主要内容已经制作完毕了,详细代码可以在这里查看。

https://github.com/oodavy41/MyToysBox/tree/master/NightWithNovae

什么?你问特效在哪?把鼠标放到标题上试试?我说的是博客的大标题!