![44728115178d1c01eb80cacebdfdee56.png](https://img-blog.csdnimg.cn/img_convert/44728115178d1c01eb80cacebdfdee56.png)
记得很久以前,还在jQuery统治前端开发的年代,我曾在网上无意中看到一个Canvas落雪效果的动画算法,非常的简单而且高度逼真,以至于多年过去我再也没有能发现任何其他类似动画能实现与之媲美的效果。
2020元旦刚过,趁着工作不忙的间歇,凭着一些对该算法的记忆,我用TypeScript将其重写,并针对兼容性和性能作了些许微调,封装为npm包,可直接应用于任何前端项目,包括Vue,React,Angular等,亦可直接使用script标签加载至传统网页。
戳这里直接看原码和演示网址
GitHub Repogithub.com以下为思路及实现过程
技术方案
在前端范畴内,动画效果一般通过CSS transition/animation或者由JS控制DOM元素去实现,但是这种普遍的方案并不适用于落雪效果,前者的局限性在于不能精确控制动画元素的行为和随机性,而后者会对页面性能造成极大的影响(上千个DOM元素持续毫秒级更新)。
SVG存在理论上的可行性但和CSS animation同样不能精确地控制动画元素的行为。那剩下的最佳方案就只有Canvas了。
什么样的雪花才更加逼真
![c1d6beab9101a14b39bf74938ec25faa.png](https://img-blog.csdnimg.cn/img_convert/c1d6beab9101a14b39bf74938ec25faa.png)
![a9640bc234e2257746e1728268957c0e.png](https://img-blog.csdnimg.cn/img_convert/a9640bc234e2257746e1728268957c0e.png)
采用第二种方案的雪花不仅看起来会更加逼真,而且因为形状简单,极大地降低了性能开销。经试验在我自己的电脑上,同等数量雪花的前提下,后者性能为前者的约30倍左右。
用最少的代码在Canvas上画出一片雪花
// 创建canvas元素
好了,这是整个效果最基础也最核心的代码,它看起来是下面这样的。
![289922b08c61504352f612f4b6fffd2a.png](https://img-blog.csdnimg.cn/img_convert/289922b08c61504352f612f4b6fffd2a.png)
也许这和你心里想想的落雪效果还差那么一点点,那么让我们来加入一个循环,渲染20片雪花并让它们随机分布。
for (let i = 0; i < 20; i++) {
// 随机确定雪花的x,y坐标
const x = Math.random() * 300;
const y = Math.random() * 200;
// ...
// 随机透明度
ctx.globalAlpha = 0.5 + Math.random() * 0.5;
// 随机大小
const radius = 1 + Math.random() * 2;
// 渲染
ctx.arc(x, y, radius, 0, Math.PI * 2, false);
// ...
}
现在它们看起来如下图所示
![f585828049c3105c37c63f52894714af.png](https://img-blog.csdnimg.cn/img_convert/f585828049c3105c37c63f52894714af.png)
让雪花动起来
现在我们已经实现了最简单的静态雪花效果,下一步则是最关键核心的步骤:让雪花动起来。
也许你已经想到了,想要让雪花动起来,其实就是改变它的当前坐标值x,y,并在极短的时间内不停重复这个步骤,模拟这种移动效果。这种移动看似简单,其实大有发挥空间,你可以赋予雪花一个速度值以及加速度值,每次刷新或者在特定时间间隔内改变这个加速度的大小和方向,使雪花的飘落产生大量随机行为。你甚至可以借助各种开源JS物理引擎的帮助,帮你来算出每一帧x,y坐标需要改变多少。
但是经过我的实际实验,添加大量的随机性和提升雪花的逼真程度并不是完全成正比,而且会极大增加性能方面的开销。因此我这里采用的最终方案为赋予雪花一个随机初始速度值,并在lifetime内保持不变。
实现这个步骤的代码如下:
// ...
// 生成y轴速度值
this.vy = 1 + Math.random() * 3;
// 生成x轴速度值 (雪花整体运动轨迹为从上至下,因此x轴横向速度远小于纵向速度)
this.vx = 0.5 - Math.random();
// 渲染当前这片雪花的下一帧
function draw() {
this.y += this.vy;
this.x += this.vx;
// ...
// 渲染
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
}
好了,以上就是最核心的运动算法(是不是非常简单?)
接下来最重要的步骤,是我们需要决定什么时候应该渲染下一帧?
有些同学可能会说setInterval(),没错这个其实也算一种方案,但对于落雪效果这个量级的动画来讲,几乎没有实用性存在。有过H5游戏编程经验的同学肯定会知道HTML5提供一个已经存在多年的动画函数:
Window.requestAnimationFrame()
文档请戳MDN网站:
Window.requestAnimationFrame()developer.mozilla.org![cb21fa54ce1e80c0116490ba501eaf51.png](https://img-blog.csdnimg.cn/img_convert/cb21fa54ce1e80c0116490ba501eaf51.png)
这个函数的作用很简单,就是让浏览器决定什么时候最适合渲染动画的下一帧。相对于setInterval,它的优势是:
- 根据电脑(浏览器)性能自动决定动画帧数
- 低性能电脑上动画会变慢(但不跳帧),而setInterval()会使动画变卡(跳帧)
- 当前网页窗口未被激活时,动画自动暂停
好了,下面我们要做的很简单,就是写一个递归函数,让渲染函数不停的调用自己。
// 渲染所有雪花的下一帧
function updateFrame() {
// 清空当前画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 调用所有雪花的渲染函数
flakes.forEach(f => {
f.draw();
});
// 通过浏览器动画接口递归请求自己
requestAnimationFrame(() => updateFrame());
}
好了,至此我们已经是现在落雪效果中最核心的所有内容。剩下的更多是体力活以及优化微调。后续需要处理的问题主要有:
- 雪花的初始位置需要均匀分布在网页顶部看不见的空间内,避免开始落雪时同时落入可视范围,产生“雪崩”效果 ┓( ´∀` )┏
- 雪花飘落到屏幕左侧,右侧或者底部看不见的空间后,要回收并重新放置到屏幕顶部,保证动画的持续性。
- 落雪效果需要有开始和停止方法,停止后正在屏幕内的雪花能继续飘落到场景外,而不是生硬的强制消失。
- 落雪效果停止并且所有雪花已经走出场景后,回收整个canvas以避免影响页面性能。
- 落雪场景DOM容器尺寸改变时,算法坐标系需要适应新的尺寸,防止网页内容未加载完即开始动画从而导致雪花作用于错误的坐标系内。
- 封装动画,提高易用性,发布npm包。
我不打算把这篇面向入门canvas动画开发的简单文章写成长篇大论,因此上面这些问题我不打算在这里做进一步的详述。感兴趣或者有疑问的同学可以阅读源代码。欢迎看过原码后通过github或者知乎向我提问,在有精力的前提下我会尽可能的回答。
源代码戳这里
https://github.com/owen26/snowflakesjsgithub.com