最新消息:你必须穷尽一生磨练技能,这就是成功的秘诀,也是让人家敬重的关键。

制作高大上的Canvas粒子动画

表现 练小习 2评论

制作粒子动画效果要解决两个问题:一个是粒子动画轨迹,另外一个是粒子执行动画的时机。
首先来看下我们准备要做的粒子动画效果是怎么样的~

是这样(粒子漂浮):

或者这样(粒子轨迹动画):

甚至是这样的 ಠ_ಠ:

很酷炫!
那如何去实现类似上面的粒子动画甚至根据自己的喜好去做更多其他轨迹的动画呢~请看下面详细的讲解。

技术选择

因为粒子数量很多,而且涉及到图像像素处理,所以这里使用Canvas是不二选择。

注意,以下演示的代码只是关键代码,重点在于解决思路。

一、绘制粒子轮廓图
首先要在canvas画布上绘制一个由粒子组成的轮廓图,记录下每一个粒子的坐标,这样才能有后续的动画。

1. 创建一个canvas元素,并获取canvas画布渲染上下文

<canvas id="myCanvas" width="600" height="400">您的浏览器不支持Canvas。</canvas>
<script type="text/javascript">
(function(){
    //获取canvas元素
    var canvas = document.getElementById('myCanvas');
    var ctx = null; //渲染上下文
    if(canvas.getContext) {
                //获取canvas画布的上下文
        ctx = canvas.getContext('2d');
    }
}())
</script>

canvas是一个双标签元素,通过width和height的值来设置画布的大小。至于ctx(画布渲染上下文),可以理解为画布上的画笔,我们可以通过画笔在画布上随心所欲的绘制图案。如果浏览器不支持canvas会直接显示canvas标签中间的文字。当然canvas标签中间也可以是一张当不支持canvas时需要替换显示的图片。

2. 使用canvas的图像操作API绘制图像

绘制图像的关键API是:

/*!
 * 参数描述
 * image: image或者canvas对象
 * sx,sy 源对象的x,y坐标 可选
 * sWidth,sHeight 源对象的宽高 可选
 * dx,dy 画布上的x,y坐标
 * dWidth,dHeight 在画布上绘制的宽高 可选
 */

ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

引用MDN上的一张图会比较清晰的看出每个参数的作用:

drawImage就是把一个image对象或者canvas上(甚至是video对象上的的每一帧)指定位置和尺寸的图像绘制到当前的画布上。而在我们的需求中,要把整个图像绘制到画布中。

//新建一个image对象
var image = new Image();
image.onload = function() {
    //把加载完的图像绘制到画布坐标为(100,100)的地方
    ctx.drawImage(image,100,100);
};
//设置image的source
image.src = 'canvas/qcloud.png';

对应浏览器看到的效果是

查看demo

3. 获取图像的像素信息,并根据像素信息重新绘制出粒子效果轮廓图

canvas有一个叫getImageData的接口的,通过该接口可以获取到画布上指定位置的全部像素的数据:

/*!
 * 参数描述
 * x,y 画布上的x和y坐标
 * width,height 指定获取图像信息的区域宽高
 */

var imageData = ctx.getImageData(x, y, width, height);

把获取的imageData输出到控制台可以看到imageData包含三个属性:

width、height是读取图像像素信息的区域宽度和高度,data是一个Uint8ClampedArray类型的一维数组,包含了整个图片区域里每个像素点的RGBA的整型数据。这里必须要理解这个数组所保存像素信息的排序规则,请看下图描述的data数组:

每一个值占据data数组索引的一个位置,一个像素有个4个值占据数组的4个索引位置。根据数列规则可以知道,要获取第n个位置的R、G、B像素信息就是:Rn = (n-1)*4 + 1,Gn = (n-1)*4 + 2,Bn = (n-1)*4 + 3 ,so easy~ 当然,实际上图像是一个包括image.height行,image.width列像素的矩形而不是单纯的一行到尾的,这个n值在矩形中要计算下:

由于一个像素是带有4个索引值(rgba)的,所以拿到图像中第i行第j列的R、G、B、A像素信息就是 Rij = [(j-1)*imageData.width + (i-1)]*4 + 1,Gij = [(j-1)*imageData.width + (i-1)]*4 + 2,Bij = [(j-1)*imageData.width + (i-1)]*4 + 3 ~ 此时,每个像素值都可以拿到了!

接下来就要把图像的粒子化轮廓图画出来了。那么,怎么做这个轮廓图呢,我们先读取每个像素的信息(用到上面的计算公式),如果这个像素的色值符合要求,就保存起来,用于重绘在画布上。另外,既然是做成粒子的效果,我们只需要把像素粒子保存一部分,展示在画布上。

具体做法是,设定每一行和每一列要显示的粒子数,分别是cols和rows,一个粒子代表一个单元格,那么每个单元格的的宽高就是imageWidth/cols和imageHeight/rows,然后循环的判断每个单元格的第一个像素是否满足像素值的条件,如果满足了,就把这个单元格的坐标保存到数组里,用作绘制图案的时候用。

关键参考代码:

var particles = [];
//计算并保存坐标
function calculate() {
    var len = image.imageData.length;
    //只保存150行,100列的像素值
    var cols = 100,
        rows = 150;
    //设成150行,100列后每个单元的宽高
    var s_width = parseInt(image.w/cols),  
        s_height = parseInt(image.h/rows);
    var pos = 0; //数组中的位置
    var par_x, par_y;  //粒子的x,y坐标
    var data = image.imageData.data;  //像素值数组
    for(var i = 0; i < cols; i++) {
        for(var j = 0; j < rows; j++) {
            //计算(i,j)在数组中的R的坐标值
            pos = [(j*s_height - 1)*image.w + (i*s_width - 1)]*4 + 1;
            //判断R值是否符合要求
            if(data[pos] > 250) {
                var particle = {
                    //偏移,x,y值都随机一下
                    x: image.x + i*s_width + (Math.random() - 0.5)*20,
                    y: image.y + j*s_height + (Math.random() - 0.5)*20,
                    fillStyle: '#006eff'
                }
                //符合要求的粒子保存到数组里
                particles.push(particle);
            }
        }
    }
}

//绘图案
function draw() {
    //清空画布                 
    canvas.ctx.clearRect(0,0,canvas.w,canvas.h);

    var len = particles.length,
            curr_particle = null;

        //把保存的粒子全部绘制到画布里
    for(var i = 0; i < len; i++) {

        curr_particle = particles[i];

        //设置填充颜色
        canvas.ctx.fillStyle = curr_particle.fillStyle;
        //绘粒子到画布上
        canvas.ctx.fillRect(curr_particle.x,curr_particle.y,1,1);
    }
}

用完整代码做出一个演示例子:

查看demo

二、制作粒子动画
制作粒子动画分两种:

一种是粒子漂浮类,这种比较简单,只需要随机的改变每个粒子的位置值,然后一直执行setInterval或者requestAnimationFrame重绘画布即可,具体的效果因人喜好而去设定,就不具体讲解了,撸主做了个简单的粒子漂浮的例子。

另一种是粒子的轨迹动画,这个相对复杂一些。这里要介绍的是每个粒子按照指定的轨迹在指定的时间内做位移,最终汇聚成指定图案的动画效果,这里可以看下撸主随便做的效果

demo1
demo2
demo3

要做成这类动画效果需要解决两个问题:一个是动画轨迹,另外一个是每个粒子执行动画的时机。

粒子动画轨迹

要动画位移的轨迹,最常见的就是单位时间内改变固定的位移值,从而达到动画效果。但要做到炫酷的效果依赖这种单调固定的位移肯定是不行的。所以位移可以依赖缓动函数去做到单位时间内改变不一样的位移值,从而达到特别的效果。

制作缓动效果有两种方法:

一种是自己设定一下控制点,然后通过贝塞尔曲线公式来计算每个单位时间的坐标值。

引用了wikipedia里面的图:


上面两个图都是在绘制一条特定曲线,可以看出二次曲线需要一个特定控制点P1,三次曲线需要两个特定控制点P1和P2来确定一条曲线,高阶曲线甚至需要更多的控制点来确定曲线轨迹。

求曲线的公式是根据德卡斯特里奥算法计算得来的(撸主线性代数渣,直接上公式)

二次曲线对应的公式:

三次曲线对应的公式:

从公式可以看出,只要确定控制点坐标、起始坐标和终点坐标后,就可以确定了一条曲线,然后就可以根据曲线公式求出每个时刻t对应的位置值B(t)。

当然使用这种方法需要自己去制定控制点坐标,计算也比较复杂,实现起来很繁琐。没事,我们还有别的办法确定曲线。

方法二就是使用已有的缓动函数,不需要自己制定控制点,这里推荐出名的Tween算法的缓动函数,用其中一个缓动函数来介绍下参数值,其他缓动函数所传的参数值是一样的:

/*!
 * 参数描述
 * t 动画执行到当前帧所进过的时间
 * b 起始的值
 * c 总的位移值
 * d 持续时间
 */

function easeInOutExpo(t, b, c, d) {
    t /= d/2;
    if (t < 1) return c/2 * Math.pow( 2, 10 * (t - 1) ) + b;
    t--;
    return c/2 * ( -Math.pow( 2, -10 * t) + 2 ) + b;
};

是不是觉得很熟悉?对没错,jquery用的动画扩展插件easing.js就是Tween算法提供的缓动函数。有了这现成的缓动函数,就可以制定粒子的起始点、终点(终点就是图案本身的坐标位置)以及动画执行持续时间来做我们要的效果。
关键参考代码:

Math.easeInOutExpo = function (t, b, c, d) {
    t /= d/2; if (t < 1) return c/2 * Math.pow( 2, 10 * (t - 1) ) + b; t--; return c/2 * ( -Math.pow( 2, -10 * t) + 2 ) + b;
};

// 保存所有粒子对象
var particles = [];

// 计算保存坐标 参照上面代码
function calculate() {
    //xxx
    //particles.push({
    //  保存每个粒子的数据
    //});
}

// 绘制
function draw() {
    //清空画布                 
    canvas.ctx.clearRect(0,0,canvas.w,canvas.h);

    var len = particles.length;
    var cur_particle = null;
    var
        cur_x,cur_y;
    var
        cur_time = 0,
        duration = 0,
        cur_delay = 0;

    for(var i = 0; i < len; i++) {
        //当前粒子
        cur_particle = particles[i];

        // 如果单位时间超过delay,开始
        if(cur_particle.count++ > cur_particle.delay) {
            // 设置画布的填充色
            canvas.ctx.fillStyle = cur_particle.fillStyle;
            //获取当前的time和持续时间和延时
            cur_time = cur_particle.currTime;
            duration = cur_particle.duration;
            cur_delay = cur_particle.interval;

            // 如果最后一个粒子动画也执行完了则停止动画并return
            if(particles[len - 1].duration < particles[len - 1].currTime) {
                // 停止动画
                cancelAnimationFrame(requestId);
                return;
            } else if(cur_time < duration){
                // 当前粒子正在运动
                // 计算出此刻x的坐标
                cur_x = Math.easeInOutQuad(cur_time, cur_particle.x0, cur_particle.x1 - cur_particle.x0, duration);
                // 计算此刻y的坐标
                cur_y = Math.easeInOutQuad(cur_delay, cur_particle.y0, cur_particle.y1 - cur_particle.y0, duration);   
                // 绘制到画布上
                canvas.ctx.fillRect(cur_x,cur_y,1,1);
                //当前时间++
                cur_particle.currTime++;                       
            } else {
                // 终点绘制在画布
                canvas.ctx.fillRect(cur_particle.x1,cur_particle.y1,1,1);
            }
        }

    }
    requestId = requestAnimationFrame(draw);   
}

根据参考代码做出一个效果:

嗯,动画效果是有了,但总感觉不太对劲。。。唔,仔细观察一下,是图案动画执行太过整体了,没有明显的颗粒动画效果,这就引出粒子动画的另一个关键点,粒子执行动画的时机。

粒子执行动画的时机

要让粒子效果比较明显,那就不能让动画效果执行太过整体了,需要让图案上每个粒子有不同的时间间隔启动,根据一定的规律交错的执行动画。这里的粒子启动间隔有两种,一种是每一行粒子执行时间间隔,要让每一行的粒子启动时间有规律错开;另外一种是每一行粒子之间启动时间随机的错开,这样执行的粒子动画才会有一种层次感和每个粒子有独立的动画的颗粒感。看下加了粒子启动时间间隔之后的效果对比:

比上面不加粒子启动时间间隔的效果好多了。

嗯,介绍差不多就是酱紫了,如果上面介绍的方法还是解决不了问题的话,还有大招。。。我把粒子动画效果和Tween的缓动函数一起封装了一下。直接配置一下就可以用了。2333… 用法就是创建一个带有id的canvas,设定好宽度和高度,引入particle.min.js,然后配置一下参数即可

    /*
     * parameters
     * canvasId: 画布id,必填
     * imgUrl: 纯色图片的路径,可以是jpg或者png,做粒子动画的图案色值应为#000,必填
     * cols/rows:分别代表图案每一行和每一列显示粒子数,此处需要设定的cols和rows要可被图片width和height整除,必填
     * startX/startY: 粒子起始位置x,y  
     * imgX/imgY: 图片左上角坐标,相对canvas左上角的偏移值
     * delay: 延迟执行动画时间,单位ms
     * duration: 持续时间,单位ms
     * fillStyle: 粒子颜色值,可带半透明
     * particleOffset:粒子偏移值
     * ease: 缓动函数 提供linear,Quad,Cubic,Quart,Quint,Sine,Expo,Circ,Elastic,Back 提供easeIn,easeOut,easeInOut
     * interval: 粒子间开始移动间隔
     */

    var particles = new Particles({
        canvasId: 'myCanvas',
        imgUrl: 'css/img/qcloud.jpg',
        cols: 100,
        rows: 150,
        startX: 700,
        startY: 300,
        imgX: 500,
        imgY: 130,
        delay: 200,
        duration: 3000,
        interval: 5,
        fillStyle: 'rgba(26,145,211,1)',
        particleOffset: 2,
        ease: 'easeOutElastic'
    });
    particles.animate();

只有canvasId、imgUrl、cols、rows是必填的,其他参数都是根据需要自己选填。 ( ͡° ͜ʖ ͡°)✧
查看demo

转载请注明:练小习的前端技术笔记 » 制作高大上的Canvas粒子动画

发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

网友最新评论 (2)

  1. 学渣路过,那个算式难懂,要消耗好多脑细胞呢
    罐头7个月前 (11-22)回复
  2. 收藏
    路过11个月前 (08-04)回复