├── .babelrc ├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── src ├── css │ └── index.scss ├── img │ ├── emotion.jpg │ ├── metal.jpg │ ├── modern.jpg │ └── retro.jpg └── js │ ├── Cursor.js │ ├── Cursors.js │ ├── Slideinfo.js │ ├── Slideshow.js │ ├── events │ ├── Events.js │ ├── Raf.js │ ├── Resize.js │ └── index.js │ ├── gl │ ├── GlObject.js │ ├── Slider.js │ ├── glsl │ │ ├── fragment.glsl │ │ └── vertex.glsl │ └── index.js │ ├── index.js │ └── utils │ ├── Mouse.js │ ├── Splitter.js │ ├── index.js │ └── preload.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-proposal-class-properties"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devloop01/webgl-slider/05aaa7cb49b8ce86c16d2a47aac4686fbc83ecd7/README.md -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebGL Slider Interaction | Using THREEJS 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 |

15 | Artist — Adam Rosol 16 |

17 |

18 | Wave effect inspired from — nightingale.world 19 |

20 |
21 | 22 |
23 |
24 | image 01 25 | image 02 26 | image 03 27 | image 04 28 |
29 |
30 |
31 |

01

32 |
33 |

MODERN

34 |

35 | In this modern world it is easy to be complex but difficult to be simple. At least try to keep your mind 36 | simple. 37 |

38 |
39 |
40 |
41 |

02

42 |
43 |

RETRO

44 |

45 | Your assumptions are your windows on the world. Scrub them off every once in a while, or the light won't 46 | come in. 47 |

48 |
49 |
50 | 51 |
52 |

03

53 |
54 |

METAL

55 |

56 | You simply have to turn your back on a culture that has gone sterile and dead and get with the program of a 57 | living world and the imagination. 58 |

59 |
60 |
61 |
62 |

04

63 |
64 |

EMOTION

65 |

66 | Every man has his secret sorrows which the world knows not; and often times we call a man cold when he is 67 | only sad. 68 |

69 |
70 |
71 |
72 | 90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 |

108 | Loading... 109 |

110 |

111 | 112 | WELCOME 113 | 114 | 117 | 120 | 123 | 126 | 129 |

130 |
131 |
132 | 133 | 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-interaction", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.dev.js", 8 | "build": "webpack --config webpack.prod.js", 9 | "clean": "rm -rf dist" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "babel-loader": "^8.1.0", 16 | "file-loader": "^6.1.0", 17 | "glsl-noise": "0.0.0", 18 | "glslify": "^7.1.1", 19 | "glslify-loader": "^2.0.0", 20 | "gsap": "^3.5.1", 21 | "imagesloaded": "^4.1.4", 22 | "raw-loader": "^4.0.1", 23 | "splitting": "^1.0.6", 24 | "three": "^0.120.1", 25 | "tiny-emitter": "^2.1.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.11.6", 29 | "@babel/plugin-proposal-class-properties": "^7.10.4", 30 | "@babel/preset-env": "^7.11.5", 31 | "css-loader": "^4.3.0", 32 | "html-loader": "^1.3.0", 33 | "html-webpack-plugin": "^4.4.1", 34 | "mini-css-extract-plugin": "^0.11.2", 35 | "node-sass": "^4.14.1", 36 | "sass-loader": "^10.0.2", 37 | "webpack": "^4.44.1", 38 | "webpack-cli": "^3.3.12", 39 | "webpack-dev-server": "^3.11.0", 40 | "webpack-merge": "^5.1.4" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/css/index.scss: -------------------------------------------------------------------------------- 1 | // fonts 2 | @import url("https://fonts.googleapis.com/css2?family=Red+Rose:wght@300;400;700&display=swap"); 3 | @import url("https://fonts.googleapis.com/css2?family=Lato:wght@300&display=swap"); 4 | 5 | :root { 6 | --cursor-stroke: #5631e9; 7 | --cursor-fill: transparent; 8 | --cursor-stroke-width: 1px; 9 | 10 | --base-image-width: 450px; 11 | --base-image-height: 600px; 12 | 13 | --image-width: 450px; 14 | --image-height: 600px; 15 | 16 | --bg-color: #27172e; 17 | } 18 | 19 | * { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | } 24 | 25 | html, 26 | body { 27 | width: 100%; 28 | height: 100%; 29 | font-size: 16px; 30 | } 31 | 32 | body { 33 | background: var(--bg-color); 34 | -webkit-font-smoothing: antialiased; 35 | -moz-osx-font-smoothing: grayscale; 36 | } 37 | 38 | main { 39 | width: 100%; 40 | height: 100%; 41 | } 42 | 43 | button { 44 | outline: none; 45 | border: none; 46 | background: none; 47 | color: inherit; 48 | font-family: inherit; 49 | } 50 | 51 | a { 52 | text-decoration: none; 53 | color: inherit; 54 | } 55 | 56 | .slider { 57 | width: 100%; 58 | height: 100%; 59 | position: relative; 60 | overflow: hidden; 61 | } 62 | 63 | .slider__image--wrapper { 64 | position: absolute; 65 | left: 50%; 66 | top: 50%; 67 | width: var(--image-width); 68 | height: var(--image-height); 69 | transform: translate(-50%, -50%); 70 | overflow: hidden; 71 | z-index: 1; 72 | user-select: none; 73 | 74 | .slider__image { 75 | position: absolute; 76 | left: 50%; 77 | transform: translateX(-50%); 78 | top: 0; 79 | height: 100%; 80 | display: none; 81 | } 82 | } 83 | 84 | .slider__slide-info--wrapper { 85 | pointer-events: none; 86 | 87 | position: absolute; 88 | left: 50%; 89 | top: 50%; 90 | width: var(--image-width); 91 | height: var(--image-height); 92 | transform: translate(-50%, -50%); 93 | z-index: 2; 94 | .slider__silde-info { 95 | color: #fff; 96 | pointer-events: auto; 97 | .slide__text--wrapper { 98 | position: absolute; 99 | bottom: 0; 100 | left: 105%; 101 | } 102 | .slide__index, 103 | .slide__text--title { 104 | font-family: "Red Rose"; 105 | font-weight: 700; 106 | } 107 | .slide__index { 108 | position: absolute; 109 | left: 0; 110 | top: 0; 111 | transform: translate(-60%, -20%); 112 | display: inline-block; 113 | font-size: 14rem; 114 | overflow: hidden; 115 | line-height: 80%; 116 | 117 | &--text { 118 | display: block; 119 | } 120 | } 121 | .slide__text--title { 122 | font-size: 7rem; 123 | transform: translateX(-35%); 124 | overflow: hidden; 125 | line-height: 80%; 126 | color: transparent; 127 | -webkit-text-stroke: 2px #fff; 128 | .word { 129 | display: flex; 130 | } 131 | } 132 | .slide__text--description { 133 | width: 350px; 134 | font-family: "Lato"; 135 | font-size: 1rem; 136 | line-height: 140%; 137 | 138 | .line { 139 | overflow: hidden; 140 | } 141 | } 142 | .slide__index, 143 | .slide__text--title, 144 | .slide__text--description { 145 | pointer-events: none; 146 | } 147 | 148 | &:not(.slide--current) { 149 | .slide__index, 150 | .slide__text--title, 151 | .slide__text--description { 152 | pointer-events: none; 153 | opacity: 0; 154 | } 155 | } 156 | } 157 | } 158 | 159 | .slider__nav { 160 | position: absolute; 161 | top: 50%; 162 | transform: translateY(-50%); 163 | width: 50px; 164 | height: 20px; 165 | cursor: pointer; 166 | z-index: 12; 167 | svg { 168 | display: block; 169 | width: 100%; 170 | height: 100%; 171 | transition: transform 250ms ease; 172 | transform: translateX(var(--tr-x, 0px)); 173 | } 174 | 175 | &--prev { 176 | left: 25vw; 177 | &:hover { 178 | svg { 179 | --tr-x: -10px; 180 | } 181 | &:active svg { 182 | --tr-x: -5px; 183 | } 184 | } 185 | } 186 | &--next { 187 | right: 25vw; 188 | &:hover { 189 | svg { 190 | --tr-x: 10px; 191 | } 192 | &:active svg { 193 | --tr-x: 5px; 194 | } 195 | } 196 | } 197 | } 198 | 199 | .dom-gl { 200 | position: fixed; 201 | z-index: -1; 202 | top: 0; 203 | left: 0; 204 | width: 100%; 205 | height: 100%; 206 | } 207 | 208 | .cursor { 209 | display: none; 210 | } 211 | 212 | .frame { 213 | position: fixed; 214 | width: 100%; 215 | height: 100%; 216 | padding: 2rem 3rem; 217 | color: #fff; 218 | font-family: "Red Rose"; 219 | font-weight: 300; 220 | z-index: 50; 221 | display: grid; 222 | justify-content: space-between; 223 | align-content: space-between; 224 | grid-template-columns: repeat(3, 1fr); 225 | grid-template-areas: 226 | "logo .. btn" 227 | "artist ... credits"; 228 | pointer-events: none; 229 | 230 | > * { 231 | pointer-events: auto; 232 | } 233 | 234 | &__logo { 235 | grid-area: logo; 236 | justify-self: left; 237 | } 238 | &__button { 239 | grid-area: btn; 240 | justify-self: right; 241 | } 242 | &__artist { 243 | grid-area: artist; 244 | justify-self: left; 245 | } 246 | &__credits { 247 | grid-area: credits; 248 | justify-self: right; 249 | } 250 | 251 | &__credits, 252 | &__artist { 253 | font-size: 0.8rem; 254 | overflow: hidden; 255 | span { 256 | display: inline-block; 257 | } 258 | a { 259 | font-weight: 400; 260 | opacity: 0.8; 261 | &:hover { 262 | opacity: 1; 263 | } 264 | } 265 | } 266 | } 267 | 268 | .loading__wrapper { 269 | position: fixed; 270 | left: 0; 271 | top: 0; 272 | width: 100%; 273 | height: 100vh; 274 | z-index: 1000; 275 | display: flex; 276 | justify-content: center; 277 | align-items: center; 278 | 279 | .loading__text { 280 | position: absolute; 281 | top: 50%; 282 | left: 50%; 283 | transform: translate(-50%, -50%); 284 | font-size: 2vmin; 285 | font-family: "Red Rose", sans-serif; 286 | color: #fff; 287 | 288 | &--inner { 289 | animation: blink 1s infinite alternate-reverse; 290 | } 291 | 292 | @keyframes blink { 293 | from { 294 | opacity: 1; 295 | } 296 | to { 297 | opacity: 0.6; 298 | } 299 | } 300 | } 301 | 302 | .text__wrapper { 303 | position: relative; 304 | font-size: 10vmin; 305 | font-family: "Red Rose", sans-serif; 306 | text-transform: uppercase; 307 | font-display: swap; 308 | color: #fff; 309 | 310 | .text__row { 311 | display: block; 312 | 313 | .text { 314 | display: block; 315 | user-select: none; 316 | opacity: 0; 317 | } 318 | 319 | &:nth-child(1) { 320 | clip-path: polygon(0% 75%, 100% 75%, 100% 100%, 0% 100%); 321 | } 322 | &:nth-child(2) { 323 | clip-path: polygon(0% 50%, 100% 50%, 100% 75.5%, 0% 75.5%); 324 | } 325 | &:nth-child(3) { 326 | clip-path: polygon(0% 25%, 100% 25%, 100% 50.5%, 0% 50.5%); 327 | } 328 | &:nth-child(4) { 329 | clip-path: polygon(0% 0%, 100% 0%, 100% 25.5%, 0% 25.5%); 330 | } 331 | &:nth-child(5) { 332 | clip-path: polygon(0% -25%, 100% -25%, 100% 0.5%, 0% 0.5%); 333 | } 334 | &:nth-child(6) { 335 | clip-path: polygon(0% -50%, 100% -50%, 100% -24.5%, 0% -24.5%); 336 | } 337 | &.text__row--sibling { 338 | position: absolute; 339 | top: 0; 340 | left: 0; 341 | user-select: none; 342 | } 343 | } 344 | } 345 | 346 | .bg__transition--slide { 347 | background: #0e0e0e; 348 | position: absolute; 349 | left: 0; 350 | top: 0; 351 | width: 100%; 352 | height: 100%; 353 | z-index: -1; 354 | } 355 | } 356 | 357 | @media (any-pointer: fine) { 358 | .cursor { 359 | position: fixed; 360 | top: 0; 361 | left: 0; 362 | display: block; 363 | pointer-events: none; 364 | mix-blend-mode: difference; 365 | z-index: 100; 366 | 367 | &--large .cursor__inner { 368 | fill: var(--cursor-fill); 369 | stroke: var(--cursor-stroke); 370 | stroke-width: var(--cursor-stroke-width); 371 | opacity: 0.7; 372 | } 373 | &--small .cursor__inner { 374 | fill: var(--cursor-stroke); 375 | stroke: var(--cursor-fill); 376 | opacity: 0.7; 377 | } 378 | 379 | &--close { 380 | fill: none; 381 | stroke: var(--cursor-stroke); 382 | stroke-linecap: round; 383 | stroke-linejoin: round; 384 | stroke-width: 32px; 385 | mix-blend-mode: difference; 386 | } 387 | } 388 | } 389 | 390 | // some dumb media queries - IDK not the best css I have written here 391 | 392 | @media only screen and (max-width: 64em) { 393 | .slider__slide-info--wrapper { 394 | .slider__silde-info { 395 | .slide__text--title { 396 | transform: translateX(-80%); 397 | } 398 | .slide__text--description { 399 | opacity: 0; 400 | height: 0; 401 | } 402 | } 403 | } 404 | 405 | .frame { 406 | &__credits { 407 | display: none; 408 | } 409 | } 410 | } 411 | 412 | @media only screen and (max-width: 53em) { 413 | :root { 414 | --image-width: calc(var(--base-image-width) / 1.125); 415 | --image-height: calc(var(--base-image-height) / 1.125); 416 | } 417 | 418 | .slider__slide-info--wrapper { 419 | .slider__silde-info { 420 | .slide__index { 421 | transform: translate(-60%, 0%); 422 | font-size: 10rem; 423 | } 424 | } 425 | } 426 | } 427 | 428 | @media only screen and (max-width: 32em) { 429 | :root { 430 | --image-width: calc(var(--base-image-width) / 1.5); 431 | --image-height: calc(var(--base-image-height) / 1.5); 432 | } 433 | .slider__slide-info--wrapper { 434 | .slider__silde-info { 435 | .slide__index { 436 | font-size: 7rem; 437 | } 438 | .slide__text--title { 439 | transform: translateX(-60%); 440 | font-size: 5rem; 441 | } 442 | } 443 | } 444 | 445 | .frame { 446 | &__artist { 447 | display: none; 448 | } 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/img/emotion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devloop01/webgl-slider/05aaa7cb49b8ce86c16d2a47aac4686fbc83ecd7/src/img/emotion.jpg -------------------------------------------------------------------------------- /src/img/metal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devloop01/webgl-slider/05aaa7cb49b8ce86c16d2a47aac4686fbc83ecd7/src/img/metal.jpg -------------------------------------------------------------------------------- /src/img/modern.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devloop01/webgl-slider/05aaa7cb49b8ce86c16d2a47aac4686fbc83ecd7/src/img/modern.jpg -------------------------------------------------------------------------------- /src/img/retro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devloop01/webgl-slider/05aaa7cb49b8ce86c16d2a47aac4686fbc83ecd7/src/img/retro.jpg -------------------------------------------------------------------------------- /src/js/Cursor.js: -------------------------------------------------------------------------------- 1 | import { gsap } from "gsap"; 2 | import { lerp, Mouse } from "./utils"; 3 | import { Events } from "./events"; 4 | 5 | let mouse = new Mouse(); 6 | 7 | export default class Cursor { 8 | constructor(el) { 9 | this.DOM = { el: el }; 10 | this.DOM.el.style.opacity = 0; 11 | 12 | this.bounds = this.DOM.el.getBoundingClientRect(); 13 | 14 | this.renderedStyles = { 15 | tx: { previous: 0, current: 0, amt: 0.2 }, 16 | ty: { previous: 0, current: 0, amt: 0.2 }, 17 | scale: { previous: 0, current: 1, amt: 0.2 }, 18 | opacity: { previous: 0, current: 1, amt: 0.15 }, 19 | }; 20 | } 21 | 22 | init() { 23 | this.onMouseMoveEv = () => { 24 | this.renderedStyles.tx.previous = this.renderedStyles.tx.current = mouse.position.x - this.bounds.width / 2; 25 | this.renderedStyles.ty.previous = this.renderedStyles.ty.previous = mouse.position.y - this.bounds.height / 2; 26 | Events.on("tick", this.render.bind(this)); 27 | window.removeEventListener("mousemove", this.onMouseMoveEv); 28 | }; 29 | window.addEventListener("mousemove", this.onMouseMoveEv); 30 | } 31 | 32 | setTranslateLerpAmount(amount) { 33 | this.renderedStyles["tx"].amt = amount; 34 | this.renderedStyles["ty"].amt = amount; 35 | return this; 36 | } 37 | scale(amount = 1) { 38 | this.renderedStyles["scale"].current = amount; 39 | return this; 40 | } 41 | opaque(amount = 1) { 42 | this.renderedStyles["opacity"].current = amount; 43 | return this; 44 | } 45 | render() { 46 | this.renderedStyles["tx"].current = mouse.position.x - this.bounds.width / 2; 47 | this.renderedStyles["ty"].current = mouse.position.y - this.bounds.height / 2; 48 | 49 | for (const key in this.renderedStyles) { 50 | this.renderedStyles[key].previous = lerp( 51 | this.renderedStyles[key].previous, 52 | this.renderedStyles[key].current, 53 | this.renderedStyles[key].amt 54 | ); 55 | } 56 | 57 | gsap.set(this.DOM.el, { 58 | translateX: this.renderedStyles["tx"].previous, 59 | translateY: this.renderedStyles["ty"].previous, 60 | scale: this.renderedStyles["scale"].previous, 61 | opacity: this.renderedStyles["opacity"].previous, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/js/Cursors.js: -------------------------------------------------------------------------------- 1 | import Cursor from "./Cursor"; 2 | 3 | export default new (class { 4 | constructor() { 5 | this.DOM = {}; 6 | 7 | this.DOM.cursorEls = { 8 | large: document.querySelector(".cursor--large"), 9 | small: document.querySelector(".cursor--small"), 10 | close: document.querySelector(".cursor--close"), 11 | }; 12 | 13 | this.cursors = { 14 | large: new Cursor(this.DOM.cursorEls.large), 15 | small: new Cursor(this.DOM.cursorEls.small), 16 | close: new Cursor(this.DOM.cursorEls.close), 17 | }; 18 | 19 | this.cursors.small.setTranslateLerpAmount(0.85); 20 | this.cursors.close.opaque(0).scale(0.5).setTranslateLerpAmount(0.5); 21 | } 22 | 23 | init() { 24 | Object.values(this.cursors).forEach((cursor) => { 25 | cursor.init(); 26 | }); 27 | this.initEvents(); 28 | } 29 | 30 | initEvents() { 31 | this.initEventsOnElements(); 32 | this.initEventsOnImage(); 33 | } 34 | 35 | initEventsOnElements() { 36 | const onMouseEnter = () => { 37 | this.cursors.large.scale(2).opaque(0); 38 | this.cursors.small.scale(5); 39 | }; 40 | 41 | const onMouseLeave = () => { 42 | this.cursors.large.scale(1).opaque(1); 43 | this.cursors.small.scale(1); 44 | }; 45 | 46 | const onMouseDown = () => { 47 | this.cursors.small.scale(4); 48 | }; 49 | 50 | const onMouseUp = () => { 51 | this.cursors.small.scale(5); 52 | }; 53 | 54 | [...document.querySelectorAll("a"), ...document.querySelectorAll("button")].forEach((element) => { 55 | element.addEventListener("mouseenter", onMouseEnter); 56 | element.addEventListener("mouseleave", onMouseLeave); 57 | element.addEventListener("mousedown", onMouseDown); 58 | element.addEventListener("mouseup", onMouseUp); 59 | }); 60 | } 61 | 62 | initEventsOnImage() { 63 | const imageWrapper = document.querySelector(".slider__image--wrapper"); 64 | 65 | const onMouseDown = () => { 66 | this.cursors.large.scale(2).opaque(0); 67 | this.cursors.small.scale(5); 68 | }; 69 | 70 | const onMouseUp = () => { 71 | this.cursors.large.scale(1).opaque(1); 72 | this.cursors.small.scale(1); 73 | }; 74 | 75 | imageWrapper.addEventListener("mousedown", onMouseDown); 76 | imageWrapper.addEventListener("mouseup", onMouseUp); 77 | } 78 | 79 | initEventsOnSlider(slider) { 80 | const imageWrapper = document.querySelector(".slider__image--wrapper"); 81 | 82 | const onMouseEnter = () => { 83 | this.cursors.large.scale(2).opaque(0); 84 | this.cursors.small.scale(5).setTranslateLerpAmount(0.25); 85 | this.cursors.close.opaque(1).scale(1); 86 | }; 87 | 88 | const onMouseLeave = () => { 89 | this.cursors.large.scale(1).opaque(1); 90 | this.cursors.small.scale(1).setTranslateLerpAmount(0.85); 91 | this.cursors.close.opaque(0).scale(0.5); 92 | }; 93 | 94 | slider.onFullscreen(() => { 95 | onMouseEnter(); 96 | imageWrapper.addEventListener("mouseenter", onMouseEnter); 97 | imageWrapper.addEventListener("mouseleave", onMouseLeave); 98 | }); 99 | 100 | slider.offFullscreen(() => { 101 | onMouseLeave(); 102 | imageWrapper.removeEventListener("mouseenter", onMouseEnter); 103 | imageWrapper.removeEventListener("mouseleave", onMouseLeave); 104 | }); 105 | } 106 | })(); 107 | -------------------------------------------------------------------------------- /src/js/Slideinfo.js: -------------------------------------------------------------------------------- 1 | import { Splitter } from "./utils"; 2 | 3 | export default class Slideinfo { 4 | constructor(el) { 5 | this.DOM = { el: el }; 6 | 7 | this.DOM.text = { 8 | index: this.DOM.el.querySelectorAll(".slide__index .char"), 9 | title: this.DOM.el.querySelectorAll(".slide__text--title .char"), 10 | description: this.DOM.el.querySelector(".slide__text--description"), 11 | }; 12 | 13 | const split = new Splitter(this.DOM.text.description); 14 | 15 | const lines = [...split.linesEl.children].map((c) => [...c.children][0]); 16 | this.DOM.text.descriptionLines = lines; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/Slideshow.js: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | 3 | import GlSlider from "./gl/Slider"; 4 | import Slideinfo from "./Slideinfo"; 5 | 6 | let clicked = false; 7 | 8 | export default class Slideshow { 9 | constructor(el) { 10 | this.DOM = { el }; 11 | this.DOM.imageWrapperEl = this.DOM.el.querySelector(".slider__image--wrapper"); 12 | this.DOM.navigation = { 13 | prev: this.DOM.el.querySelector(".slider__nav--prev"), 14 | next: this.DOM.el.querySelector(".slider__nav--next"), 15 | }; 16 | this.slideInfos = []; 17 | [...this.DOM.el.querySelectorAll(".slider__silde-info")].forEach((slide) => this.slideInfos.push(new Slideinfo(slide))); 18 | this.current = 0; 19 | this.slidesTotal = this.slideInfos.length; 20 | 21 | this.GlSlider = new GlSlider(); 22 | this.GlSlider.init(document.querySelector(".slider__image--wrapper")); 23 | 24 | this.initEvents(); 25 | } 26 | 27 | init() { 28 | const currentSlideInfo = this.slideInfos[this.current]; 29 | 30 | gsap.set([currentSlideInfo.DOM.text.index, currentSlideInfo.DOM.text.title], { 31 | yPercent: 120, 32 | rotation: -3, 33 | stagger: -0.02, 34 | }); 35 | gsap.set(currentSlideInfo.DOM.text.descriptionLines, { 36 | yPercent: 100, 37 | stagger: 0.05, 38 | }); 39 | gsap.set(this.DOM.navigation.prev, { 40 | translateX: 100, 41 | opacity: 0, 42 | }); 43 | gsap.set(this.DOM.navigation.next, { 44 | translateX: -100, 45 | opacity: 0, 46 | }); 47 | 48 | gsap.set(this.DOM.imageWrapperEl, { 49 | translateY: "150%", 50 | onUpdate: () => { 51 | this.GlSlider.setBounds(); 52 | }, 53 | }); 54 | } 55 | 56 | initAnimation() { 57 | const currentSlideInfo = this.slideInfos[this.current]; 58 | 59 | const tl = gsap 60 | .timeline({ 61 | defaults: { duration: 1, ease: "power4.inOut" }, 62 | delay: 0.25, 63 | }) 64 | .addLabel("start", 0) 65 | .addLabel("upcoming", 1.25); 66 | tl.to( 67 | this.DOM.imageWrapperEl, 68 | { 69 | duration: 1.25, 70 | translateY: 0, 71 | ease: "sine.out", 72 | onUpdate: () => { 73 | this.GlSlider.setBounds(); 74 | }, 75 | }, 76 | "start" 77 | ) 78 | .to( 79 | this.GlSlider.material.uniforms.uAmplitude, 80 | { 81 | duration: 1, 82 | value: 1, 83 | repeat: 1, 84 | yoyo: true, 85 | yoyoEase: "sine.out", 86 | ease: "expo.out", 87 | onComplete: () => { 88 | this.GlSlider.material.uniforms.uTranslating = false; 89 | }, 90 | }, 91 | "start" 92 | ) 93 | .to( 94 | [currentSlideInfo.DOM.text.index, currentSlideInfo.DOM.text.title], 95 | { 96 | yPercent: 0, 97 | rotation: 0, 98 | stagger: -0.02, 99 | }, 100 | "upcoming" 101 | ) 102 | .to( 103 | currentSlideInfo.DOM.text.descriptionLines, 104 | { 105 | yPercent: 0, 106 | stagger: 0.05, 107 | }, 108 | "upcoming" 109 | ) 110 | .to( 111 | [this.DOM.navigation.prev, this.DOM.navigation.next], 112 | { 113 | translateX: 0, 114 | opacity: 1, 115 | }, 116 | "upcoming" 117 | ); 118 | } 119 | 120 | initEvents() { 121 | this.onClickPrevEv = () => this.navigate("prev"); 122 | this.onClickNextEv = () => this.navigate("next"); 123 | this.onImageClickEv = () => { 124 | if (this.isAnimating) return; 125 | 126 | clicked = !clicked; 127 | 128 | const currentSlideInfo = this.slideInfos[this.current]; 129 | 130 | const tl = gsap 131 | .timeline({ 132 | defaults: { duration: 1, ease: "power4.inOut" }, 133 | onStart: () => { 134 | this.isAnimating = true; 135 | if (clicked) { 136 | this.GlSlider.scaleImage("up"); 137 | if (this.onFullscreenCallbackFn) this.onFullscreenCallbackFn(); 138 | } else { 139 | this.GlSlider.scaleImage("down"); 140 | if (this.offFullscreenCallbackFn) this.offFullscreenCallbackFn(); 141 | } 142 | }, 143 | onComplete: () => { 144 | this.isAnimating = false; 145 | }, 146 | }) 147 | .addLabel("start", clicked ? 0 : 0.2); 148 | 149 | tl.fromTo( 150 | [currentSlideInfo.DOM.text.index, currentSlideInfo.DOM.text.title], 151 | { 152 | yPercent: clicked ? 0 : 120, 153 | rotation: clicked ? 0 : -3, 154 | }, 155 | { 156 | yPercent: clicked ? -120 : 0, 157 | rotation: clicked ? 3 : 0, 158 | stagger: clicked ? 0.02 : -0.02, 159 | }, 160 | "start" 161 | ) 162 | .fromTo( 163 | currentSlideInfo.DOM.text.descriptionLines, 164 | { 165 | yPercent: clicked ? 0 : 100, 166 | }, 167 | { 168 | yPercent: clicked ? -100 : 0, 169 | stagger: 0.05, 170 | }, 171 | "start" 172 | ) 173 | .fromTo( 174 | this.DOM.navigation.prev, 175 | { 176 | translateX: clicked ? 0 : 100, 177 | opacity: clicked ? 1 : 0, 178 | }, 179 | { 180 | translateX: clicked ? -100 : 0, 181 | opacity: clicked ? 0 : 1, 182 | }, 183 | "start" 184 | ) 185 | .fromTo( 186 | this.DOM.navigation.next, 187 | { 188 | translateX: clicked ? 0 : -100, 189 | opacity: clicked ? 1 : 0, 190 | }, 191 | { 192 | translateX: clicked ? 100 : 0, 193 | opacity: clicked ? 0 : 1, 194 | }, 195 | "start" 196 | ) 197 | .set([this.DOM.navigation.prev, this.DOM.navigation.next], { pointerEvents: clicked ? "none" : "auto" }); 198 | }; 199 | 200 | this.DOM.navigation.prev.addEventListener("click", () => this.onClickPrevEv()); 201 | this.DOM.navigation.next.addEventListener("click", () => this.onClickNextEv()); 202 | this.DOM.imageWrapperEl.addEventListener("click", () => this.onImageClickEv()); 203 | } 204 | 205 | onSlideChange(callback) { 206 | if (typeof callback == "function") { 207 | this.onSlideChangeCallbackFn = callback; 208 | } 209 | } 210 | 211 | onFullscreen(callback) { 212 | if (typeof callback == "function") { 213 | this.onFullscreenCallbackFn = callback; 214 | } 215 | } 216 | 217 | offFullscreen(callback) { 218 | if (typeof callback == "function") { 219 | this.offFullscreenCallbackFn = callback; 220 | } 221 | } 222 | 223 | navigate(direction) { 224 | if (this.GlSlider.state.animating) return; 225 | 226 | const incrementSlideIndex = (val) => { 227 | if (val > 0 && this.current + val < this.slidesTotal) { 228 | this.current += val; 229 | } else if (val > 0) { 230 | this.current = 0; 231 | } else if (val < 0 && this.current + val < 0) { 232 | this.current = this.slidesTotal - 1; 233 | } else { 234 | this.current += val; 235 | } 236 | }; 237 | 238 | const increment = direction == "prev" ? -1 : 1; 239 | 240 | const currentSlideInfo = this.slideInfos[this.current]; 241 | incrementSlideIndex(increment); 242 | const nextSlideInfo = this.slideInfos[this.current]; 243 | 244 | this.GlSlider.switchTextures(this.current, increment); 245 | 246 | gsap.timeline({ 247 | defaults: { duration: 1, ease: "power4.inOut" }, 248 | onStart: () => { 249 | this.GlSlider.switchTextures(this.current, increment); 250 | if (this.onSlideChangeCallbackFn) this.onSlideChangeCallbackFn(this.current); 251 | this.isAnimating = true; 252 | }, 253 | onComplete: () => { 254 | currentSlideInfo.DOM.el.classList.remove("slide--current"); 255 | this.isAnimating = false; 256 | }, 257 | }) 258 | .addLabel("start", 0) 259 | .to( 260 | [currentSlideInfo.DOM.text.index, currentSlideInfo.DOM.text.title], 261 | { 262 | yPercent: direction === "next" ? -120 : 120, 263 | rotation: direction === "next" ? 3 : -3, 264 | stagger: direction === "next" ? 0.02 : -0.02, 265 | }, 266 | "start" 267 | ) 268 | .to( 269 | currentSlideInfo.DOM.text.descriptionLines, 270 | { 271 | yPercent: direction === "next" ? -100 : 100, 272 | stagger: direction === "next" ? 0.05 : -0.05, 273 | }, 274 | "start" 275 | ) 276 | .addLabel("upcoming", 0.4) 277 | .add(() => { 278 | gsap.set([nextSlideInfo.DOM.text.index, nextSlideInfo.DOM.text.title], { 279 | yPercent: direction === "next" ? 120 : -120, 280 | rotation: direction === "next" ? -3 : 3, 281 | }); 282 | gsap.set(nextSlideInfo.DOM.text.descriptionLines, { 283 | yPercent: direction === "next" ? 100 : -100, 284 | }); 285 | nextSlideInfo.DOM.el.classList.add("slide--current"); 286 | }, "upcoming") 287 | .to( 288 | [nextSlideInfo.DOM.text.index, nextSlideInfo.DOM.text.title], 289 | { 290 | yPercent: 0, 291 | rotation: 0, 292 | stagger: direction === "next" ? 0.02 : -0.02, 293 | }, 294 | "upcoming" 295 | ) 296 | .to( 297 | nextSlideInfo.DOM.text.descriptionLines, 298 | { 299 | yPercent: 0, 300 | stagger: direction === "next" ? 0.05 : -0.05, 301 | }, 302 | "upcoming" 303 | ); 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/js/events/Events.js: -------------------------------------------------------------------------------- 1 | import Emitter from "tiny-emitter"; 2 | 3 | export default new Emitter(); 4 | -------------------------------------------------------------------------------- /src/js/events/Raf.js: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import Events from "./Events"; 3 | 4 | class Raf { 5 | constructor() { 6 | this.init(); 7 | } 8 | 9 | tick() { 10 | Events.emit("tick"); 11 | } 12 | 13 | on() { 14 | gsap.ticker.add(this.tick.bind(this)); 15 | } 16 | 17 | init() { 18 | this.on(); 19 | } 20 | } 21 | 22 | export default new Raf(); 23 | -------------------------------------------------------------------------------- /src/js/events/Resize.js: -------------------------------------------------------------------------------- 1 | import Events from "./Events"; 2 | 3 | class Resize { 4 | constructor() { 5 | this.init(); 6 | } 7 | 8 | onResize() { 9 | Events.emit("resize"); 10 | } 11 | 12 | on() { 13 | window.addEventListener("resize", this.onResize); 14 | } 15 | 16 | init() { 17 | this.on(); 18 | } 19 | } 20 | 21 | export default new Resize(); 22 | -------------------------------------------------------------------------------- /src/js/events/index.js: -------------------------------------------------------------------------------- 1 | import Events from "./Events"; 2 | import GlobalResize from "./Resize"; 3 | import GlobalRaf from "./Raf"; 4 | 5 | export { Events, GlobalResize, GlobalRaf }; 6 | -------------------------------------------------------------------------------- /src/js/gl/GlObject.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import Gl from "./index"; 3 | import gsap from "gsap"; 4 | 5 | export default class extends THREE.Object3D { 6 | init(el) { 7 | this.el = el; 8 | this.resize(); 9 | } 10 | 11 | resize() { 12 | this.setBounds(); 13 | } 14 | 15 | setBounds() { 16 | this.rect = this.el.getBoundingClientRect(); 17 | 18 | this.bounds = { 19 | left: this.rect.left, 20 | top: this.rect.top + window.scrollY, 21 | width: this.rect.width, 22 | height: this.rect.height, 23 | }; 24 | 25 | this.updateSize(); 26 | this.updatePosition(); 27 | } 28 | 29 | updateSize() { 30 | this.camUnit = this.calculateUnitSize(Gl.camera.position.z - this.position.z); 31 | 32 | const x = this.bounds.width / window.innerWidth; 33 | const y = this.bounds.height / window.innerHeight; 34 | 35 | if (!x || !y) return; 36 | 37 | this.scale.x = this.camUnit.width * x; 38 | this.scale.y = this.camUnit.height * y; 39 | } 40 | 41 | calculateUnitSize(distance = this.position.z) { 42 | const vFov = (Gl.camera.fov * Math.PI) / 180; 43 | const height = 2 * Math.tan(vFov / 2) * distance; 44 | const width = height * Gl.camera.aspect; 45 | 46 | return { width, height }; 47 | } 48 | 49 | updateY(y = 0) { 50 | const { top, height } = this.bounds; 51 | 52 | this.position.y = this.camUnit.height / 2 - this.scale.y / 2; 53 | this.position.y -= ((top - y) / window.innerHeight) * this.camUnit.height; 54 | 55 | this.progress = gsap.utils.clamp(0, 1, 1 - (-y + top + height) / (window.innerHeight + height)); 56 | } 57 | 58 | updateX(x = 0) { 59 | const { left } = this.bounds; 60 | 61 | this.position.x = -(this.camUnit.width / 2) + this.scale.x / 2; 62 | this.position.x += ((left + x) / window.innerWidth) * this.camUnit.width; 63 | } 64 | 65 | updatePosition(y) { 66 | this.updateY(y); 67 | this.updateX(0); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/js/gl/Slider.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import gsap from "gsap"; 3 | 4 | import Gl from "./index"; 5 | import GlObject from "./GlObject"; 6 | import vertexShader from "./glsl/vertex.glsl"; 7 | import fragmentShader from "./glsl/fragment.glsl"; 8 | 9 | let mouse = new THREE.Vector2(); 10 | window.addEventListener("mousemove", (event) => { 11 | event.preventDefault(); 12 | mouse.x = (event.clientX / window.innerWidth) * 2 - 1; 13 | mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 14 | }); 15 | let mouseOver = false, 16 | mouseDown = false; 17 | 18 | const planeGeometry = new THREE.PlaneBufferGeometry(1, 1, 32, 32); 19 | const planeMaterial = new THREE.ShaderMaterial({ 20 | vertexShader, 21 | fragmentShader, 22 | side: THREE.DoubleSide, 23 | defines: { 24 | PI: Math.PI, 25 | }, 26 | }); 27 | 28 | export default class extends GlObject { 29 | init(el) { 30 | super.init(el); 31 | 32 | this.geometry = planeGeometry; 33 | this.material = planeMaterial.clone(); 34 | 35 | this.material.uniforms = { 36 | uCurrTex: { value: 0 }, 37 | uNextTex: { value: 0 }, 38 | uTime: { value: 0 }, 39 | uProg: { value: 0 }, 40 | uAmplitude: { value: 0 }, 41 | uProgDirection: { value: 0 }, 42 | uMeshSize: { value: [this.rect.width, this.rect.height] }, 43 | uImageSize: { value: [0, 0] }, 44 | uMousePos: { value: [0, 0] }, 45 | uMouseOverAmp: { value: 0 }, 46 | uAnimating: { value: false }, 47 | uRadius: { value: 0.08 }, 48 | uTranslating: { value: true }, 49 | }; 50 | 51 | this.imageScale = 1; 52 | 53 | this.textures = []; 54 | 55 | this.raycaster = new THREE.Raycaster(); 56 | this.mouse = new THREE.Vector2(); 57 | this.mouseLerpAmount = 0.1; 58 | 59 | this.state = { 60 | animating: false, 61 | current: 0, 62 | }; 63 | 64 | this.mesh = new THREE.Mesh(this.geometry, this.material); 65 | this.add(this.mesh); 66 | 67 | Gl.scene.add(this); 68 | 69 | this.loadTextures(); 70 | this.addEvents(); 71 | } 72 | 73 | loadTextures() { 74 | const manager = new THREE.LoadingManager(() => { 75 | this.material.uniforms.uCurrTex.value = this.textures[0]; 76 | }); 77 | const loader = new THREE.TextureLoader(manager); 78 | const imgs = [...this.el.querySelectorAll("img")]; 79 | 80 | imgs.forEach((img) => { 81 | loader.load(img.src, (texture) => { 82 | texture.minFilter = THREE.LinearFilter; 83 | texture.generateMipmaps = false; 84 | 85 | this.material.uniforms.uImageSize.value = [img.naturalWidth, img.naturalHeight]; 86 | this.textures.push(texture); 87 | }); 88 | }); 89 | } 90 | 91 | switchTextures(index, direction) { 92 | if (this.state.animating) return; 93 | 94 | gsap.timeline({ 95 | onStart: () => { 96 | this.state.animating = true; 97 | this.material.uniforms.uAnimating.value = true; 98 | this.material.uniforms.uProgDirection.value = direction; 99 | this.material.uniforms.uNextTex.value = this.textures[index]; 100 | }, 101 | onComplete: () => { 102 | this.state.animating = false; 103 | this.material.uniforms.uAnimating.value = false; 104 | this.material.uniforms.uCurrTex.value = this.textures[index]; 105 | this.currentAmp = 0; 106 | }, 107 | }) 108 | .fromTo( 109 | this.material.uniforms.uProg, 110 | { 111 | value: 0, 112 | }, 113 | { 114 | value: 1, 115 | duration: 1, 116 | ease: "ease.out", 117 | }, 118 | 0 119 | ) 120 | .fromTo( 121 | this.material.uniforms.uAmplitude, 122 | { 123 | value: 0, 124 | }, 125 | { 126 | duration: 0.8, 127 | value: 1, 128 | repeat: 1, 129 | yoyo: true, 130 | yoyoEase: "sine.out", 131 | ease: "expo.out", 132 | }, 133 | 0 134 | ); 135 | } 136 | 137 | updateTime(time) { 138 | this.material.uniforms.uTime.value = time; 139 | this.run(); 140 | } 141 | 142 | addEvents() { 143 | this.el.addEventListener("mouseenter", () => (mouseOver = true)); 144 | this.el.addEventListener("mouseleave", () => (mouseOver = false)); 145 | this.el.addEventListener("mousedown", () => (mouseDown = true)); 146 | this.el.addEventListener("mouseup", () => (mouseDown = false)); 147 | } 148 | 149 | scaleImage(direction) { 150 | const imageTl = gsap.timeline({ 151 | defaults: { 152 | duration: 1.2, 153 | ease: "elastic.out(1, 1)", 154 | onUpdate: () => { 155 | this.resize(); 156 | }, 157 | }, 158 | }); 159 | if (direction == "up") { 160 | imageTl.to(this.el, { 161 | scale: window.innerHeight / 600, 162 | }); 163 | } else if (direction == "down") { 164 | imageTl.to(this.el, { 165 | scale: 1, 166 | }); 167 | } 168 | } 169 | 170 | run() { 171 | let m = mouseOver ? mouse : new THREE.Vector2(0, 0); 172 | this.mouse.lerp(m, this.mouseLerpAmount); 173 | 174 | this.raycaster.setFromCamera(this.mouse, Gl.camera); 175 | let intersects = this.raycaster.intersectObject(this.mesh); 176 | if (intersects.length > 0) { 177 | this.material.uniforms.uMousePos.value = [intersects[0].uv.x, intersects[0].uv.y]; 178 | } 179 | 180 | if (mouseOver) { 181 | this.material.uniforms.uMouseOverAmp.value = THREE.MathUtils.lerp(this.material.uniforms.uMouseOverAmp.value, 1, 0.08); 182 | this.mouseLerpAmount = THREE.MathUtils.lerp(this.mouseLerpAmount, 0.1, 0.5); 183 | } else { 184 | this.material.uniforms.uMouseOverAmp.value = THREE.MathUtils.lerp(this.material.uniforms.uMouseOverAmp.value, 0, 0.08); 185 | this.mouseLerpAmount = THREE.MathUtils.lerp(this.mouseLerpAmount, 0, 0.5); 186 | } 187 | 188 | if (mouseOver && mouseDown) { 189 | this.material.uniforms.uRadius.value = THREE.MathUtils.lerp(this.material.uniforms.uRadius.value, 1, 0.01); 190 | } else if (mouseOver && !mouseDown) { 191 | this.material.uniforms.uRadius.value = THREE.MathUtils.lerp(this.material.uniforms.uRadius.value, 0.08, 0.08); 192 | } 193 | 194 | if (this.state.animating) { 195 | this.material.uniforms.uMouseOverAmp.value = THREE.MathUtils.lerp(this.material.uniforms.uMouseOverAmp.value, 0, 0.1); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/js/gl/glsl/fragment.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 vUv; 4 | varying float vWave; 5 | 6 | uniform float uTime; 7 | uniform float uProg; 8 | uniform float uProgDirection; 9 | 10 | uniform sampler2D uCurrTex; 11 | uniform sampler2D uNextTex; 12 | 13 | uniform vec2 uMeshSize; 14 | uniform vec2 uImageSize; 15 | 16 | vec2 backgroundCoverUv(vec2 uv,vec2 canvasSize,vec2 textureSize){ 17 | vec2 ratio=vec2( 18 | min((canvasSize.x/canvasSize.y)/(textureSize.x/textureSize.y),1.), 19 | min((canvasSize.y/canvasSize.x)/(textureSize.y/textureSize.x),1.) 20 | ); 21 | 22 | vec2 uvWithRatio=uv*ratio; 23 | 24 | return vec2( 25 | uvWithRatio.x+(1.-ratio.x)*.5, 26 | uvWithRatio.y+(1.-ratio.y)*.5 27 | ); 28 | } 29 | 30 | void main(){ 31 | vec2 texUv=backgroundCoverUv(vUv,uMeshSize,uImageSize); 32 | 33 | float x=uProg; 34 | float y; 35 | if(uProgDirection==1.)y=(x*2.+(vUv.x-1.)); 36 | else y=((x*2.)-vUv.x); 37 | x=smoothstep(0.,1.,y); 38 | 39 | float w=vWave; 40 | 41 | float r1=texture2D(uCurrTex,texUv+w*.04).r; 42 | float g1=texture2D(uCurrTex,texUv+w*.01).g; 43 | float b1=texture2D(uCurrTex,texUv+w*-.03).b; 44 | vec3 tex1=vec3(r1,g1,b1); 45 | 46 | float r2=texture2D(uNextTex,texUv+w*.04).r; 47 | float g2=texture2D(uNextTex,texUv+w*.01).g; 48 | float b2=texture2D(uNextTex,texUv+w*-.03).b; 49 | vec3 tex2=vec3(r2,g2,b2); 50 | 51 | float scaleUp=(.4+.6*(1.-uProg)); 52 | float scaleDown=(.6+.4*uProg); 53 | 54 | vec4 f1=mix( 55 | texture2D(uCurrTex,texUv*(1.-x)*scaleUp+vec2(.15)*uProg), 56 | texture2D(uNextTex,texUv*x*scaleDown), 57 | x); 58 | 59 | vec3 f2=mix(tex1,tex2,x); 60 | 61 | vec4 final=mix(f1,vec4(f2,1.),.12); 62 | 63 | gl_FragColor=final; 64 | // gl_FragColor=vec4(vec3(vWave),1.); 65 | } -------------------------------------------------------------------------------- /src/js/gl/glsl/vertex.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | varying vec2 vUv; 4 | varying float vWave; 5 | 6 | uniform float uTime; 7 | uniform float uAmplitude; 8 | uniform float uProgDirection; 9 | uniform float uMouseOverAmp; 10 | uniform float uRadius; 11 | 12 | uniform vec2 uMeshSize; 13 | uniform vec2 uMousePos; 14 | 15 | uniform bool uAnimating; 16 | uniform bool uTranslating; 17 | 18 | float mapVal(in float n,in float start1,in float stop1,in float start2,in float stop2){ 19 | return((n-start1)/(stop1-start1))*(stop2-start2)+start2; 20 | } 21 | 22 | void main(){ 23 | vec3 pos=position; 24 | vUv=uv; 25 | 26 | vec2 center=vUv-uMousePos; 27 | center.x*=uMeshSize.x/uMeshSize.y; 28 | float dist=length(center); 29 | 30 | float radius=uRadius; 31 | 32 | float mask=smoothstep(radius,radius*5.,dist); 33 | float d=mapVal(mask,-1.,1.,-1.,0.); 34 | 35 | if(uAnimating){ 36 | pos.z=sin(pos.x*5.+uTime*10.*uProgDirection)*uAmplitude; 37 | pos.z*=2.5; 38 | }else{ 39 | pos.z=d*uMouseOverAmp; 40 | pos.z*=15.; 41 | } 42 | 43 | if(uTranslating){ 44 | pos.z=sin(pos.y*6.+uTime*10.)*uAmplitude; 45 | pos.z*=3.5; 46 | } 47 | 48 | vWave=pos.z; 49 | 50 | gl_Position=projectionMatrix*modelViewMatrix*vec4(pos,1.); 51 | } -------------------------------------------------------------------------------- /src/js/gl/index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | import gsap from "gsap"; 3 | import { Events } from "../events"; 4 | 5 | export default new (class { 6 | constructor() { 7 | this.scene = new THREE.Scene(); 8 | 9 | this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); 10 | this.camera.position.z = 50; 11 | 12 | this.renderer = new THREE.WebGLRenderer({ 13 | alpha: true, 14 | antialias: true, 15 | }); 16 | this.renderer.setPixelRatio(gsap.utils.clamp(1.5, 1, window.devicePixelRatio)); 17 | this.renderer.setSize(window.innerWidth, window.innerHeight); 18 | this.renderer.setClearColor(0xf2f2f2, 0); 19 | 20 | this.clock = new THREE.Clock(); 21 | 22 | this.init(); 23 | } 24 | 25 | init() { 26 | this.addToDom(); 27 | this.addEvents(); 28 | this.run(); 29 | } 30 | 31 | addToDom() { 32 | const canvas = this.renderer.domElement; 33 | canvas.classList.add("dom-gl"); 34 | document.body.appendChild(canvas); 35 | } 36 | 37 | addEvents() { 38 | Events.on("resize", this.resize.bind(this)); 39 | Events.on("tick", this.run.bind(this)); 40 | } 41 | 42 | resize() { 43 | this.renderer.setSize(window.innerWidth, window.innerHeight); 44 | this.camera.updateProjectionMatrix(); 45 | 46 | for (let i = 0; i < this.scene.children.length; i++) { 47 | const plane = this.scene.children[i]; 48 | if (plane.resize) plane.resize(); 49 | } 50 | } 51 | 52 | run() { 53 | let elapsed = this.clock.getElapsedTime(); 54 | 55 | for (let i = 0; i < this.scene.children.length; i++) { 56 | const plane = this.scene.children[i]; 57 | if (plane.updateTime) plane.updateTime(elapsed); 58 | } 59 | 60 | this.render(); 61 | } 62 | 63 | render() { 64 | this.renderer.render(this.scene, this.camera); 65 | } 66 | })(); 67 | -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import "splitting/dist/splitting.css"; 2 | import "splitting/dist/splitting-cells.css"; 3 | import "../css/index.scss"; 4 | 5 | import Splitting from "splitting"; 6 | import gsap from "gsap"; 7 | 8 | import Slideshow from "./Slideshow"; 9 | import Cursors from "./Cursors"; 10 | import { preloadImages } from "./utils"; 11 | 12 | Splitting(); 13 | 14 | const bgColors = ["#27172e", "#1f1322", "#454d53", "#2d1f2d"]; 15 | 16 | const masterTl = gsap.timeline(); 17 | 18 | preloadImages(document.querySelectorAll(".slider__image")).then(() => { 19 | const slider = new Slideshow(document.querySelector(".slider")); 20 | slider.init(); 21 | 22 | const loadedAnimationTl = gsap 23 | .timeline({ 24 | onStart: () => { 25 | gsap.set(".text__row .text", { autoAlpha: 1 }); 26 | }, 27 | }) 28 | .to(".loading__text", { 29 | duration: 1, 30 | opacity: 0, 31 | }) 32 | .from(".text__row .text", { 33 | duration: 3, 34 | translateY: (i) => -100 + i * -25 + "%", 35 | ease: "expo.out", 36 | stagger: 0.1, 37 | }) 38 | .to(".text__row .text", { 39 | duration: 2, 40 | translateY: (i) => 100 + i * 25 + "%", 41 | ease: "expo.in", 42 | stagger: 0.25, 43 | }) 44 | .to(".bg__transition--slide", { 45 | duration: 1, 46 | scaleY: 0, 47 | transformOrigin: "top center", 48 | ease: "expo.out", 49 | onComplete: () => { 50 | slider.initAnimation(); 51 | gsap.set(".loading__wrapper", { 52 | pointerEvents: "none", 53 | autoAlpha: 0, 54 | }); 55 | }, 56 | }); 57 | 58 | const pageAnimationTl = gsap 59 | .timeline({ 60 | delay: loadedAnimationTl.duration(), 61 | onComplete: () => { 62 | Cursors.init(); 63 | Cursors.initEventsOnSlider(slider); 64 | }, 65 | }) 66 | .from([".frame__logo", ".frame__button", ".frame__artist > span", ".frame__credits > span"], { 67 | duration: 1, 68 | opacity: 0, 69 | yPercent: 100, 70 | stagger: 0.1, 71 | ease: "expo.out", 72 | }); 73 | 74 | masterTl.add(loadedAnimationTl, 0); 75 | masterTl.add(pageAnimationTl, pageAnimationTl.duration() - 0.5); 76 | 77 | slider.onSlideChange((currentSlideIndex) => { 78 | gsap.to("body", { 79 | duration: 1.2, 80 | backgroundColor: bgColors[currentSlideIndex], 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/js/utils/Mouse.js: -------------------------------------------------------------------------------- 1 | import { getMousePos } from "./index"; 2 | 3 | export default class Mouse { 4 | constructor() { 5 | this.position = { 6 | x: 0, 7 | y: 0, 8 | }; 9 | this.isMoving = false; 10 | 11 | this.mouseEvent = { 12 | previous: null, 13 | current: null, 14 | }; 15 | 16 | this.initEvents(); 17 | this.updateMovingState(); 18 | } 19 | initEvents() { 20 | window.addEventListener("mousemove", (ev) => { 21 | this.mouseEvent.current = ev; 22 | this.position = getMousePos(ev); 23 | }); 24 | } 25 | updateMovingState() { 26 | setInterval(() => { 27 | if (this.mouseEvent.previous && this.mouseEvent.current) { 28 | const moveX = Math.abs(this.mouseEvent.current.screenX - this.mouseEvent.previous.screenX); 29 | const moveY = Math.abs(this.mouseEvent.current.screenY - this.mouseEvent.previous.screenY); 30 | const movement = Math.sqrt(moveX * moveX + moveY * moveY); 31 | 32 | if (movement == 0) this.isMoving = false; 33 | else this.isMoving = true; 34 | } 35 | 36 | this.mouseEvent.previous = this.mouseEvent.current; 37 | }, 100); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/js/utils/Splitter.js: -------------------------------------------------------------------------------- 1 | export default class Splitter { 2 | constructor(el) { 3 | this.DOM = { el }; 4 | this.DOMElComputedStyles = getComputedStyle(this.DOM.el); 5 | 6 | this.init(); 7 | } 8 | 9 | init() { 10 | const lines = this.split(); 11 | this.clearElement(); 12 | this.insertLines(lines); 13 | } 14 | 15 | split() { 16 | const maxwidth = this.DOM.el.getBoundingClientRect().width; 17 | const textContent = this.DOM.el.innerText; 18 | const words = textContent.split(" "); 19 | 20 | const lines = []; 21 | let curline = []; 22 | 23 | const fontWeight = this.DOMElComputedStyles["font-weight"]; 24 | const fontSize = this.DOMElComputedStyles["font-size"]; 25 | const fontFamily = this.DOMElComputedStyles["font-family"]; 26 | 27 | const canvasEl = document.createElement("canvas"); 28 | const ghost = "OffscreenCanvas" in window ? canvasEl.transferControlToOffscreen() : canvasEl; 29 | const context = ghost.getContext("2d"); 30 | 31 | context.font = `${fontWeight} ${fontSize} ${fontFamily}`; 32 | 33 | for (let i = 0; i < words.length; i++) { 34 | curline.push(words[i]); 35 | if (context.measureText(curline.join(" ")).width >= maxwidth) { 36 | const cache = curline.pop(); 37 | lines.push(curline.join(" ")); 38 | curline = [cache]; 39 | } 40 | } 41 | lines.push(curline.join(" ")); 42 | return lines; 43 | } 44 | 45 | insertLines(lines) { 46 | this.linesEl = document.createElement("span"); 47 | this.linesEl.className = "lines"; 48 | this.linesEl.style.display = "block"; 49 | 50 | lines.forEach((line) => { 51 | const lineEl = document.createElement("span"); 52 | const lineInnerTextEl = document.createElement("span"); 53 | 54 | lineEl.className = "line"; 55 | lineInnerTextEl.className = "line--innertext"; 56 | 57 | lineEl.style.display = "block"; 58 | lineInnerTextEl.style.display = "block"; 59 | 60 | lineInnerTextEl.innerText = line; 61 | 62 | lineEl.appendChild(lineInnerTextEl); 63 | this.linesEl.appendChild(lineEl); 64 | }); 65 | 66 | this.DOM.el.appendChild(this.linesEl); 67 | } 68 | 69 | clearElement() { 70 | this.DOM.el.innerHTML = ""; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/js/utils/index.js: -------------------------------------------------------------------------------- 1 | import Splitter from "./Splitter"; 2 | import Mouse from "./Mouse"; 3 | import { preloadImages } from "./preload"; 4 | 5 | const lerp = (a, b, n) => (1 - n) * a + n * b; 6 | 7 | const getMousePos = (e) => { 8 | let posx = 0; 9 | let posy = 0; 10 | if (!e) e = window.event; 11 | if (e.pageX || e.pageY) { 12 | posx = e.pageX; 13 | posy = e.pageY; 14 | } else if (e.clientX || e.clientY) { 15 | posx = e.clientX + body.scrollLeft + document.documentElement.scrollLeft; 16 | posy = e.clientY + body.scrollTop + document.documentElement.scrollTop; 17 | } 18 | 19 | return { x: posx, y: posy }; 20 | }; 21 | 22 | export { lerp, getMousePos, Splitter, Mouse, preloadImages }; 23 | -------------------------------------------------------------------------------- /src/js/utils/preload.js: -------------------------------------------------------------------------------- 1 | import imagesLoaded from "imagesloaded"; 2 | 3 | export function preloadImages(selector) { 4 | return new Promise((resolve, reject) => { 5 | imagesLoaded(selector, { background: true }, resolve); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | 4 | module.exports = { 5 | entry: { 6 | index: "./src/js/index.js", 7 | }, 8 | 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | use: "babel-loader", 14 | exclude: /node_modules/, 15 | }, 16 | 17 | { 18 | test: /\.css$/, 19 | use: [MiniCssExtractPlugin.loader, "css-loader"], 20 | }, 21 | { 22 | test: /\.scss$/, 23 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 24 | }, 25 | { 26 | test: /\.html$/, 27 | loader: "html-loader", 28 | }, 29 | { 30 | test: /\.(png|jpe?g|gif|svg)$/i, 31 | loader: "file-loader", 32 | options: { 33 | name: "[name].[ext]", 34 | outputPath: "images/", 35 | esModule: false, 36 | }, 37 | }, 38 | 39 | { 40 | test: /\.glsl$/, 41 | use: ["raw-loader", "glslify-loader"], 42 | }, 43 | ], 44 | }, 45 | 46 | plugins: [ 47 | new HtmlWebpackPlugin({ 48 | filename: "index.html", 49 | template: "index.html", 50 | inject: true, 51 | chunks: ["index"], 52 | hash: true, 53 | }), 54 | new MiniCssExtractPlugin({ 55 | filename: "styles/[name].css", 56 | chunkFilename: "[id].css", 57 | }), 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const common = require("./webpack.common"); 3 | const { merge } = require("webpack-merge"); 4 | 5 | module.exports = merge(common, { 6 | mode: "development", 7 | output: { 8 | filename: "scripts/[name].js", 9 | path: path.resolve(__dirname, "dist"), 10 | }, 11 | devServer: { 12 | port: 8080, 13 | stats: "errors-only", 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const common = require("./webpack.common"); 3 | const { merge } = require("webpack-merge"); 4 | 5 | module.exports = merge(common, { 6 | mode: "production", 7 | output: { 8 | filename: "scripts/[name].js", 9 | path: path.resolve(__dirname, "dist"), 10 | }, 11 | }); 12 | --------------------------------------------------------------------------------