├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── css └── base.css ├── favicon.ico ├── img ├── 1.jpg ├── 10.jpg ├── 11.jpg ├── 12.jpg ├── 13.jpg ├── 14.jpg ├── 15.jpg ├── 16.jpg ├── 17.jpg ├── 18.jpg ├── 19.jpg ├── 2.jpg ├── 20.jpg ├── 21.jpg ├── 22.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg ├── 9.jpg └── bg.jpg ├── index.html └── js ├── card.js ├── card2.js ├── card3.js ├── gsap.min.js ├── imagesloaded.pkgd.min.js ├── index.js ├── splitting.min.js └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .parcel-cache 4 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2022 [Codrops](https://tympanus.net/codrops) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grid Item Hover Effect 2 | 3 | Some hover effects based on the [grid design](https://dribbble.com/shots/21567003-SOTA-Branding) by [Alena Orlova](https://dribbble.com/Al_Eagle). 4 | 5 | ![Grid Hover](https://tympanus.net/codrops/wp-content/uploads/2023/09/griditemhover.gif?x39121) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=73537) 8 | 9 | [Demo](http://tympanus.net/Development/GridItemHoverEffect/) 10 | 11 | ## Installation 12 | 13 | Run this demo on a [local server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/set_up_a_local_testing_server). 14 | 15 | ## Credits 16 | 17 | - Images generated with [Midjourney](https://midjourney.com) 18 | 19 | ## Misc 20 | 21 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/) 22 | 23 | ## License 24 | [MIT](LICENSE) 25 | 26 | Made with :blue_heart: by [Codrops](http://www.codrops.com) 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 16px; 9 | --color-text: #fdf17b; 10 | --color-text-alt: #8bff85; 11 | --color-text-grid: #fff; 12 | --color-bg: #000; 13 | --color-link: #54ad8a; 14 | --color-link-hover: #a054fd; 15 | --cut: 20px; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | color: var(--color-text); 21 | background: var(--color-bg) url(../img/bg.jpg) no-repeat 50% 50% fixed; 22 | background-size: cover; 23 | font-family: "iki-mono-variable", sans-serif; 24 | font-variation-settings: "wdth" 80, "wght" 300; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | /* Page Loader */ 30 | .js .loading::before, 31 | .js .loading::after { 32 | content: ''; 33 | position: fixed; 34 | z-index: 1000; 35 | } 36 | 37 | .js .loading::before { 38 | top: 0; 39 | left: 0; 40 | width: 100%; 41 | height: 100%; 42 | background: var(--color-bg); 43 | } 44 | 45 | .js .loading::after { 46 | top: 50%; 47 | left: 50%; 48 | width: 60px; 49 | height: 60px; 50 | margin: -30px 0 0 -30px; 51 | border-radius: 50%; 52 | opacity: 0.4; 53 | background: var(--color-link); 54 | animation: loaderAnim 0.7s linear infinite alternate forwards; 55 | 56 | } 57 | 58 | @keyframes loaderAnim { 59 | to { 60 | opacity: 1; 61 | transform: scale3d(0.5,0.5,1); 62 | } 63 | } 64 | 65 | a { 66 | text-decoration: underline; 67 | color: var(--color-link); 68 | outline: none; 69 | cursor: pointer; 70 | } 71 | 72 | a:hover { 73 | color: var(--color-link-hover); 74 | outline: none; 75 | text-decoration: none; 76 | } 77 | 78 | /* Better focus styles from https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible */ 79 | a:not(.card__image):focus { 80 | /* Provide a fallback style for browsers 81 | that don't support :focus-visible */ 82 | outline: none; 83 | background: lightgrey; 84 | } 85 | 86 | a:not(.card__image):focus:not(:focus-visible) { 87 | /* Remove the focus indicator on mouse-focus for browsers 88 | that do support :focus-visible */ 89 | background: transparent; 90 | } 91 | 92 | a:not(.card__image):focus-visible { 93 | /* Draw a very noticeable focus style for 94 | keyboard-focus on browsers that do support 95 | :focus-visible */ 96 | outline: 2px solid red; 97 | background: transparent; 98 | } 99 | 100 | .unbutton { 101 | background: none; 102 | border: 0; 103 | padding: 0; 104 | margin: 0; 105 | font: inherit; 106 | cursor: pointer; 107 | } 108 | 109 | .unbutton:focus { 110 | outline: none; 111 | } 112 | 113 | .frame { 114 | z-index: 100; 115 | position: relative; 116 | padding: 2vw; 117 | display: grid; 118 | grid-template-columns: auto 1fr; 119 | grid-template-areas: 'title title' 'prev back' 'credits credits' 'sponsor sponsor' ; 120 | grid-gap: 1rem; 121 | pointer-events: none; 122 | } 123 | 124 | body #cdawrap { 125 | justify-self: start; 126 | } 127 | 128 | .frame a { 129 | pointer-events: auto; 130 | } 131 | 132 | .frame__title { 133 | grid-area: title; 134 | font-size: inherit; 135 | margin: 0; 136 | font-weight: inherit; 137 | } 138 | 139 | .frame__credits { 140 | grid-area: credits; 141 | } 142 | 143 | .frame__back { 144 | grid-area: back; 145 | justify-self: start; 146 | } 147 | 148 | .frame__prev { 149 | grid-area: prev; 150 | justify-self: start; 151 | } 152 | 153 | .frame__demos { 154 | grid-area: demos; 155 | display: flex; 156 | align-items: center; 157 | gap: 0.5rem; 158 | align-self: start; 159 | flex-wrap: wrap; 160 | } 161 | 162 | .frame__demos-item:not(:first-child) { 163 | width: 2rem; 164 | display: block; 165 | flex: none; 166 | border-radius: 10px; 167 | aspect-ratio: 1; 168 | border: 1px solid var(--color-link-hover); 169 | display: grid; 170 | place-items: center; 171 | } 172 | 173 | span.frame__demos-item:not(:first-child) { 174 | border-color: #fff; 175 | } 176 | 177 | .grid { 178 | display: grid; 179 | position: relative; 180 | width: min-content; 181 | margin: 0 0 30vh auto; 182 | padding: 13vw 10vh 3vh 3vh; 183 | grid-template-columns: 300px; 184 | grid-auto-rows: 300px; 185 | gap: 1rem; 186 | z-index: 200; 187 | color: var(--color-text-grid); 188 | } 189 | 190 | .grid__title { 191 | text-transform: uppercase; 192 | font-size: 6.75vw; 193 | line-height: 0.8; 194 | opacity: 0.4; 195 | font-family: "tt-autonomous", -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; 196 | font-weight: 200; 197 | line-height: 0.8; 198 | background-color: #fff; 199 | background-image: linear-gradient(45deg, #fdf17b, #54ad8a, #a054fd); 200 | background-size: 100%; 201 | background-repeat: repeat; 202 | -webkit-background-clip: text; 203 | -webkit-text-fill-color: transparent; 204 | -moz-background-clip: text; 205 | -moz-text-fill-color: transparent; 206 | } 207 | 208 | .card { 209 | aspect-ratio: 1; 210 | overflow: hidden; 211 | position: relative; 212 | display: grid; 213 | grid-template-columns: repeat(2,1fr); 214 | grid-template-rows: repeat(2,1fr); 215 | gap: 0.5rem; 216 | padding: 0.5rem; 217 | grid-template-areas: 'box-a box-b' 'box-c box-d'; 218 | cursor: pointer; 219 | } 220 | 221 | .card__img { 222 | background-size: cover; 223 | background-position: 50% 50%; 224 | width: 100%; 225 | height: 100%; 226 | position: absolute; 227 | top: 0; 228 | left: 0; 229 | z-index: 1; 230 | will-change: filter, transform; 231 | } 232 | 233 | [data-effect="hover-2"] .card__img { 234 | transform: scale3d(1.3,1.3,1.3); 235 | } 236 | 237 | [data-effect="hover-3"] .card__img { 238 | transform: scale3d(1.2,1.2,1.2); 239 | } 240 | 241 | .card__box { 242 | opacity: 0; 243 | z-index: 2; 244 | padding: 1rem; 245 | display: flex; 246 | position: relative; 247 | overflow: hidden; 248 | flex-direction: column; 249 | background-color: rgba(255, 255, 255, .15); 250 | backdrop-filter: blur(5px); 251 | } 252 | 253 | .card__box--a { 254 | grid-area: box-a; 255 | } 256 | 257 | .card__box--b { 258 | grid-area: box-b; 259 | } 260 | 261 | .card__box--c { 262 | grid-area: box-c; 263 | } 264 | 265 | .card__box--d { 266 | grid-area: box-d; 267 | } 268 | 269 | .card__box--b, 270 | .card__box--d { 271 | align-items: flex-end; 272 | text-align: right; 273 | } 274 | 275 | /* Corners made with https://css-generators.com/custom-corners/ by Temani Afif */ 276 | 277 | .card__box--a, 278 | .card__box--d { 279 | clip-path: polygon(0 0,calc(100% - var(--cut)) 0,100% var(--cut),100% 100%,var(--cut) 100%,0 calc(100% - var(--cut))); 280 | } 281 | 282 | .card__box--b, 283 | .card__box--c { 284 | clip-path: polygon(0 var(--cut),var(--cut) 0,100% 0,100% calc(100% - var(--cut)),calc(100% - var(--cut)) 100%,0 100%); 285 | } 286 | 287 | .card__box-number { 288 | font-size: clamp(2rem, 10vw, 3.5rem); 289 | font-family: "tt-autonomous", -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif; 290 | font-weight: 200; 291 | line-height: 0.8; 292 | grid-area: number; 293 | } 294 | 295 | .card__box--alt { 296 | color: var(--color-text-alt); 297 | } 298 | 299 | .card__box-tags, 300 | .card__box-category { 301 | font-size: 0.85rem; 302 | font-weight: 600; 303 | margin-top: auto; 304 | } 305 | 306 | .card__box-tags { 307 | grid-area: tags; 308 | font-size: 0.65rem; 309 | } 310 | 311 | .card__box-category { 312 | grid-area: category; 313 | } 314 | 315 | .card__box > span { 316 | display: inline-bock; 317 | position: relative; 318 | overflow: hidden; 319 | } 320 | 321 | @media screen and (min-width: 53em) { 322 | .frame { 323 | position: fixed; 324 | bottom: 0; 325 | } 326 | .grid__title { 327 | position: absolute; 328 | top: 0; 329 | left: -13vw; 330 | } 331 | .grid { 332 | grid-template-columns: repeat(2,300px); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/favicon.ico -------------------------------------------------------------------------------- /img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/1.jpg -------------------------------------------------------------------------------- /img/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/10.jpg -------------------------------------------------------------------------------- /img/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/11.jpg -------------------------------------------------------------------------------- /img/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/12.jpg -------------------------------------------------------------------------------- /img/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/13.jpg -------------------------------------------------------------------------------- /img/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/14.jpg -------------------------------------------------------------------------------- /img/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/15.jpg -------------------------------------------------------------------------------- /img/16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/16.jpg -------------------------------------------------------------------------------- /img/17.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/17.jpg -------------------------------------------------------------------------------- /img/18.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/18.jpg -------------------------------------------------------------------------------- /img/19.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/19.jpg -------------------------------------------------------------------------------- /img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/2.jpg -------------------------------------------------------------------------------- /img/20.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/20.jpg -------------------------------------------------------------------------------- /img/21.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/21.jpg -------------------------------------------------------------------------------- /img/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/22.jpg -------------------------------------------------------------------------------- /img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/3.jpg -------------------------------------------------------------------------------- /img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/4.jpg -------------------------------------------------------------------------------- /img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/5.jpg -------------------------------------------------------------------------------- /img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/6.jpg -------------------------------------------------------------------------------- /img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/7.jpg -------------------------------------------------------------------------------- /img/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/8.jpg -------------------------------------------------------------------------------- /img/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/9.jpg -------------------------------------------------------------------------------- /img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/GridItemHoverEffect/c9a29492d52134d52b3206efb594b69fedfe153d/img/bg.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Grid Item Hover Effect | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

Grid Item Hover Effect

20 | Based on SOTA Branding by Alena Orlova 21 | Previous demo 22 | Article 23 |
24 |
25 |

Effect
01

26 |
27 |
28 |
29 | 02 30 | #techno #rave 31 |
32 |
33 | Pulse Club 34 |
35 |
36 |
37 |
38 |
39 |
40 | 07 41 | #house #deephouse 42 |
43 |
44 | Stellar Lounge 45 |
46 |
47 |
48 |
49 |
50 | 09 51 | #dubstep #bass 52 |
53 |
54 | Bass Arena 55 |
56 |
57 |
58 |
59 |
60 |
61 | 13 62 | #trance #progressive 63 |
64 |
65 | Euphoria Hall 66 |
67 |
68 |
69 |
70 |
71 | 17 72 | #drumandbass #jungle 73 |
74 |
75 | Groove Hive 76 |
77 |
78 |
79 |
80 |
81 | 19 82 | #ambient #chillout 83 |
84 |
85 | Tranquil Gardens 86 |
87 |
88 |
89 |
90 |

Effect
02

91 |
92 |
93 |
94 | 21 95 | #trap #bassmusic 96 |
97 |
98 | SubBass Arena 99 |
100 |
101 |
102 |
103 |
104 |
105 | 23 106 | #hardstyle #euphoric 107 |
108 |
109 | HyperPulse Club 110 |
111 |
112 |
113 |
114 |
115 | 26 116 | #futurebass #melodic 117 |
118 |
119 | SynthScape Dome 120 |
121 |
122 |
123 |
124 |
125 |
126 | 30 127 | #breakbeat #bigbeat 128 |
129 |
130 | BeatBunker Hall 131 |
132 |
133 |
134 |
135 |
136 | 13 137 | #trance #progressive 138 |
139 |
140 | Euphoria Hall 141 |
142 |
143 |
144 |
145 |
146 | 17 147 | #drumandbass #jungle 148 |
149 |
150 | Groove Hive 151 |
152 |
153 |
154 |
155 |
156 | 19 157 | #ambient #chillout 158 |
159 |
160 | Tranquil Gardens 161 |
162 |
163 |
164 |
165 |

Effect
03

166 |
167 |
168 |
169 | 02 170 | #techno #rave 171 |
172 |
173 | Pulse Club 174 |
175 |
176 |
177 |
178 |
179 |
180 | 07 181 | #house #deephouse 182 |
183 |
184 | Stellar Lounge 185 |
186 |
187 |
188 |
189 |
190 | 09 191 | #dubstep #bass 192 |
193 |
194 | Bass Arena 195 |
196 |
197 |
198 |
199 |
200 |
201 | 13 202 | #trance #progressive 203 |
204 |
205 | Euphoria Hall 206 |
207 |
208 |
209 |
210 |
211 | 17 212 | #drumandbass #jungle 213 |
214 |
215 | Groove Hive 216 |
217 |
218 |
219 |
220 |
221 | 19 222 | #ambient #chillout 223 |
224 |
225 | Tranquil Gardens 226 |
227 |
228 |
229 |
230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /js/card.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class that represents a box inside a card. 3 | */ 4 | class CardBox { 5 | 6 | // Define properties related to the DOM elements and their styles. 7 | DOM = { 8 | el: null, // The main container of the box 9 | number: null, // The number inside the box 10 | numberChars: null, // Individual characters of the number (after using Splitting.js) 11 | tags: null, // The tags associated with this box 12 | category: null, // The category of the box 13 | categoryChars: null, // Individual characters of the category (after using Splitting.js) 14 | }; 15 | 16 | /** 17 | * Constructor: Initializes the properties with the actual DOM elements. 18 | * @param {HTMLElement} DOM_el - The main container of the box. 19 | */ 20 | constructor(DOM_el) { 21 | // Associate the provided element with the main container 22 | this.DOM.el = DOM_el; 23 | // Fetch various child elements 24 | this.DOM.number = this.DOM.el.querySelector('.card__box-number'); 25 | this.DOM.tags = this.DOM.el.querySelector('.card__box-tags'); 26 | this.DOM.category = this.DOM.el.querySelector('.card__box-category'); 27 | 28 | // Prepare the number and category elements for splitting using Splitting.js 29 | if ( this.DOM.number ) { 30 | this.DOM.number.dataset.splitting = ''; 31 | } 32 | if ( this.DOM.category ) { 33 | this.DOM.category.dataset.splitting = ''; 34 | } 35 | 36 | // Use Splitting.js to split the text content into individual characters for animation/effects. 37 | Splitting(); 38 | 39 | // Store references to the individual characters after the split. 40 | if (this.DOM.number) { 41 | this.DOM.numberChars = this.DOM.number.querySelectorAll('.char'); 42 | } 43 | if (this.DOM.category) { 44 | this.DOM.categoryChars = this.DOM.category.querySelectorAll('.char'); 45 | } 46 | } 47 | 48 | } 49 | 50 | /** 51 | * Class representing Card1. 52 | */ 53 | export class Card1 { 54 | 55 | // Define properties related to the DOM elements and their styles. 56 | DOM = { 57 | el: null, // Main card element 58 | img: null, // Image within the card 59 | boxes: null // Array of card boxes within the card 60 | }; 61 | cardBoxesArr = []; // Array to hold instances of CardBox associated with this card. 62 | 63 | /** 64 | * Constructor: Initializes properties with actual DOM elements and sets up event listeners. 65 | * @param {HTMLElement} DOM_el - The main card element. 66 | */ 67 | constructor(DOM_el) { 68 | this.DOM.el = DOM_el; 69 | this.DOM.img = this.DOM.el.querySelector('.card__img'); 70 | this.boxes = [...this.DOM.el.querySelectorAll('.card__box')]; 71 | 72 | // For each box element, create a new CardBox instance and add it to cardBoxesArr. 73 | this.boxes.forEach(boxEl => this.cardBoxesArr.push(new CardBox(boxEl))); 74 | 75 | // Setup event listeners for the card. 76 | this.initEvents(); 77 | } 78 | 79 | /** 80 | * Getters to retrieve specific parts of card boxes in an array format. 81 | */ 82 | get cardBoxElements() { 83 | return this.cardBoxesArr.map(box => box.DOM.el); 84 | } 85 | get cardBoxNumberChars() { 86 | return this.cardBoxesArr.map(box => box.DOM.numberChars); 87 | } 88 | get cardBoxCategoryChars() { 89 | return this.cardBoxesArr.map(box => box.DOM.categoryChars) 90 | .filter(chars => chars !== null); 91 | } 92 | 93 | /** 94 | * Initialize the event listeners for the card. 95 | */ 96 | initEvents() { 97 | // Attach event listeners for mouse enter and leave events. 98 | this.DOM.el.addEventListener('mouseenter', event => this.enter(event)); 99 | this.DOM.el.addEventListener('mouseleave', event => this.leave(event)); 100 | } 101 | 102 | /** 103 | * Action to perform when mouse enters the card. Creates animations using gsap. 104 | */ 105 | enter() { 106 | if ( this.leaveTimeline ) { 107 | this.leaveTimeline.kill(); 108 | } 109 | 110 | // Define the animations to play on mouse enter using gsap. 111 | this.enterTimeline = gsap 112 | .timeline({ 113 | defaults: { 114 | duration: 0.5, 115 | ease: 'expo' 116 | } 117 | }) 118 | .addLabel('start', 0) 119 | // Various animations for different elements are defined below: 120 | .fromTo(this.DOM.img, { 121 | filter: 'saturate(100%) brightness(100%)', 122 | }, { 123 | scale: 0.85, 124 | filter: 'saturate(200%) brightness(70%)' 125 | }, 'start') 126 | .fromTo(this.cardBoxElements, { 127 | opacity: 0, 128 | xPercent: (_, target) => { 129 | if (target.classList.contains('card__box--a')) { 130 | return -100; 131 | } 132 | else if (target.classList.contains('card__box--b')) { 133 | return 100; 134 | } 135 | else if (target.classList.contains('card__box--c')) { 136 | return -100; 137 | } 138 | else if (target.classList.contains('card__box--d')) { 139 | return 100; 140 | } 141 | return 0; 142 | }, 143 | yPercent: (_, target) => { 144 | if (target.classList.contains('card__box--a')) { 145 | return -100; 146 | } 147 | else if (target.classList.contains('card__box--b')) { 148 | return -100; 149 | } 150 | else if (target.classList.contains('card__box--c')) { 151 | return 100; 152 | } 153 | else if (target.classList.contains('card__box--d')) { 154 | return 100; 155 | } 156 | return 0; 157 | } 158 | }, { 159 | opacity: 1, 160 | xPercent: 0, 161 | yPercent: 0, 162 | }, 'start') 163 | .fromTo(this.cardBoxNumberChars, { 164 | opacity: 0, 165 | }, { 166 | duration: 0.3, 167 | opacity: 1, 168 | stagger: 0.1, 169 | }, 'start+=.2') 170 | .fromTo(this.cardBoxCategoryChars, { 171 | opacity: 0 172 | }, { 173 | duration: 0.1, 174 | opacity: 1, 175 | stagger: { 176 | from: 'random', 177 | each: 0.05 178 | }, 179 | }, 'start+=.2') 180 | } 181 | 182 | /** 183 | * Action to perform when mouse leaves the card. Creates animations using gsap. 184 | */ 185 | leave() { 186 | if ( this.enterTimeline ) { 187 | this.enterTimeline.kill(); 188 | } 189 | 190 | // Define the animations to play on mouse leave using gsap. 191 | this.leaveTimeline = gsap 192 | .timeline({ 193 | defaults: { 194 | duration: 0.8, 195 | ease: 'expo' 196 | } 197 | }) 198 | .addLabel('start', 0) 199 | // Various animations for different elements are defined below: 200 | .to(this.DOM.img, { 201 | scale: 1, 202 | filter: 'saturate(100%) brightness(100%)' 203 | }, 'start') 204 | .to(this.cardBoxElements, { 205 | opacity: 0, 206 | xPercent: (_, target) => { 207 | if (target.classList.contains('card__box--a')) { 208 | return -100; 209 | } 210 | else if (target.classList.contains('card__box--b')) { 211 | return 100; 212 | } 213 | else if (target.classList.contains('card__box--c')) { 214 | return -100; 215 | } 216 | else if (target.classList.contains('card__box--d')) { 217 | return 100; 218 | } 219 | return 0; 220 | }, 221 | yPercent: (_, target) => { 222 | if (target.classList.contains('card__box--a')) { 223 | return -100; 224 | } 225 | else if (target.classList.contains('card__box--b')) { 226 | return -100; 227 | } 228 | else if (target.classList.contains('card__box--c')) { 229 | return 100; 230 | } 231 | else if (target.classList.contains('card__box--d')) { 232 | return 100; 233 | } 234 | return 0; 235 | } 236 | }, 'start'); 237 | } 238 | } -------------------------------------------------------------------------------- /js/card2.js: -------------------------------------------------------------------------------- 1 | import { getMouseEnterDirection } from './utils.js' 2 | 3 | let lastMouseX = null; 4 | let lastMouseY = null; 5 | 6 | document.addEventListener('mousemove', (e) => { 7 | lastMouseX = e.clientX; 8 | lastMouseY = e.clientY; 9 | }); 10 | 11 | class CardBox { 12 | 13 | DOM = { 14 | el: null, 15 | number: null, 16 | numberChars: null, 17 | tags: null, 18 | category: null, 19 | categoryChars: null, 20 | }; 21 | 22 | constructor(DOM_el) { 23 | this.DOM.el = DOM_el; 24 | this.DOM.number = this.DOM.el.querySelector('.card__box-number'); 25 | this.DOM.tags = this.DOM.el.querySelector('.card__box-tags'); 26 | this.DOM.category = this.DOM.el.querySelector('.card__box-category'); 27 | 28 | if ( this.DOM.number ) { 29 | this.DOM.number.dataset.splitting = ''; 30 | } 31 | if ( this.DOM.category ) { 32 | this.DOM.category.dataset.splitting = ''; 33 | } 34 | Splitting(); 35 | if ( this.DOM.number ) 36 | this.DOM.numberChars = this.DOM.number.querySelectorAll('.char'); 37 | if ( this.DOM.category ) 38 | this.DOM.categoryChars = this.DOM.category.querySelectorAll('.char'); 39 | } 40 | 41 | } 42 | 43 | export class Card2 { 44 | 45 | DOM = { 46 | el: null, 47 | img: null, 48 | boxes: null 49 | }; 50 | cardBoxesArr = []; 51 | 52 | constructor(DOM_el) { 53 | this.DOM.el = DOM_el; 54 | this.DOM.img = this.DOM.el.querySelector('.card__img'); 55 | this.boxes = [...this.DOM.el.querySelectorAll('.card__box')]; 56 | // Initialize CardBox instances 57 | this.boxes.forEach(boxEl => this.cardBoxesArr.push(new CardBox(boxEl))); 58 | 59 | this.initEvents(); 60 | } 61 | 62 | get cardBoxElements() { 63 | return this.cardBoxesArr.map(box => box.DOM.el); 64 | } 65 | 66 | get cardBoxNumberChars() { 67 | return this.cardBoxesArr.map(box => box.DOM.numberChars); 68 | } 69 | 70 | get cardBoxCategoryChars() { 71 | return this.cardBoxesArr.map(box => box.DOM.categoryChars) 72 | .filter(chars => chars !== null); 73 | } 74 | 75 | initEvents() { 76 | const mouseenterFn = event => this.enter(event); 77 | const mouseLeaveFn = event => this.leave(event); 78 | this.DOM.el.addEventListener('mouseenter', mouseenterFn); 79 | this.DOM.el.addEventListener('mouseleave', mouseLeaveFn); 80 | } 81 | 82 | enter(event) { 83 | const direction = getMouseEnterDirection(event.target, lastMouseX, lastMouseY); 84 | event.target.dataset.direction = direction; 85 | 86 | if ( this.leaveTimeline ) { 87 | this.leaveTimeline.kill(); 88 | } 89 | 90 | this.enterTimeline = gsap 91 | .timeline({ 92 | defaults: { 93 | duration: 0.7, 94 | ease: 'expo' 95 | } 96 | }) 97 | .addLabel('start', 0) 98 | .fromTo(this.DOM.img, { 99 | filter: 'grayscale(0%)', 100 | }, { 101 | //scale: 1, 102 | xPercent: () => { 103 | if ( direction === 'left' ) { 104 | return -10; 105 | } 106 | else if ( direction === 'right' ) { 107 | return 10; 108 | } 109 | else return 0; 110 | }, 111 | yPercent: () => { 112 | if ( direction === 'top' ) { 113 | return -10; 114 | } 115 | else if ( direction === 'bottom' ) { 116 | return 10; 117 | } 118 | else return 0; 119 | }, 120 | filter: 'grayscale(40%)' 121 | }, 'start') 122 | .fromTo(this.cardBoxElements, { 123 | opacity: 0, 124 | xPercent: () => { 125 | if ( direction === 'left' ) { 126 | return -20; 127 | } 128 | else if ( direction === 'right' ) { 129 | return 20; 130 | } 131 | else return 0; 132 | }, 133 | yPercent: () => { 134 | if ( direction === 'top' ) { 135 | return -20; 136 | } 137 | else if ( direction === 'bottom' ) { 138 | return 20; 139 | } 140 | else return 0; 141 | }, 142 | rotation: -10 143 | }, { 144 | opacity: 1, 145 | xPercent: 0, 146 | yPercent: 0, 147 | rotation: 0, 148 | stagger: 0.08 149 | }, 'start') 150 | .fromTo(this.cardBoxNumberChars, { 151 | opacity: 0, 152 | }, { 153 | duration: 0.3, 154 | opacity: 1, 155 | stagger: 0.1, 156 | }, 'start+=.2') 157 | .fromTo(this.cardBoxCategoryChars, { 158 | opacity: 0 159 | }, { 160 | duration: 0.1, 161 | opacity: 1, 162 | stagger: { 163 | from: 'random', 164 | each: 0.05 165 | }, 166 | }, 'start+=.2') 167 | } 168 | 169 | leave(event) { 170 | const direction = event.target.dataset.direction; 171 | 172 | if ( this.enterTimeline ) { 173 | this.enterTimeline.kill(); 174 | } 175 | this.leaveTimeline = gsap 176 | .timeline({ 177 | defaults: { 178 | duration: 0.8, 179 | ease: 'expo' 180 | } 181 | }) 182 | .addLabel('start', 0) 183 | .to(this.DOM.img, { 184 | //scale: 1.3, 185 | xPercent: 0, 186 | yPercent: 0, 187 | filter: 'grayscale(0%)' 188 | }, 'start') 189 | .to(this.cardBoxElements, { 190 | //scale: 0, 191 | opacity: 0, 192 | 193 | xPercent: () => { 194 | if ( direction === 'left' ) { 195 | return -20; 196 | } 197 | else if ( direction === 'right' ) { 198 | return 20; 199 | } 200 | else return 0; 201 | }, 202 | yPercent: () => { 203 | if ( direction === 'top' ) { 204 | return -20; 205 | } 206 | else if ( direction === 'bottom' ) { 207 | return 20; 208 | } 209 | else return 0; 210 | }, 211 | rotation: -10 212 | }, 'start'); 213 | } 214 | } -------------------------------------------------------------------------------- /js/card3.js: -------------------------------------------------------------------------------- 1 | class CardBox { 2 | 3 | DOM = { 4 | el: null, 5 | number: null, 6 | numberChars: null, 7 | tags: null, 8 | category: null, 9 | categoryChars: null, 10 | }; 11 | 12 | constructor(DOM_el) { 13 | this.DOM.el = DOM_el; 14 | this.DOM.number = this.DOM.el.querySelector('.card__box-number'); 15 | this.DOM.tags = this.DOM.el.querySelector('.card__box-tags'); 16 | this.DOM.category = this.DOM.el.querySelector('.card__box-category'); 17 | 18 | if ( this.DOM.number ) { 19 | this.DOM.number.dataset.splitting = ''; 20 | } 21 | if ( this.DOM.category ) { 22 | this.DOM.category.dataset.splitting = ''; 23 | } 24 | Splitting(); 25 | if ( this.DOM.number ) 26 | this.DOM.numberChars = this.DOM.number.querySelectorAll('.char'); 27 | if ( this.DOM.category ) 28 | this.DOM.categoryChars = this.DOM.category.querySelectorAll('.char'); 29 | } 30 | 31 | } 32 | 33 | export class Card3 { 34 | 35 | DOM = { 36 | el: null, 37 | img: null, 38 | boxes: null 39 | }; 40 | cardBoxesArr = []; 41 | 42 | constructor(DOM_el) { 43 | this.DOM.el = DOM_el; 44 | this.DOM.img = this.DOM.el.querySelector('.card__img'); 45 | this.boxes = [...this.DOM.el.querySelectorAll('.card__box')]; 46 | this.boxes.forEach(boxEl => this.cardBoxesArr.push(new CardBox(boxEl))); 47 | 48 | this.initEvents(); 49 | } 50 | 51 | get cardBoxElements() { 52 | return this.cardBoxesArr.map(box => box.DOM.el); 53 | } 54 | 55 | get cardBoxNumberChars() { 56 | return this.cardBoxesArr.map(box => box.DOM.numberChars); 57 | } 58 | 59 | get cardBoxCategoryChars() { 60 | return this.cardBoxesArr.map(box => box.DOM.categoryChars) 61 | .filter(chars => chars !== null); 62 | } 63 | 64 | initEvents() { 65 | const mouseenterFn = event => this.enter(event); 66 | const mouseLeaveFn = event => this.leave(event); 67 | this.DOM.el.addEventListener('mouseenter', mouseenterFn); 68 | this.DOM.el.addEventListener('mouseleave', mouseLeaveFn); 69 | } 70 | 71 | enter(event) { 72 | if ( this.leaveTimeline ) { 73 | this.leaveTimeline.kill(); 74 | } 75 | 76 | this.enterTimeline = gsap 77 | .timeline({ 78 | defaults: { 79 | duration: 0.7, 80 | ease: 'expo' 81 | } 82 | }) 83 | .addLabel('start', 0) 84 | .fromTo(this.DOM.img, { 85 | filter: 'grayscale(0%)', 86 | }, { 87 | filter: 'grayscale(90%)' 88 | }, 'start') 89 | .to(this.DOM.img, { 90 | ease: 'power4', 91 | duration: 0.6, 92 | scaleY: 1 93 | }, 'start') 94 | .to(this.DOM.img, { 95 | duration: 1.5, 96 | scaleX: 1 97 | }, 'start') 98 | .fromTo(this.cardBoxElements, { 99 | opacity: 0, 100 | scale: 0, 101 | rotation: -10 102 | }, { 103 | opacity: 1, 104 | scale: 1, 105 | rotation: 0, 106 | stagger: 0.08 107 | }, 'start') 108 | .fromTo(this.cardBoxNumberChars, { 109 | opacity: 0, 110 | }, { 111 | duration: 0.3, 112 | opacity: 1, 113 | stagger: 0.1, 114 | }, 'start+=.2') 115 | .fromTo(this.cardBoxCategoryChars, { 116 | opacity: 0 117 | }, { 118 | duration: 0.1, 119 | opacity: 1, 120 | stagger: { 121 | from: 'random', 122 | each: 0.05 123 | }, 124 | }, 'start+=.2') 125 | } 126 | 127 | leave(event) { 128 | if ( this.enterTimeline ) { 129 | this.enterTimeline.kill(); 130 | } 131 | this.leaveTimeline = gsap 132 | .timeline({ 133 | defaults: { 134 | duration: 0.8, 135 | ease: 'expo' 136 | } 137 | }) 138 | .addLabel('start', 0) 139 | .to(this.DOM.img, { 140 | scale: 1.3, 141 | filter: 'grayscale(0%)' 142 | }, 'start') 143 | .to(this.cardBoxElements, { 144 | scale: 0, 145 | opacity: 0, 146 | rotation: -10 147 | }, 'start'); 148 | } 149 | } -------------------------------------------------------------------------------- /js/gsap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * GSAP 3.12.2 3 | * https://greensock.com 4 | * 5 | * @license Copyright 2023, GreenSock. All rights reserved. 6 | * Subject to the terms at https://greensock.com/standard-license or for Club GreenSock members, the agreement issued with that membership. 7 | * @author: Jack Doyle, jack@greensock.com 8 | */ 9 | 10 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function P(t){return(i=yt(t,ot))&&Ee}function Q(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function R(t,e){return!e&&console.warn(t)}function S(t,e){return t&&(ot[t]=e)&&i&&(i[t]=e)||ot}function T(){return 0}function ea(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=gt.length;r--&&!gt[r].targetTest(i););e=gt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Vt(t[r],e)))||t.splice(r,1);return t}function fa(t){return t._gsap||ea(Ot(t))[0]._gsap}function ga(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ha(t,e){return(t=t.split(",")).forEach(e)||t}function ia(t){return Math.round(1e5*t)/1e5||0}function ja(t){return Math.round(1e7*t)/1e7||0}function ka(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function la(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++ia;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function ya(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function za(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Aa(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Ca(t,e,r,i){return t._startAt&&(L?t._startAt.revert(ht):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ea(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ga(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ha(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||X)||0))}function Ia(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0X)&&e.render(r,!0)),Aa(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur(n=Math.abs(n))&&(a=i,o=n);return a}function tb(t){return za(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&At(t,"onInterrupt"),t}function wb(t){if(x()&&t){var e=(t=!t.name&&t.default||t).name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:T,render:he,add:Qt,kill:ce,modifier:fe,rawVars:0},a={targetTest:0,get:0,getSetter:ne,aliases:{},register:0};if(Ft(),t!==i){if(pt[e])return;qa(i,qa(ua(t,n),a)),yt(i.prototype,yt(n,ua(t,a))),pt[i.prop=e]=i,t.targetTest&&(gt.push(i),ft[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}S(e,i),t.register&&t.register(Ee,i,_e)}else t&&Ct.push(t)}function zb(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&St,e&St]:0:Et.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Et[e])p=Et[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&St,p&St,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&St,e&St]}else if("hsl"===e.substr(0,3))if(p=d=e.match(tt),r){if(~e.indexOf("="))return p=e.match(et),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3=U?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*($(i)?i[0]:i).totalDuration()),1=r&&te)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0=i&&(a instanceof Zt?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(ya(this,t),t===this._recent&&(this._recent=this._last),Aa(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=ja(Rt.time-(0r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=xt(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=Zt.to(i,qa({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||X,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Ra(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,qa({startAt:{time:xt(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),rb(this,xt(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),rb(this,xt(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+X)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);for(var i,n=this._first,a=this.labels;n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Aa(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Aa(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=U;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Ka(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=r/a._ts,a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ra(a,a===I&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(I._ts&&(na(I,Ga(t,I)),f=Rt.frame),Rt.frame>=mt){mt+=q.autoSleep||120;var e=I._first;if((!e||!e._ts)&&q.autoSleep&&Rt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Rt.sleep()}}},Timeline}(Ut);qa(Xt.prototype,{_lock:0,_hasPause:0,_forcing:0});function ac(t,e,i,n,a,o){var u,h,l,f;if(pt[t]&&!1!==(u=new pt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Kt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||Z(t))return r(t)?Kt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Kt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new _e(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==c))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function gc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if($(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Nt,Wt,Qt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var c,d=t[e],p="get"!==i?i:s(d)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():d,_=s(d)?l?re:te:$t;if(r(n)&&(~n.indexOf("random(")&&(n=ob(n)),"="===n.charAt(1)&&(!(c=ka(p,n)+(Ya(p)||0))&&0!==c||(n=c))),!f||p!==n||Wt)return isNaN(p*n)||""===n?(d||e in t||Q(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,c,d,p,_=new _e(this._pt,t,e,0,1,ue,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(d=~(i+="").indexOf("random("))&&(i=ob(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(it)||[];o=it.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(c=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:c,c:"="===l.charAt(1)?ka(c,l)-c:parseFloat(l)-c,m:h&&h<4?Math.round:0},m=it.lastIndex);return _.c=m")}),s.duration();else{for(l in u={},x)"ease"===l||"easeEach"===l||gc(l,x[l],u,x.easeEach);for(l in u)for(C=u[l].sort(function(t,e){return t.t-e.t}),o=D=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&za(t,1),r||L||(At(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),z.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i){d||Rt.wake(),this._ts||this.play();var n,a=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Gt(this,a),n=this._ease(a/this._dur),function _updatePropTweens(t,e,r,i,n,a,s){var o,u,h,l,f=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!f)for(f=t._ptCache[e]=[],h=t._ptLookup,l=t._targets.length;l--;){if((o=h[l][e])&&o.d&&o.d._pt)for(o=o.d._pt;o&&o.p!==e&&o.fp!==e;)o=o._next;if(!o)return Wt=1,t.vars[e]="+=0",Gt(t,s),Wt=0,1;f.push(o)}for(l=f.length;l--;)(o=(u=f[l])._pt||u).s=!i&&0!==i||n?o.s+(i||0)+a*o.c:i,o.c=r-o.s,u.e&&(u.e=ia(r)+Ya(u.e)),u.b&&(u.b=o.s+Ya(u.b))}(this,t,e,r,i,n,a)?this.resetTo(t,e,r,i):(Ia(this,0),this.parent||xa(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?tb(this):this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Nt&&!0!==Nt.vars.overwrite)._first||tb(this),this.parent&&i!==this.timeline.totalDuration()&&Ra(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,c=t?Ot(t):f,d=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,c))return"all"===e&&(this._pt=0),tb(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ha(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?fa(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=yt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~c.indexOf(f[l]))for(u in a=d[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||ya(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&tb(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Va(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Va(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return I.killTweensOf(t,e,r)},Tween}(Ut);qa(Zt.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ha("staggerTo,staggerFrom,staggerFromTo",function(r){Zt[r]=function(){var t=new Xt,e=Mt.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function oc(t,e,r){return t.setAttribute(e,r)}function wc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var $t=function _setterPlain(t,e,r){return t[e]=r},te=function _setterFunc(t,e,r){return t[e](r)},re=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},ne=function _getSetter(t,e){return s(t[e])?te:u(t[e])&&t.setAttribute?oc:$t},ae=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},se=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},ue=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},he=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},fe=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},ce=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?ya(this,i,"_pt"):i.dep||(e=1),i=r;return!e},pe=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},_e=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=wc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ae,this.d=s||this,this.set=o||$t,this.pr=u||0,(this._next=t)&&(t._prev=this)}ha(vt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ft[t]=1}),ot.TweenMax=ot.TweenLite=Zt,ot.TimelineLite=ot.TimelineMax=Xt,I=new Xt({sortChildren:!1,defaults:V,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),q.stringFilter=Fb;function Ec(t){return(ye[t]||Te).map(function(t){return t()})}function Fc(){var t=Date.now(),o=[];2{setTimeout((()=>{this.progress(t,e,i)}))};this.images.forEach((function(e){e.once("progress",t),e.check()}))},n.prototype.progress=function(t,e,i){this.progressedCount++,this.hasAnyBroken=this.hasAnyBroken||!t.isLoaded,this.emitEvent("progress",[this,t,e]),this.jqDeferred&&this.jqDeferred.notify&&this.jqDeferred.notify(this,t),this.progressedCount===this.images.length&&this.complete(),this.options.debug&&s&&s.log(`progress: ${i}`,t,e)},n.prototype.complete=function(){let t=this.hasAnyBroken?"fail":"done";if(this.isComplete=!0,this.emitEvent(t,[this]),this.emitEvent("always",[this]),this.jqDeferred){let t=this.hasAnyBroken?"reject":"resolve";this.jqDeferred[t](this)}},h.prototype=Object.create(e.prototype),h.prototype.check=function(){this.getIsImageComplete()?this.confirm(0!==this.img.naturalWidth,"naturalWidth"):(this.proxyImage=new Image,this.img.crossOrigin&&(this.proxyImage.crossOrigin=this.img.crossOrigin),this.proxyImage.addEventListener("load",this),this.proxyImage.addEventListener("error",this),this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.proxyImage.src=this.img.currentSrc||this.img.src)},h.prototype.getIsImageComplete=function(){return this.img.complete&&this.img.naturalWidth},h.prototype.confirm=function(t,e){this.isLoaded=t;let{parentNode:i}=this.img,s="PICTURE"===i.nodeName?i:this.img;this.emitEvent("progress",[this,s,e])},h.prototype.handleEvent=function(t){let e="on"+t.type;this[e]&&this[e](t)},h.prototype.onload=function(){this.confirm(!0,"onload"),this.unbindEvents()},h.prototype.onerror=function(){this.confirm(!1,"onerror"),this.unbindEvents()},h.prototype.unbindEvents=function(){this.proxyImage.removeEventListener("load",this),this.proxyImage.removeEventListener("error",this),this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype=Object.create(h.prototype),d.prototype.check=function(){this.img.addEventListener("load",this),this.img.addEventListener("error",this),this.img.src=this.url,this.getIsImageComplete()&&(this.confirm(0!==this.img.naturalWidth,"naturalWidth"),this.unbindEvents())},d.prototype.unbindEvents=function(){this.img.removeEventListener("load",this),this.img.removeEventListener("error",this)},d.prototype.confirm=function(t,e){this.isLoaded=t,this.emitEvent("progress",[this,this.element,e])},n.makeJQueryPlugin=function(e){(e=e||t.jQuery)&&(i=e,i.fn.imagesLoaded=function(t,e){return new n(this,t,e).jqDeferred.promise(i(this))})},n.makeJQueryPlugin(),n})); -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | // Importing the necessary functions and classes from other modules. 2 | import { preloadImages } from './utils.js'; // Utility function to preload images 3 | import { Card1 } from './card.js'; // Class Card1 for data-effect hover-1 4 | import { Card2 } from './card2.js'; // Class Card2 for data-effect hover-2 5 | import { Card3 } from './card3.js'; // Class Card3 for data-effect hover-3 6 | 7 | // Array to hold the instantiated card objects. 8 | let cardsArr = []; 9 | 10 | // Query all non-empty card elements and for each card: 11 | [...document.querySelectorAll('.card:not(.card--empty)')].forEach(cardEl => { 12 | // Find the closest parent grid element of the card and get its data-effect attribute value. 13 | let gridEffect = cardEl.closest('.grid').getAttribute('data-effect'); 14 | 15 | // Depending on the data-effect value, instantiate the appropriate Card class. 16 | switch(gridEffect) { 17 | case 'hover-1': 18 | cardsArr.push(new Card1(cardEl)); // For data-effect hover-1, use Card1 19 | break; 20 | case 'hover-2': 21 | cardsArr.push(new Card2(cardEl)); // For data-effect hover-2, use Card2 22 | break; 23 | case 'hover-3': 24 | cardsArr.push(new Card3(cardEl)); // For data-effect hover-3, use Card3 25 | break; 26 | default: 27 | console.warn('Unknown data-effect', gridEffect); 28 | break; 29 | } 30 | }); 31 | 32 | // Preload all images 33 | // Once all images are preloaded, remove the 'loading' class from the body. 34 | preloadImages('.card__img').then(() => document.body.classList.remove('loading')); 35 | -------------------------------------------------------------------------------- /js/splitting.min.js: -------------------------------------------------------------------------------- 1 | !function(n,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):n.Splitting=t()}(this,function(){"use strict" 2 | var u=document,l=u.createTextNode.bind(u) 3 | function d(n,t,e){n.style.setProperty(t,e)}function f(n,t){return n.appendChild(t)}function p(n,t,e,r){var i=u.createElement("span") 4 | return t&&(i.className=t),e&&(!r&&i.setAttribute("data-"+t,e),i.textContent=e),n&&f(n,i)||i}function h(n,t){return n.getAttribute("data-"+t)}function m(n,t){return n&&0!=n.length?n.nodeName?[n]:[].slice.call(n[0].nodeName?n:(t||u).querySelectorAll(n)):[]}function o(n){for(var t=[];n--;)t[n]=[] 5 | return t}function g(n,t){n&&n.some(t)}function c(t){return function(n){return t[n]}}var a={} 6 | function n(n,t,e,r){return{by:n,depends:t,key:e,split:r}}function e(n){return function t(e,n,r){var i=r.indexOf(e) 7 | if(-1==i)r.unshift(e),g(a[e].depends,function(n){t(n,e,r)}) 8 | else{var u=r.indexOf(n) 9 | r.splice(i,1),r.splice(u,0,e)}return r}(n,0,[]).map(c(a))}function t(n){a[n.by]=n}function v(n,r,i,u,o){n.normalize() 10 | var c=[],a=document.createDocumentFragment() 11 | u&&c.push(n.previousSibling) 12 | var s=[] 13 | return m(n.childNodes).some(function(n){if(!n.tagName||n.hasChildNodes()){if(n.childNodes&&n.childNodes.length)return s.push(n),void c.push.apply(c,v(n,r,i,u,o)) 14 | var t=n.wholeText||"",e=t.trim() 15 | e.length&&(" "===t[0]&&s.push(l(" ")),g(e.split(i),function(n,t){t&&o&&s.push(p(a,"whitespace"," ",o)) 16 | var e=p(a,r,n) 17 | c.push(e),s.push(e)})," "===t[t.length-1]&&s.push(l(" ")))}else s.push(n)}),g(s,function(n){f(a,n)}),n.innerHTML="",f(n,a),c}var s=0 18 | var i="words",r=n(i,s,"word",function(n){return v(n,"word",/\s+/,0,1)}),y="chars",w=n(y,[i],"char",function(n,e,t){var r=[] 19 | return g(t[i],function(n,t){r.push.apply(r,v(n,"char","",e.whitespace&&t))}),r}) 20 | function b(t){var f=(t=t||{}).key 21 | return m(t.target||"[data-splitting]").map(function(a){var s=a["🍌"] 22 | if(!t.force&&s)return s 23 | s=a["🍌"]={el:a} 24 | var n=e(t.by||h(a,"splitting")||y),l=function(n,t){for(var e in t)n[e]=t[e] 25 | return n}({},t) 26 | return g(n,function(n){if(n.split){var t=n.by,e=(f?"-"+f:"")+n.key,r=n.split(a,l,s) 27 | e&&(i=a,c=(o="--"+e)+"-index",g(u=r,function(n,t){Array.isArray(n)?g(n,function(n){d(n,c,t)}):d(n,c,t)}),d(i,o+"-total",u.length)),s[t]=r,a.classList.add(t)}var i,u,o,c}),a.classList.add("splitting"),s})}function N(n,t,e){var r=m(t.matching||n.children,n),i={} 28 | return g(r,function(n){var t=Math.round(n[e]);(i[t]||(i[t]=[])).push(n)}),Object.keys(i).map(Number).sort(x).map(c(i))}function x(n,t){return n-t}b.html=function(n){var t=(n=n||{}).target=p() 29 | return t.innerHTML=n.content,b(n),t.outerHTML},b.add=t 30 | var T=n("lines",[i],"line",function(n,t,e){return N(n,{matching:e[i]},"offsetTop")}),L=n("items",s,"item",function(n,t){return m(t.matching||n.children,n)}),k=n("rows",s,"row",function(n,t){return N(n,t,"offsetTop")}),A=n("cols",s,"col",function(n,t){return N(n,t,"offsetLeft")}),C=n("grid",["rows","cols"]),M="layout",S=n(M,s,s,function(n,t){var e=t.rows=+(t.rows||h(n,"rows")||1),r=t.columns=+(t.columns||h(n,"columns")||1) 31 | if(t.image=t.image||h(n,"image")||n.currentSrc||n.src,t.image){var i=m("img",n)[0] 32 | t.image=i&&(i.currentSrc||i.src)}t.image&&d(n,"background-image","url("+t.image+")") 33 | for(var u=e*r,o=[],c=p(s,"cell-grid");u--;){var a=p(c,"cell") 34 | p(a,"cell-inner"),o.push(a)}return f(n,c),o}),H=n("cellRows",[M],"row",function(n,t,e){var r=t.rows,i=o(r) 35 | return g(e[M],function(n,t,e){i[Math.floor(t/(e.length/r))].push(n)}),i}),O=n("cellColumns",[M],"col",function(n,t,e){var r=t.columns,i=o(r) 36 | return g(e[M],function(n,t){i[t%r].push(n)}),i}),j=n("cells",["cellRows","cellColumns"],"cell",function(n,t,e){return e[M]}) 37 | return t(r),t(w),t(T),t(L),t(k),t(A),t(C),t(S),t(H),t(O),t(j),b}) -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Preload images function 3 | * @param {string} selector - The CSS selector for the images to be preloaded. Default is 'img'. 4 | * @returns {Promise} A promise that resolves when all the images are loaded. 5 | */ 6 | const preloadImages = (selector = 'img') => { 7 | return new Promise((resolve) => { 8 | // Use the imagesLoaded library to ensure all images are fully loaded. 9 | // This is particularly useful for getting accurate dimensions, avoiding layout shifts, etc. 10 | // `background: true` ensures background images are also loaded. 11 | imagesLoaded(document.querySelectorAll(selector), {background: true}, resolve); 12 | }); 13 | }; 14 | 15 | /** 16 | * Determines the direction from which the mouse entered an element. 17 | * 18 | * @param {HTMLElement} element - The element to check against. 19 | * @param {number} lastX - The mouse's X-coordinate right before entering the element. 20 | * @param {number} lastY - The mouse's Y-coordinate right before entering the element. 21 | * @returns {string} - A string indicating the direction ("top", "right", "bottom", "left", or "unknown"). 22 | */ 23 | const getMouseEnterDirection = (element, lastX, lastY) => { 24 | const { top, right, bottom, left } = element.getBoundingClientRect(); 25 | 26 | if (lastY <= Math.floor(top)) return "top"; 27 | if (lastY >= Math.floor(bottom)) return "bottom"; 28 | if (lastX <= Math.floor(left)) return "left"; 29 | if (lastX >= Math.floor(right)) return "right"; 30 | 31 | return "unknown"; 32 | } 33 | 34 | export { 35 | preloadImages, 36 | getMouseEnterDirection, 37 | }; 38 | --------------------------------------------------------------------------------