CSS&JS/👀Study and Copy

[CSS+JS]카운트다운 SVG 애니메이션

arancia_ 2022. 12. 30. 10:01

Online Tutorials 영상 카피

  • JS로 카운트다운(D-day) 핵심 : GAP = new Date("yyyy-mm-dd, 00:00:00").getTime() - Date.now()
    • 일 : Math.floor( GAP / (밀리세컨드 * 60초 * 60분 * 24시간) )
    • 시간 : Math.floor( GAP % (밀리세컨드 * 60초 * 60분 * 24시간) / (밀리세컨드 * 60초 * 60분) )
    • 분 : Math.floor( GAP % (밀리세컨드 * 60초 * 60분) / (밀리세컨드 * 60초))
    • 초 : Math.floor( GAP % (밀리세컨드 * 60초) / 밀리세컨드 )
  • svg 스트로크 애니메이션 핵심 : 
    • stroke-dasharray 를 $svg.getTotalLength() 로 구해와서 설정해준다.
    • stroke-dasheoffset 을 시간에 맞춰 바꿔준다.

HTML

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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Countdown Timer in Vanilla Javascript | CSS SVG Circle Countdown Time Animation</title>
    <link rel="stylesheet" href="./style.css"/>
    <script src="./main.js" type="module"></script>
</head>
<body>
    <a href="https://www.youtube.com/watch?v=8y4dFoE-nlU" target="_blank">Countdown Timer in Vanilla Javascript | CSS SVG Circle Countdown Time Animation</a>
 
    <section id="wrap-time">
        <!-- [template] -->
        <template id="temp-circle">
            <article class="circle-wrap" id="circle-wrap-xxx" style="--clr:#000;">
                <svg class="circle">
                    <circle cx="70" cy="70" r="70"></circle>
                    <circle cx="70" cy="70" r="70"></circle>
                </svg>
                <div class="circle-dot"></div>
                <div class="circle-time"><span id="circle-time-xxx">00</span> xxx</div>
            </article><!-- circle -->
        </template>
    </section><!-- wrap-time -->
 
    <h2 id="wrap-newyear">
        <strong id="newyear">0000</strong><br>Happy New Year!
    </h2>
</body>
</html>
cs

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
@charset "utf-8";
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap');
:root{
    --bg:#2f363e;
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{
    font-family: 'Outfit', sans-serif;
}
body{
    display:flex;flex-flow:column nowrap;
    justify-content:center; align-items:center;
    gap:40px;
    min-height:100vh;
    background:var(--bg); color:#fff;
}
 
/*  */
#wrap-time{
    display:flex;flex-flow:row wrap;
    justify-content:center; align-items:flex-start;
    gap:30px;
    position:relative;
    width:90%;
}
 
.circle-wrap{
    --wid:150px;
    /* outline:1px solid red; */
    display:flex; flex-flow:row wrap;
    justify-content:center;align-items:center;
    position:relative;
    width:var(--wid); aspect-ratio:1/1;
    text-align:center;
}
 
/* svg */
.circle{
    /* outline:1px solid red; */
    --stroke-width:8;
    --half:calc(var(--stroke-width) / 2 * 1px);
    position:relative;
    width:var(--wid); aspect-ratio:1/1;
}
.circle circle{
    width:100%; aspect-ratio:1/1;
    fill:transparent;
    stroke-width:var(--stroke-width);
    stroke:#282828;
    transform:translate(5px,5px);
}
/*
https://mong-blog.tistory.com/entry/svg%EA%B0%80-%EA%B7%B8%EB%A0%A4%EC%A7%80%EB%8A%94-%ED%9A%A8%EA%B3%BCwith-stroke-dasharray-stroke-dashoffset
*/
/* 
https://github.com/OhIkmyeong/svg-pie_chart/blob/main/src/pie.js
 */
.circle circle:nth-child(2){
    transform-origin:center center;
    transform:rotate(-90deg) translate(var(--half), var(--half));
    stroke:var(--clr);
}
 
/* time */
.circle-time{
    position:absolute;
    width:100%;
    font-size:16px;
    text-transform:uppercase;
}
.circle-time span{font-size:1.9em;font-weight:bold;}
 
/* dot */
.circle-dot{
    position:absolute;
    width:100%; aspect-ratio:1/1;
    border-radius:50%;
    transition:transform .5s;
}
.circle-dot::before{
    content:''; display:block; position:absolute;
    top:0;left:50%;
    transform:translate(-80%,0%);
    width:10px; aspect-ratio:1/1;
    background:var(--clr);
    border-radius:50%;
    box-shadow:
        0 0 5px var(--bg),
        0 0 10px var(--bg),
        0 0 20px var(--clr),
        0 0 30px var(--clr),
        0 0 40px var(--clr),
        0 0 50px var(--clr),
        0 0 60px var(--clr);
}
 
/* [New Year] */
#wrap-newyear{
    position:relative;
    width:100%;
    font-size:5vmin; text-align:center; font-weight:normal;
}
#wrap-newyear #newyear{font-size:1.5em;}
cs

main.js

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
/** 📌[텍스트 및 시간 변경] */
class CountDown {
    /** 텍스트 및 시간 변경 */
    constructor() {
        this.currDate = new Date();
        this.nextYear = new Date(`${this.currDate.getFullYear() + 1}-01-0100:00:00`);
        this.nextTime = this.nextYear.getTime();
        this.IDS = ["day""hour""min""sec"];
 
        this.SVG = new SvgAnimation(this);
    }//constructor
 
    /** 실행 */
    init() {
        console.log('new Date("yyyy-mm-dd, 00:00:00").getTime()'"-""Date.now()");
        this.add_dom();
        this.SVG.set_svg_total_length();
        this.set_new_year_text();
        this.count_down();
    }//init
 
    /** DOM 삽입 */
    add_dom() {
        const $temp = document.getElementById('temp-circle');
        const $wrap = document.getElementById('wrap-time');
 
        for (let i = 0; i < this.IDS.length; i++) {
            const $clone = document.importNode($temp.contenttrue);
            const $circle = $clone.querySelector('.circle-wrap');
            $circle.id = `circle-wrap-${this.IDS[i]}`;
            $circle.style.setProperty('--clr'this.SVG.COLORS[i]);
            const $time = $clone.querySelector('.circle-time');
            $time.innerHTML = `<span id="circle-time-${this.IDS[i]}">00</span><br>${this.IDS[i]}`
            $wrap.appendChild($clone);
        }
    }//add_dom
 
    /** 하단의 Happy NewYear 문구에 내년 연도를 설정한다 */
    set_new_year_text() {
        const $newyear = document.getElementById('newyear');
        $newyear.textContent = this.nextYear.getFullYear();
    }//set_new_year_text
 
    /** 카운트 다운 계산 */
    count_down() {
        const GAP = this.nextTime - Date.now();
        const [msec, sec, min, hour] = [1000606024];
        const cday = msec * sec * min * hour;
        const chour = msec * sec * min;
        const cmin = msec * sec;
 
        const DD = Math.floor(GAP / cday);
        const HH = Math.floor((GAP % cday) / chour);
        const MM = Math.floor((GAP % chour) / cmin);
        const SEC = Math.floor((GAP % cmin) / msec);
 
        const timeInfo = [DD, HH, MM, SEC];
        this.change_time_text(timeInfo);
        this.SVG.animate_stroke(timeInfo);
        this.SVG.animate_dots(timeInfo);
 
        setTimeout(() => { this.count_down() }, 1000);
    }//count_down
 
    /** 
     * 카운트 다운 시간 텍스트 변경 
     * @param {Array}timeInfo [DD,HH,MM,SEC]
     * */
    change_time_text(timeInfo) {
        this.IDS.forEach((ID, idx) => {
            const $time = document.getElementById(`circle-time-${ID}`);
            $time.textContent = String(timeInfo[idx]).padStart(2"0");
        })
    }//change_time_text
}//class-CountDown
 
/** 📌[svg 애니메이션 관련] */
class SvgAnimation {
    /** svg 애니메이션 관련 
     * @param {Class}CNTD CountDown class 받아옴
    */
    constructor(CNTD) {
        this.CNTD = CNTD;
        this.COLORS = ["#ffffff""#ff2972""#fee800""#4fcc43"];
        this.svgTotalLength = null;
    }//constructor
 
    /** svg circle의 stroke-dasharray를 설정하기 위해 totalLength 값을 가져온다 */
    set_svg_total_length() {
        const $$svg = document.getElementsByTagName('svg');
        this.totalLength = Math.ceil(document.getElementsByTagName('circle')[0].getTotalLength());
        console.log('svg.getTotalLength() : 'this.totalLength);
 
        for (let $svg of $$svg) {
            const $circle = $svg.getElementsByTagName('circle')[1];
            $circle.style.strokeDasharray = this.totalLength;
            $circle.style.strokeDashoffset = this.totalLength;
        }//for
    }//set_svg_total_length
 
    /** 
     * 스트로크 애니메이션 
     * @param {Array}timeInfo [DD,HH,MM,SEC]
    */
    animate_stroke(timeInfo) {
        const [DD, HH, MM, SEC] = timeInfo;
        const [$day, $hour, $min, $sec] = Array
            .from(document.getElementsByTagName('svg'))
            .map($svg => $svg.getElementsByTagName('circle')[1]);
        $day.style.strokeDashoffset = this.totalLength - (this.totalLength * DD) / 365;
        $hour.style.strokeDashoffset = this.totalLength - (this.totalLength * HH) / 24;
        $min.style.strokeDashoffset = this.totalLength - (this.totalLength * MM) / 60;
        $sec.style.strokeDashoffset = this.totalLength - (this.totalLength * SEC) / 60;
    }//animate_stroke
 
    /** 
     * 점 움직이기 
     * @param {Array}timeInfo [DD,HH,MM,SEC]
     * */
    animate_dots(timeInfo){
        const [DD,HH,MM,SEC] = timeInfo;
        const [$day,$hour,$min,$sec] = Array.from(document.getElementsByClassName('circle-dot'))
        $day.style.transform = `rotate(${DD / 365 * 360}deg)`;
        $hour.style.transform = `rotate(${HH / 24 * 360}deg)`;
        $min.style.transform = `rotate(${MM / 60 * 360}deg)`;
        $sec.style.transform = `rotate(${SEC / 60 * 360}deg)`;
    }//animate_dots
}//SvgAnimation
 
/* 📌[실행] */
new CountDown().init();
cs