1. 程式人生 > >CSS+JS實現流星雨動畫

CSS+JS實現流星雨動畫

引言

       平常會做一些有意思的小案例練手,通常都會發到codepen上,但是codepen不能寫分析。

       所以就在部落格上開個案例分享系列,對demo做個剖析。目的以分享為主,然後也希望各路大神能給出改進的想法,在review中提升技術,發現樂趣~

1、效果圖

完整效果,請移步 codepen-流星雨案例

2、原始碼

  • HTML
<body>
    <div class="container">
        <div></div>
        <div></div>
        <div></div>
        <div></div>
        <div class="cloud cloud-1"></div>
        <div class="cloud cloud-2"></div>
        <div class="cloud cloud-3"></div>
    </div>
</body>
  • CSS
  /* ------------reset------------ */
 
 * {
     margin: 0;
     padding: 0;
 }
 
 html,
 body {
     width: 100%;
     min-width: 1000px;
     height: 100%;
     min-height: 400px;
     overflow: hidden;
 }

 /*  ------------畫布 ------------ */
 .container {
     position: relative;
     height: 100%;
 }
 /* 遮罩層 */
 
 #mask {
     position: absolute;
     width: 100%;
     height: 100%;
     background: rgba(0, 0, 0, .8);
     z-index: 900;
 }
 /* 天空背景 */
 
 #sky {
     width: 100%;
     height: 100%;
     background: linear-gradient(rgba(0, 150, 255, 1), rgba(0, 150, 255, .8), rgba(0, 150, 255, .5));
 }
 /* 月亮 */
 
 #moon {
     position: absolute;
     top: 50px;
     right: 200px;
     width: 120px;
     height: 120px;
     background: rgba(251, 255, 25, 0.938);
     border-radius: 50%;
     box-shadow: 0 0 20px rgba(251, 255, 25, 0.5);
     z-index: 9999;
 }
 /* 閃爍星星 */
 
 .blink {
     position: absolute;
     background: rgb(255, 255, 255);
     border-radius: 50%;
     box-shadow: 0 0 5px rgb(255, 255, 255);
     opacity: 0;
     z-index: 10000;
 }
 /* 流星 */
 
 .star {
     position: absolute;
     opacity: 0;
     z-index: 10000;
 }
 
 .star::after {
     content: "";
     display: block;
     border: solid;
     border-width: 2px 0 2px 80px;
     /*流星隨長度逐漸縮小*/
     border-color: transparent transparent transparent rgba(255, 255, 255, 1);
     border-radius: 2px 0 0 2px;
     transform: rotate(-45deg);
     transform-origin: 0 0 0;
     box-shadow: 0 0 20px rgba(255, 255, 255, .3);
 }
 /* 雲 */
 
 .cloud {
     position: absolute;
     width: 100%;
     height: 100px;
 }
 
 .cloud-1 {
     bottom: -100px;
     z-index: 1000;
     opacity: 1;
     transform: scale(1.5);
     -webkit-transform: scale(1.5);
     -moz-transform: scale(1.5);
     -ms-transform: scale(1.5);
     -o-transform: scale(1.5);
 }
 
 .cloud-2 {
     left: -100px;
     bottom: -50px;
     z-index: 999;
     opacity: .5;
     transform: rotate(7deg);
     -webkit-transform: rotate(7deg);
     -moz-transform: rotate(7deg);
     -ms-transform: rotate(7deg);
     -o-transform: rotate(7deg);
 }
 
 .cloud-3 {
     left: 120px;
     bottom: -50px;
     z-index: 999;
     opacity: .1;
     transform: rotate(-10deg);
     -webkit-transform: rotate(-10deg);
     -moz-transform: rotate(-10deg);
     -ms-transform: rotate(-10deg);
     -o-transform: rotate(-10deg);
 }
 
 .circle {
     position: absolute;
     border-radius: 50%;
     background: #fff;
 }
 
 .circle-1 {
     width: 100px;
     height: 100px;
     top: -50px;
     left: 10px;
 }
 
 .circle-2 {
     width: 150px;
     height: 150px;
     top: -50px;
     left: 30px;
 }
 
 .circle-3 {
     width: 300px;
     height: 300px;
     top: -100px;
     left: 80px;
 }
 
 .circle-4 {
     width: 200px;
     height: 200px;
     top: -60px;
     left: 300px;
 }
 
 .circle-5 {
     width: 80px;
     height: 80px;
     top: -30px;
     left: 450px;
 }
 
 .circle-6 {
     width: 200px;
     height: 200px;
     top: -50px;
     left: 500px;
 }
 
 .circle-7 {
     width: 100px;
     height: 100px;
     top: -10px;
     left: 650px;
 }
 
 .circle-8 {
     width: 50px;
     height: 50px;
     top: 30px;
     left: 730px;
 }
 
 .circle-9 {
     width: 100px;
     height: 100px;
     top: 30px;
     left: 750px;
 }
 
 .circle-10 {
     width: 150px;
     height: 150px;
     top: 10px;
     left: 800px;
 }
 
 .circle-11 {
     width: 150px;
     height: 150px;
     top: -30px;
     left: 850px;
 }
 
 .circle-12 {
     width: 250px;
     height: 250px;
     top: -50px;
     left: 900px;
 }
 
 .circle-13 {
     width: 200px;
     height: 200px;
     top: -40px;
     left: 1000px;
 }
 
 .circle-14 {
     width: 300px;
     height: 300px;
     top: -70px;
     left: 1100px;
 }
 
  • JS
//流星動畫
setInterval(function() {
    const obj = addChild("#sky", "div", 2, "star");

    for (let i = 0; i < obj.children.length; i++) {
        const top = -50 + Math.random() * 200 + "px",
            left = 200 + Math.random() * 1200 + "px",
            scale = 0.3 + Math.random() * 0.5;
        const timer = 1000 + Math.random() * 1000;

        obj.children[i].style.top = top;
        obj.children[i].style.left = left;
        obj.children[i].style.transform = `scale(${scale})`;

        requestAnimation({
            ele: obj.children[i],
            attr: ["top", "left", "opacity"],
            value: [150, -150, .8],
            time: timer,
            flag: false,
            fn: function() {
                requestAnimation({
                    ele: obj.children[i],
                    attr: ["top", "left", "opacity"],
                    value: [150, -150, 0],
                    time: timer,
                    flag: false,
                    fn: () => {
                        obj.parent.removeChild(obj.children[i]);
                    }
                })
            }
        });
    }

}, 1000);

//閃爍星星動畫
setInterval(function() {
    const obj = addChild("#stars", "div", 2, "blink");

    for (let i = 0; i < obj.children.length; i++) {
        const top = -50 + Math.random() * 500 + "px",
            left = 200 + Math.random() * 1200 + "px",
            round = 1 + Math.random() * 2 + "px";
        const timer = 1000 + Math.random() * 4000;

        obj.children[i].style.top = top;
        obj.children[i].style.left = left;
        obj.children[i].style.width = round;
        obj.children[i].style.height = round;

        requestAnimation({
            ele: obj.children[i],
            attr: "opacity",
            value: .5,
            time: timer,
            flag: false,
            fn: function() {
                requestAnimation({
                    ele: obj.children[i],
                    attr: "opacity",
                    value: 0,
                    time: timer,
                    flag: false,
                    fn: function() {
                        obj.parent.removeChild(obj.children[i]);
                    }
                });
            }
        });
    }

}, 1000);

//月亮移動
requestAnimation({
    ele: "#moon",
    attr: "right",
    value: 1200,
    time: 10000000,
});


// 新增雲
const clouds = addChild(".cloud", "div", 14, "circle", true);
for (let i = 0; i < clouds.children.length; i++) {
    for (let j = 0; j < clouds.children[i].length;) {
        clouds.children[i][j].classList.add(`circle-${++j}`);
    }
}
//雲動畫

let flag = 1;
setInterval(
    function() {
        const clouds = document.querySelectorAll(".cloud");
        const left = Math.random() * 5;
        bottom = Math.random() * 5;

        let timer = 0;
        for (let i = 0; i < clouds.length; i++) {
            requestAnimation({
                ele: clouds[i],
                attr: ["left", "bottom"],
                value: flag % 2 ? [-left, -bottom] : [left, bottom],
                time: timer += 500,
                flag: false,
                fn: function() {
                    requestAnimation({
                        ele: clouds[i],
                        attr: ["left", "bottom"],
                        value: flag % 2 ? [left, bottom] : [-left, -bottom],
                        time: timer,
                        flag: false
                    })
                }
            });
        }

        flag++;
    }, 2000)
  • 封裝方法
//———————————————————————————————————————————動畫———————————————————————————————————————————————————
//運動動畫,呼叫Tween.js
//ele: dom | class | id | tag  節點 | 類名 | id名 | 標籤名,只支援選擇一個節點,class類名以及標籤名只能選擇頁面中第一個
//attr: attribute 屬性名
//value: target value 目標值
//time: duration 持續時間
//tween: timing function 函式方程
//flag: Boolean 判斷是按值移動還是按位置移動,預設按位置移動
//fn: callback 回撥函式
//增加返回值: 將內部引數物件返回,可以通過設定返回物件的屬性stop為true打斷動畫
function requestAnimation(obj) {
    //—————————————————————————————————————引數設定—————————————————————————————————————————————
    //預設屬性
    const parameter = {
        ele: null,
        attr: null,
        value: null,
        time: 1000,
        tween: "linear",
        flag: true,
        stop: false,
        fn: ""
    }

    //合併傳入屬性
    Object.assign(parameter, obj); //覆蓋重名屬性

    //—————————————————————————————————————動畫設定—————————————————————————————————————————————
    //建立運動方程初始引數,方便複用
    let start = 0; //用於儲存初始時間戳
    let target = (typeof parameter.ele === "string" ? document.querySelector(parameter.ele) : parameter.ele), //目標節點
        attr = parameter.attr, //目標屬性
        beginAttr = parseFloat(getComputedStyle(target)[attr]), //attr起始值
        value = parameter.value, //運動目標值
        count = value - beginAttr, //實際運動值
        time = parameter.time, //運動持續時間,
        tween = parameter.tween, //運動函式
        flag = parameter.flag,
        callback = parameter.fn, //回撥函式
        curVal = 0; //運動當前值

    //判斷傳入函式是否為陣列,多段運動
    (function() {
        if (attr instanceof Array) {
            beginAttr = [];
            count = [];
            for (let i of attr) {
                const val = parseFloat(getComputedStyle(target)[i]);
                beginAttr.push(val);
                count.push(value - val);
            }
        }
        if (value instanceof Array) {
            for (let i in value) {
                count[i] = value[i] - beginAttr[i];
            }
        }
    })();

    //運動函式
    function animate(timestamp) {
        if (parameter.stop) return; //打斷
        //儲存初始時間戳
        if (!start) start = timestamp;
        //已運動時間
        let t = timestamp - start;
        //判斷多段運動
        if (beginAttr instanceof Array) {
            // const len = beginAttr.length //存陣列長度,複用

            //多段運動第1類——多屬性,同目標,同時間/不同時間
            if (typeof count === "number") { //同目標
                //同時間
                if (typeof time === "number") {
                    if (t > time) t = time; //判斷是否超出目標值

                    //迴圈attr,分別賦值
                    for (let i in beginAttr) {
                        if (flag) curVal = Tween[tween](t, beginAttr[i], count, time); //呼叫Tween,返回當前屬性值,此時計算方法為移動到寫入位置
                        else curVal = Tween[tween](t, beginAttr[i], count + beginAttr[i], time); //呼叫Tween,返回當前屬性值,此時計算方法為移動了寫入距離
                        if (attr[i] === "opacity") target.style[attr[i]] = curVal; //給屬性賦值
                        else target.style[attr[i]] = curVal + "px"; //給屬性賦值

                        if (t < time) requestAnimationFrame(animate); //判斷是否運動完
                        else callback && callback(); //呼叫回撥函式
                    }
                    return;
                }

                //不同時間
                if (time instanceof Array) {
                    //迴圈time,attr,分別賦值
                    for (let i in beginAttr) {
                        //錯誤判斷
                        if (!time[i] && time[i] !== 0) {
                            throw new Error(
                                "The input time's length is not equal to attribute's length");
                        }

                        //判斷是否已經完成動畫,完成則跳過此次迴圈
                        if (parseFloat(getComputedStyle(target)[attr[i]]) === (typeof value === "number" ? value : value[i]))
                            continue;
                        // t = timestamp - start; //每次迴圈初始化t
                        if (t > time[i]) t = time[i]; //判斷是否超出目標值

                        if (flag || attr[i] === "opacity") curVal = Tween[tween](t, beginAttr[i], count, i); //呼叫Tween,返回當前屬性值,此時計算方法為移動到寫入位置
                        else curVal = Tween[tween](t, beginAttr[i], count + beginAttr[i], i); //呼叫Tween,返回當前屬性值,此時計算方法為移動了寫入距離
                        if (attr[i] === "opacity") target.style[attr[i]] = curVal; //給屬性賦值
                        else target.style[attr[i]] = curVal + "px"; //給屬性賦值
                    }

                    if (t < Math.max(...time)) requestAnimationFrame(animate); //判斷函式是否運動完
                    else callback && callback(); //如果已經執行完時間最長的動畫,則呼叫回撥函式
                    return;
                }
            }

            //多段運動第2類——多屬性,不同目標,同時間/不同時間
            if (count instanceof Array) {
                //同時間
                if (typeof time === "number") {

                    if (t > time) t = time; //判斷是否超出目標值

                    for (let i in beginAttr) { //迴圈attr,count,分別賦值
                        //錯誤判斷
                        if (!count[i] && count[i] !== 0) {
                            throw new Error(
                                "The input value's length is not equal to attribute's length");
                        }

                        if (flag || attr[i] === "opacity") curVal = Tween[tween](t, beginAttr[i], count[i], time); //呼叫Tween,返回當前屬性值,此時計算方法為移動到寫入位置
                        else curVal = Tween[tween](t, beginAttr[i], count[i] + beginAttr[i], time); //呼叫Tween,返回當前屬性值,此時計算方法為移動了寫入距離
                        if (attr[i] === "opacity") target.style[attr[i]] = curVal; //給屬性賦值
                        else target.style[attr[i]] = curVal + "px"; //給屬性賦值
                    }

                    if (t < time) requestAnimationFrame(animate); //判斷函式是否運動完
                    else callback && callback(); //如果已經執行完時間最長的動畫,則呼叫回撥函式
                    return;
                }

                //不同時間
                if (time instanceof Array) {
                    for (let i in beginAttr) {
                        //錯誤判斷
                        if (!time[i] && time[i] !== 0) {
                            throw new Error(
                                "The input time's length is not equal to attribute's length");
                        }

                        //判斷是否已經完成動畫,完成則跳過此次迴圈
                        if (parseFloat(getComputedStyle(target)[attr[i]]) === (typeof value === "number" ? value : value[i]))
                            continue;

                        if (t > time[i]) t = time[i]; //判斷是否超出目標值

                        //錯誤判斷
                        if (!count[i] && count[i] !== 0) {
                            throw new Error(
                                "The input value's length is not equal to attribute's length");
                        }

                        if (flag || attr[i] === "opacity") curVal = Tween[tween](t, beginAttr[i], count[i], time[i]); //呼叫Tween,返回當前屬性值,此時計算方法為移動到寫入位置
                        else curVal = Tween[tween](t, beginAttr[i], count[i] + beginAttr[i], time[i]); //呼叫Tween,返回當前屬性值,此時計算方法為移動了寫入距離
                        if (attr[i] === "opacity") target.style[attr[i]] = curVal;
                        else target.style[attr[i]] = curVal + "px";
                    }

                    if (t < Math.max(...time)) requestAnimationFrame(animate);
                    else callback && callback();
                    return;
                }
            }

        }

        //單運動模式
        if (t > time) t = time;
        if (flag || attr === "opacity") curVal = Tween[tween](t, beginAttr, count, time); //呼叫Tween,返回當前屬性值,此時計算方法為移動到寫入位置
        else curVal = Tween[tween](t, beginAttr[i], count + beginAttr, time); //呼叫Tween,返回當前屬性值,此時計算方法為移動了寫入距離
        if (attr === "opacity") target.style[attr] = curVal;
        else target.style[attr] = curVal + "px";

        if (t < time) requestAnimationFrame(animate);
        else callback && callback();

    }

    requestAnimationFrame(animate);
    return parameter; //返回物件,供打斷或其他用途
}
//Tween.js
/*
 * t : time 已過時間
 * b : begin 起始值
 * c : count 總的運動值
 * d : duration 持續時間
 *
 * 曲線方程
 *
 * http://www.cnblogs.com/bluedream2009/archive/2010/06/19/1760909.html
 * */

let Tween = {
    linear: function(t, b, c, d) { //勻速
        return c * t / d + b;
    },
    easeIn: function(t, b, c, d) { //加速曲線
        return c * (t /= d) * t + b;
    },
    easeOut: function(t, b, c, d) { //減速曲線
        return -c * (t /= d) * (t - 2) + b;
    },
    easeBoth: function(t, b, c, d) { //加速減速曲線
        if ((t /= d / 2) < 1) {
            return c / 2 * t * t + b;
        }
        return -c / 2 * ((--t) * (t - 2) - 1) + b;
    },
    easeInStrong: function(t, b, c, d) { //加加速曲線
        return c * (t /= d) * t * t * t + b;
    },
    easeOutStrong: function(t, b, c, d) { //減減速曲線
        return -c * ((t = t / d - 1) * t * t * t - 1) + b;
    },
    easeBothStrong: function(t, b, c, d) { //加加速減減速曲線
        if ((t /= d / 2) < 1) {
            return c / 2 * t * t * t * t + b;
        }
        return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
    },
    elasticIn: function(t, b, c, d, a, p) { //正弦衰減曲線(彈動漸入)
        if (t === 0) {
            return b;
        }
        if ((t /= d) == 1) {
            return b + c;
        }
        if (!p) {
            p = d * 0.3;
        }
        if (!a || a < Math.abs(c)) {
            a = c;
            var s = p / 4;
        } else {
            var s = p / (2 * Math.PI) * Math.asin(c / a);
        }
        return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
    },
    elasticOut: function(t, b, c, d, a, p) { //正弦增強曲線(彈動漸出)
        if (t === 0) {
            return b;
        }
        if ((t /= d) == 1) {
            return b + c;
        }
        if (!p) {
            p = d * 0.3;
        }
        if (!a || a < Math.abs(c)) {
            a = c;
            var s = p / 4;
        } else {
            var s = p / (2 * Math.PI) * Math.asin(c / a);
        }
        return a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
    },
    elasticBoth: function(t, b, c, d, a, p) {
        if (t === 0) {
            return b;
        }
        if ((t /= d / 2) == 2) {
            return b + c;
        }
        if (!p) {
            p = d * (0.3 * 1.5);
        }
        if (!a || a < Math.abs(c)) {
            a = c;
            var s = p / 4;
        } else {
            var s = p / (2 * Math.PI) * Math.asin(c / a);
        }
        if (t < 1) {
            return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) *
                Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
        }
        return a * Math.pow(2, -10 * (t -= 1)) *
            Math.sin((t * d - s) * (2 * Math.PI) / p) * 0.5 + c + b;
    },
    backIn: function(t, b, c, d, s) { //回退加速(回退漸入)
        if (typeof s == 'undefined') {
            s = 1.70158;
        }
        return c * (t /= d) * t * ((s + 1) * t - s) + b;
    },
    backOut: function(t, b, c, d, s) {
        if (typeof s == 'undefined') {
            s = 3.70158; //回縮的距離
        }
        return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
    },
    backBoth: function(t, b, c, d, s) {
        if (typeof 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;
    },
    bounceIn: function(t, b, c, d) { //彈球減振(彈球漸出)
        return c - Tween['bounceOut'](d - t, 0, c, d) + b;
    },
    bounceOut: 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 + 0.75) + b;
        } else if (t < (2.5 / 2.75)) {
            return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
        }
        return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
    },
    bounceBoth: function(t, b, c, d) {
        if (t < d / 2) {
            return Tween['bounceIn'](t * 2, 0, c, d) * 0.5 + b;
        }
        return Tween['bounceOut'](t * 2 - d, 0, c, d) * 0.5 + c * 0.5 + b;
    }
}


//———————————————————————————————————————————DOM操作———————————————————————————————————————————————————
// 新增節點
// ele: 父節點,支援輸入變數,id值,class值,標籤值。除變數外,其餘值必須為字串
// node: 新增的標籤名,值為字串
// n: 節點添加個數
// className: 節點繫結的class名,,值為字串,多個class名用空格隔開
// boolean: 是否選中所有目標父節點。可選引數,不輸入則判定為false,則只匹配選中的第一個節點
function addChild(ele, node, n, className, boolean) {
    //獲取節點
    let parent = null;

    if (typeof ele !== "string") parent = ele;
    else if (ele[0] === "#") parent = document.getElementById(ele.slice(1));
    else if (ele[0] === ".") {
        if (boolean === false) parent = document.getElementsByClassName(ele.slice(1))[0];
        else parent = document.getElementsByClassName(ele.slice(1));
    } else {
        if (boolean === false) parent = docuemnt.getElementsByTagName(ele)[0];
        else parent = document.getElementsByTagNameNS(ele);
    }

    //宣告用於儲存父節點及子節點的物件 
    const obj = {
        "parent": parent,
        "children": []
    };

    //新增節點
    if (boolean) {
        for (let i = 0; i < parent.length; i++) {
            //建立容器碎片
            const fragment = document.createDocumentFragment();
            //儲存子節點,用於返回值
            obj.children[i] = [];

            for (let j = 0; j < n; j++) {
                const target = document.createElement(node);
                target.className = className;
                fragment.appendChild(target);
                //新增子節點到陣列,用於返回值
                obj.children[i][j] = target;
            }

            parent[i].appendChild(fragment)
        }
    } else {
        //建立碎片容器
        const fragment = document.createDocumentFragment();

        for (let i = 0; i < n; i++) {
            const target = document.createElement(node);
            target.className = className;
            fragment.appendChild(target);
            //新增子節點,用於返回值
            obj.children[i] = target;
        }
        //將碎片容器一次性新增到父節點
        parent.appendChild(fragment);
    }

    //返回引數,供動畫函式呼叫
    return obj;
}

3、案例解析

  • HTML

       由於節點很多,並且我想盡量做得逼真有趣一點,就給節點加了隨機位置。所以節點的輸出都是用JS控制的,HTML這邊只寫了幾個父元素盒子,加上相應的id名和class類名,結構相對簡單。

  • CSS

       CSS部分的難點就是流星的樣式和用圈圈畫雲層,然後將雲層堆疊出立體效果。

首先說一下流星的樣式:

#sky .star {
     position: absolute;
     opacity: 0;
     z-index: 10000;
 }
 
 .star::after {
     content: "";
     display: block;
     border: solid;
     border-width: 2px 0 2px 80px;
     /*流星隨長度逐漸縮小*/
     border-color: transparent transparent transparent rgba(255, 255, 255, 1);
     border-radius: 2px 0 0 2px;
     transform: rotate(-45deg);
     transform-origin: 0 0 0;
     box-shadow: 0 0 20px rgba(255, 255, 255, .3);
 }

       先提取了公共樣式,新增定位屬性;

       然後在star後通過after偽類新增流星,用border特性畫:

       1)模型繪製: border-width的順序為四邊top、right、bottom、left,同理border-color的順序也為四邊top、right、bottom、left。這樣將border-width與border-color一一對應後,就能看出2px是流星的寬度,80px是流星的長度,而0px就是流星的尾巴。這樣就形成了一個頭部2px寬,尾部0px,長度80px的流星模型;

       2)稍微逼真: 通過border-radius給流星的頭部增加個圓角,讓它看起來emmmmmmm更逼真?最後通過roteta旋轉一個角度,讓它看起來像是往下掉;

       3)增加閃光: 通過box-shadow 給流星增加一點光暈,讓它看起來有閃光的效果;



       通過以上3步,一個流星就畫好了。

然後是畫雲:

因為雲的程式碼比較長,這裡就不貼出來了。方法無非是通過一個一個的圓,相互疊加覆蓋,完成一個雲朵的形狀;
完成一個雲層之後,copy一個,然後多個雲層通過rotate、opacity、left定位等,做出一個漸隱疊加的立體效果;
  • JS

JS部分以流星舉例說明

setInterval(function() {
    const obj = addChild("#sky", "div", 2, "star"); //插入流星

    for (let i = 0; i < obj.children.length; i++) {
        //隨機位置
        const top = -50 + Math.random() * 200 + "px",
            left = 200 + Math.random() * 1200 + "px",
            scale = 0.3 + Math.random() * 0.5;
        const timer = 1000 + Math.random() * 1000;

        obj.children[i].style.top = top;
        obj.children[i].style.left = left;
        obj.children[i].style.transform = `scale(${scale})`;
        
        //新增動畫
        requestAnimation({
            ele: obj.children[i],
            attr: ["top", "left", "opacity"],
            value: [150, -150, .8],
            time: timer,
            flag: false,
            fn: function() {
                requestAnimation({
                    ele: obj.children[i],
                    attr: ["top", "left", "opacity"],
                    value: [150, -150, 0],
                    time: timer,
                    flag: false,
                    fn: () => {
                        obj.parent.removeChild(obj.children[i]); //動畫結束刪除節點
                    }
                })
            }
        });
    }

}, 1000);

       這裡邊用到了我自己封裝的兩個方法,一個是基於requestAnimationFrame的requestAnimation,以及基於appendChild的addChild

       為了達成星星位置隨機的效果,通過定時器setInterval不停的插入刪除流星:

       首先,每次新增2個流星到頁面,但是定時器的間隔時間小於流星的動畫時間,這樣就能保證頁面中的流星的數量不是一個固定值,但肯定是大於2的。不然一次2個流星略顯冷清;

       然後,通過for迴圈(也可以用for-in,for-of,都行。for-of最簡單)給每個新新增到頁面中的流星一個隨機的位置(top、left)、隨機的大小(scale)、隨機的動畫執行時間(timer);
       最後,在for迴圈中,給每個新新增到頁面中的流星加上動畫,並通過回撥函式在執行完動畫後刪除節點。這裡要注意的是,動畫要分成兩個階段出現消失,主要是opacity控制)。另外我這裡的處理,每個流星都移動相同的距離300px,這個距離我覺得也可以通過隨機數控制,但我犯了個懶,就沒有做。

4、小問題

       目前我發現的問題有2個:

       一是DOM操作本身的問題。頁面不停的新增與刪除節點,造成不停地迴流與重繪,很耗效能;
       二是requestAnimationFrame本身的問題。因為定時器不斷在新增節點,而requestAnimationFrame的特性——當離開當前頁面去瀏覽其他頁面時,動畫會暫停。這就造成了一個問題,節點一直在加,但動畫全停在那沒有執行。那麼下次再回到這個頁面的時候,就boom!!!動畫就炸了,你會看到畫面一卡,很多小蝌蚪集體出動去找媽媽;

5、結語

       這個小案例雖然從難度上來看很簡單,但是它可拓展性很高——比如表個白啊、寄個相思、耍個浪漫啊等等(手動狗頭doge),而且用純CSS也可以實現(我也寫了一版純CSS的,因為不能隨機位置隨機大小,所以看起來比較靜態一點,就不貼了)。所以對於瞭解CSS動畫與JS動畫,是個很不錯的練手小案例。

       感謝看完這篇文章的可愛的人兒,希望你能從中獲得靈感~如果能給你帶來幫助,那我是極高興地~如果你把靈感告訴我,那我就can't happniess anymore了~

       最後,再次感謝,如果有優化寫法,歡迎指導~

原文地址:https://www.cnblogs.com/keepStudying/p/9886697.html