CSS&JS/👀Study and Copy

[Hyperplexed] 인터랙티브한 마우스 커서

arancia_ 2023. 6. 13. 11:11

https://www.youtube.com/watch?v=CZIJKkwc8l8 (The Mouse Trailer With Smart Features - Hyperplexed)

데모 : https://ohikmyeong.github.io/hpp-mouse-trailer

 

Document

 

ohikmyeong.github.io

우아하게 마우스 커서를 따라다니는 개체 (단순히 style에서 transform을 지정한게 아닌, animate 사용),
그리고 커서가 상호작용하는 개체에 따라 크기와 안의 내용이 바뀌는 인터랙티브한 마우스 커서....

개인적으론 외국 사이트에서 이렇게 마우스 커서 따라다니는 애들이 재밌게 움직여서 대체 어떻게 구현하는걸까...했는데 animate()를 사용하는거였다... 😯 정말 끝이 없구나 끝이 없어... closest도 북마크 해놓고 머릿속에선 싹 날아가서 그동안 자체 구현으로 가져오곤 했는데 어휴...ㅠ...내장API가 있을줄이야...

여기서 알게 된 유용한 요소

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
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="./fontawsome/css/all.min.css"/>
    <link rel="stylesheet" href="./css/style.css">
    <script src="./js/main.js" type="module"></script>
</head>
<body>
    <a href="https://www.youtube.com/watch?v=CZIJKkwc8l8" target="_blank">The Mouse Trailer With Smart Features</a>
    <section class="wrap">
        <div class="interactable" data-type="link">
            <a href="#">
                <img src="https://images.unsplash.com/photo-1685168639874-fc788a335132?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=708&q=80"/>
            </a>
        </div>
        <div class="interactable" data-type="video">
            <a href="#">
                <img src="https://images.unsplash.com/photo-1686166113835-5ae3a533828c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80"/>
            </a>
        </div>
    </section>
</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
@charset "utf-8";
@import url('https://fonts.googleapis.com/css2?family=Orbit&display=swap');
*{margin:0;padding:0;box-sizing:border-box;}
body{
    background:rgb(20,20,20);
    min-height:100vh;
    font-family: 'Orbit', sans-serif;
}
/* mouse trailer */
#trailer{
    display:flex; flex-flow:row wrap;
    justify-content:center; align-items:center;
    gap:5px;
    position:fixed;
    top:0;left:0;
    z-index:9999;
    background:#fff;
    border-radius:50%;
    pointer-events:none;
    transition:opacity 0.5s ease;
    opacity:0; 
    letter-spacing:2px;
    text-align:center; font-size:14px;
}
 
body:hover #trailer{opacity:1;}
 
#trailer i {color:#000; font-size:1.5rem;}
 
/* interactable */
.wrap{
    display:grid;
    grid-template-columns:repeat(2,1fr);
    gap :20px;
    position:relative;
    width:90%;
    height:50vh;
    margin:0 auto;
}
.interactable{
    position:relative;overflow:hidden;
    width:100%;height:100%;
    opacity:.3;
    transition:opacity 0.5s ease;
}
.interactable:hover{
    opacity:1;
}
.interactable img{width:100%;height:100%;object-fit:cover;object-position:center;}
cs

main.js

1
2
3
4
5
import { MouseTrailer } from "./MouseTrailer.js";
 
const MT = new MouseTrailer();
 
MT.init();
cs

MouseTrailer.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
export class MouseTrailer{
    #sizeDefault = 20;
    constructor(){
        this.$trailer = null;
        this.$i = null;
        this.$text = null;
 
        this.icons = {
            arrow : "fa-solid fa-arrow-right",
            play : "fa-solid fa-play"
        }
    }//constructor
 
    /**
     * 🍒
     * 마우스 트레일러 작동 시작
     */
    init(){
        this.make_trailer();
        this.add_mouse_move();        
    }//init_mouse_move
 
    /**
     * 🍒
     * 마우스 트레일러 개체 만들기
     */
    make_trailer(){
        this.$trailer = document.createElement('DIV');
        this.$trailer.id = "trailer";
        this.$trailer.style.width = `${this.#sizeDefault}px`;
        this.$trailer.style.height = `${this.#sizeDefault}px`;
 
        this.$i = document.createElement('I');
        this.$trailer.appendChild(this.$i);
 
        this.$text = document.createElement('SPAN');
        this.$trailer.appendChild(this.$text);
 
        document.body.appendChild(this.$trailer);
    }
 
    /**
     * 🍒
     * 마우스 움직임 이벤트 추가
     * @url https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
     */
    add_mouse_move(){
        window.addEventListener('mousemove', e =>{
            const $interactable = e.target.closest(".interactable");
            const interacting = $interactable !== null;
            this.switch_i($interactable);
            this.animate_trailer(e, interacting);
        });
    }//add_mouse_move
 
    /**
     * 🍌
     * 커서의 위치와 크기를 조절하는 애니메이션
     * @param {Event} e 
     * @param {Boolean} interacting 
     * @url https://developer.mozilla.org/en-US/docs/Web/API/Element/animate
     */
    animate_trailer(e,interacting){
        const {clientX, clientY} = e;
 
        const sizeHalf = this.$trailer.offsetWidth / (interacting ? 1.5 : 2);
 
        /* old way */
        // this.$trailer.style.transform = `translate(${clientX - sizeHalf}px, ${clientY - sizeHalf}px)`;
        
        // const keyframes = [{
        //     transform : `translate(
        //         ${clientX - sizeHalf}px, ${clientY - sizeHalf}px)
        //         scale(${interacting ? 8 : 1})
        //     `,
        // }];
        const keyframes = [{
            transform : `translate(${clientX - sizeHalf}px, ${clientY - sizeHalf}px)`,
            width : `${interacting ? this.#sizeDefault * 8 : this.#sizeDefault}px`,
            height : `${interacting ? this.#sizeDefault * 8 : this.#sizeDefault}px`,
        }];
        const options = {
            duration : 800,
            fill : "forwards"
        };
        this.$trailer.animate(keyframes,options);
    }//animate_trailer
 
    /**
     * 🍌 커서 안의 아이콘과 텍스트 내용을 바꿈
     * @param {DOM} $interactable e.target.closest(.interactable)
     */
    switch_i($interactable){
        const dataType = $interactable?.dataset?.type;
        let clssName = ""
 
        switch(dataType){
            case "link" : {
                clssName = this.icons.arrow;
                this.$text.textContent = "VISIT";
            }break;
            
            case "video" : {
                clssName = this.icons.play;
                this.$text.textContent = "WATCH";
            }break;
            
            default : {
                this.$text.textContent = "";
            }break;
        }//switch
 
        this.$i.className = clssName;
    }//switch_i
}//class-MouseTrailer
cs