# CSS3动画实现3D倒计时

先介绍CSS3动画如何使用?两步即可搞定。

# 定义动画规则

@keyframe animationName{  
    timeStamp{  
        attr1:value1;  
        attr2:value2;  
        ...
        attrn,valuen;  
    }  
}
1
2
3
4
5
6
7
8

说明:

  • animationName表示动画名称;
  • timeStamp表示时间点,可取值from,to或0-100%,from表示0,to表示100%;
  • 每个时间点可以指定多个样式属性attr与对应值value。

# 元素应用动画

elemSelector{  
    animation: animationName animationDuration;   
}
1
2
3

说明:

  • elemSelector表示元素选择器
  • 执行一个动画至少指定动画名称animationName和动画时长animationDuration
  • animation属性是以下属性的简写形式,每个属性的含义说明如下表,
    属性名称含义
    animation-name@keyframe动画的名称
    animation-duration动画的时长,需指定单位如s或ms,默认值为0
    animation-timing-function动画的速度曲线,默认值为ease,其他取值有ease-in、ease-out、ease-in-out、linear、step-start、step-end、贝塞尔曲线函数cubic-bezier、步进函数steps
    animation-delay动画延时多久开始,需指定指定单位如s或ms,默认为0,,取正值表示延时,负值表示超前
    animation-iteration-count动画播放次数,默认为1
    animation-direction动画是否在下一周期逆向播放,默认是normal,其他取值有reverse、alternate、alternate-reverse
    animation-play-state动画的播放状态,是运行还是暂停,默认是running,其他取值有paused
    animation-fill-mode动画执行前、后是否应用目标状态,默认是none,其他取值有forwards、backwards、both

重点来了,一个倒计时效果是如何实现的,先看效果。由于是压缩生成的gif,所以看起来会很快。

倒计时

# 代码说明

  • HTML部分

    <!--用于呈现数字-->
    <div id="number"></div>
    <!--用于重新倒计时-->
    <button class="button">再来一次</button>
        <!--用于最后的声音播放。-->
    <audio ></audio>
    
    1
    2
    3
    4
    5
    6
  • CSS部分

    /*初始化数字div样式*/
    #number {
        position: absolute;
        top: 50%;
        left: 50%;
    
        text-align: center;
    
        -webkit-transform: translate3d(-50%, -50%, 0);
        -moz-transform: translate3d(-50%, -50%, 0);
        transform: translate3d(-50%, -50%, 0);
    }
    
    /*绑定动画*/
    .number-anim {
        -webkit-animation: animation_in 10s linear;
        -moz-animation: animation_in 10s linear;
        /*动画时长是10s,且动画的速度是线性的*/
        animation: animation_in 10s linear;
    }
    
    /*定义动画规则*/
    @keyframes animation_in {
        0 {}
        10% {
            /*由于动画时长是10s,此时是1s时刻,字体大小变为100px,z方向距离主屏幕为300px*/
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        11% {
            /*随后字体大小变为300px,z方向距离主屏幕为0,后面每到整秒数都会重复进行*/
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        20% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        21% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        30% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        31% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        40% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        41% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        50% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        51% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        60% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        61% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        70% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        71% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        80% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        81% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        90% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
        91% {
            font-size: 300px;
            -webkit-transform: translate3d(-50%, -50%, 0);
            -moz-transform: translate3d(-50%, -50%, 0);
            transform: translate3d(-50%, -50%, 0);
        }
        100% {
            font-size: 100px;
            -webkit-transform: translate3d(-50%, -50%, -300px);
            -moz-transform: translate3d(-50%, -50%, -300px);
            transform: translate3d(-50%, -50%, -300px);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
  • JS部分

    window.onload = function() {
    
    //第一步:定义全局变量
    num = 10;//数字内容
    isCounting = true;//是否正在计数
    timer = null;//定时器
    numberDiv = document.querySelector("#number");
    audio = document.querySelector("audio");
    button = document.querySelector(".button");
    
    //第二步:初始化
    init();
    
    //第三步:开始倒计时
    timer = setInterval(count, 1000);
        
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    初始化函数声明如下,

    function init() {
        numberDiv.innerHTML = num;
        numberDiv.style.color =  getRandomColor();
        numberDiv.style.fontSize = "300px";
        numberDiv.className = "number-anim";
        
        button.addEventListener("click", function() {
            if (isCounting) {
                return;
            }
            isCounting = true;
            num = 10;
    
            numberDiv.innerHTML = num;
            numberDiv.style.color = getRandomColor();   
            numberDiv.style.fontSize = "300px";
            
            //先解绑,再使用setTimeout使浏览器重新渲染页面,重新绑定
            //setTimeout只是一种方式,只要能使浏览器重新渲染即可      
            numberDiv.className = null;
            setTimeout(function() {
                numberDiv.className = "number-anim";
    
                timer = setInterval(count, 1000);
            }, 30); 
        }, false);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27

    倒计数函数声明如下,

    function count() {
    
        numberDiv.innerHTML = --num;
        numberDiv.style.color = getRandomColor();
        
        if (num == 0) {
            clearInterval(timer);
    
            audio.src="./audio/readygo.mp3";
            audio.play();
    
            numberDiv.innerHTML = "Ready Go!";
            numberDiv.style.color = getRandomColor();
            numberDiv.style.fontSize = "100px";
    
            numberDiv.style.opacity = 0.8;
            numberDiv.style.filter = "alpha(opacity=80)";
            CompatibleFunc(numberDiv, "Transition", "opacity 1s");
    
            isCounting = false;
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

这里有一个地方需要特别注意,避免以后踩坑。

# 关于动画样式的解绑与绑定。

动画样式animation一执行结束,就不再起作用了,要使其重新生效,需要重新绑定。我试了4种方式。

  • 第一种是直接解绑,然后再绑定。

    numberDiv.className = null;
    numberDiv.className = "number-anim";
    
    1
    2

    结果是不能使动画重新生效。

  • 第二种是使用classlist属性进行解绑定。

    numberDiv.classList.toggle("number-anim");
    numberDiv.classList.toggle("number-anim");
    
    1
    2

    classList返回类名列表对象,调用toggle方法,若类名存在则删除,返回false,若类名不存在则添加,返回true,所以要调用两次,第一次删除类名,第二次添加类名。但是结果依然是不能使动画重新生效。

  • 第三种是开启定时器。

    先解绑,再利用定时器使浏览器重新渲染页面,重新绑定。setTimeout只是一种方式,只要能使浏览器重新渲染即可,最终动画重新生效。

    numberDiv.className = null;
    setTimeout(function() {
        numberDiv.className = "number-anim";
    }, 30);
    
    1
    2
    3
    4

    在mozilla官方文档介绍了另一种重新渲染方式,

    https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Animations/Tips (opens new window)

    应用在这里的话,写法是,

    numberDiv.className = null;
    window.requestAnimationFrame(function(){
        window.requestAnimationFrame(function(){
            numberDiv.className = "number-anim";
        });
    });
    
    1
    2
    3
    4
    5
    6
  • 第四种是借助其他重新渲染途径,如颜色、内容、大小的变化。

    numberDiv.className = null;
    
    numberDiv.innerHTML = num;
    numberDiv.style.color = getRandomColor();   
    numberDiv.style.fontSize = "300px";
    
    numberDiv.className = "number-anim";
    
    1
    2
    3
    4
    5
    6
    7

    结果IE和Microsoft Edge 浏览器是支持的,但是FireFox和Chrome不支持, 但这是不是也说明了Chrome和FireFox已经对浏览器渲染做了优化?

简而言之,要使动画重新生效,需要触发浏览器重新渲染。
好了,附上源码链接。

https://github.com/muzhidong/frontend-demo/tree/master/countdown (opens new window)