前端切图学习-倒计时动画

Animated Countdown

Posted by R1NG on August 30, 2021 Viewed Times

倒计时动画 Animated Countdown

1. 概述

项目本体展示了可循环播放的倒计时动画效果.

本项目中涉及的知识点:

  1. 使用 scale() 结合 animation: hide/show 实现缩放动效
  2. 使用 transform-origin 控制变形动画的变形原点
  3. 使用 @keyframes 构造回弹动画
  4. 使用伪元素实现选中按钮时, 按钮宽度增加露出箭头的指示动画

效果:

20210830222552


2. 结构和切图

网页的基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
    <div class="counter">
        <div class="nums">
            <span class="in">9</span>
        </div>
        <h4>Get Ready</h4>
    </div>

    <div class="final">
        <h1>GO</h1>
        <button id="replay">
            <span>Replay</span>
        </button>
    </div>
</body>

我们分别使用了两个容器容纳倒计时进行中时需要展现的倒计时数字与提示文本, 以及倒计时结束后需要展现的提示文本与 ”重新开始“ 按钮.


3.编写 CSS 样式

首先设定提示文本的格式:

1
2
3
4
5
h4 {
    font-size: 20px;
    margin: 5px;
    text-transform: uppercase;
}

然后调整倒计时数字的样式:

1
2
3
4
5
6
7
8
.nums {
    color: #3498db;
    font-size: 50px;
    position: relative;
    overflow: hidden;
    width: 250px;
    height: 50px;
}

分别设定两个容器的定位方式和位置. 在此处我们均使用 绝对定位.

1
2
3
4
5
6
7
8
9
10
11
.counter {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
}
.counter.hide {
    transform: translate(-50%, -50%) scale(0);
    animation: hide .2s ease-out;
}

以及

1
2
3
4
5
6
7
8
9
10
11
.final {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0);
    text-align: center;
}
.final.show {
    transform: translate(-50%, -50%) scale(1);
    animation: show .2s ease-out;
}

然后设定倒计时数字的初始样式, 动画起始样式和动画结束样式: 结合之前对容器高度的设定不难看出此处的排版只对 $0-9$ 的倒计时范围有效, 一旦倒计时秒数超过 $10$ 就会出现元素溢出的问题. 思考一下, 可以如何解决这个问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.nums span {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(120deg);
    transform-origin: bottom center;
}
.nums span.in {
    transform: translate(-50%, -50%) rotate(0deg);
    animation: goIn .5s ease-in-out;
}
.nums span.out {
    animation: goOut .5s ease-in-out;
}

然后定义重新开始倒计时的按钮样式:

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
#replay {
    background-color: #3498db;
    border-radius: 3px;
    border: none;
    color: aliceblue;
    padding: 5px;
    text-align: center;
    display: inline-block;
    cursor: pointer;
    transition: all .3s;
}

#replay span {
    cursor: pointer;
    display: inline-block;
    position: relative;
    transition: .3s;
}

#replay span:after {
    content: '\00bb';
    position: absolute;
    opacity: 0;
    top: 0;
    right: -20px;
    transition: .5s;
}
#replay:hover span {
    padding-right: 25px;
}
#replay:hover span:after {
    opacity: 1;
    right: 0;
}

注意此处对按钮悬浮时效果的处理方式.

最后开始定义在结束/重新开始倒计时的时候两个容器之间的缩放切换动画以及在进行倒计时的时候数字旋转回弹动画的关键帧.

首先处理缩放切换动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@keyframes hide {
    0% {
        transform: translate(-50% -50%) scale(1);
    }
    100% {
        transform: translate(-50%, -50%) scale(0);
    }
}

@keyframes show {
    0% {
        transform: translate(-50%, -50%) scale(0);
    }
    30% {
        transform: translate(-50%, -50%) scale(1.4);
    }
    100% {
        transform: translate(-50%, -50%) scale(1);
    }
}

对关键帧的定义很简单, 只提及了其缩放幅度. 注意在缩放淡入动画关键帧的处理上我们通过增加一个动画进度 $30%$ 时的关键帧实现了缩放动画的回弹效果.

其次处理回弹动画, 此处为了实现回弹定义的关键帧较为复杂, 但原理和上面的 show 逻辑是一样的.

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
@keyframes goIn {
    0% {
        transform: translate(-50%, -50%) rotate(120deg);
    }

    30% {
        transform: translate(-50%, -50%) rotate(-20deg);
    }

    60% {
        transform: translate(-50%, -50%) rotate(10deg);
    }

    100% {
        transform: translate(-50%, -50%) rotate(0deg);
    }
}

@keyframes goOut {
    0% {
        transform: translate(-50%, -50%) rotate(0deg);
    }

    60% {
        transform: translate(-50%, -50%) rotate(20deg);
    }

    100% {
        transform: translate(-50%, -50%) rotate(-120deg);
    }
}

完整的 CSS 样式表如下:

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
142
143
144
145
146
147
148
149
150
151
* {
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    margin: 0;
    height: 100vh;
    overflow: hidden;
}

h4 {
    font-size: 20px;
    margin: 5px;
    text-transform: uppercase;
}

.counter {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
}
.counter.hide {
    transform: translate(-50%, -50%) scale(0);
    animation: hide .2s ease-out;
}

@keyframes hide {
    0% {
        transform: translate(-50% -50%) scale(1);
    }
    100% {
        transform: translate(-50%, -50%) scale(0);
    }
}

.final {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0);
    text-align: center;
}
.final.show {
    transform: translate(-50%, -50%) scale(1);
    animation: show .2s ease-out;
}

@keyframes show {
    0% {
        transform: translate(-50%, -50%) scale(0);
    }
    30% {
        transform: translate(-50%, -50%) scale(1.4);
    }
    100% {
        transform: translate(-50%, -50%) scale(1);
    }
}

.nums {
    color: #3498db;
    font-size: 50px;
    position: relative;
    overflow: hidden;
    width: 250px;
    height: 50px;
}
.nums span {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) rotate(120deg);
    transform-origin: bottom center;
}
.nums span.in {
    transform: translate(-50%, -50%) rotate(0deg);
    animation: goIn .5s ease-in-out;
}
.nums span.out {
    animation: goOut .5s ease-in-out;
}

@keyframes goIn {
    0% {
        transform: translate(-50%, -50%) rotate(120deg);
    }

    30% {
        transform: translate(-50%, -50%) rotate(-20deg);
    }

    60% {
        transform: translate(-50%, -50%) rotate(10deg);
    }

    100% {
        transform: translate(-50%, -50%) rotate(0deg);
    }
}

@keyframes goOut {
    0% {
        transform: translate(-50%, -50%) rotate(0deg);
    }

    60% {
        transform: translate(-50%, -50%) rotate(20deg);
    }

    100% {
        transform: translate(-50%, -50%) rotate(-120deg);
    }
}

#replay {
    background-color: #3498db;
    border-radius: 3px;
    border: none;
    color: aliceblue;
    padding: 5px;
    text-align: center;
    display: inline-block;
    cursor: pointer;
    transition: all .3s;
}

#replay span {
    cursor: pointer;
    display: inline-block;
    position: relative;
    transition: .3s;
}

#replay span:after {
    content: '\00bb';
    position: absolute;
    opacity: 0;
    top: 0;
    right: -20px;
    transition: .5s;
}
#replay:hover span {
    padding-right: 25px;
}
#replay:hover span:after {
    opacity: 1;
    right: 0;
}


4. JavaScript

最后编写 JavaScript 函数:

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
// construct spans which contain countdown nums
numInit();

// select DOM elements
const nums = document.querySelectorAll('.nums span');
const counter = document.querySelector('.counter');
const finalMessage = document.querySelector('.final');
const replay = document.querySelector('#replay');

// initialize and run the animation from the beginning
runAnimation();

// initialize properties
function numInit() {
    for (i=0; i<9; i++) {
        singNum = document.createElement('span');
        singNum.innerHTML=`${8-i}`;
        document.getElementsByClassName('nums')[0].appendChild(singNum);
    }
}


function resetDOM() {
    counter.classList.remove('hide');
    finalMessage.classList.remove('show');

    nums.forEach((num) => {
        num.classList.value = ''
    })

    nums[0].classList.add('in');
}

function runAnimation() {
    nums.forEach((num, idx) => {
        const nextToLast = nums.length - 1;

        num.addEventListener('animationend', (e) => {
            if (e.animationName === 'goIn' && idx !== nextToLast) {
                num.classList.remove('in');
                num.classList.add('out');
            } else if (e.animationName === 'goOut' && num.nextElementSibling) {
                num.nextElementSibling.classList.add('in');
            } else {
                counter.classList.add('hide');
                finalMessage.classList.add('show');
            }
        })
    })
}

replay.addEventListener('click', () => {
    resetDOM();
    runAnimation();
})

最后, 完整的网页演示可见 此处