手把手教你系列 - Particle粒子特效(下)

本着瞎折腾的学习态度,在闲暇之余就是要搞点奇奇怪怪的事情。文中如有哪不对的地方,还请大家指出。本文项目github地址:https://github.com/SmallStoneSK/particle-effect


回顾

在上一篇文章中,我们简单地介绍了如何利用canvas来实现Particle粒子特效。通过把动画看作是不断地重复clear(), paint()这两步,我们只要把精力更多地关注在如何绘制每一帧画面即可。因此在上篇文章中,我们着重介绍了如何绘制画布上的运动中的粒子、粒子间的连线、以及粒子和鼠标的交互。那么在接下来的这篇文章中,我们将介绍如何优化、完善、改进现有的代码。其中,主要包括以下内容:

  • 粒子动效的参数可配置化
  • canvas自适应窗口大小
  • requestAnimationFrame代替setInterval
  • 缓存windowSize

粒子动效的参数可配置化

在之前的代码中,我们将粒子的一些属性都直接用硬编码写死了。如果要将这个ParticleEffect.js做成插件给别人用的话,其他人岂不是还要先看懂我们的代码?所以,我们需要把其中一些可配置的参数暴露出来,做到参数可配置化,方便使用的人自己定制想要的效果。

需要注意的是,我们需要有一个自己的默认保底配置。也就是说,如果用户没有传相应的配置时,粒子特效也能正常运行。而当用户配置了相应的参数时,我们就应该以新的配置为准。话不多说,具体实现看下面代码:

var ParticleEffect = {
    // ... 省去其他代码
    config: {
        count: 100,                     // 默认创建粒子数量
        radius: 5,                      // 默认粒子半径
        vxRange: [-1, 1],               // 默认粒子横向移动速度范围
        vyRange: [-1, 1],               // 默认粒子纵向移动速度范围
        scaleRange: [.5, 1],            // 默认粒子缩放比例范围
        lineLenThreshold: 125,          // 默认连线长度阈值
        color: 'rgba(255,255,255,.2)'   // 默认粒子、线条的颜色
    },
    init: function() {
    
        // ... 省去其他代码
    
        // 更新config配置
        var _this = this;
        newConfig && Object.keys(newConfig).forEach(function(key) {
            _this.config[key] = newConfig[key];
        });
    
        // 生成粒子
        var times = this.config.count;
        this.particles = [];
        while(times--) {
            this.particles.push(new Particle({
                x: Utils.rangeRandom(this.config.radius, windowSize.width - this.config.radius),
                y: Utils.rangeRandom(this.config.radius, windowSize.height - this.config.radius),
                vx: Utils.rangeRandom(this.config.vxRange[0], this.config.vxRange[1]),
                vy: Utils.rangeRandom(this.config.vyRange[0], this.config.vyRange[1]),
                color: this.config.color,
                scale: Utils.rangeRandom(this.config.scaleRange[0], this.config.scaleRange[1]),
                radius: this.config.radius
            }));
        }
    },
    draw: function() {
    
        // ... 省去其他代码
        
        var color = this.config.color;
        var lineLenThreshold = this.config.lineLenThreshold;
    
        // 绘制粒子之间的连线
        for(var i = 0; i < this.particles.length; i++) {
            for(var j = i + 1; j < this.particles.length; j++) {
                var distance = Math.sqrt(Math.pow(this.particles[i].x - this.particles[j].x, 2) + Math.pow(this.particles[i].y - this.particles[j].y, 2));
                if(distance < lineLenThreshold) {
                    this.ctx.strokeStyle = color;
                    this.ctx.beginPath();
                    this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
                    this.ctx.lineTo(this.particles[j].x, this.particles[j].y);
                    this.ctx.closePath();
                    this.ctx.stroke();
                }
            }
        }

        // 绘制粒子和鼠标之间的连线
        for(i = 0; i < this.particles.length; i++) {
            distance = Math.sqrt(Math.pow(this.particles[i].x - this.mouseCoordinates.x, 2) + Math.pow(this.particles[i].y - this.mouseCoordinates.y, 2));
            if(distance < lineLenThreshold) {
                this.ctx.strokeStyle = color;
                this.ctx.beginPath();
                this.ctx.moveTo(this.particles[i].x, this.particles[i].y);
                this.ctx.lineTo(this.mouseCoordinates.x, this.mouseCoordinates.y);
                this.ctx.closePath();
                this.ctx.stroke();
            }
        }
    },
    run: function(config) {
        this.init(config);
        setInterval(this.draw.bind(this), 1000 / 60);
    }
};
    

经过改造之后,我们就可以像下面这样往ParticleEffect.run方法中传入想要的配置参数了。

ParticleEffect.run({
    radius: 8,
    color: 'rgba(20,200,150,.2)'
});

canvas自适应窗口大小

说实话,这其实应该算是一个Bug,不信你可以改变窗口大小,然后再看看效果如何。究其原因,还是因为当窗口大小发生变化的时候,我们的canvas宽高还没有发生改变。所以解决办法也很简单,监听window的resize事件,当浏览器窗口大小发生变化的时候,相应地我们也同时更新canvas的宽高就可以了。具体代码如下:

var ParticleEffect = {
    // ... 省去其他代码
    init: function() {
    
        // ... 省去其他代码
    
        // 监听窗口大小改变事件
        window.addEventListener('resize', this.handleWindowResize.bind(this), false);
    },
    handleWindowResize: function() {
        var windowSize = Utils.getWindowSize();
        this.canvas.width = windowSize.width;
        this.canvas.height = windowSize.height;
    }
};

requestAnimationFrame代替setInterval

在之前的代码中,我们在run方法里通过setInterval来不断地重新绘制canvas上的内容。但是实际上浏览器给我们提供了一个更友好的API来绘制,那就是requestAnimationFrame,它可以跟随浏览器自身的重绘时机来绘制我们的内容。这样性能更好,也可以避免setInterval时间间隔不准的问题(具体介绍可以看看鑫神的这篇文章)。

不过,也并不是所有的浏览器都支持这个API,所以我们还得加上相应的兼容处理。具体代码如下:

var ParticleEffect = {
    // ... 省去其他代码
    init: function() {
    
        // ... 省去其他代码
    
        // 兼容requestAnimationFrame
        this.supportRequestAnimationFrame();
    },
    draw: function() {

        // ... 省去其他代码

        // 循环调用draw方法
        window.requestAnimationFrame(this.draw.bind(this));
    },
    supportRequestAnimationFrame: function() {
        if(!window.requestAnimationFrame) {
            window.requestAnimationFrame = (
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.oRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                function (callback) {
                    setInterval(callback, 1000 / 60)
                }
            );
        }
    },
    run: function() {
        this.init(config);
        window.requestAnimationFrame(this.draw.bind(this));
    }
};

缓存windowSize

这里为什么要缓存windowSize呢,或者说怎么缓存法?其实根据上面的代码我们可以看到,Utils对象提供了一个getWindowSize方法,所以凡是要获取窗口大小的地方都会实时地去获取一次真实的大小,而且这也是跟DOM相关的。但是仔细想想:一方面,窗口大小并不总是在一直变化的,用到windowSize的时候没必要每次都实时获取windowSize;另一方面,我们已经监听了window的resize事件,所以每次窗口大小变化的时候,更新一下缓存下来的windowSize就可以了。口说无凭,做个实验就知道了,代码如下:

/**
 * 测试方法
 */
function test(executeFunc, times) {

    var start, end, num = times;

    start = new Date();
    while(times--) {
        executeFunc();
    }
    end = new Date();

    console.log(executeFunc.name + ' executes ' + num + 'times and takes ' + (end.getTime() - start.getTime()) / 1000 + 's.');
}

/**
 * 实时获取窗口大小
 */
function getWindowSizeRealTime() {
    return {
        width: window.innerWidth || document.documentElement.clientWidth,
        height: window.innerHeight || document.documentElement.clientHeight
    };
}

/**
 * 窗口大小从缓存中获取
 */
var cache = {width: 1024, height: 780};
function getWindowSizeFromCache() {
    return cache;
}

// 执行测试
[1000, 10000, 100000, 1000000].forEach(function(times) {
    test(getWindowSizeRealTime, times);
    test(getWindowSizeFromCache, times);
});

// 输出结果
getWindowSizeRealTime executes 1000 times and takes 0.001s.
getWindowSizeFromCache executes 1000 times and takes 0s.
getWindowSizeRealTime executes 10000 times and takes 0.007s.
getWindowSizeFromCache executes 10000 times and takes 0s.
getWindowSizeRealTime executes 100000 times and takes 0.051s.
getWindowSizeFromCache executes 100000 times and takes 0.001s.
getWindowSizeRealTime executes 1000000 times and takes 0.405s.
getWindowSizeFromCache executes 1000000 times and takes 0.005s.

从上面的测试结果中可以看到,当执行次数少的时候,两种方法的速度并没有多大区别。但是当执行次数的数量级上十万、甚至百万的时候,差距就非常明显了。所以说前面的猜测并没有错,性能上的确是有差距,但是这有优化的必要吗?因为只要代码写得好,避免这种短时间内重复获取的情况就行,而且如果缓存窗口大小,势必也会增加维护cache的开销。

不过,我是觉得虽然频繁改变窗口的场景很少,但是每个人写代码的水平不同,可能有人就是把getWindowSize放在了一个循环中,那就gg了。。。所以,最终决定还是把windowSize的缓存给加上去(说是缓存,其实就是一个变量。。。),毕竟代码实现也很简单。具体代码如下:

var ParticleEffect = {
    // ... 省去其他代码
    init: function(newConfig) {
    
        // ... 省去其他代码

        // 初始化的时候,第一次获取窗口大小之前要先更新一下(其实更准确的应该在window.onload中更新)
        Utils.updateWindowSize();
        var windowSize = Utils.getWindowSize();
    
        // 设置canvas宽高
        this.canvas.width = windowSize.width;
        this.canvas.height = windowSize.height;

        // ... 省去其他代码

    },
    handleWindowResize: function() {
        // 窗口大小发生变化的时候,需要更新缓存中的windowSize    
        Utils.updateWindowSize();
        var windowSize = Utils.getWindowSize();
        this.canvas.width = windowSize.width;
        this.canvas.height = windowSize.height;
    }
}


var Utils = {
    _windowSize: {
        width: 0,
        height: 0
    },
    getWindowSize: function() {
        return this._windowSize;
    },
    updateWindowSize: function() {
        this._windowSize.width = this.getWindowWidth();
        this._windowSize.height = this.getWindowHeight();
    }
    // ... 省去其他代码
}

写在最后

Particle粒子特效到这就已经基本上搞定了,其他的话还可以再进一步把这个ParticleEffect.js文件好好封装一下,做成插件给他人使用。源码都已经上传到我的github上,喜欢的可以star一个哦~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容