用canvas写一个粒子动画

本文章适合于有一定canvas画图经验的人阅读,能够熟悉基本的绘图API,废话不多说,下面就开始了。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>

    <style type="text/css">
        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
        .canvasview {
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
    </style>
</head>
<body>
<div class="canvasview"></div>
</body>

<script type="text/javascript" src="granule.js" ></script>
<script>

    var setting = {
        "el": ".canvasview",        // (必选)指定一个父容器
        "bgcolor": "#222222",       // (可选)画布的背景颜色
        "color": "#ffffff",         // (可选)粒子的颜色
        "concentration": 0.2,       // (可选)浓度
        "radius": 5,                // (可选)例子半径
        "opacity": 0.9,             // (可选)粒子透明度
        "duration": 16,             // (可选)运动的时间(秒)大概值不一定精确
        "rangeRadius": 512,         // (可选)粒子运动的范围

    }

    var test = new granule(setting);    // 设置动画的属性
    test.startAnimation();              // 启动,开始运行

</script>
</html>

上面是html代码,有一个canvasView的大容器,等下我们会在这里绘制canvas。
setting里面是一些比较总要的参数,包括粒子浓度,运动范围,背景颜色等。

首先第一步,写好入口文件

var granule = function(setting) {
    this.TWEEN = ["easeInOutQuad", "easeInOutCubic", "easeInOutBack"];
    this.sett = setting
    this._init();
    this.randomCreateAllBall();
}

/**
 * 用于初始化
 * 
 * 创建画布: this.canvas;
 * 画图环境:this.cc
 * 计算宽高:this.cw, this.ch
 * 圆的半径: this.radius , 默认5px
 * 例子的浓度: this.concentration
 * 粒子的运动时间: this.duration
 * 粒子的 活动范围半径:this.rangeRadius
 * 计算粒子数量: this.cg
 */
granule.prototype._init = function() {
    var el = document.querySelector(this.sett.el);
    var elbox = el.getBoundingClientRect();
        this.canvas = document.createElement("canvas");
    this.cc = this.canvas.getContext("2d");
    this.cw = parseInt(this.sett.width) || elbox.width;
    this.ch = parseInt(this.sett.height) || elbox.height;
    this.radius = this.sett.radius || 5;
    this.opacity = this.sett.opacity || 1;
    this.concentration = this.sett.concentration || 1;
    this.duration = this.sett.duration || 8;
    this.rangeRadius = this.sett.rangeRadius || (this.cw + this.ch) / 4;
    this.tweenType = this.sett.tweenType || "";
    this.tweenAni = this.sett.tweenAni || "easeInOutCubic";
        this.cg = Math.floor(this.cw * this.ch / (this.radius * this.radius * 8 * 8) * this.concentration);
    this.color = this.sett.color || "#ffffff";
    this.bgcolor = this.sett.bgcolor || "#222222";
        this.canvas.width = this.cw;
    this.canvas.height = this.ch;
    el.appendChild(this.canvas);
}

1.this.rangeRadius 是粒子运动的幅度大小
2.this.cd 是运动的粒子数量
如何保证粒子不间断的运动? 需要给每个粒子设定一个运动的范围,运动的形式可以用缓动函数,下面会给出来。在还没到达这个运动终点时,需要不停的重绘,当到达时有需要给这个粒子重新设置一个运动的轨迹,大概原理就是这样,先看一下如何画出所有粒子。

granule.prototype.randomCreateAllBall = function() {
    var all = [];
    for(var i = 0; i < this.cg; i++) {
        var ball = {};
        ball.stauts = "0"; // 增加一个特殊状态,这个状态表示该粒子目前时候处于 [自定义动画中](自定义动画:输入指令而显示的动画)。0:表示目前没有自定义动画
        ball.color = this.color;

        ball.r = this.radius;
        ball.x = this.rdmmm(0, this.cw); // 实际坐标x, 对应tween中的b:起点
        ball.y = this.rdmmm(0, this.ch); // 实际坐标y,

        ball.txb = ball.x; // 对应tween中的b:起点
        ball.tyb = ball.y; // 起点, 对应y坐标

        ball.txc = this.rdmmm(-this.rangeRadius, this.rangeRadius); // 对应tween中的c:位移(可以随机一个)
        ball.tyc = this.rdmmm(-this.rangeRadius, this.rangeRadius);

        ball.td = (this.duration + this.rdmmm(-this.duration / 2, this.duration / 2)) * 60; // 对应tween中的d:终止时间,建议设置为60的倍数
        ball.tt = this.rdmmm(0, ball.td); // 对应tween中的t:时间  注意这里的时间不是从0开始 意味着一开始绘制的时候粒子的坐标并不是刚生成时的坐标

        if(this.tweenType) {
            ball.tp = this.tweenType
        } else {
            ball.tp = this.TWEEN[this.rdmmm(0, this.TWEEN.length)];
                     //每一粒子的运动函数都是随机的
        }

        all.push(ball);
    }
    this.balls = all;
}

/**
 * 生成指定范围的随机数
 * @param {int} min: 随机数的下限
 * @param {int} max: 随机数的上限
 */
granule.prototype.rdmmm = function(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}

为了方便理解上面代码,看一下给出的运动函数

/**
this.TWEEN = ["easeInOutQuad", "easeInOutCubic", "easeInOutBack"];
 * 二次缓动, 具体请查看Tween源码
 * @param {int} t:当前时间
 * @param {int} b:初始值
 * @param {int} c:变化量, 位移
 * @param {int} d:持续时间
 */
granule.prototype.easeInOutQuad = function(t, b, c, d) {
    if((t /= d / 2) < 1) return c / 2 * t * t + b;
    return -c / 2 * ((--t) * (t - 2) - 1) + b;
}
// 三次缓动
granule.prototype.easeInOutCubic = function(t, b, c, d) {
    if((t /= d / 2) < 1) return c / 2 * t * t * t + b;
    return c / 2 * ((t -= 2) * t * t + 2) + b;
}
// bakc
granule.prototype.easeInOutBack = function(t, b, c, d, s) {
    if(s == undefined) s = 1.70158;
    if((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
    return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
}
// 弹簧效果
granule.prototype.easeOutBounce = function(t, b, c, d) {
    if((t /= d) < (1 / 2.75)) {
        return c * (7.5625 * t * t) + b;
    } else if(t < (2 / 2.75)) {
        return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
    } else if(t < (2.5 / 2.75)) {
        return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
    } else {
        return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
    }
}

我来介绍一下function(t,b,c,d)里面的参数
t:动画开始时间 b:动画开始位置 c:动画结束位置 d:动画持续时间
因此我们可以通过给t设定一个值,得到他该时间点上的具体位置。

好了,所有的粒子都已经画出来了,运动轨迹也已经设置好,接下来就是每一帧的重绘了。

granule.prototype.updataBallPosition = function(b) {
    // 根据 tween 计算在当前帧的位置
    var newx = this[b.tp](b.tt, b.txb, b.txc, b.td);
    var newy = this[b.tp](b.tt, b.tyb, b.tyc, b.td);

    // 四个方向的碰撞检测
    if(newx < 0) {
        newx = -newx;
    }
    if(newy < 0) {
        newy = -newy;
    }
    if(newx > this.cw) {
        newx = 2 * this.cw - newx;
    }
    if(newy > this.ch) {
        newy = 2 * this.ch - newy;
    }

    b.x = newx;
    b.y = newy;

    // 当运动时间结束之后   普通的粒子重新随机一个位移
    if(b.stauts == 0) {
        if(++b.tt >= b.td) {
            b.txb = b.x;
            b.tyb = b.y;
            b.txc = this.rdmmm(-this.rangeRadius, this.rangeRadius);
            b.tyc = this.rdmmm(-this.rangeRadius, this.rangeRadius);
            b.tt = 0;
        }
    } 
    
    return b;
}

当b.tt >= b.td 也就是该粒子运动的时间到了之后,需要重新给他设置一下。
最后就是要启动该动画

granule.prototype.startAnimation = function() {
    var self = this;
    var raf = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

    start();

    function start() {
        for(var i = 0; i < self.balls.length; i++) {
            var ball = self.balls[i];
            var newb = self.updataBallPosition(ball);
            self.balls[i] = newb;
        }
        self.drawBalls();
        raf(start);
    }
}

/**
 * 绘制所有的小球
 */
granule.prototype.drawBalls = function() {
    this.drawbg();
    this.cc.globalAlpha = this.opacity;
       //基本画圆Api
    for(var i = 0; i < this.balls.length; i++) {
        var ball = this.balls[i];
        this.cc.beginPath();
        this.cc.fillStyle = ball.color || this.color;
        this.cc.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
        this.cc.closePath();
        this.cc.fill();
    }
}

如果对requestAnimationFrame不熟悉的同学自行google。在每一帧调用self.updataBallPosition(ball)计算出该求得位置,self.drawBalls()画出该球。

好了,到这里也还差不多结束了,需要思考的是小球的运动轨迹改变和每一帧的重绘,当碰撞后怎么办。。。

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

推荐阅读更多精彩内容