CSS&JS/⚡Thinkers

[JS] .animate() 또는 setTimeout과 Promise로 marquee 효과 만들기

arancia_ 2023. 7. 13. 14:38

보통 웹사이트에서 배너가 끝없이 반복되며 한쪽으로 흐르는 효과를 재현해보자.
방법을 크게 2가지로 써봤는데,
1. js의 신규 내장 API인 $elem.animate(keyframes,options)를 사용하여 쉽고 효과적으로 재현하기
2. Promise와 setTimeout을 사용하여 원시적으로 재현하기

슬프게도 2번이 보기가 더 안 좋다..! 미세하지만 툭 끊기는 느낌이 나기 때문. 화면을 다시 그리고 이벤트 루프가 돌고...화면을 다시 페인팅하고 어쩌고...이런것 때문에 js는 생각대로 마음처럼 움직여주지 않는다.
어찌하였건 똑똑한 개발자들이 animate를 만들어주었으니 더 부드러운 효과가 재현 가능한거다...!

css에서 animation이 종료되었을시 이벤트를 추가하고 싶다면 animationend를 썼듯,
animate()는 finish를 사용한다.
다만 setTimeout이나 setInterval처럼 변수로 한번 받아줘야함.

const ani = $elem.animate(keyframes,options);
ani.addEventListener('finish',callback);

animate 사용

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
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
<style>
    *{margin:0;padding:0;box-sizing:border-box;}
    body{
        display:flex;flex-flow:column nowrap;
        justify-content:center;align-items:center;
        gap:30px;
        width:100%;height:100vh;
        background:#ccc;
    }
    li{list-style-type:none;}
 
    #wrap{
        position:relative;
        overflow:hidden;
        background:#bbb;
        padding:20px;
    }
 
    #marquee{
        display:flex;flex-flow:row nowrap;
        justify-content:flex-start; align-items:center;
        gap:20px;
        position:relative;
        font-size:3rem;
    }
    #marquee li{
        position:relative;
        padding:0.5rem 3rem 0.8rem;
        background:#fff;
        border:1px solid #aaa; border-radius:4px;
    }
 
    #marquee li.cloned{color:#ccc;}
</style>
</head>
<body>
    <h1>marquee test</h1>
    <div id="wrap">
        <ul id="marquee">
            <li>lorem</li>
            <li>ipsum</li>
            <li>dolo</li>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
 
<script>
    const $mq = document.getElementById('marquee');
 
    function marquee_elem($elem){
        const $wrap = $elem.parentElement
        const widWrap = $wrap.getBoundingClientRect().width;
        $wrap.style.width = `${widWrap}px`;
        
        const $$li = $elem.querySelectorAll('LI');
        const $frag = document.createDocumentFragment();
        $$li.forEach($li =>{
            const $newLi = $li.cloneNode(true);
            $newLi.classList.add('cloned'); //복제품 비교용
            $frag.appendChild($newLi);
        });
        $elem.appendChild($frag);
 
        move_elem($elem);
    }//marquee_elem
 
    function move_elem($elem){
        const ani = $elem.animate([{
            transform : 'translateX(calc(-100% - 20px))'
        }],{
            duration : 5000,
            easing : "linear"
        });
 
        ani.addEventListener('finish',()=>{
            move_elem($elem);
        },{once:true});
    }//move_elem
 
    marquee_elem($mq);
</script>
</body>
</html>
cs

setTimeout과 Promise 사용

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
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
<style>
    *{margin:0;padding:0;box-sizing:border-box;}
    body{
        display:flex;flex-flow:column nowrap;
        justify-content:center;align-items:center;
        gap:30px;
        width:100%;height:100vh;
        background:#ccc;
    }
    li{list-style-type:none;}
 
    #wrap{
        position:relative;
        overflow:hidden;
        background:#bbb;
        padding:20px;
    }
 
    #marquee{
        display:flex;flex-flow:row nowrap;
        justify-content:flex-start; align-items:center;
        gap:20px;
        position:relative;
        font-size:3rem;
    }
    #marquee.on{
        transition:transform 5s linear;
    }
    #marquee li{
        position:relative;
        padding:0.5rem 3rem 0.8rem;
        background:#fff;
        border:1px solid #aaa; border-radius:4px;
    }
 
    #marquee li.cloned{color:#ccc;}
</style>
</head>
<body>
    <h1>marquee test</h1>
    <div id="wrap">
        <ul id="marquee">
            <li>lorem</li>
            <li>ipsum</li>
            <li>dolo</li>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
 
<script>
    const $mq = document.getElementById('marquee');
 
    function marquee_elem($elem){
        const $wrap = $elem.parentElement
        const widWrap = $wrap.getBoundingClientRect().width;
        $wrap.style.width = `${widWrap}px`;
        
        const $$li = $elem.querySelectorAll('LI');
        const $frag = document.createDocumentFragment();
        $$li.forEach($li =>{
            const $newLi = $li.cloneNode(true);
            $newLi.classList.add('cloned'); //복제품 비교용
            $frag.appendChild($newLi);
        });
        $elem.appendChild($frag);
 
        move_elem($elem);
    }//marquee_elem
 
    function move_elem($elem){
        console.log('move_elem');
        $elem.classList.add('on');
        $elem.style.transform = `translate(calc(-100- 20px))`;
 
        $elem.addEventListener('transitionend',async ()=>{
            await reset_elem($elem);
            move_elem($elem);
        },{once:true});
    }//move_elem
 
    function reset_elem($elem){
        return new Promise(res =>{
            $elem.classList.remove('on');
            $elem.style.transform = `translate(0)`;
            setTimeout(()=>{
                res();
            },0);
        });
    }//reset_elem
 
    (async function(){
        await marquee_elem($mq);
    })();
</script>
</body>
</html>
cs