canvas动画之粒子效果
在《canvas动画系列》前两章分别介绍了基础知识和绘制动画的思路,最近在某个项目活动中,需要使用canvas绘制酷炫的爆炸效果,因此这里整理canvas绘制粒子动画,作为canvas动画系列的第三篇。
参考
- 用JavaScript玩转游戏物理(一)运动学模拟与粒子系统,Milo Yip大佬写的,强烈推荐
- 使用Canvas给DOM元素添加粒子效果
- 打造高大上的Canvas粒子动画
什么是粒子效果
关于粒子效果的定义,可以参考这里的维基百科。 之前看见过许多酷炫的canvas动画,大部分都是通过粒子效果实现的。比如这里canvas粒子生成人物面部轮廓、7款让人惊叹的HTML5粒子动画特效等。
粒子效果可以绘制精致的动画效果。
代码实现
下面让我们看看实现粒子动画需要的两个步骤:获取粒子和根据粒子制作动画。
获取粒子
粒子效果可以简单理解为就是画布上一个个像素点。canvas提供了getImageData接口,用于获取画布某个区域的像素数据。
该接口返回一个imageData对象,其中的data属性为一个Uint8ClampedArray
数组,其值类似于
let data = [0,0,0,1,255,255,255,1]
我们知道,一个像素对主要的特性就是其颜色值,包含RED、GREEN、BLUE三色相和透明度四个参数值。将imageData.data
数据以四个长度为一组进行分组,表示某个像素点的RGBA属性,上面的数据表示图形包含一个rgba(0,0,0,1)
和一个rgba(255,255,255,1)
两个像素点。
换个思路,
- 先使用
drawImage
接口将图片绘制在画布上, - 然后使用
getImageData
,就可以获取该图片的像素数据了
这样就完成了从图形到像素的转变,而像素,就可以看做是静止的粒子了。下面是一个简单的demo,将一个图片模糊处理成像素图片。
let canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");
let img = document.getElementById("testImg");
let app = {
init() {
this.img = img;
this.drawImage();
this.drawParticals();
},
drawImage() {
let { width, height } = this.img;
ctx.drawImage(img, 0, 0, width, height);
},
getImageData() {
let { width, height } = this.img;
let imgData = ctx.getImageData(0, 0, width, height);
return imgData;
},
getParticals(reductionFactor) {
let { data } = this.getImageData();
let praticales = [];
let { width, height } = this.img;
let count = 0;
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
count++;
// 控制粒子生成的数量
if (count % reductionFactor !== 0) {
continue;
}
let index = (y * width + x) * 4; // 获取当前位置像素点的颜色信息
let rgbaColorArr = data.slice(index, index + 4);
let pratical = {
x,
y,
rgbaColorArr,
radius: 4};
praticales.push(pratical);
}
}
return praticales;
},
clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
},
drawParticals() {
let praticales = this.getParticals(50);
this.clear();
praticales.forEach(p => {
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle =
"rgba(" +
p.rgbaColorArr[0] +
"," +
p.rgbaColorArr[1] +
"," +
p.rgbaColorArr[2] +
", 1)";
ctx.fill();
});
}
};
window.onload = function(){
app.init();
}
上面代码展示了一个粒子具备的基础属性,包括
- 初始位置
- 颜色
有了这两个属性,我们就可以在画布上绘制出对应的粒子了。
制作动画
在上一章我们了解到了canvas动画绘制的基本思路,实际上制作粒子动画也是类似的。下面是一个爆炸动画的粒子类
class ExplodingParticle {
constructor(x, y, rbaArray) {
this.startX = x
this.tartY = y
this.rgbArray = rbaArray
// 设置想要的粒子动画的时长
this.animationDuration = 500; // in ms
// 设置粒子的速度
this.speed = {x: -5 + Math.random() * 10, y: -5 + Math.random() * 10};
// 粒子大小
this.radius = 5 + Math.random() * 5;
// 为粒子设定一个最大的生存时间
this.life = 30 + Math.random() * 10;
this.remainingLife = this.life;
}
// 判断粒子是否还存活
isAlive() {
return this.remainingLife > 0 && this.radius > 0
}
// 这个函数稍后将会调用动画相关的逻辑
draw(ctx) {
let p = this;
if (this.isAlive()) {
// 在当前位置绘制一个圆
ctx.beginPath();
ctx.arc(p.startX, p.startY, p.radius, 0, Math.PI * 2);
ctx.fillStyle = "rgba(" + this.rgbArray[0] + ',' + this.rgbArray[1] + ',' + this.rgbArray[2] + ", 1)";
ctx.fill();
// 更新粒子的位置和生命
p.remainingLife--;
p.radius -= 0.25;
p.startX += p.speed.x;
p.startY += p.speed.y;
}
}
}
只需要在每一帧中调用draw
方法,更新粒子的属性,然后清除画布重新绘制即可。 上面的draw方法实现的动画比较简陋,因为只是线性修改了粒子的位置,还可以扩展思路,比如
- 根据粒子存在时间修改其透明度
- 为粒子位移添加物理效果,如重力加速度、碰撞等
遇见的一些问题
下面是在开发过程中遇见的一些问题。
画布污染
由于canvas部分接口存在跨域限制,其中就包括了getImageData
,在调用ctx.getImageData生成粒子时,如果图片资源与页面域名不一致,会报跨域错误,
在chrome中会提示如下错误
Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data
解决办法是使用内联的base64图片,代替网络图片资源。
// img.js
module.exports = {
bigImg: `data:image/png;base64, VBORw0KGgoAAAANS...`
}
// game.js
let imgData = require('./img.js')
let img = new Image()
img.onload = ()=>{}
img.src = imgData.bigImg
通过base64形式,相当于直接加载了本地图片资源,不存在跨域问题,因此也可以顺利解决getImageData
存在的跨域问题。这种解决方式对于绘制静态图片的例子效果较有帮助,但是如果是动态的第三方图片,则最好的解决办法是将图片下载到当前域名下
下面是使用php下载远程图片的代码(所以说PHP是世界上...)
$pic = 'http://avatar.csdn.net/7/5/0/1_molaifeng.jpg';
$arr = getimagesize($pic);
$pic = "data:{$arr['mime']};base64," . base64_encode(file_get_contents($pic));
?>
<img src="<?php echo $pic ?>" />
参考:
getImageData兼容问题
在iOS低版本(8.2)及部分Android机器上,粒子效果未生效的兼容性bug,后面排查发现原因是,ctx.getImageData接口获取到的data像素数据,是一个Uint8ClampedArray
类型的数组,在低版本手机上没有slice方法,导致生成的粒子数目为0,使用Array.prototype.slice.call可以解决
let colorData = ctx.getImageData(this.x, this.y, width, height).data
// 低版本的手机上 Uint8ClampedArray 对象没有实现slice方法
let rgbaColorArr = Array.prototype.slice.call(colorData, index, index + 4)
小结
了解粒子效果的实现原理之后,就可以发挥想象力,绘制各种酷炫的动画。
当然,还有一个非常重要的问题需要解决:canvas动画的性能问题。如果性能不达标,则再酷炫的动画也没有实际意义了。关于canvas的性能优化问题,涉及方面过多,那么就让我们在《canvas动画系列》的下一章继续。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。