canvas学习案例之简单烟花效果

固定一个起始点,烟花最高点是随机获取的,  散开后的落点是在一个半径为R的圆的范围内的点。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<canvas id="firework" width="800" height="500"></canvas>

<script src="firework.js"></script>
</body>
</html>

firework.js

let cxt;
let start = {
    x: 400,
    y: 400
};
let end = {
    x: 200,
    y: 200,
};
let sparks = [];
let sparkEndLength = 0;
let sparkColor = [0, 100, 160, 220, 280];
let animateLength = 0;//统计requestAnimationFrame动画次数   在中间的时候需要清除一下


function rand(min, max) {
    //~~运算符是取整 按位取整
    return (Math.random() * (max - min)) + min;
}

function initCanvas() {
    cxt = document.getElementById('firework').getContext('2d');
    cxt.fillRect(0, 0, 800, 500);

    /*****
     * 过程
     * 一束烟花从地面发射 确定开始坐标和结束坐标
     * 烟花以一定的加速度向某个方向飞去
     * 飞行一定时间后到达结束坐标并爆炸
     * 爆炸产生一定数量的小火花
     * 小火花向四周散开,并逐渐变暗消失
     */

    startAnimate();


}

function getRandEnd() {

    end = {
        x: rand(200, 600),
        y: 200
    };
}

function startAnimate() {
    getRandEnd();
    initVariate();
    //todo 这里如果直接这样写的话(顺序写动画)会存在问题   不是异步执行的(js是单线程的),
    //todo 换个思路:一直轮询,几个操作同时执行,完全不知道那个先,那个后不就实现了动画同时执行(这里还包括爆炸分散开的动画)

    new Promise(function (resolve, reject) {
        beforeBoom(resolve);
    }).then(function () {
        boomGenerateSpark();
    }).catch(function (e) {
        console.log('error', e);
    });
}

function initVariate() {
    sparks = [];
    sparkEndLength = 0;
    sparkColor = [0, 100, 160, 220, 280];

    animateLength = 0;//统计requestAnimationFrame动画次数   在中间的时候需要清除一下
}

class PathForSmall {
    constructor(cxt) {
        this.width = 2;
        this.cxt = cxt;
        this.cxt.fillStyle = '#fff';
        this.percent = 1;
        this.index = 0;
        this.pathPosLength = 30;
        this.paths = [];

    }

    move(resolve) {
        this.initPath();

        this.animation(resolve);
    }

    animation(resolve) {

        this.drawPath();
        this.percent = (this.percent + 1) % this.pathPosLength;
        if (this.percent != 0) {
            let that = this;
            requestAnimationFrame(function () {
                that.animation(resolve);
            });
        } else {
            resolve();
        }

    }

    initPath() {
        for (let i = 0; i < this.pathPosLength; i++) {
            let x = (end.x - start.x) / this.pathPosLength * i + start.x;
            let y = start.y - (start.y - end.y) / this.pathPosLength * (i);
            this.paths.push({
                x: x,
                y: y,
                opacity: i / this.pathPosLength
            })
        }
    }

    //绘制线条动画
    drawPath() {
        cxt.fillStyle = '#000';
        cxt.fillRect(0, 0, 800, 500);
        cxt.beginPath();
        cxt.strokeStyle = '#fff';
        cxt.lineWidth = this.width;
        cxt.globalAlpha = this.paths[this.percent - 1].opacity;
        cxt.moveTo(this.paths[this.percent].x, this.paths[this.percent].y);
        //这里用这么多次循环是为了动画  percent 只是为了拿取数组   操作都是拿去数组中的东西
        for (let i = 0; i < 1 / this.pathPosLength; i = i + 0.001) {

            //todo 优化:这里用贝塞尔二次曲线的话 我们需要知道最后一个下落的终点坐标   这里测试一下 最后一个点坐标 这里的每次是小距离
            let x = quadraticBezierOne(this.paths[this.percent - 1].x, this.paths[this.percent].x, i);
            let y = quadraticBezierOne(this.paths[this.percent - 1].y, this.paths[this.percent].y, i);
            cxt.lineTo(x, y);
            this.index++;
        }

        cxt.stroke();
    }


}

function beforeBoom(resolve) {
    let path = new PathForSmall(cxt);
    path.move(resolve);
}

function quadraticBezierOne(p0, p1, t) {
    return (1 - t) * p0 + t * p1;
}

//贝塞尔曲线获取点坐标  二次贝赛尔曲线方程
function quadraticBezier(p0, p1, p2, t) {
    let k = 1 - t;
    return k * k * p0 + 2 * k * t * p1 + t * t * p2;
}

//烟花散开需要三次贝塞尔曲线方程
function quadraticBezierThree(p0, p1, p2, p3, t) {
    let k = 1 - t;
    return k * k * k * p0 + 3 * t * k * k * p1 + 3 * t * 2 * k * p2 + 3 * t * p3;
}

class Spark {
    constructor(x, y) {
        this.start = {
            x: x || end.x,
            y: y || end.y
        };
        this.end = {
            x: 0,
            y: 0
        };
        this.lineWidth = 3;
        this.pathPosLength = 30;//平分为多少个段 进行动画  这个参数 还需要优化跳转
        this.sparksPaths = [];
        this.percent = 0;
        this.color = sparkColor[parseInt(Math.random() * 5)];
        this.isEnd = false;
    }

    initSparksPath() {
        let start = this.start;
        let end = this.end;
        for (let i = 0; i < this.pathPosLength; i++) {
            let x = (end.x - start.x) / this.pathPosLength * i + start.x;
            let y = (start.y + (end.y - start.y) / this.pathPosLength * (i));
            this.sparksPaths.push({
                x: x,
                y: y,
                opacity: i / this.pathPosLength + (i * 5) / this.pathPosLength
            })
        }
    }

    clearSparksPath() {
        if (this.sparksPaths.length > 1) {
            this.sparksPaths = [];
        }
    }

    drawPath() {
        cxt.save();
        cxt.beginPath();
        cxt.strokeStyle = this.color;
        cxt.strokeStyle = 'hsla(' + this.color + ',100%,' + rand(50, 100) + '%,1)';
        cxt.lineWidth = this.lineWidth;
        cxt.globalAlpha = this.sparksPaths[this.percent - 1].opacity;
        cxt.moveTo(this.sparksPaths[this.percent].x, this.sparksPaths[this.percent].y);
        //这里用这么多次循环是为了动画  percent 只是为了拿取数组   操作都是拿去数组中的东西 试一下二次曲线
        for (let i = 0; i < 1 / this.pathPosLength; i = i + 0.001) {
            let x = quadraticBezierOne(this.sparksPaths[this.percent - 1].x, this.sparksPaths[this.percent].x, i);
            let y = quadraticBezierOne(this.sparksPaths[this.percent - 1].y, this.sparksPaths[this.percent].y, i);
            cxt.lineTo(x, y);

        }

        cxt.closePath();
        cxt.stroke();
        cxt.restore();
    }
}

function boomGenerateSpark() {
    initSparkAttr();

    // console.log('sparks', sparks);
    sparkAnimation();
}

function initSparkAttr() {
    //
    let length = 50;
    for (let i = 0; i < length; i++) {

        let spark = new Spark();
        // let degree = 360 / length * i;//角度数
        //1.这个是获取圆上的点为end点
        // spark.end = getEndPointForCircle(spark.start,degree);
        //2.随机获取点为end点  在圆内
        spark.end = getRandomEndPoint(spark.start, i % 2);
        // console.log('spark',spark.end);
        // debugger
        sparks.push(spark);
    }
}

function getIsSamePoint(point) {
    let isSame = false;
    for (let i = 0; i < sparks.length; i++) {
        let spark = sparks[i];
        if ((spark.x == point.x) && (spark.y == point.y)) {
            isSame = true;
            break;
        }

    }

    return isSame;
}

function getEndPointForCircle(center, degree) {
    let radius = 150;

    let x = center.x + Math.cos(Math.PI * 2 / 360 * degree) * radius;
    let y = center.y + Math.sin(Math.PI * 2 / 360 * degree) * radius;

    return {
        x: x,
        y: y
    }

}

function getRandomEndPoint(center, type) {
    //保证随机性
    let radius = 150;
    //type true  false
    let x = center.x + (type ? Math.random() * radius : -Math.random() * radius);
    let y = center.y + (type ? -Math.random() * radius : Math.random() * radius);
    switch (parseInt(Math.random() * 3)) {
        case 0:
            x = center.x + (type ? -Math.random() * radius : Math.random() * radius);
            y = center.y + (type ? -Math.random() * radius : Math.random() * radius);
            break;
        case 1:
            x = center.x + (type ? -Math.random() * radius : Math.random() * radius);
            y = center.y + (type ? -Math.random() * radius : -Math.random() * radius);
            break;
        case 2:
            x = center.x + (type ? -Math.random() * radius : -Math.random() * radius);
            y = center.y + (type ? Math.random() * radius : -Math.random() * radius);
            break;
    }
    if (getIsSamePoint({
        x: x,
        y: y
    })) {
        getRandomEndPoint(center, type);
    } else {
        return {
            x: x,
            y: y
        }
    }

}

function sparkAnimation() {

    for (let i = 0; i < sparks.length; i++) {
        let spark = sparks[i];
        spark.percent = (spark.percent + 1) % 18;
        if (spark.percent != 0) {
            if (spark.sparksPaths.length == 0) {
                spark.initSparksPath();
            }
            spark.drawPath();
        }
        // console.log('spark',spark.percent);
        if (spark.percent <= 0) {
            sparks[i].isEnd = true;
        }

    }

    for (let i = 0; i < sparks.length; i++) {
        if (sparks[i].isEnd) {
            sparkEndLength++;
        }
    }

    if (sparkEndLength > sparks.length) {
        clearCanvas();
    } else {
        requestAnimFrame(function () {
            animateLength++;
            //这里给个变量进行中途清除  然动画的视觉效果看起来是向外扩散的  不清除的话会看见一整条线  清除了就只有单独的某几段啦
            if (animateLength % 10) {
                cxt.fillRect(0, 0, 800, 500);
            }
            sparkAnimation();
        });
    }

}

function clearCanvas() {

    for (let i = 0; i < sparks.length; i++) {
        let spark = sparks[i];
        spark.clearSparksPath();
    }
    sparks = [];
    console.log('clear');

    startAnimate();
}

initCanvas();

PS:这里还可以使用贝塞尔二次曲线优化动画,让动画看起来更自然的下落

猜你喜欢

转载自blog.csdn.net/wangshang1320/article/details/89134488