├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── .DS_Store ├── img1.webp ├── img10.webp ├── img11.webp ├── img12.webp ├── img13.webp ├── img14.webp ├── img15.webp ├── img16.webp ├── img17.webp ├── img18.webp ├── img19.webp ├── img2.webp ├── img20.webp ├── img21.webp ├── img22.webp ├── img23.webp ├── img24.webp ├── img25.webp ├── img26.webp ├── img27.webp ├── img28.webp ├── img29.webp ├── img3.webp ├── img30.webp ├── img31.webp ├── img32.webp ├── img33.webp ├── img34.webp ├── img35.webp ├── img36.webp ├── img37.webp ├── img38.webp ├── img39.webp ├── img4.webp ├── img40.webp ├── img41.webp ├── img42.webp ├── img43.webp ├── img44.webp ├── img45.webp ├── img46.webp ├── img47.webp ├── img48.webp ├── img49.webp ├── img5.webp ├── img50.webp ├── img51.webp ├── img52.webp ├── img53.webp ├── img54.webp ├── img55.webp ├── img56.webp ├── img57.webp ├── img58.webp ├── img59.webp ├── img6.webp ├── img60.webp ├── img61.webp ├── img62.webp ├── img63.webp ├── img64.webp ├── img65.webp ├── img66.webp ├── img67.webp ├── img68.webp ├── img69.webp ├── img7.webp ├── img70.webp ├── img71.webp ├── img72.webp ├── img8.webp └── img9.webp ├── css └── base.css ├── index.html └── js ├── .DS_Store ├── ScrollSmoother.min.js ├── ScrollToPlugin.min.js ├── ScrollTrigger.min.js ├── SplitText.min.js ├── gsap.min.js ├── imagesloaded.pkgd.min.js ├── index.js └── utils.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/.DS_Store -------------------------------------------------------------------------------- /.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 - 2024 [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 | # Om-Scroll 3D Carousel 2 | 3 | A scroll based 3D carousel animation with a page transition effect. 4 | 5 | ![Image](https://tympanus.net/codrops/wp-content/uploads/2025/05/scroll3dcarousel_featured_final-1536x1152.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=93330) 8 | 9 | [Demo](https://tympanus.net/Development/3DCarousel/) 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: [Bluesky](https://bsky.app/profile/codrops.bsky.social), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/), [X](http://www.x.com/codrops) 22 | 23 | ## License 24 | 25 | [MIT](LICENSE) 26 | -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/.DS_Store -------------------------------------------------------------------------------- /assets/img1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img1.webp -------------------------------------------------------------------------------- /assets/img10.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img10.webp -------------------------------------------------------------------------------- /assets/img11.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img11.webp -------------------------------------------------------------------------------- /assets/img12.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img12.webp -------------------------------------------------------------------------------- /assets/img13.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img13.webp -------------------------------------------------------------------------------- /assets/img14.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img14.webp -------------------------------------------------------------------------------- /assets/img15.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img15.webp -------------------------------------------------------------------------------- /assets/img16.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img16.webp -------------------------------------------------------------------------------- /assets/img17.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img17.webp -------------------------------------------------------------------------------- /assets/img18.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img18.webp -------------------------------------------------------------------------------- /assets/img19.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img19.webp -------------------------------------------------------------------------------- /assets/img2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img2.webp -------------------------------------------------------------------------------- /assets/img20.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img20.webp -------------------------------------------------------------------------------- /assets/img21.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img21.webp -------------------------------------------------------------------------------- /assets/img22.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img22.webp -------------------------------------------------------------------------------- /assets/img23.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img23.webp -------------------------------------------------------------------------------- /assets/img24.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img24.webp -------------------------------------------------------------------------------- /assets/img25.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img25.webp -------------------------------------------------------------------------------- /assets/img26.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img26.webp -------------------------------------------------------------------------------- /assets/img27.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img27.webp -------------------------------------------------------------------------------- /assets/img28.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img28.webp -------------------------------------------------------------------------------- /assets/img29.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img29.webp -------------------------------------------------------------------------------- /assets/img3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img3.webp -------------------------------------------------------------------------------- /assets/img30.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img30.webp -------------------------------------------------------------------------------- /assets/img31.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img31.webp -------------------------------------------------------------------------------- /assets/img32.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img32.webp -------------------------------------------------------------------------------- /assets/img33.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img33.webp -------------------------------------------------------------------------------- /assets/img34.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img34.webp -------------------------------------------------------------------------------- /assets/img35.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img35.webp -------------------------------------------------------------------------------- /assets/img36.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img36.webp -------------------------------------------------------------------------------- /assets/img37.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img37.webp -------------------------------------------------------------------------------- /assets/img38.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img38.webp -------------------------------------------------------------------------------- /assets/img39.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img39.webp -------------------------------------------------------------------------------- /assets/img4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img4.webp -------------------------------------------------------------------------------- /assets/img40.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img40.webp -------------------------------------------------------------------------------- /assets/img41.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img41.webp -------------------------------------------------------------------------------- /assets/img42.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img42.webp -------------------------------------------------------------------------------- /assets/img43.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img43.webp -------------------------------------------------------------------------------- /assets/img44.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img44.webp -------------------------------------------------------------------------------- /assets/img45.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img45.webp -------------------------------------------------------------------------------- /assets/img46.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img46.webp -------------------------------------------------------------------------------- /assets/img47.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img47.webp -------------------------------------------------------------------------------- /assets/img48.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img48.webp -------------------------------------------------------------------------------- /assets/img49.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img49.webp -------------------------------------------------------------------------------- /assets/img5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img5.webp -------------------------------------------------------------------------------- /assets/img50.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img50.webp -------------------------------------------------------------------------------- /assets/img51.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img51.webp -------------------------------------------------------------------------------- /assets/img52.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img52.webp -------------------------------------------------------------------------------- /assets/img53.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img53.webp -------------------------------------------------------------------------------- /assets/img54.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img54.webp -------------------------------------------------------------------------------- /assets/img55.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img55.webp -------------------------------------------------------------------------------- /assets/img56.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img56.webp -------------------------------------------------------------------------------- /assets/img57.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img57.webp -------------------------------------------------------------------------------- /assets/img58.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img58.webp -------------------------------------------------------------------------------- /assets/img59.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img59.webp -------------------------------------------------------------------------------- /assets/img6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img6.webp -------------------------------------------------------------------------------- /assets/img60.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img60.webp -------------------------------------------------------------------------------- /assets/img61.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img61.webp -------------------------------------------------------------------------------- /assets/img62.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img62.webp -------------------------------------------------------------------------------- /assets/img63.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img63.webp -------------------------------------------------------------------------------- /assets/img64.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img64.webp -------------------------------------------------------------------------------- /assets/img65.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img65.webp -------------------------------------------------------------------------------- /assets/img66.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img66.webp -------------------------------------------------------------------------------- /assets/img67.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img67.webp -------------------------------------------------------------------------------- /assets/img68.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img68.webp -------------------------------------------------------------------------------- /assets/img69.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img69.webp -------------------------------------------------------------------------------- /assets/img7.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img7.webp -------------------------------------------------------------------------------- /assets/img70.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img70.webp -------------------------------------------------------------------------------- /assets/img71.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img71.webp -------------------------------------------------------------------------------- /assets/img72.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img72.webp -------------------------------------------------------------------------------- /assets/img8.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img8.webp -------------------------------------------------------------------------------- /assets/img9.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/assets/img9.webp -------------------------------------------------------------------------------- /css/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 14px; 9 | --color-text: #fff; 10 | --color-bg: #0f0e0e; 11 | --color-link: #ffffff; 12 | --color-link-hover: rgba(255, 255, 255, 0.6); 13 | --page-padding: 0.5rem; 14 | --aspect: 4/5; 15 | --grid-item-height: 20vh; 16 | --c-gap: 3rem; 17 | --r-gap: 3rem; 18 | --column: 80px; 19 | --column-count: 3; 20 | --border-radius: 4px; 21 | } 22 | 23 | html { 24 | height: 100%; 25 | overflow-x: hidden; 26 | } 27 | 28 | body { 29 | height: 100%; 30 | width: 100%; 31 | font-family: 'forma-djr-mono', sans-serif; 32 | text-transform: uppercase; 33 | margin: 0; 34 | color: var(--color-text); 35 | background-color: var(--color-bg); 36 | -webkit-font-smoothing: antialiased; 37 | -moz-osx-font-smoothing: grayscale; 38 | } 39 | 40 | @media (scripting: enabled) { 41 | .loading { 42 | &::before, 43 | &::after { 44 | content: ''; 45 | position: fixed; 46 | z-index: 10000; 47 | } 48 | 49 | &::before { 50 | top: 0; 51 | left: 0; 52 | width: 100%; 53 | height: 100%; 54 | background: var(--color-bg); 55 | } 56 | 57 | &::after { 58 | top: 50%; 59 | left: 50%; 60 | width: 100px; 61 | height: 1px; 62 | margin: 0 0 0 -50px; 63 | background: var(--color-link); 64 | animation: loaderAnim 1.5s ease-in-out infinite alternate forwards; 65 | } 66 | } 67 | } 68 | 69 | @keyframes loaderAnim { 70 | 0% { 71 | transform: scaleX(0); 72 | transform-origin: 0% 50%; 73 | } 74 | 75 | 50% { 76 | transform: scaleX(1); 77 | transform-origin: 0% 50%; 78 | } 79 | 80 | 50.1% { 81 | transform: scaleX(1); 82 | transform-origin: 100% 50%; 83 | } 84 | 85 | 100% { 86 | transform: scaleX(0); 87 | transform-origin: 100% 50%; 88 | } 89 | } 90 | 91 | a { 92 | text-decoration: none; 93 | color: var(--color-link); 94 | outline: none; 95 | cursor: pointer; 96 | transition: all 0.3s ease; 97 | 98 | &:hover { 99 | text-decoration: none; 100 | color: var(--color-link-hover); 101 | } 102 | 103 | &:focus { 104 | outline: none; 105 | background: lightgrey; 106 | 107 | &:not(:focus-visible) { 108 | background: transparent; 109 | } 110 | 111 | &:focus-visible { 112 | outline: 2px solid red; 113 | background: transparent; 114 | } 115 | } 116 | } 117 | 118 | .line { 119 | display: inline-block; 120 | overflow: hidden; 121 | position: relative; 122 | vertical-align: top; 123 | 124 | &::before { 125 | background: var(--color-link-hover); 126 | bottom: 0; 127 | content: ''; 128 | height: 1px; 129 | left: 0; 130 | position: absolute; 131 | transition: transform 0.4s ease; 132 | width: 100%; 133 | transform: scaleX(0); 134 | transform-origin: right center; 135 | } 136 | 137 | &:hover::before { 138 | transform: scaleX(1); 139 | transform-origin: left center; 140 | } 141 | } 142 | 143 | .frame { 144 | display: grid; 145 | z-index: 1000; 146 | width: 100%; 147 | height: 100vh; 148 | position: fixed; 149 | grid-column-gap: var(--c-gap); 150 | grid-row-gap: 0.5rem; 151 | pointer-events: none; 152 | justify-items: start; 153 | padding: var(--page-padding); 154 | align-content: space-between; 155 | justify-content: space-between; 156 | align-items: end; 157 | grid-template-areas: 'title links' 'tags sponsor'; 158 | 159 | #cdawrap { 160 | justify-self: start; 161 | grid-area: sponsor; 162 | max-width: 230px; 163 | text-align: right; 164 | } 165 | 166 | a, 167 | button { 168 | pointer-events: auto; 169 | color: var(--color-text); 170 | } 171 | 172 | .frame__title { 173 | font-size: inherit; 174 | margin: 0; 175 | font-weight: inherit; 176 | grid-area: title; 177 | text-align: right; 178 | } 179 | 180 | .frame__tags, 181 | .frame__links { 182 | grid-area: tags; 183 | display: flex; 184 | gap: 1rem; 185 | align-items: start; 186 | } 187 | 188 | .frame__links { 189 | grid-area: links; 190 | justify-self: end; 191 | } 192 | 193 | &.frame--footer { 194 | display: flex; 195 | min-height: 300px; 196 | align-items: end; 197 | justify-content: space-between; 198 | } 199 | } 200 | 201 | .scene { 202 | perspective: 900px; 203 | position: relative; 204 | height: 100vh; 205 | display: flex; 206 | justify-content: center; 207 | align-items: center; 208 | } 209 | 210 | .scene__title { 211 | position: relative; 212 | z-index: 10; 213 | margin: 0; 214 | } 215 | 216 | .scene__title a { 217 | display: block; 218 | } 219 | 220 | .scene__title span { 221 | display: inline-block; 222 | } 223 | 224 | .scene__title .char { 225 | display: inline-block; 226 | transform-style: preserve-3d; 227 | transform-origin: 50% 0%; 228 | } 229 | 230 | .carousel { 231 | width: 400px; 232 | height: 500px; 233 | top: 50%; 234 | left: 50%; 235 | margin: -250px 0 0 -200px; 236 | position: absolute; 237 | transform-style: preserve-3d; 238 | will-change: transform; 239 | transform: translateZ(-550px) rotateY(0deg); 240 | } 241 | 242 | .scene:nth-child(even) .carousel { 243 | transform: translateZ(-550px) rotateY(45deg); 244 | } 245 | 246 | .carousel__cell { 247 | position: absolute; 248 | width: 350px; 249 | height: 420px; 250 | left: 0; 251 | top: 0; 252 | transform-style: preserve-3d; 253 | } 254 | 255 | .card { 256 | width: 100%; 257 | height: 100%; 258 | position: relative; 259 | transform-style: preserve-3d; 260 | } 261 | 262 | .card__face { 263 | width: 100%; 264 | height: 100%; 265 | position: absolute; 266 | backface-visibility: hidden; 267 | background-image: var(--img); 268 | background-size: cover; 269 | } 270 | 271 | .card__face img { 272 | width: 100%; 273 | height: 100%; 274 | object-fit: cover; 275 | border-radius: var(--border-radius); 276 | } 277 | 278 | .card__face--back { 279 | transform: rotateY(180deg); 280 | } 281 | 282 | .preview { 283 | position: fixed; 284 | top: 0; 285 | left: 0; 286 | width: 100%; 287 | height: 100%; 288 | padding: 0 15vw; 289 | display: grid; 290 | align-content: center; 291 | justify-items: center; 292 | grid-row-gap: 1rem; 293 | opacity: 0; 294 | pointer-events: none; 295 | } 296 | 297 | .preview__header { 298 | display: flex; 299 | width: 100%; 300 | align-items: flex-end; 301 | justify-content: space-between; 302 | line-height: 1; 303 | } 304 | 305 | .preview__title { 306 | margin: 0; 307 | } 308 | 309 | .preview__close { 310 | background: none; 311 | text-transform: inherit; 312 | border: 0; 313 | padding: 0; 314 | margin: 0; 315 | font: inherit; 316 | cursor: pointer; 317 | color: var(--color-close); 318 | &:hover, 319 | &:focus { 320 | outline: none; 321 | color: var(--color-link-hover); 322 | } 323 | } 324 | 325 | .grid { 326 | padding: 1rem 0; 327 | display: grid; 328 | grid-template-columns: repeat(var(--column-count), minmax(var(--column), 1fr)); 329 | grid-column-gap: var(--c-gap); 330 | grid-row-gap: var(--r-gap); 331 | width: 100%; 332 | perspective: 900px; 333 | } 334 | 335 | .grid__item { 336 | margin: 0; 337 | padding: 0; 338 | display: flex; 339 | flex-direction: column; 340 | gap: 0.25rem; 341 | will-change: transform, clip-path; 342 | overflow: hidden; 343 | border-radius: var(--border-radius); 344 | position: relative; 345 | transform-style: preserve-3d; 346 | } 347 | 348 | .grid__item-image { 349 | width: 100%; 350 | aspect-ratio: var(--aspect); 351 | background-size: 100%; 352 | background-position: 50% 50%; 353 | transition: opacity 0.15s cubic-bezier(0.2, 0, 0.2, 1); 354 | } 355 | 356 | .grid__item:hover .grid__item-image { 357 | opacity: 0.7; 358 | } 359 | 360 | .grid__item-caption h3 { 361 | font-size: 1rem; 362 | font-weight: 400; 363 | margin: 0; 364 | } 365 | 366 | @media screen and (min-width: 65em) { 367 | :root { 368 | --column-count: 6; 369 | } 370 | } 371 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | On-Scroll 3D Carousel | Codrops 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 |
19 |

On-Scroll 3D Carousel

20 | 25 | 30 |
31 |
32 |
33 | 34 |
35 |

36 | Haute Couture Nights — Paris 37 |

38 | 64 |
65 | 66 |
67 |

68 | Vogue Evolution — New York City 69 |

70 | 96 |
97 | 98 |
99 |

100 | Glamour in the Desert — Dubai 101 |

102 | 128 |
129 | 130 |
131 |

132 | Chic Couture Runway — Milan 133 |

134 | 160 |
161 | 162 |
163 |

164 | Style Showcase — London 165 |

166 | 204 |
205 | 206 |
207 |

208 | Future Fashion Forward — Tokyo 209 |

210 | 236 |
237 | 238 |
239 |
240 |
241 |
242 |

Haute Couture Nights — Paris

243 | 244 |
245 |
246 | 252 | 258 | 264 | 270 | 276 | 282 | 288 | 294 | 300 | 306 | 312 | 318 |
319 |
320 | 321 |
322 |
323 |

Vogue Evolution — New York City

324 | 325 |
326 |
327 | 333 | 339 | 345 | 351 | 357 | 363 | 369 | 375 | 381 | 387 | 393 | 399 |
400 |
401 | 402 |
403 |
404 |

Glamour in the Desert — Dubai

405 | 406 |
407 |
408 | 414 | 420 | 426 | 432 | 438 | 444 | 450 | 456 | 462 | 468 | 474 | 480 |
481 |
482 | 483 |
484 |
485 |

Chic Couture Runway — Milan

486 | 487 |
488 |
489 | 495 | 501 | 507 | 513 | 519 | 525 | 531 | 537 | 543 | 549 | 555 | 561 |
562 |
563 | 564 |
565 |
566 |

Style Showcase — London

567 | 568 |
569 |
570 | 576 | 582 | 588 | 594 | 600 | 606 | 612 | 618 | 624 | 630 | 636 | 642 |
643 |
644 | 645 |
646 |
647 |

Future Fashion Forward — Tokyo

648 | 649 |
650 |
651 | 657 | 663 | 669 | 675 | 681 | 687 | 693 | 699 | 705 | 711 | 717 | 723 |
724 |
725 | 726 |
727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | -------------------------------------------------------------------------------- /js/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/3DCarousel/f354a11d19777b4e573d47b016354951b1ecace3/js/.DS_Store -------------------------------------------------------------------------------- /js/ScrollSmoother.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ScrollSmoother 3.13.0 3 | * https://gsap.com 4 | * 5 | * @license Copyright 2025, GreenSock. All rights reserved. 6 | * Subject to the terms at https://gsap.com/standard-license. 7 | * @author: Jack Doyle, jack@greensock.com 8 | */ 9 | 10 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function _defineProperties(e,t){for(var r=0;r=v())&&(n=((r=v())-(t=e.ratio<0||1Math.abs(l)?a:l)/(1-t),f=-c*t;return 0t.end&&(s+=t.distance),n-=t.distance}o=d+s+y*((I.utils.clamp(e.start,e.end,r)-e.start-s)/(n-e.start)-c)}m.length&&!a&&m.forEach(function(e){return e(o-s)}),o=function _round(e){return Math.round(1e5*e)/1e5||0}(o+f),l?(l.resetTo("y",o,-F,!0),M&&l.progress(1)):(g.y=o+"px",g.renderTransform(1))}}})),I.core.getCache(s.trigger).stRevert=Ra,s.startY=d,s.pins=p,s.markers=m,s.ratio=i,s.autoSpeed=a,r.style.willChange="transform"),s}var n,S,e,i,b,s,a,l,c,f,r,u,h,d,g,p,m=t.smoothTouch,w=t.onUpdate,T=t.onStop,_=t.smooth,C=t.onFocusIn,P=t.normalizeScroll,x=t.wholePixels,R=this,E=t.effectsPrefix||"",k=Y.getScrollFunc(U),H=1===Y.isTouch?!0===m?.8:parseFloat(m)||0:0===_||!1===_?0:parseFloat(_)||.8,A=H&&+t.speed||1,N=0,F=0,M=1,z=J(0),B={y:0},L="undefined"!=typeof ResizeObserver&&!1!==t.autoResize&&new ResizeObserver(function(){if(!Y.isRefreshing){var e=v(S)*A;e<-N&&Ha(e),$.restart(!0)}});function refreshHeight(){return e=n.clientHeight,n.style.overflow="visible",K.style.height=U.innerHeight+(e-U.innerHeight)/A+"px",e-U.innerHeight}Pa(),Y.addEventListener("killAll",Pa),I.delayedCall(.5,function(){return M=0}),this.scrollTop=Ha,this.scrollTo=function(e,t,r){var n=I.utils.clamp(0,v(),isNaN(e)?o.offset(e,r,!!t&&!f):+e);t?f?I.to(o,{duration:H,scrollTop:n,overwrite:"auto",ease:W}):k(n):Ha(n)},this.offset=function(e,t,r){var n,o=(e=q(e)[0]).style.cssText,i=Y.create({trigger:e,start:t||"top top"});return b&&(M?Y.refresh():Na([i],!0)),n=i.start/(r?A:1),i.kill(!1),e.style.cssText=o,I.core.getCache(e).uncache=1,n},this.content=function(e){if(arguments.length){var t=q(e||"#smooth-content")[0]||console.warn("ScrollSmoother needs a valid content element.")||K.children[0];return t!==n&&(c=(n=t).getAttribute("style")||"",L&&L.observe(n),I.set(n,{overflow:"visible",width:"100%",boxSizing:"border-box",y:"+=0"}),H||I.set(n,{clearProps:"transform"})),this}return n},this.wrapper=function(e){return arguments.length?(S=q(e||"#smooth-wrapper")[0]||function _wrap(e){var t=j.querySelector(".ScrollSmoother-wrapper");return t||((t=j.createElement("div")).classList.add("ScrollSmoother-wrapper"),e.parentNode.insertBefore(t,e),t.appendChild(e)),t}(n),l=S.getAttribute("style")||"",refreshHeight(),I.set(S,H?{overflow:"hidden",position:"fixed",height:"100%",width:"100%",top:0,left:0,right:0,bottom:0}:{overflow:"visible",position:"relative",width:"100%",height:"auto",top:"auto",bottom:"auto",left:"auto",right:"auto"}),this):S},this.effects=function(e,t){if(b=b||[],!e)return b.slice(0);(e=q(e)).forEach(function(e){for(var t=b.length;t--;)b[t].trigger===e&&b[t].kill()});t=t||{};var r,n,o=t.speed,i=t.lag,s=t.effectsPadding,a=[];for(r=0;rr._dp._time,u=N,B.y=0,H&&(1===Y.isTouch&&(S.style.position="absolute"),S.scrollTop=0,1===Y.isTouch&&(S.style.position="fixed"))}},onRefresh:function onRefresh(e){e.animation.invalidate(),e.setPositions(e.start,refreshHeight()/A),h||Fa(e),B.y=-k()*A,Ga(B.y),M||(h&&(g=!1),e.animation.progress(I.utils.clamp(0,1,u/A/-e.end))),h&&(e.progress-=.001,e.update()),ScrollSmoother.isRefreshing=!1},id:"ScrollSmoother",scroller:U,invalidateOnRefresh:!0,start:0,refreshPriority:-9999,end:function end(){return refreshHeight()/A},onScrubComplete:function onScrubComplete(){z.reset(),T&&T(o)},scrub:H||!0}),this.smooth=function(e){return arguments.length&&(A=(H=e||0)&&+t.speed||1,i.scrubDuration(e)),i.getTween()?i.getTween().duration():0},i.getTween()&&(i.getTween().vars.ease=t.ease||W),this.scrollTrigger=i,t.effects&&this.effects(!0===t.effects?"[data-"+E+"speed], [data-"+E+"lag]":t.effects,{effectsPadding:t.effectsPadding,refresh:!1}),t.sections&&this.sections(!0===t.sections?"[data-section]":t.sections),O.forEach(function(e){e.vars.scroller=S,e.revert(!1,!0),e.init(e.vars,e.animation)}),this.paused=function(e,t){return arguments.length?(!!f!==e&&(e?(i.getTween()&&i.getTween().pause(),k(-N/A),z.reset(),(r=Y.normalizeScroll())&&r.disable(),(f=Y.observe({preventDefault:!0,type:"wheel,touch,scroll",debounce:!1,allowClicks:!0,onChangeY:function onChangeY(){return Ha(-N)}})).nested=X(G,"wheel,touch,scroll",!0,!1!==t)):(f.nested.kill(),f.kill(),f=0,r&&r.enable(),i.progress=(-N/A-i.start)/(i.end-i.start),Fa(i))),this):!!f},this.kill=this.revert=function(){o.paused(!1),Fa(i),i.kill();for(var e=(b||[]).concat(s||[]),t=e.length;t--;)e[t].kill();Y.scrollerProxy(S),Y.removeEventListener("killAll",Pa),Y.removeEventListener("refresh",Oa),S.style.cssText=l,n.style.cssText=c;var r=Y.defaults({});r&&r.scroller===S&&Y.defaults({scroller:U}),o.normalizer&&Y.normalizeScroll(!1),clearInterval(a),Q=null,L&&L.disconnect(),K.style.removeProperty("height"),U.removeEventListener("focusin",Ka)},this.refresh=function(e,t){return i.refresh(e,t)},P&&(this.normalizer=Y.normalizeScroll(!0===P?{debounce:!0,content:!H&&n}:P)),Y.config(t),"scrollBehavior"in U.getComputedStyle(K)&&I.set([K,G],{scrollBehavior:"auto"}),U.addEventListener("focusin",Ka),a=setInterval(Ba,250),"loading"===j.readyState||requestAnimationFrame(function(){return Y.refresh()})}r.version="3.13.0",r.create=function(e){return Q&&e&&Q.content()===q(e.content)[0]?Q:new r(e)},r.get=function(){return Q},t()&&I.registerPlugin(r),e.ScrollSmoother=r,e.default=r;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}}); 11 | 12 | -------------------------------------------------------------------------------- /js/ScrollToPlugin.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ScrollToPlugin 3.13.0 3 | * https://gsap.com 4 | * 5 | * @license Copyright 2025, GreenSock. All rights reserved. 6 | * Subject to the terms at https://gsap.com/standard-license. 7 | * @author: Jack Doyle, jack@greensock.com 8 | */ 9 | 10 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e=e||self).window=e.window||{})}(this,function(e){"use strict";function l(){return"undefined"!=typeof window}function m(){return f||l()&&(f=window.gsap)&&f.registerPlugin&&f}function n(e){return"string"==typeof e}function o(e){return"function"==typeof e}function p(e,t){var o="x"===t?"Width":"Height",n="scroll"+o,r="client"+o;return e===T||e===i||e===c?Math.max(i[n],c[n])-(T["inner"+o]||i[r]||c[r]):e[n]-e["offset"+o]}function q(e,t){var o="scroll"+("x"===t?"Left":"Top");return e===T&&(null!=e.pageXOffset?o="page"+t.toUpperCase()+"Offset":e=null!=i[o]?i:c),function(){return e[o]}}function s(e,t){if(!(e=y(e)[0])||!e.getBoundingClientRect)return console.warn("scrollTo target doesn't exist. Using 0")||{x:0,y:0};var o=e.getBoundingClientRect(),n=!t||t===T||t===c,r=n?{top:i.clientTop-(T.pageYOffset||i.scrollTop||c.scrollTop||0),left:i.clientLeft-(T.pageXOffset||i.scrollLeft||c.scrollLeft||0)}:t.getBoundingClientRect(),l={x:o.left-r.left,y:o.top-r.top};return!n&&t&&(l.x+=q(t,"x")(),l.y+=q(t,"y")()),l}function t(e,t,o,r,l){return isNaN(e)||"object"==typeof e?n(e)&&"="===e.charAt(1)?parseFloat(e.substr(2))*("-"===e.charAt(0)?-1:1)+r-l:"max"===e?p(t,o)-l:Math.min(p(t,o),s(e,t)[o]-l):parseFloat(e)-l}function u(){f=m(),l()&&f&&"undefined"!=typeof document&&document.body&&(T=window,c=document.body,i=document.documentElement,y=f.utils.toArray,f.config({autoKillThreshold:7}),h=f.config(),a=1)}var f,a,T,i,c,y,h,v,r={version:"3.13.0",name:"scrollTo",rawVars:1,register:function register(e){f=e,u()},init:function init(e,r,l,i,s){a||u();var p=this,c=f.getProperty(e,"scrollSnapType");p.isWin=e===T,p.target=e,p.tween=l,r=function _clean(e,t,r,l){if(o(e)&&(e=e(t,r,l)),"object"!=typeof e)return n(e)&&"max"!==e&&"="!==e.charAt(1)?{x:e,y:e}:{y:e};if(e.nodeType)return{y:e,x:e};var i,s={};for(i in e)s[i]="onAutoKill"!==i&&o(e[i])?e[i](t,r,l):e[i];return s}(r,i,e,s),p.vars=r,p.autoKill=!!("autoKill"in r?r:h).autoKill,p.getX=q(e,"x"),p.getY=q(e,"y"),p.x=p.xPrev=p.getX(),p.y=p.yPrev=p.getY(),v=v||f.core.globals().ScrollTrigger,"smooth"===f.getProperty(e,"scrollBehavior")&&f.set(e,{scrollBehavior:"auto"}),c&&"none"!==c&&(p.snap=1,p.snapInline=e.style.scrollSnapType,e.style.scrollSnapType="none"),null!=r.x?(p.add(p,"x",p.x,t(r.x,e,"x",p.x,r.offsetX||0),i,s),p._props.push("scrollTo_x")):p.skipX=1,null!=r.y?(p.add(p,"y",p.y,t(r.y,e,"y",p.y,r.offsetY||0),i,s),p._props.push("scrollTo_y")):p.skipY=1},render:function render(e,t){for(var o,n,r,l,i,s=t._pt,c=t.target,u=t.tween,f=t.autoKill,a=t.xPrev,y=t.yPrev,d=t.isWin,g=t.snap,x=t.snapInline;s;)s.r(e,s.d),s=s._next;o=d||!t.skipX?t.getX():a,r=(n=d||!t.skipY?t.getY():y)-y,l=o-a,i=h.autoKillThreshold,t.x<0&&(t.x=0),t.y<0&&(t.y=0),f&&(!t.skipX&&(i=Math.abs(r)?t:r}function P(){(Ae=Se.core.globals().ScrollTrigger)&&Ae.core&&function _integrate(){var e=Ae.core,r=e.bridge||{},t=e._scrollers,n=e._proxies;t.push.apply(t,ze),n.push.apply(n,Ye),ze=t,Ye=n,o=function _bridge(e,t){return r[e](t)}}()}function Q(e){return Se=e||r(),!ke&&Se&&"undefined"!=typeof document&&document.body&&(Te=window,Me=(Ce=document).documentElement,Ee=Ce.body,t=[Te,Ce,Me,Ee],Se.utils.clamp,Be=Se.core.context||function(){},Oe="onpointerenter"in Ee?"pointer":"mouse",Pe=E.isTouch=Te.matchMedia&&Te.matchMedia("(hover: none), (pointer: coarse)").matches?1:"ontouchstart"in Te||0=i,n=Math.abs(t)>=i;k&&(r||n)&&k(se,e,t,me,ye),r&&(m&&0Math.abs(t)?"x":"y",oe=!0),"y"!==ae&&(me[2]+=e,se._vx.update(e,!0)),"x"!==ae&&(ye[2]+=t,se._vy.update(t,!0)),n?ee=ee||requestAnimationFrame(kf):kf()}function nf(e){if(!hf(e,1)){var t=(e=N(e,s)).clientX,r=e.clientY,n=t-se.x,i=r-se.y,o=se.isDragging;se.x=t,se.y=r,(o||(n||i)&&(Math.abs(se.startX-t)>=a||Math.abs(se.startY-r)>=a))&&(re=o?2:1,o||(se.isDragging=!0),mf(n,i))}}function qf(e){return e.touches&&1=e)return a[n];return a[n-1]}for(n=a.length,e+=r;n--;)if(a[n]<=e)return a[n];return a[0]}:function(e,t,r){void 0===r&&(r=.001);var n=o(e);return!t||Math.abs(n-e)r&&(n*=t/100),e=e.substr(0,r-1)),e=n+(e in q?q[e]*t:~e.indexOf("%")?parseFloat(e)*t/100:parseFloat(e)||0)}return e}function Eb(e,t,r,n,i,o,a,s){var l=i.startColor,c=i.endColor,u=i.fontSize,f=i.indent,d=i.fontWeight,p=Fe.createElement("div"),g=Ma(r)||"fixed"===z(r,"pinType"),h=-1!==e.indexOf("scroller"),v=g?We:r,b=-1!==e.indexOf("start"),m=b?l:c,y="border-color:"+m+";font-size:"+u+";color:"+m+";font-weight:"+d+";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;";return y+="position:"+((h||s)&&g?"fixed;":"absolute;"),!h&&!s&&g||(y+=(n===qe?I:Y)+":"+(o+parseFloat(f))+"px;"),a&&(y+="box-sizing:border-box;text-align:left;width:"+a.offsetWidth+"px;"),p._isStart=b,p.setAttribute("class","gsap-marker-"+e+(t?" marker-"+t:"")),p.style.cssText=y,p.innerText=t||0===t?e+"-"+t:e,v.children[0]?v.insertBefore(p,v.children[0]):v.appendChild(p),p._offset=p["offset"+n.op.d2],X(p,0,n,b),p}function Jb(){return 34We.clientWidth)||(ze.cache++,v?T=T||requestAnimationFrame(Z):Z(),st||V("scrollStart"),st=at())}function Lb(){y=Xe.innerWidth,m=Xe.innerHeight}function Mb(e){ze.cache++,!0!==e&&(Ke||h||Fe.fullscreenElement||Fe.webkitFullscreenElement||b&&y===Xe.innerWidth&&!(Math.abs(Xe.innerHeight-m)>.25*Xe.innerHeight))||c.restart(!0)}function Pb(){return yb(ne,"scrollEnd",Pb)||Mt(!0)}function Sb(e){for(var t=0;tt,n=e._startClamp&&e.start>=t;(r||n)&&e.setPositions(n?t-1:e.start,r?Math.max(n?t:e.start+1,t):e.end,!0)}),$b(!1),et=0,r.forEach(function(e){return e&&e.render&&e.render(-1)}),ze.forEach(function(e){Ua(e)&&(e.smooth&&requestAnimationFrame(function(){return e.target.style.scrollBehavior="smooth"}),e.rec&&e(e.rec))}),Ub(_,1),c.pause(),Ct++,Z(rt=2),kt.forEach(function(e){return Ua(e.vars.onRefresh)&&e.vars.onRefresh(e)}),rt=ne.isRefreshing=!1,V("refresh")}else xb(ne,"scrollEnd",Pb)},j=0,Et=1,Z=function _updateAll(e){if(2===e||!rt&&!k){ne.isUpdating=!0,it&&it.update(0);var t=kt.length,r=at(),n=50<=r-D,i=t&&kt[0].scroll();if(Et=i=Ra(be,he)){if(oe&&Ae()&&!de)for(o=oe.parentNode;o&&o!==We;)o._pinOffset&&(B-=o._pinOffset,I-=o._pinOffset),o=o.parentNode}else i=nb(ae),s=he===qe,a=Ae(),Q=parseFloat(K(he.a))+w,!y&&1=I})},ke.update=function(e,t,r){if(!de||r||e){var n,i,o,a,s,l,c,u=!0===rt?re:ke.scroll(),f=e?0:(u-B)/F,d=f<0?0:1u+(u-D)/(at()-Ve)*E&&(d=.9999)),d!==p&&ke.enabled){if(a=(s=(n=ke.isActive=!!d&&d<1)!=(!!p&&p<1))||!!d!=!!p,ke.direction=p=Ra(be,he),fe)if(e||!n&&!l)pc(ae,V);else{var g=_t(ae,!0),h=u-B;pc(ae,We,g.top+(he===qe?h:0)+xt,g.left+(he===qe?0:h)+xt)}Pt(n||l?W:G),$&&d<1&&n||b(Q+(1!==d||l?0:j))}}else b(Ja(Q+j*d));!ue||A.tween||Ke||ot||te.restart(!0),T&&(s||ce&&d&&(d<1||!tt))&&Je(T.targets).forEach(function(e){return e.classList[n||ce?"add":"remove"](T.className)}),!k||ve||e||k(ke),a&&!Ke?(ve&&(c&&("complete"===o?O.pause().totalProgress(1):"reset"===o?O.restart(!0).pause():"restart"===o?O.restart(!0):O[o]()),k&&k(ke)),!s&&tt||(C&&s&&Ya(ke,C),xe[i]&&Ya(ke,xe[i]),ce&&(1===d?ke.kill(!1,1):xe[i]=0),s||xe[i=1===d?1:3]&&Ya(ke,xe[i])),pe&&!n&&Math.abs(ke.getVelocity())>(Va(pe)?pe:2500)&&(Xa(ke.callbackAnimation),ee?ee.progress(1):Xa(O,"reverse"===o?1:!d,1))):ve&&k&&!Ke&&k(ke)}if(x){var v=de?u/de.duration()*(de._caScrollDist||0):u;y(v+(q._isFlipped?1:0)),x(v)}S&&S(-u/de.duration()*(de._caScrollDist||0))}},ke.enable=function(e,t){ke.enabled||(ke.enabled=!0,xb(be,"resize",Mb),me||xb(be,"scroll",Kb),Te&&xb(ScrollTrigger,"refreshInit",Te),!1!==e&&(ke.progress=Oe=0,R=D=Ee=Ae()),!1!==t&&ke.refresh())},ke.getTween=function(e){return e&&A?A.tween:ee},ke.setPositions=function(e,t,r,n){if(de){var i=de.scrollTrigger,o=de.duration(),a=i.end-i.start;e=i.start+a*e/o,t=i.start+a*t/o}ke.refresh(!1,!1,{start:Ea(e,r&&!!ke._startClamp),end:Ea(t,r&&!!ke._endClamp)},n),ke.update()},ke.adjustPinSpacing=function(e){if(Z&&e){var t=Z.indexOf(he.d)+1;Z[t]=parseFloat(Z[t])+e+xt,Z[1]=parseFloat(Z[1])+e+xt,Pt(Z)}},ke.disable=function(e,t){if(ke.enabled&&(!1!==e&&ke.revert(!0,!0),ke.enabled=ke.isActive=!1,t||ee&&ee.pause(),re=0,n&&(n.uncache=1),Te&&yb(ScrollTrigger,"refreshInit",Te),te&&(te.pause(),A.tween&&A.tween.kill()&&(A.tween=0)),!me)){for(var r=kt.length;r--;)if(kt[r].scroller===be&&kt[r]!==ke)return;yb(be,"resize",Mb),me||yb(be,"scroll",Kb)}},ke.kill=function(e,t){ke.disable(e,t),ee&&!t&&ee.kill(),a&&delete Tt[a];var r=kt.indexOf(ke);0<=r&&kt.splice(r,1),r===je&&0o&&(b()>o?a.progress(1)&&b(o):a.resetTo("scrollY",o))}Wa(e)||(e={}),e.preventDefault=e.isNormalizer=e.allowClicks=!0,e.type||(e.type="wheel,touch"),e.debounce=!!e.debounce,e.id=e.id||"normalizer";var n,o,l,i,a,c,u,s,f=e.normalizeScrollX,t=e.momentum,r=e.allowNestedScroll,d=e.onRelease,p=J(e.target)||Ue,g=Ne.core.globals().ScrollSmoother,h=g&&g.get(),v=R&&(e.content&&J(e.content)||h&&!1!==e.content&&!h.smooth()&&h.content()),b=L(p,qe),m=L(p,He),y=1,x=(E.isTouch&&Xe.visualViewport?Xe.visualViewport.scale*Xe.visualViewport.width:Xe.outerWidth)/Xe.innerWidth,_=0,w=Ua(t)?function(){return t(n)}:function(){return t||2.8},S=yc(p,e.type,!0,r),k=Ia,T=Ia;return v&&Ne.set(v,{y:"+=0"}),e.ignoreCheck=function(e){return R&&"touchmove"===e.type&&function ignoreDrag(){if(i){requestAnimationFrame(Gq);var e=Ja(n.deltaY/2),t=T(b.v-e);if(v&&t!==b.v+b.offset){b.offset=t-b.v;var r=Ja((parseFloat(v&&v._gsap.y)||0)-b.offset);v.style.transform="matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, "+r+", 0, 1)",v._gsap.y=r+"px",b.cacheID=ze.cache,Z()}return!0}b.offset&&Kq(),i=!0}()||1.05=o||o-1<=r)&&Ne.to({},{onUpdate:Qq,duration:i})}else s.restart(!0);d&&d(e)},e.onWheel=function(){a._ts&&a.pause(),1e3G||U.register(window.gsap),V=typeof Intl!="undefined"?new Intl.Segmenter:0,P=e=>typeof e=="string"?P(document.querySelectorAll(e)):"length"in e?Array.from(e):[e],X=e=>P(e).filter(t=>t instanceof HTMLElement),J=[],K=function(){},oe=/\s+/g,Y=new RegExp("\\p{RI}\\p{RI}|\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?(\\u{200D}\\p{Emoji}(\\p{EMod}|\\u{FE0F}\\u{20E3}?|[\\u{E0020}-\\u{E007E}]+\\u{E007F})?)*|.","gu"),Z={left:0,top:0,width:0,height:0},ee=(e,t)=>{if(t){let s=new Set(e.join("").match(t)||J),i=e.length,o,c,n,a;if(s.size)for(;--i>-1;){c=e[i];for(n of s)if(n.startsWith(c)&&n.length>c.length){for(o=0,a=c;n.startsWith(a+=e[i+ ++o])&&a.lengthwindow.getComputedStyle(e).display==="inline"&&(e.style.display="inline-block"),z=(e,t,s)=>t.insertBefore(typeof e=="string"?document.createTextNode(e):e,s),Q=(e,t,s)=>{let i=t[e+"sClass"]||"",{tag:o="div",aria:c="auto",propIndex:n=!1}=t,a=e==="line"?"block":"inline-block",h=i.indexOf("++")>-1,b=x=>{let g=document.createElement(o),C=s.length+1;return i&&(g.className=i+(h?" "+i+C:"")),n&&g.style.setProperty("--"+e,C+""),c!=="none"&&g.setAttribute("aria-hidden","true"),o!=="span"&&(g.style.position="relative",g.style.display=a),g.textContent=x,s.push(g),g};return h&&(i=i.replace("++","")),b.collection=s,b},ae=(e,t,s,i)=>{let o=Q("line",s,i),c=window.getComputedStyle(e).textAlign||"left";return(n,a)=>{let h=o("");for(h.style.textAlign=c,e.insertBefore(h,t[n]);n{var x;let g=Array.from(e.childNodes),C=0,{wordDelimiter:R,reduceWhiteSpace:L=!0,prepareText:$}=t,q=e.getBoundingClientRect(),j=q,D=!L&&window.getComputedStyle(e).whiteSpace.substring(0,3)==="pre",E=0,v=s.collection,r,f,H,l,m,y,I,d,u,W,S,O,T,F,w,p,k,N;for(typeof R=="object"?(H=R.delimiter||R,f=R.replaceWith||""):f=R===""?"":R||" ",r=f!==" ";C-1?(y=v[v.length-1],y.appendChild(document.createTextNode(i?"":p))):(y=s(i?"":p),z(y,e,l),E&&u===1&&!I&&y.insertBefore(E,y.firstChild)),i)for(S=V?ee([...V.segment(p)].map(M=>M.segment),h):p.match(a)||J,N=0;Nj.top&&W.left<=j.left){for(O=e.cloneNode(),T=e.childNodes[0];T&&T!==y;)F=T,T=T.nextSibling,O.appendChild(F);e.parentNode.insertBefore(O,e),o&&te(O)}j=W}(u=m.length?" ":r&&p.slice(-1)===" "?" "+f:f,e,l)}e.removeChild(l),E=0}else l.nodeType===1&&(n&&n.indexOf(l)>-1?(v.indexOf(l.previousSibling)>-1&&v[v.length-1].appendChild(l),E=l):(ie(l,t,s,i,o,c,n,a,h,!0),E=0),o&&te(l))};const ne=class se{constructor(t,s){this.isSplit=!1,re(),this.elements=X(t),this.chars=[],this.words=[],this.lines=[],this.masks=[],this.vars=s,this._split=()=>this.isSplit&&this.split(this.vars);let i=[],o,c=()=>{let n=i.length,a;for(;n--;){a=i[n];let h=a.element.offsetWidth;if(h!==a.width){a.width=h,this._split();return}}};this._data={orig:i,obs:typeof ResizeObserver!="undefined"&&new ResizeObserver(()=>{clearTimeout(o),o=setTimeout(c,200)})},K(this),this.split(s)}split(t){this.isSplit&&this.revert(),this.vars=t=t||this.vars||{};let{type:s="chars,words,lines",aria:i="auto",deepSlice:o=!0,smartWrap:c,onSplit:n,autoSplit:a=!1,specialChars:h,mask:b}=this.vars,x=s.indexOf("lines")>-1,g=s.indexOf("chars")>-1,C=s.indexOf("words")>-1,R=g&&!C&&!x,L=h&&("push"in h?new RegExp("(?:"+h.join("|")+")","gu"):h),$=L?new RegExp(L.source+"|"+Y.source,"gu"):Y,q=!!t.ignore&&X(t.ignore),{orig:j,animTime:D,obs:E}=this._data,v;return(g||C||x)&&(this.elements.forEach((r,f)=>{j[f]={element:r,html:r.innerHTML,ariaL:r.getAttribute("aria-label"),ariaH:r.getAttribute("aria-hidden")},i==="auto"?r.setAttribute("aria-label",(r.textContent||"").trim()):i==="hidden"&&r.setAttribute("aria-hidden","true");let H=[],l=[],m=[],y=g?Q("char",t,H):null,I=Q("word",t,l),d,u,W,S;if(ie(r,t,I,y,R,o&&(x||R),q,$,L,!1),x){let O=P(r.childNodes),T=ae(r,O,t,m),F,w=[],p=0,k=O.map(M=>M.nodeType===1?M.getBoundingClientRect():Z),N=Z;for(d=0;dN.top&&k[d].left<=N.left&&(T(p,d),p=d),N=k[d]));p{var le;return(le=M.parentNode)==null?void 0:le.removeChild(M)})}if(!C){for(d=0;d{let f=r.cloneNode();return r.replaceWith(f),f.appendChild(r),r.className&&(f.className=r.className.replace(/(\b\w+\b)/g,"$1-mask")),f.style.overflow="clip",f}))),this.isSplit=!0,_&&(a?_.addEventListener("loadingdone",this._split):_.status==="loading"&&console.warn("SplitText called before fonts loaded")),(v=n&&n(this))&&v.totalTime&&(this._data.anim=D?v.totalTime(D):v),x&&a&&this.elements.forEach((r,f)=>{j[f].width=r.offsetWidth,E&&E.observe(r)}),this}revert(){var t,s;let{orig:i,anim:o,obs:c}=this._data;return c&&c.disconnect(),i.forEach(({element:n,html:a,ariaL:h,ariaH:b})=>{n.innerHTML=a,h?n.setAttribute("aria-label",h):n.removeAttribute("aria-label"),b?n.setAttribute("aria-hidden",b):n.removeAttribute("aria-hidden")}),this.chars.length=this.words.length=this.lines.length=i.length=this.masks.length=0,this.isSplit=!1,_==null||_.removeEventListener("loadingdone",this._split),o&&(this._data.animTime=o.totalTime(),o.revert()),(s=(t=this.vars).onRevert)==null||s.call(t,this),this}static create(t,s){return new se(t,s)}static register(t){A=A||t||window.gsap,A&&(P=A.utils.toArray,K=A.core.context||K),!G&&window.innerWidth>0&&(_=document.fonts,G=!0)}};ne.version="3.13.0";let U=ne;B.SplitText=U,B.default=U,Object.defineProperty(B,"__esModule",{value:!0})}); 12 | -------------------------------------------------------------------------------- /js/gsap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * GSAP 3.13.0 3 | * https://gsap.com 4 | * 5 | * @license Copyright 2025, GreenSock. All rights reserved. 6 | * Subject to the terms at https://gsap.com/standard-license. 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))&&ze}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 Zt(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 za(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 Aa(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Ba(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 Da(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 Fa(t){return t._repeat?Tt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ha(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ia(t){return t._end=ja(t._start+(t._tDur/Math.abs(t._ts||t._rts||U)||0))}function Ja(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=ja(r._time-(0U)&&e.render(r,!0)),Ba(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 ub(t){return Aa(t),t.scrollTrigger&&t.scrollTrigger.kill(!!L),t.progress()<1&&Pt(t,"onInterrupt"),t}function xb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:T,render:ue,add:Vt,kill:de,modifier:he,rawVars:0},a={targetTest:0,get:0,getSetter:ie,aliases:{},register:0};if(Ft(),t!==i){if(pt[e])return;ra(i,ra(va(t,n),a)),yt(i.prototype,yt(n,va(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(ze,i,ge)}else Ct.push(t)}function Ab(t,e,r){return(6*(t+=t<0?1:1>16,e>>8&St,e&St]:0:Dt.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Dt[e])p=Dt[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=c=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=N?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 Jt?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):(t.parent===this&&za(this,t),t===this._recent&&(this._recent=this._last),Ba(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=Jt.to(i,ra({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())||U,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&&Sa(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,ra({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),sb(this,xt(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),sb(this,xt(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+U)},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 Ba(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={}),Ba(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=N;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,La(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;Sa(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&&(oa(I,Ha(t,I)),f=Rt.frame),Rt.frame>=mt){mt+=X.autoSleep||120;var e=I._first;if((!e||!e._ts)&&X.autoSleep&&Rt._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||Rt.sleep()}}},Timeline}(Nt);ra(Qt.prototype,{_lock:0,_hasPause:0,_forcing:0});function bc(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=Gt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||J(t))return r(t)?Gt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Gt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new ge(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==d))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function hc(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 Ut,qt,Vt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var d,c=t[e],p="get"!==i?i:s(c)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():c,_=s(c)?l?re:te:$t;if(r(n)&&(~n.indexOf("random(")&&(n=pb(n)),"="===n.charAt(1)&&(!(d=ka(p,n)+(Za(p)||0))&&0!==d||(n=d))),!f||p!==n||qt)return isNaN(p*n)||""===n?(c||e in t||Q(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new ge(this._pt,t,e,0,1,oe,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=pb(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++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?ka(d,l)-d:parseFloat(l)-d,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||hc(l,x[l],u,x.easeEach);for(l in u)for(C=u[l].sort(function(t,e){return t.t-e.t}),o=z=0;o=t._tDur||e<0)&&t.ratio===u&&(u&&Aa(t,1),r||L||(Pt(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),E.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){c||Rt.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Wt(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,d=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!d)for(d=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return qt=1,t.vars[e]="+=0",Wt(t,s),qt=0,o?R(e+" not eligible for reset"):1;d.push(u)}for(f=d.length;f--;)(u=(h=d[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ia(r)+Za(h.e)),h.b&&(h.b=u.s+Za(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(Ja(this,0),this.parent||ya(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?ub(this):this.scrollTrigger&&this.scrollTrigger.kill(!!L),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Ut&&!0!==Ut.vars.overwrite)._first||ub(this),this.parent&&i!==this.timeline.totalDuration()&&Sa(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,d=t?Ot(t):f,c=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,d))return"all"===e&&(this._pt=0),ub(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(~d.indexOf(f[l]))for(u in a=c[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)||za(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&ub(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Wa(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 Wa(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}(Nt);ra(Jt.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ha("staggerTo,staggerFrom,staggerFromTo",function(r){Jt[r]=function(){var t=new Qt,e=kt.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function pc(t,e,r){return t.setAttribute(e,r)}function xc(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)},ie=function _getSetter(t,e){return s(t[e])?te:u(t[e])&&t.setAttribute?pc:$t},ne=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)},oe=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)},ue=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},he=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},de=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?za(this,i,"_pt"):i.dep||(e=1),i=r;return!e},_e=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},ge=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=xc,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||ne,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=Jt,ot.TimelineLite=ot.TimelineMax=Qt,I=new Qt({sortChildren:!1,defaults:Z,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),X.stringFilter=Gb;function Fc(t){return(be[t]||Me).map(function(t){return t()})}function Gc(){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 | // Import utility function for preloading images 2 | import { preloadImages } from './utils.js'; 3 | 4 | // Register the GSAP plugins 5 | gsap.registerPlugin(ScrollTrigger, ScrollSmoother, ScrollToPlugin, SplitText); 6 | 7 | // Initialize GSAP's ScrollSmoother for smooth scrolling and scroll-based effects 8 | const smoother = ScrollSmoother.create({ 9 | smooth: 1, // How long (in seconds) it takes to "catch up" 10 | effects: true, // Enable data-speed and data-lag-based scroll effects 11 | normalizeScroll: true, // Normalizes scroll behavior across browsers 12 | }); 13 | 14 | // Reference to the container that wraps all the 3D scene elements 15 | const sceneWrapper = document.querySelector('.scene-wrapper'); 16 | 17 | // Global flag to prevent multiple animations from overlapping or triggering at once 18 | let isAnimating = false; 19 | 20 | // A Map to store SplitText instances keyed by DOM elements (used for animating text characters) 21 | const splitMap = new Map(); 22 | 23 | /** 24 | * Returns an array of transform strings to evenly space carousel cells in 3D 25 | * 26 | * @param {number} count - Number of carousel cells 27 | * @param {number} radius - Radius of the circular layout 28 | * @returns {string[]} Array of transform strings for each cell 29 | */ 30 | const getCarouselCellTransforms = (count, radius) => { 31 | const angleStep = 360 / count; // Divide 360° by number of cells to get angle step 32 | return Array.from({ length: count }, (_, i) => { 33 | const angle = i * angleStep; 34 | return `rotateY(${angle}deg) translateZ(${radius}px)`; // 3D rotation + translation 35 | }); 36 | }; 37 | 38 | /** 39 | * Applies 3D transforms to each cell in a given carousel 40 | * 41 | * @param {Element} carousel - DOM element representing the carousel 42 | * @returns {void} 43 | */ 44 | const setupCarouselCells = (carousel) => { 45 | const wrapper = carousel.closest('.scene'); 46 | const radius = parseFloat(wrapper.dataset.radius) || 500; // Read radius from data attribute or default to 500 47 | const cells = carousel.querySelectorAll('.carousel__cell'); 48 | 49 | const transforms = getCarouselCellTransforms(cells.length, radius); // Get transform strings 50 | cells.forEach((cell, i) => { 51 | cell.style.transform = transforms[i]; // Apply transform to each cell 52 | }); 53 | }; 54 | 55 | /** 56 | * Creates a scroll-linked GSAP timeline for a given carousel scene 57 | * 58 | * @param {Element} carousel - DOM element of the carousel 59 | * @returns {GSAPTimeline} Scroll-driven animation timeline 60 | */ 61 | const createScrollAnimation = (carousel) => { 62 | const wrapper = carousel.closest('.scene'); 63 | const cards = carousel.querySelectorAll('.card'); 64 | const titleSpan = wrapper.querySelector('.scene__title span'); 65 | const split = splitMap.get(titleSpan); 66 | const chars = split?.chars || []; 67 | 68 | // Create scroll-driven timeline 69 | carousel._timeline = gsap.timeline({ 70 | defaults: { ease: 'sine.inOut' }, 71 | scrollTrigger: { 72 | trigger: wrapper, 73 | start: 'top bottom', // Start when top of wrapper hits bottom of viewport 74 | end: 'bottom top', // End when bottom of wrapper hits top of viewport 75 | scrub: true, // Smooth animation based on scroll position 76 | }, 77 | }); 78 | 79 | carousel._timeline 80 | .fromTo(carousel, { rotationY: 0 }, { rotationY: -180 }, 0) // Rotate carousel horizontally 81 | .fromTo( 82 | carousel, 83 | { rotationZ: 3, rotationX: 3 }, 84 | { rotationZ: -3, rotationX: -3 }, 85 | 0 86 | ) // Subtle 3D tilt 87 | .fromTo( 88 | cards, 89 | { filter: 'brightness(250%)' }, 90 | { filter: 'brightness(80%)', ease: 'power3' }, 91 | 0 92 | ) // Brightness dimming 93 | .fromTo(cards, { rotationZ: 10 }, { rotationZ: -10, ease: 'none' }, 0); // Rotate cards around Z 94 | 95 | // Animate title characters in on scroll 96 | if (chars.length > 0) { 97 | animateChars(chars, 'in', { 98 | scrollTrigger: { 99 | trigger: wrapper, 100 | start: 'top center', 101 | toggleActions: 'play none none reverse', 102 | }, 103 | }); 104 | } 105 | 106 | return carousel._timeline; 107 | }; 108 | 109 | /** 110 | * Initializes SplitText instances on key animated elements 111 | * 112 | * @returns {void} 113 | */ 114 | const initTextsSplit = () => { 115 | document 116 | .querySelectorAll( 117 | '.scene__title span, .preview__title span, .preview__close' 118 | ) 119 | .forEach((span) => { 120 | const split = SplitText.create(span, { 121 | type: 'chars', // Split by characters 122 | charsClass: 'char', // Assign class to each character 123 | autoSplit: true, // Revert and re-split whenever the fonts finish loading 124 | }); 125 | splitMap.set(span, split); // Store split instance for reuse 126 | }); 127 | }; 128 | 129 | /** 130 | * Returns interpolated rotation values based on scroll progress 131 | * 132 | * @param {number} progress - Scroll progress (0 to 1) 133 | * @returns {Object} Object with interpolated rotationX, rotationY, rotationZ values 134 | */ 135 | const getInterpolatedRotation = (progress) => ({ 136 | rotationY: gsap.utils.interpolate(0, -180, progress), // Horizontal spin from 0° to -180° 137 | rotationX: gsap.utils.interpolate(3, -3, progress), // Tilt forward/backward 138 | rotationZ: gsap.utils.interpolate(3, -3, progress), // Z-axis twist 139 | }); 140 | 141 | /** 142 | * Animates a single grid item into view with position, scale, and 3D depth 143 | * 144 | * @param {Element} el - DOM element to animate 145 | * @param {number} dx - Horizontal distance from center 146 | * @param {number} dy - Vertical distance from center 147 | * @param {number} rotationY - Y-axis rotation direction 148 | * @param {number} delay - Delay before animation starts 149 | * @returns {void} 150 | */ 151 | const animateGridItemIn = (el, dx, dy, rotationY, delay) => { 152 | // Animate 2D transform and opacity 153 | gsap.fromTo( 154 | el, 155 | { 156 | transformOrigin: `% 50% ${dx > 0 ? -dx * 0.8 : dx * 0.8}px`, 157 | //x: dx, // Offset based on distance from center 158 | autoAlpha: 0, 159 | y: dy * 0.5, // Slight vertical offset 160 | scale: 0.5, // Scaled down 161 | rotationY: dx < 0 ? rotationY : rotationY, // Rotate in from left/right 162 | }, 163 | { 164 | //x: 0, 165 | y: 0, 166 | scale: 1, 167 | rotationY: 0, 168 | autoAlpha: 1, 169 | duration: 0.4, 170 | ease: 'sine', 171 | delay: delay + 0.1, 172 | } 173 | ); 174 | 175 | // Animate z-position separately for 3D pop 176 | gsap.fromTo( 177 | el, 178 | { z: -3500 }, 179 | { 180 | z: 0, 181 | duration: 0.3, 182 | ease: 'expo', 183 | delay, 184 | } 185 | ); 186 | }; 187 | 188 | /** 189 | * Animates a single grid item out of view with depth and fade 190 | * 191 | * @param {Element} el - DOM element to animate 192 | * @param {number} dx - Horizontal distance from center 193 | * @param {number} dy - Vertical distance from center 194 | * @param {number} rotationY - Y-axis rotation direction 195 | * @param {number} delay - Delay before animation starts 196 | * @param {boolean} isLast - Whether this is the last item (for onComplete) 197 | * @param {Function} [onComplete] - Callback when animation finishes 198 | * @returns {void} 199 | */ 200 | const animateGridItemOut = ( 201 | el, 202 | dx, 203 | dy, 204 | rotationY, 205 | delay, 206 | isLast, 207 | onComplete 208 | ) => { 209 | // Animate 2D transform and opacity 210 | gsap.to(el, { 211 | startAt: { 212 | transformOrigin: `50% 50% ${dx > 0 ? -dx * 0.8 : dx * 0.8}px`, 213 | }, 214 | //x: dx, 215 | y: dy * 0.4, 216 | rotationY: dx < 0 ? rotationY : rotationY, 217 | scale: 0.4, 218 | autoAlpha: 0, 219 | duration: 0.4, 220 | ease: 'sine.in', 221 | delay, 222 | }); 223 | gsap.to(el, { 224 | z: -3500, 225 | duration: 0.4, 226 | ease: 'expo.in', 227 | delay: delay + 0.9, 228 | onComplete: isLast ? onComplete : undefined, // Call onComplete only for the last item 229 | }); 230 | }; 231 | 232 | /** 233 | * Animates all grid items in or out with a distance-based stagger and easing 234 | * 235 | * @param {Object} options 236 | * @param {NodeList} options.items - Collection of grid item DOM elements 237 | * @param {number} options.centerX - X-coordinate of the center 238 | * @param {number} options.centerY - Y-coordinate of the center 239 | * @param {'in' | 'out'} [options.direction='in'] - Animation direction 240 | * @param {Function} [options.onComplete] - Callback after all animations complete 241 | * @returns {void} 242 | */ 243 | const animateGridItems = ({ 244 | items, 245 | centerX, 246 | centerY, 247 | direction = 'in', 248 | onComplete, 249 | }) => { 250 | // Measure position of each item and calculate distance from center 251 | const itemData = Array.from(items).map((el) => { 252 | const rect = el.getBoundingClientRect(); 253 | const elCenterX = rect.left + rect.width / 2; 254 | const elCenterY = rect.top + rect.height / 2; 255 | const dx = centerX - elCenterX; 256 | const dy = centerY - elCenterY; 257 | const dist = Math.hypot(dx, dy); // Euclidean distance from center 258 | const isLeft = elCenterX < centerX; 259 | return { el, dx, dy, dist, isLeft }; 260 | }); 261 | 262 | const maxDist = Math.max(...itemData.map((d) => d.dist)); // Farthest distance 263 | const totalStagger = 0.025 * (itemData.length - 1); // Total stagger duration 264 | 265 | let latest = { delay: -1, el: null }; // Track latest delay item 266 | 267 | itemData.forEach(({ el, dx, dy, dist, isLeft }) => { 268 | const norm = maxDist ? dist / maxDist : 0; // Normalize distance 269 | const exponential = Math.pow(direction === 'in' ? 1 - norm : norm, 1); // Easing 270 | const delay = exponential * totalStagger; 271 | const rotationY = isLeft ? 100 : -100; // Directional rotation 272 | 273 | if (direction === 'in') { 274 | animateGridItemIn(el, dx, dy, rotationY, delay); 275 | } else { 276 | if (delay > latest.delay) { 277 | latest = { delay, el }; 278 | } 279 | animateGridItemOut(el, dx, dy, rotationY, delay, false, onComplete); 280 | } 281 | }); 282 | 283 | // Ensure onComplete runs only after the last item finishes 284 | if (direction === 'out' && latest.el) { 285 | const { el, dx, dy, isLeft } = itemData.find((d) => d.el === latest.el); 286 | const rotationY = isLeft ? 100 : -100; 287 | animateGridItemOut(el, dx, dy, rotationY, latest.delay, true, onComplete); 288 | } 289 | }; 290 | 291 | /** 292 | * Animates all grid items in the preview into view 293 | * 294 | * @param {Element} preview - Preview DOM element containing grid items 295 | * @returns {void} 296 | */ 297 | const animatePreviewGridIn = (preview) => { 298 | const items = preview.querySelectorAll('.grid__item'); 299 | // Clear any inline styles from previous animations 300 | gsap.set(items, { clearProps: 'all' }); 301 | // Trigger grid item entrance animation from center of screen 302 | animateGridItems({ 303 | items, 304 | centerX: window.innerWidth / 2, 305 | centerY: window.innerHeight / 2, 306 | direction: 'in', 307 | }); 308 | }; 309 | 310 | /** 311 | * Animates all grid items in the preview out of view 312 | * @param {HTMLElement} preview - The preview container 313 | */ 314 | const animatePreviewGridOut = (preview) => { 315 | const items = preview.querySelectorAll('.grid__item'); 316 | // Trigger grid item exit animation toward edges 317 | const onComplete = () => 318 | gsap.set(preview, { pointerEvents: 'none', autoAlpha: 0 }); 319 | animateGridItems({ 320 | items, 321 | centerX: window.innerWidth / 2, 322 | centerY: window.innerHeight / 2, 323 | direction: 'out', 324 | onComplete, 325 | }); 326 | }; 327 | 328 | /** 329 | * Retrieves relevant DOM elements and text splits from a scene title 330 | * @param {HTMLElement} titleEl - The `.scene__title` element 331 | * @returns {Object} wrapper, carousel, cards, span, chars 332 | */ 333 | const getSceneElementsFromTitle = (titleEl) => { 334 | const wrapper = titleEl.closest('.scene'); // Scene container 335 | const carousel = wrapper?.querySelector('.carousel'); // Carousel in the scene 336 | const cards = carousel?.querySelectorAll('.card'); // All card elements 337 | const span = titleEl.querySelector('span'); // Title span 338 | const chars = splitMap.get(span)?.chars || []; // SplitText chars 339 | return { wrapper, carousel, cards, span, chars }; 340 | }; 341 | 342 | /** 343 | * Retrieves scene-related elements from a preview element 344 | * @param {HTMLElement} previewEl - The `.preview` element 345 | * @returns {Object} All scene elements and corresponding titleEl 346 | */ 347 | const getSceneElementsFromPreview = (previewEl) => { 348 | const previewId = `#${previewEl.id}`; 349 | const titleLink = document.querySelector( 350 | `.scene__title a[href="${previewId}"]` 351 | ); 352 | const titleEl = titleLink?.closest('.scene__title'); // Corresponding title element 353 | return { ...getSceneElementsFromTitle(titleEl), titleEl }; 354 | }; 355 | 356 | /** 357 | * Animates SplitText character elements in or out 358 | * 359 | * @param {HTMLElement[]} chars - Array of character elements to animate 360 | * @param {'in' | 'out'} direction - Direction of the animation ('in' for fade in, 'out' for fade out) 361 | * @param {Object} [opts={}] - Optional GSAP config overrides (e.g. scrollTrigger) 362 | */ 363 | const animateChars = (chars, direction = 'in', opts = {}) => { 364 | const base = { 365 | autoAlpha: direction === 'in' ? 1 : 0, 366 | duration: 0.02, 367 | ease: 'none', 368 | stagger: { each: 0.04, from: direction === 'in' ? 'start' : 'end' }, 369 | ...opts, 370 | }; 371 | 372 | gsap.fromTo(chars, { autoAlpha: direction === 'in' ? 0 : 1 }, base); 373 | }; 374 | 375 | /** 376 | * Animates title and close button characters in a preview 377 | * 378 | * @param {HTMLElement} preview - The preview container 379 | * @param {'in' | 'out'} direction - Animation direction 380 | * @param {string} [selector='.preview__title span, .preview__close'] - Selector for elements to animate 381 | */ 382 | const animatePreviewTexts = ( 383 | preview, 384 | direction = 'in', 385 | selector = '.preview__title span, .preview__close' 386 | ) => { 387 | preview.querySelectorAll(selector).forEach((el) => { 388 | const chars = splitMap.get(el)?.chars || []; 389 | animateChars(chars, direction); 390 | }); 391 | }; 392 | 393 | /** 394 | * Handles transition from carousel view to preview grid 395 | * 396 | * @param {Event} e - Click event triggered from `.scene__title` 397 | */ 398 | const activatePreviewFromCarousel = (e) => { 399 | e.preventDefault(); 400 | if (isAnimating) return; 401 | isAnimating = true; 402 | 403 | const titleEl = e.currentTarget; 404 | const { wrapper, carousel, cards, chars } = 405 | getSceneElementsFromTitle(titleEl); 406 | 407 | // Calculate scroll position to center the scene 408 | const offsetTop = wrapper.getBoundingClientRect().top + window.scrollY; 409 | const targetY = offsetTop - window.innerHeight / 2 + wrapper.offsetHeight / 2; 410 | 411 | // Temporarily disable scroll-based animations 412 | ScrollTrigger.getAll().forEach((t) => t.disable(false)); 413 | 414 | gsap 415 | .timeline({ 416 | defaults: { duration: 1.5, ease: 'power2.inOut' }, 417 | onComplete: () => { 418 | isAnimating = false; 419 | ScrollTrigger.getAll().forEach((t) => t.enable()); 420 | carousel._timeline.scrollTrigger.scroll(targetY); 421 | }, 422 | }) 423 | .to(window, { 424 | onStart: () => { 425 | lockUserScroll(); 426 | }, 427 | onComplete: () => { 428 | unlockUserScroll(); 429 | smoother.paused(true); 430 | }, 431 | scrollTo: { y: targetY, autoKill: true }, 432 | }) 433 | .to( 434 | chars, 435 | { 436 | autoAlpha: 0, 437 | duration: 0.02, 438 | ease: 'none', 439 | stagger: { each: 0.04, from: 'end' }, 440 | }, 441 | 0 442 | ) 443 | .to(carousel, { rotationX: 90, rotationY: -360, z: -2000 }, 0) 444 | .to( 445 | carousel, 446 | { 447 | duration: 2.5, 448 | ease: 'power3.inOut', 449 | z: 1500, 450 | rotationZ: 270, 451 | onComplete: () => gsap.set(sceneWrapper, { autoAlpha: 0 }), 452 | }, 453 | 0.7 454 | ) 455 | .to(cards, { rotationZ: 0 }, 0) 456 | .add(() => { 457 | const previewSelector = titleEl.querySelector('a')?.getAttribute('href'); 458 | const preview = document.querySelector(previewSelector); 459 | gsap.set(preview, { pointerEvents: 'auto', autoAlpha: 1 }); 460 | animatePreviewGridIn(preview); 461 | animatePreviewTexts(preview, 'in'); 462 | }, '<+=1.9'); 463 | }; 464 | 465 | /** 466 | * Handles transition from preview grid back to carousel view 467 | * 468 | * @param {Event} e - Click event triggered from `.preview__close` 469 | */ 470 | const deactivatePreviewToCarousel = (e) => { 471 | if (isAnimating) return; 472 | isAnimating = true; 473 | 474 | const preview = e.currentTarget.closest('.preview'); 475 | if (!preview) return; 476 | 477 | const { carousel, cards, chars } = getSceneElementsFromPreview(preview); 478 | 479 | animatePreviewTexts(preview, 'out'); 480 | animatePreviewGridOut(preview); 481 | 482 | gsap.set(sceneWrapper, { autoAlpha: 1 }); 483 | 484 | const progress = 0.5; // halfway 485 | /* 486 | BUG: progress should always be 0.5 but for some reason it's 0 sometimes 487 | const timeline = carousel._timeline; 488 | const scrollTrigger = timeline?.scrollTrigger; 489 | const progress = scrollTrigger?.progress ?? 0; 490 | */ 491 | 492 | const { rotationX, rotationY, rotationZ } = getInterpolatedRotation(progress); 493 | 494 | gsap 495 | .timeline({ 496 | delay: 0.7, 497 | defaults: { duration: 1.3, ease: 'expo' }, 498 | onComplete: () => { 499 | smoother.paused(false); 500 | isAnimating = false; 501 | }, 502 | }) 503 | .fromTo( 504 | chars, 505 | { autoAlpha: 0 }, 506 | { 507 | autoAlpha: 1, 508 | duration: 0.02, 509 | ease: 'none', 510 | stagger: { each: 0.04, from: 'start' }, 511 | } 512 | ) 513 | .fromTo( 514 | carousel, 515 | { 516 | z: -550, 517 | rotationX, 518 | rotationY: -720, 519 | rotationZ, 520 | yPercent: 300, 521 | }, 522 | { 523 | rotationY, 524 | yPercent: 0, 525 | }, 526 | 0 527 | ) 528 | .fromTo(cards, { autoAlpha: 0 }, { autoAlpha: 1 }, 0.3); 529 | }; 530 | 531 | /** 532 | * Adds click event listeners to scene titles and preview close buttons 533 | * 534 | * @returns {void} 535 | */ 536 | const initEventListeners = () => { 537 | // When a scene title is clicked, activate the preview 538 | document.querySelectorAll('.scene__title').forEach((title) => { 539 | title.addEventListener('click', activatePreviewFromCarousel); 540 | }); 541 | 542 | // When a preview close button is clicked, deactivate the preview 543 | document.querySelectorAll('.preview__close').forEach((btn) => { 544 | btn.addEventListener('click', deactivatePreviewToCarousel); 545 | }); 546 | }; 547 | 548 | /** 549 | * Initializes all carousels on the page 550 | * 551 | * @returns {void} 552 | */ 553 | const initCarousels = () => { 554 | document.querySelectorAll('.carousel').forEach((carousel) => { 555 | setupCarouselCells(carousel); // Position carousel cells in 3D 556 | carousel._timeline = createScrollAnimation(carousel); // Attach scroll animation timeline 557 | }); 558 | }; 559 | 560 | function preventScroll(e) { 561 | e.preventDefault(); 562 | } 563 | 564 | function lockUserScroll() { 565 | window.addEventListener('wheel', preventScroll, { passive: false }); 566 | window.addEventListener('touchmove', preventScroll, { passive: false }); 567 | window.addEventListener('keydown', preventArrowScroll, false); 568 | } 569 | 570 | function unlockUserScroll() { 571 | window.removeEventListener('wheel', preventScroll); 572 | window.removeEventListener('touchmove', preventScroll); 573 | window.removeEventListener('keydown', preventArrowScroll); 574 | } 575 | 576 | function preventArrowScroll(e) { 577 | const keys = [ 578 | 'ArrowUp', 579 | 'ArrowDown', 580 | 'PageUp', 581 | 'PageDown', 582 | 'Home', 583 | 'End', 584 | ' ', 585 | ]; 586 | if (keys.includes(e.key)) e.preventDefault(); 587 | } 588 | 589 | /** 590 | * Initializes text splitting, carousels, and event listeners 591 | * 592 | * @returns {void} 593 | */ 594 | const init = () => { 595 | initTextsSplit(); // Prepare character-level splits for animations 596 | initCarousels(); // Set up carousels with transforms and scroll triggers 597 | initEventListeners(); // Bind all interactive handlers 598 | window.addEventListener('resize', ScrollTrigger.refresh); // Refresh triggers on resize 599 | }; 600 | 601 | // Start app once images are preloaded 602 | preloadImages('.grid__item-image').then(() => { 603 | document.body.classList.remove('loading'); // Remove loading state from body 604 | init(); // Begin initialization 605 | }); 606 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Preloads images specified by the CSS selector. 3 | * @function 4 | * @param {string} [selector='img'] - CSS selector for target images. 5 | * @returns {Promise} - Resolves when all specified images are loaded. 6 | */ 7 | const preloadImages = (selector = 'img') => { 8 | return new Promise((resolve) => { 9 | // The imagesLoaded library is used to ensure all images (including backgrounds) are fully loaded. 10 | imagesLoaded(document.querySelectorAll(selector), {background: true}, resolve); 11 | }); 12 | }; 13 | 14 | // Exporting utility functions for use in other modules. 15 | export { 16 | preloadImages 17 | }; --------------------------------------------------------------------------------