├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── 1.dc197a9a.jpg ├── 2.3ca6bb44.jpg ├── 3.a433b89d.jpg ├── 4.c6d96be5.jpg ├── base.98fd6c19.css ├── favicon.26242483.ico ├── index.html └── js.00a46daa.js ├── package.json └── src ├── css └── base.css ├── favicon.ico ├── img ├── 1.jpg ├── 2.jpg ├── 3.jpg └── 4.jpg ├── index.html └── js ├── cursor.js ├── index.js ├── thumbnailAnimation.js └── utils.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.cache 3 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009 - 2020 [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 | # Thumbnail to Full Width Image Animation 2 | 3 | An experimental animation where an image goes from thumbnail to full width, allowing for a classy entry to an article view. Based on this [animation](https://twitter.com/wrongakram/status/1279204900661071875) by [Akram Khalid](https://twitter.com/wrongakram). 4 | 5 | ![Thumbnail to Full Width Image Animation Image](https://tympanus.net/codrops/wp-content/uploads/2020/08/ThumbToFull_featured-1.jpg) 6 | 7 | [Article on Codrops](https://tympanus.net/codrops/?p=50733) 8 | 9 | [Demo](http://tympanus.net/Development/ThumbFullTransition/) 10 | 11 | 12 | ## Installation 13 | 14 | Install dependencies: 15 | 16 | ``` 17 | npm install 18 | ``` 19 | 20 | Compile the code for development and start a local server: 21 | 22 | ``` 23 | npm start 24 | ``` 25 | 26 | Create the build: 27 | 28 | ``` 29 | npm run build 30 | ``` 31 | 32 | ## Credits 33 | 34 | - Images by [DeMorris Byrd](https://unsplash.com/@byrdman85) 35 | 36 | ## Misc 37 | 38 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [GitHub](https://github.com/codrops), [Instagram](https://www.instagram.com/codropsss/) 39 | 40 | ## License 41 | [MIT](LICENSE) 42 | 43 | Made with :blue_heart: by [Codrops](http://www.codrops.com) 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /dist/1.dc197a9a.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/dist/1.dc197a9a.jpg -------------------------------------------------------------------------------- /dist/2.3ca6bb44.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/dist/2.3ca6bb44.jpg -------------------------------------------------------------------------------- /dist/3.a433b89d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/dist/3.a433b89d.jpg -------------------------------------------------------------------------------- /dist/4.c6d96be5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/dist/4.c6d96be5.jpg -------------------------------------------------------------------------------- /dist/base.98fd6c19.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 15px; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | --color-text: #111110; 14 | --color-bg: #E9E6E2; 15 | --color-link: #000; 16 | --color-link-hover: #111110; 17 | --image-height-start: 555px; 18 | --image-width-end: 260px; 19 | --image-height-end: 320px; 20 | --image-offset: 50px; 21 | --meta-offset: 40px; 22 | --color-footer-bg: #dad6cf; 23 | --content-padding: 50px; 24 | --content-width: 850px; 25 | color: var(--color-text); 26 | background-color: var(--color-bg); 27 | font-family: mentone, sans-serif; 28 | overflow: hidden; 29 | overflow-y: scroll; 30 | --cursor-stroke: none; 31 | --cursor-fill: #c5b4b2; 32 | --cursor-stroke-width: 1px; 33 | transform: translate3d(0,0,0); 34 | } 35 | 36 | /* Page Loader */ 37 | .js .loading::before, 38 | .js .loading::after { 39 | content: ''; 40 | position: fixed; 41 | z-index: 1000; 42 | } 43 | 44 | .js .loading::before { 45 | top: 0; 46 | left: 0; 47 | width: 100%; 48 | height: 100%; 49 | background: var(--color-bg); 50 | } 51 | 52 | .js .loading::after { 53 | top: 50%; 54 | left: 50%; 55 | width: 60px; 56 | height: 60px; 57 | margin: -30px 0 0 -30px; 58 | border-radius: 50%; 59 | opacity: 0.4; 60 | background: var(--color-link); 61 | animation: loaderAnim 0.7s linear infinite alternate forwards; 62 | 63 | } 64 | 65 | @keyframes loaderAnim { 66 | to { 67 | opacity: 1; 68 | transform: scale3d(0.5,0.5,1); 69 | } 70 | } 71 | 72 | a { 73 | text-decoration: underline; 74 | color: var(--color-link); 75 | outline: none; 76 | } 77 | 78 | a:hover, 79 | a:focus { 80 | text-decoration: none; 81 | color: var(--color-link-hover); 82 | outline: none; 83 | } 84 | 85 | .frame { 86 | padding: 1.25rem; 87 | width: 100%; 88 | position: absolute; 89 | z-index: 1000; 90 | text-transform: uppercase; 91 | font-size: 0.85rem; 92 | } 93 | 94 | .frame__title { 95 | font-size: 0.85rem; 96 | font-weight: 600; 97 | margin: 0 0 0.25rem; 98 | } 99 | 100 | .frame__tagline { 101 | margin: 0.5rem 0; 102 | } 103 | 104 | .frame__links { 105 | margin-bottom: 1rem; 106 | } 107 | 108 | .frame__links a:not(:last-child) { 109 | margin-right: 1rem; 110 | } 111 | 112 | .frame__button { 113 | cursor: not-allowed; 114 | font-weight: 600; 115 | text-decoration: none; 116 | } 117 | 118 | .content { 119 | position: relative; 120 | } 121 | 122 | .js .content { 123 | overflow: hidden; 124 | height: 100vh; 125 | pointer-events: none; 126 | } 127 | 128 | .js .content--scroll { 129 | overflow: visible; 130 | height: auto; 131 | pointer-events: auto; 132 | } 133 | 134 | .inner { 135 | max-width: var(--content-width); 136 | padding: 0 var(--content-padding); 137 | margin: 0 auto; 138 | } 139 | 140 | .content__header { 141 | display: flex; 142 | flex-direction: column; 143 | padding-top: 10rem; 144 | } 145 | 146 | .content__meta { 147 | position: relative; 148 | font-size: 0.85rem; 149 | width: var(--image-width-end); 150 | } 151 | 152 | .content__meta-number { 153 | font-weight: bold; 154 | } 155 | 156 | .content__meta-title { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | text-transform: uppercase; 161 | transform: translateY(calc(var(--meta-offset) * 2 + var(--image-height-end))) translateY(-100%); 162 | } 163 | 164 | .js .content__meta-title { 165 | opacity: 0; 166 | } 167 | 168 | .content__title { 169 | letter-spacing: -0.15rem; 170 | margin: 1rem 0; 171 | line-height: 1; 172 | font-family: nocturne-serif, serif; 173 | font-weight: 600; 174 | font-size: clamp(1.5rem, 10vw, 8rem); 175 | } 176 | 177 | .js .anim-block-wrap { 178 | position: relative; 179 | overflow: hidden; 180 | } 181 | 182 | .js .anim-block { 183 | display: block; 184 | } 185 | 186 | .content__subtitle { 187 | font-family: nocturne-serif, serif; 188 | font-weight: 600; 189 | font-size: 1.25rem; 190 | margin: 0.5rem 0 3rem 0; 191 | line-height: 1.2; 192 | } 193 | 194 | .content__breakout { 195 | width: 100vw; 196 | left: 50%; 197 | right: 50%; 198 | margin: auto -50vw 0; 199 | position: relative; 200 | } 201 | 202 | .content__intro { 203 | height: var(--image-height-start); 204 | overflow: hidden; 205 | display: flex; 206 | align-items: center; 207 | pointer-events: auto; 208 | cursor: pointer; 209 | will-change: transform; 210 | } 211 | 212 | .content--scroll .content__intro { 213 | cursor: default; 214 | } 215 | 216 | .content__intro-imgWrap { 217 | will-change: transform; 218 | } 219 | 220 | .content__intro-img { 221 | width: 100vw; 222 | height: auto; 223 | position: relative; 224 | } 225 | 226 | .content__body { 227 | padding: 0 0 10rem 0; 228 | font-weight: 400; 229 | line-height: 1.6; 230 | } 231 | 232 | .js .content__body { 233 | opacity: 0; 234 | } 235 | 236 | .js .content--scroll .content__body { 237 | opacity: 1; 238 | } 239 | 240 | .content__body-para { 241 | margin: 4rem 0; 242 | padding: 0 1rem; 243 | } 244 | 245 | .anim-show { 246 | transform: translateY(100%); 247 | opacity: 0; 248 | transition: all .8s cubic-bezier(0.2,1,0.3,1); 249 | } 250 | 251 | .anim-show.is-inview { 252 | transform: translateY(0%); 253 | opacity: 1; 254 | } 255 | 256 | .cell--1-3 { 257 | grid-column: 1 / span 3; 258 | } 259 | 260 | .cell--2-3 { 261 | grid-column: 2 / span 2; 262 | } 263 | 264 | .cell--3 { 265 | grid-column: 3; 266 | } 267 | 268 | .cell--1-3 { 269 | grid-column: 1 / span 3; 270 | } 271 | 272 | .content__body-imgwrap { 273 | width: 100%; 274 | height: calc(((var(--content-width) - var(--content-padding)) * 828/1000) - 100px); 275 | overflow: hidden; 276 | position: relative; 277 | display: flex; 278 | align-items: center; 279 | justify-content: center; 280 | } 281 | 282 | .content__body-image { 283 | width: 100%; 284 | height: calc((var(--content-width) - var(--content-padding)) * 828/1000); 285 | background-size: cover; 286 | background-position: 50% 50%; 287 | } 288 | 289 | .content__footer { 290 | padding: 3rem 4rem 3.5rem; 291 | background: var(--color-footer-bg); 292 | } 293 | 294 | .inner--grid { 295 | text-align: left; 296 | } 297 | 298 | .content__footer-copyright { 299 | text-decoration: none; 300 | } 301 | 302 | .content__footer-next { 303 | display: block; 304 | font-family: nocturne-serif, serif; 305 | font-weight: 600; 306 | font-size: 3rem; 307 | cursor: not-allowed; 308 | } 309 | 310 | .cursor { 311 | display: none; 312 | } 313 | 314 | .thumbs { 315 | opacity: 0; 316 | pointer-events: none; 317 | position: fixed; 318 | top: 0; 319 | left: 0; 320 | width: 100%; 321 | height: 100vh; 322 | padding: 45px; 323 | display: flex; 324 | align-items: center; 325 | justify-content: space-between; 326 | perspective: 1000px; 327 | } 328 | 329 | .thumb { 330 | position: relative; 331 | display: flex; 332 | flex-direction: column; 333 | justify-content: center; 334 | font-size: 0.85rem; 335 | width: var(--image-width-end); 336 | will-change: transform; 337 | } 338 | 339 | .thumb__img { 340 | cursor: not-allowed; 341 | width: 100%; 342 | height: var(--image-height-end); 343 | background-size: cover; 344 | background-position: 50% 50%; 345 | } 346 | 347 | .thumb__number { 348 | position: absolute; 349 | font-weight: bold; 350 | bottom: 100%; 351 | transform: translateY(calc(-1 * var(--meta-offset) + 100%)); 352 | } 353 | 354 | .thumb__title { 355 | position: absolute; 356 | top: 100%; 357 | left: 0; 358 | text-transform: uppercase; 359 | transform: translateY(calc(var(--meta-offset) - 100%)); 360 | } 361 | 362 | /*! locomotive-scroll v3.5.4 | MIT License | https://github.com/locomotivemtl/locomotive-scroll */ 363 | html.has-scroll-smooth { 364 | overflow: hidden; 365 | } 366 | 367 | html.has-scroll-dragging { 368 | -webkit-user-select: none; 369 | -moz-user-select: none; 370 | -ms-user-select: none; 371 | user-select: none; 372 | } 373 | 374 | .has-scroll-smooth body { 375 | overflow: hidden; 376 | } 377 | 378 | .has-scroll-smooth [data-scroll-container] { 379 | min-height: 100vh; 380 | } 381 | 382 | .c-scrollbar { 383 | position: absolute; 384 | right: 0; 385 | top: 0; 386 | width: 11px; 387 | height: 100vh; 388 | transform-origin: center right; 389 | transition: transform 0.3s, opacity 0.3s; 390 | opacity: 0; 391 | } 392 | .c-scrollbar:hover { 393 | transform: scaleX(1.45); 394 | } 395 | .c-scrollbar:hover, .has-scroll-scrolling .c-scrollbar, .has-scroll-dragging .c-scrollbar { 396 | opacity: 1; 397 | } 398 | 399 | .c-scrollbar_thumb { 400 | position: absolute; 401 | top: 0; 402 | right: 0; 403 | background-color: black; 404 | opacity: 0.5; 405 | width: 7px; 406 | border-radius: 10px; 407 | margin: 2px; 408 | cursor: -webkit-grab; 409 | cursor: grab; 410 | } 411 | .has-scroll-dragging .c-scrollbar_thumb { 412 | cursor: -webkit-grabbing; 413 | cursor: grabbing; 414 | } 415 | 416 | @media screen and (min-width: 53em) { 417 | body { 418 | --meta-offset: 100px; 419 | } 420 | .frame { 421 | padding: 1.25rem 3rem; 422 | text-align: left; 423 | display: grid; 424 | align-content: space-between; 425 | max-width: none; 426 | pointer-events: none; 427 | grid-template-columns: 1fr 1fr 8rem; 428 | grid-template-rows: auto; 429 | grid-template-areas: 'title links menu'; 430 | } 431 | .frame__title-wrap { 432 | grid-area: title; 433 | } 434 | .frame__button { 435 | justify-self: end; 436 | grid-area: menu; 437 | } 438 | .frame__links { 439 | grid-area: links; 440 | margin: 0; 441 | justify-self: end; 442 | } 443 | .frame a { 444 | pointer-events: auto; 445 | } 446 | .content__body { 447 | display: grid; 448 | grid-template-columns: 50% 25% 25%; 449 | align-items: start; 450 | } 451 | .text-right { 452 | text-align: right; 453 | } 454 | .inner--grid { 455 | display: grid; 456 | grid-template-columns: 50% 50%; 457 | align-items: center; 458 | } 459 | .thumbs { 460 | opacity: 1; 461 | } 462 | .thumb__img { 463 | pointer-events: auto; 464 | } 465 | } 466 | 467 | @media screen and (max-height: 700px) { 468 | body { 469 | --meta-offset: 40px; 470 | } 471 | } 472 | 473 | @media (any-pointer: fine) { 474 | .cursor { 475 | position: fixed; 476 | top: 0; 477 | left: 0; 478 | display: block; 479 | pointer-events: none; 480 | mix-blend-mode: color-burn; 481 | } 482 | .cursor__inner { 483 | fill: var(--cursor-fill); 484 | stroke: var(--cursor-stroke); 485 | stroke-width: var(--cursor-stroke-width); 486 | opacity: 0.7; 487 | } 488 | .credits { 489 | padding-left: 25vw; 490 | } 491 | } -------------------------------------------------------------------------------- /dist/favicon.26242483.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/dist/favicon.26242483.ico -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Thumbnail to Full Width Image Animation | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 |
26 |
27 |
28 |

Image Animation & Scroll

29 |

Recreation of Akram Khalid's animation

30 |
31 | 36 | Menu 37 |
38 |
39 |
40 |
41 | 45 |

46 | Gnostic Love 47 |

48 |

49 | What is wanted is not the will to believe, but the wish
to find out, which is its exact opposite.
50 |

51 |
52 |
53 | Some image 54 |
55 |
56 |
57 |
58 |

Then they opened the firmaments and the Light descended below to the lower regions and to those who were without form, having no true likeness. It was thus that they got the likeness of the Light for themselves. Some rejoiced because the Light had come to them, and that they had been made rich thereby. Others mourned because they were made poor and that which they thought they had was taken away from them. Thus came He, who went forth full of grace, and was taken captive with a captivity. A light of glory was given to the æons who had received the Spark, and guardian spirits were sent to them who are Gamanêl, Etrempsouchos, and Agramas, and those who are with them. They bring help to those who have believed in the Spark of Light.

59 |

The Crown that the Deathless pray for

60 |
61 |
62 |
63 |

Now in the Space of the Indivisible Atom are twelve Founts, above which are the twelve Paternities who surround the Indivisible [Queen] like Deeps or like Skies and make for Her a crown in which is every kind of life: all modes of Triple-powered life, of Uncontainable life, of Infinite life, of Ineffable life, of Silent life, of Unknown life, of Solitary life, of Unshakable life, of First-manifested life, of Self-born life, of True life. All is therein. Every species is in it, all Gnôses and every power which has received the Light, yea, all Mind manifests itself therein. This is the Crown which the Father of the Universe has placed upon the Indivisible [Queen] with three hundred and sixty-five kinds in it, brilliant and filling the Universe with an incorruptible and unfailing light.

64 |
65 | 71 |
72 |
73 |
74 |
75 | 07 76 |
77 | Into Light 78 |
79 |
80 | 02 81 |
82 | Surrender 83 |
84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ThumbFullTransition", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "parcel src/index.html --open", 8 | "clean": "rm -rf dist/*", 9 | "build:parcel": "parcel build src/index.html --no-content-hash --no-minify --no-source-maps --public-url ./", 10 | "build": "npm run clean && npm run build:parcel" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/codrops/ThumbFullTransition.git" 15 | }, 16 | "keywords": [], 17 | "author": "Codrops", 18 | "license": "MIT", 19 | "homepage": "[HOMEPAGE]", 20 | "bugs": { 21 | "url": "https://github.com/codrops/ThumbFullTransition/issues" 22 | }, 23 | "dependencies": { 24 | "gsap": "^3.5.0", 25 | "imagesloaded": "^4.1.4", 26 | "locomotive-scroll": "^3.5.4", 27 | "parcel-bundler": "^1.12.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/css/base.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | } 6 | 7 | :root { 8 | font-size: 15px; 9 | } 10 | 11 | body { 12 | margin: 0; 13 | --color-text: #111110; 14 | --color-bg: #E9E6E2; 15 | --color-link: #000; 16 | --color-link-hover: #111110; 17 | --image-height-start: 555px; 18 | --image-width-end: 260px; 19 | --image-height-end: 320px; 20 | --image-offset: 50px; 21 | --meta-offset: 40px; 22 | --color-footer-bg: #dad6cf; 23 | --content-padding: 50px; 24 | --content-width: 850px; 25 | color: var(--color-text); 26 | background-color: var(--color-bg); 27 | font-family: mentone, sans-serif; 28 | overflow: hidden; 29 | overflow-y: scroll; 30 | --cursor-stroke: none; 31 | --cursor-fill: #c5b4b2; 32 | --cursor-stroke-width: 1px; 33 | transform: translate3d(0,0,0); 34 | } 35 | 36 | /* Page Loader */ 37 | .js .loading::before, 38 | .js .loading::after { 39 | content: ''; 40 | position: fixed; 41 | z-index: 1000; 42 | } 43 | 44 | .js .loading::before { 45 | top: 0; 46 | left: 0; 47 | width: 100%; 48 | height: 100%; 49 | background: var(--color-bg); 50 | } 51 | 52 | .js .loading::after { 53 | top: 50%; 54 | left: 50%; 55 | width: 60px; 56 | height: 60px; 57 | margin: -30px 0 0 -30px; 58 | border-radius: 50%; 59 | opacity: 0.4; 60 | background: var(--color-link); 61 | animation: loaderAnim 0.7s linear infinite alternate forwards; 62 | 63 | } 64 | 65 | @keyframes loaderAnim { 66 | to { 67 | opacity: 1; 68 | transform: scale3d(0.5,0.5,1); 69 | } 70 | } 71 | 72 | a { 73 | text-decoration: underline; 74 | color: var(--color-link); 75 | outline: none; 76 | } 77 | 78 | a:hover, 79 | a:focus { 80 | text-decoration: none; 81 | color: var(--color-link-hover); 82 | outline: none; 83 | } 84 | 85 | .frame { 86 | padding: 1.25rem; 87 | width: 100%; 88 | position: absolute; 89 | z-index: 1000; 90 | text-transform: uppercase; 91 | font-size: 0.85rem; 92 | } 93 | 94 | .frame__title { 95 | font-size: 0.85rem; 96 | font-weight: 600; 97 | margin: 0 0 0.25rem; 98 | } 99 | 100 | .frame__tagline { 101 | margin: 0.5rem 0; 102 | } 103 | 104 | .frame__links { 105 | margin-bottom: 1rem; 106 | } 107 | 108 | .frame__links a:not(:last-child) { 109 | margin-right: 1rem; 110 | } 111 | 112 | .frame__button { 113 | cursor: not-allowed; 114 | font-weight: 600; 115 | text-decoration: none; 116 | } 117 | 118 | .content { 119 | position: relative; 120 | } 121 | 122 | .js .content { 123 | overflow: hidden; 124 | height: 100vh; 125 | pointer-events: none; 126 | } 127 | 128 | .js .content--scroll { 129 | overflow: visible; 130 | height: auto; 131 | pointer-events: auto; 132 | } 133 | 134 | .inner { 135 | max-width: var(--content-width); 136 | padding: 0 var(--content-padding); 137 | margin: 0 auto; 138 | } 139 | 140 | .content__header { 141 | display: flex; 142 | flex-direction: column; 143 | padding-top: 10rem; 144 | } 145 | 146 | .content__meta { 147 | position: relative; 148 | font-size: 0.85rem; 149 | width: var(--image-width-end); 150 | } 151 | 152 | .content__meta-number { 153 | font-weight: bold; 154 | } 155 | 156 | .content__meta-title { 157 | position: absolute; 158 | top: 0; 159 | left: 0; 160 | text-transform: uppercase; 161 | transform: translateY(calc(var(--meta-offset) * 2 + var(--image-height-end))) translateY(-100%); 162 | } 163 | 164 | .js .content__meta-title { 165 | opacity: 0; 166 | } 167 | 168 | .content__title { 169 | letter-spacing: -0.15rem; 170 | margin: 1rem 0; 171 | line-height: 1; 172 | font-family: nocturne-serif, serif; 173 | font-weight: 600; 174 | font-size: clamp(1.5rem, 10vw, 8rem); 175 | } 176 | 177 | .js .anim-block-wrap { 178 | position: relative; 179 | overflow: hidden; 180 | } 181 | 182 | .js .anim-block { 183 | display: block; 184 | } 185 | 186 | .content__subtitle { 187 | font-family: nocturne-serif, serif; 188 | font-weight: 600; 189 | font-size: 1.25rem; 190 | margin: 0.5rem 0 3rem 0; 191 | line-height: 1.2; 192 | } 193 | 194 | .content__breakout { 195 | width: 100vw; 196 | left: 50%; 197 | right: 50%; 198 | margin: auto -50vw 0; 199 | position: relative; 200 | } 201 | 202 | .content__intro { 203 | height: var(--image-height-start); 204 | overflow: hidden; 205 | display: flex; 206 | align-items: center; 207 | pointer-events: auto; 208 | cursor: pointer; 209 | will-change: transform; 210 | } 211 | 212 | .content--scroll .content__intro { 213 | cursor: default; 214 | } 215 | 216 | .content__intro-imgWrap { 217 | will-change: transform; 218 | } 219 | 220 | .content__intro-img { 221 | width: 100vw; 222 | height: auto; 223 | position: relative; 224 | } 225 | 226 | .content__body { 227 | padding: 0 0 10rem 0; 228 | font-weight: 400; 229 | line-height: 1.6; 230 | } 231 | 232 | .js .content__body { 233 | opacity: 0; 234 | } 235 | 236 | .js .content--scroll .content__body { 237 | opacity: 1; 238 | } 239 | 240 | .content__body-para { 241 | margin: 4rem 0; 242 | padding: 0 1rem; 243 | } 244 | 245 | .anim-show { 246 | transform: translateY(100%); 247 | opacity: 0; 248 | transition: all .8s cubic-bezier(0.2,1,0.3,1); 249 | } 250 | 251 | .anim-show.is-inview { 252 | transform: translateY(0%); 253 | opacity: 1; 254 | } 255 | 256 | .cell--1-3 { 257 | grid-column: 1 / span 3; 258 | } 259 | 260 | .cell--2-3 { 261 | grid-column: 2 / span 2; 262 | } 263 | 264 | .cell--3 { 265 | grid-column: 3; 266 | } 267 | 268 | .cell--1-3 { 269 | grid-column: 1 / span 3; 270 | } 271 | 272 | .content__body-imgwrap { 273 | width: 100%; 274 | height: calc(((var(--content-width) - var(--content-padding)) * 828/1000) - 100px); 275 | overflow: hidden; 276 | position: relative; 277 | display: flex; 278 | align-items: center; 279 | justify-content: center; 280 | } 281 | 282 | .content__body-image { 283 | width: 100%; 284 | height: calc((var(--content-width) - var(--content-padding)) * 828/1000); 285 | background-size: cover; 286 | background-position: 50% 50%; 287 | } 288 | 289 | .content__footer { 290 | padding: 3rem 4rem 3.5rem; 291 | background: var(--color-footer-bg); 292 | } 293 | 294 | .inner--grid { 295 | text-align: left; 296 | } 297 | 298 | .content__footer-copyright { 299 | text-decoration: none; 300 | } 301 | 302 | .content__footer-next { 303 | display: block; 304 | font-family: nocturne-serif, serif; 305 | font-weight: 600; 306 | font-size: 3rem; 307 | cursor: not-allowed; 308 | } 309 | 310 | .cursor { 311 | display: none; 312 | } 313 | 314 | .thumbs { 315 | opacity: 0; 316 | pointer-events: none; 317 | position: fixed; 318 | top: 0; 319 | left: 0; 320 | width: 100%; 321 | height: 100vh; 322 | padding: 45px; 323 | display: flex; 324 | align-items: center; 325 | justify-content: space-between; 326 | perspective: 1000px; 327 | } 328 | 329 | .thumb { 330 | position: relative; 331 | display: flex; 332 | flex-direction: column; 333 | justify-content: center; 334 | font-size: 0.85rem; 335 | width: var(--image-width-end); 336 | will-change: transform; 337 | } 338 | 339 | .thumb__img { 340 | cursor: not-allowed; 341 | width: 100%; 342 | height: var(--image-height-end); 343 | background-size: cover; 344 | background-position: 50% 50%; 345 | } 346 | 347 | .thumb__number { 348 | position: absolute; 349 | font-weight: bold; 350 | bottom: 100%; 351 | transform: translateY(calc(-1 * var(--meta-offset) + 100%)); 352 | } 353 | 354 | .thumb__title { 355 | position: absolute; 356 | top: 100%; 357 | left: 0; 358 | text-transform: uppercase; 359 | transform: translateY(calc(var(--meta-offset) - 100%)); 360 | } 361 | 362 | /*! locomotive-scroll v3.5.4 | MIT License | https://github.com/locomotivemtl/locomotive-scroll */ 363 | html.has-scroll-smooth { 364 | overflow: hidden; 365 | } 366 | 367 | html.has-scroll-dragging { 368 | -webkit-user-select: none; 369 | -moz-user-select: none; 370 | -ms-user-select: none; 371 | user-select: none; 372 | } 373 | 374 | .has-scroll-smooth body { 375 | overflow: hidden; 376 | } 377 | 378 | .has-scroll-smooth [data-scroll-container] { 379 | min-height: 100vh; 380 | } 381 | 382 | .c-scrollbar { 383 | position: absolute; 384 | right: 0; 385 | top: 0; 386 | width: 11px; 387 | height: 100vh; 388 | transform-origin: center right; 389 | transition: transform 0.3s, opacity 0.3s; 390 | opacity: 0; 391 | } 392 | .c-scrollbar:hover { 393 | transform: scaleX(1.45); 394 | } 395 | .c-scrollbar:hover, .has-scroll-scrolling .c-scrollbar, .has-scroll-dragging .c-scrollbar { 396 | opacity: 1; 397 | } 398 | 399 | .c-scrollbar_thumb { 400 | position: absolute; 401 | top: 0; 402 | right: 0; 403 | background-color: black; 404 | opacity: 0.5; 405 | width: 7px; 406 | border-radius: 10px; 407 | margin: 2px; 408 | cursor: -webkit-grab; 409 | cursor: grab; 410 | } 411 | .has-scroll-dragging .c-scrollbar_thumb { 412 | cursor: -webkit-grabbing; 413 | cursor: grabbing; 414 | } 415 | 416 | @media screen and (min-width: 53em) { 417 | body { 418 | --meta-offset: 100px; 419 | } 420 | .frame { 421 | padding: 1.25rem 3rem; 422 | text-align: left; 423 | display: grid; 424 | align-content: space-between; 425 | max-width: none; 426 | pointer-events: none; 427 | grid-template-columns: 1fr 1fr 8rem; 428 | grid-template-rows: auto; 429 | grid-template-areas: 'title links menu'; 430 | } 431 | .frame__title-wrap { 432 | grid-area: title; 433 | } 434 | .frame__button { 435 | justify-self: end; 436 | grid-area: menu; 437 | } 438 | .frame__links { 439 | grid-area: links; 440 | margin: 0; 441 | justify-self: end; 442 | } 443 | .frame a { 444 | pointer-events: auto; 445 | } 446 | .content__body { 447 | display: grid; 448 | grid-template-columns: 50% 25% 25%; 449 | align-items: start; 450 | } 451 | .text-right { 452 | text-align: right; 453 | } 454 | .inner--grid { 455 | display: grid; 456 | grid-template-columns: 50% 50%; 457 | align-items: center; 458 | } 459 | .thumbs { 460 | opacity: 1; 461 | } 462 | .thumb__img { 463 | pointer-events: auto; 464 | } 465 | } 466 | 467 | @media screen and (max-height: 700px) { 468 | body { 469 | --meta-offset: 40px; 470 | } 471 | } 472 | 473 | @media (any-pointer: fine) { 474 | .cursor { 475 | position: fixed; 476 | top: 0; 477 | left: 0; 478 | display: block; 479 | pointer-events: none; 480 | mix-blend-mode: color-burn; 481 | } 482 | .cursor__inner { 483 | fill: var(--cursor-fill); 484 | stroke: var(--cursor-stroke); 485 | stroke-width: var(--cursor-stroke-width); 486 | opacity: 0.7; 487 | } 488 | .credits { 489 | padding-left: 25vw; 490 | } 491 | } -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/src/favicon.ico -------------------------------------------------------------------------------- /src/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/src/img/1.jpg -------------------------------------------------------------------------------- /src/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/src/img/2.jpg -------------------------------------------------------------------------------- /src/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/src/img/3.jpg -------------------------------------------------------------------------------- /src/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codrops/ThumbFullTransition/6ae767e260e0696b9c4555832f8941d427dea33b/src/img/4.jpg -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Thumbnail to Full Width Image Animation | Codrops 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |

Image Animation & Scroll

21 |

Recreation of Akram Khalid's animation

22 |
23 | 28 | Menu 29 |
30 |
31 |
32 |
33 | 37 |

38 | Gnostic Love 39 |

40 |

41 | What is wanted is not the will to believe, but the wish
to find out, which is its exact opposite.
42 |

43 |
44 |
45 | Some image 46 |
47 |
48 |
49 |
50 |

Then they opened the firmaments and the Light descended below to the lower regions and to those who were without form, having no true likeness. It was thus that they got the likeness of the Light for themselves. Some rejoiced because the Light had come to them, and that they had been made rich thereby. Others mourned because they were made poor and that which they thought they had was taken away from them. Thus came He, who went forth full of grace, and was taken captive with a captivity. A light of glory was given to the æons who had received the Spark, and guardian spirits were sent to them who are Gamanêl, Etrempsouchos, and Agramas, and those who are with them. They bring help to those who have believed in the Spark of Light.

51 |

The Crown that the Deathless pray for

52 |
53 |
54 |
55 |

Now in the Space of the Indivisible Atom are twelve Founts, above which are the twelve Paternities who surround the Indivisible [Queen] like Deeps or like Skies and make for Her a crown in which is every kind of life: all modes of Triple-powered life, of Uncontainable life, of Infinite life, of Ineffable life, of Silent life, of Unknown life, of Solitary life, of Unshakable life, of First-manifested life, of Self-born life, of True life. All is therein. Every species is in it, all Gnôses and every power which has received the Light, yea, all Mind manifests itself therein. This is the Crown which the Father of the Universe has placed upon the Indivisible [Queen] with three hundred and sixty-five kinds in it, brilliant and filling the Universe with an incorruptible and unfailing light.

56 |
57 | 63 |
64 |
65 |
66 |
67 | 07 68 |
69 | Into Light 70 |
71 |
72 | 02 73 |
74 | Surrender 75 |
76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/js/cursor.js: -------------------------------------------------------------------------------- 1 | import { gsap } from 'gsap'; 2 | import { lerp, getMousePos } from './utils'; 3 | 4 | // Track the mouse position 5 | let mouse = {x: 0, y: 0}; 6 | window.addEventListener('mousemove', ev => mouse = getMousePos(ev)); 7 | 8 | export default class Cursor { 9 | constructor(el) { 10 | this.DOM = {el: el}; 11 | this.DOM.el.style.opacity = 0; 12 | 13 | this.bounds = this.DOM.el.getBoundingClientRect(); 14 | 15 | this.renderedStyles = { 16 | tx: {previous: 0, current: 0, amt: 0.2}, 17 | ty: {previous: 0, current: 0, amt: 0.2}, 18 | scale: {previous: 1, current: 1, amt: 0.15}, 19 | //opacity: {previous: 1, current: 1, amt: 0.1} 20 | }; 21 | 22 | this.onMouseMoveEv = () => { 23 | this.renderedStyles.tx.previous = this.renderedStyles.tx.current = mouse.x - this.bounds.width/2; 24 | this.renderedStyles.ty.previous = this.renderedStyles.ty.previous = mouse.y - this.bounds.height/2; 25 | gsap.to(this.DOM.el, {duration: 0.9, ease: 'Power3.easeOut', opacity: 1}); 26 | requestAnimationFrame(() => this.render()); 27 | window.removeEventListener('mousemove', this.onMouseMoveEv); 28 | }; 29 | window.addEventListener('mousemove', this.onMouseMoveEv); 30 | } 31 | enter() { 32 | this.renderedStyles['scale'].current = 4.5; 33 | //this.renderedStyles['opacity'].current = 0.5; 34 | } 35 | leave() { 36 | this.renderedStyles['scale'].current = 1; 37 | //this.renderedStyles['opacity'].current = 1; 38 | } 39 | render() { 40 | this.renderedStyles['tx'].current = mouse.x - this.bounds.width/2; 41 | this.renderedStyles['ty'].current = mouse.y - this.bounds.height/2; 42 | 43 | for (const key in this.renderedStyles ) { 44 | this.renderedStyles[key].previous = lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].amt); 45 | } 46 | 47 | this.DOM.el.style.transform = `translateX(${(this.renderedStyles['tx'].previous)}px) translateY(${this.renderedStyles['ty'].previous}px) scale(${this.renderedStyles['scale'].previous})`; 48 | //this.DOM.el.style.opacity = this.renderedStyles['opacity'].previous; 49 | 50 | requestAnimationFrame(() => this.render()); 51 | } 52 | } -------------------------------------------------------------------------------- /src/js/index.js: -------------------------------------------------------------------------------- 1 | import { preloadImages, preloadFonts } from './utils'; 2 | import { gsap } from 'gsap'; 3 | import Cursor from './cursor'; 4 | import LocomotiveScroll from 'locomotive-scroll'; 5 | import ThumbnailAnimation from './thumbnailAnimation'; 6 | 7 | // Preload images and fonts 8 | Promise.all([preloadImages(), preloadFonts('lne5uqk')]).then(() => { 9 | // Remove loader (loading class) 10 | document.body.classList.remove('loading'); 11 | 12 | // Initialize custom cursor 13 | const cursor = new Cursor(document.querySelector('.cursor')); 14 | 15 | // Initialize the thumb animation obj 16 | const thumbAnimation = new ThumbnailAnimation(); 17 | 18 | // In a real case scenario the visible thumbs would be part of some sort of slideshow 19 | // We are just showing how to do the click/expand animation, so for now let's just animate out the two thumbs (left and right) once we click the thumb in the middle. 20 | const sideThumbs = document.querySelectorAll('.thumbs > .thumb'); 21 | 22 | // Mouse entering the middle thumb 23 | thumbAnimation.on('enter', () => { 24 | cursor.enter(); 25 | 26 | // Slightly move the side thumbs away 27 | gsap.to(sideThumbs, { 28 | duration: 1, 29 | ease: 'expo', 30 | opacity: 0.8, 31 | x: i => i ? '25%' : '-25%', 32 | //scale: 1.1, 33 | rotateY: i => i ? -15 : 15, 34 | z: 40 35 | }); 36 | }); 37 | 38 | // Mouse leaving the middle thumb 39 | thumbAnimation.on('leave', () => { 40 | cursor.leave(); 41 | 42 | // Move the side thumbs back into its original position 43 | gsap.to(sideThumbs, { 44 | duration: 1, 45 | ease: 'expo', 46 | opacity: 1, 47 | x: '0%', 48 | //scale: 1 49 | rotateY: 0, 50 | z: 0 51 | }); 52 | }); 53 | 54 | // Middle thumb click and start animation 55 | thumbAnimation.on('start', () => { 56 | cursor.leave(); 57 | 58 | // Move the side thumbs away 59 | gsap.to(sideThumbs, { 60 | duration: 0.8, 61 | ease: 'power3.inOut', 62 | opacity: 0, 63 | x: i => i ? '150%' : '-150%', 64 | //scale: 0.6 65 | rotateY: i => i ? -45 : 45, 66 | z: 200 67 | }); 68 | }); 69 | 70 | // Middle thumb image got fully expanded 71 | thumbAnimation.on('scrollready', () => { 72 | // Initialize the Locomotive scroll 73 | new LocomotiveScroll({ 74 | el: document.querySelector('[data-scroll-container]'), 75 | smooth: true 76 | }); 77 | }); 78 | 79 | [...document.querySelectorAll('a')].forEach(link => { 80 | link.addEventListener('mouseenter', () => cursor.enter()); 81 | link.addEventListener('mouseleave', () => cursor.leave()); 82 | }); 83 | }); -------------------------------------------------------------------------------- /src/js/thumbnailAnimation.js: -------------------------------------------------------------------------------- 1 | import { gsap } from 'gsap'; 2 | import { EventEmitter } from 'events'; 3 | import { calcWinsize } from './utils'; 4 | 5 | // Calculate the viewport size 6 | let winsize = calcWinsize(); 7 | window.addEventListener('resize', () => winsize = calcWinsize()); 8 | 9 | export default class ThumbnailAnimation extends EventEmitter { 10 | constructor(el) { 11 | super(); 12 | // Cache some DOM elements 13 | this.getDOMElems(); 14 | this.config = { 15 | // True if not yet expanded 16 | isThumbView: true 17 | }; 18 | // Calculate the necessary transforms to adjust the middle image size and position 19 | this.layout(); 20 | this.initEvents(); 21 | } 22 | getDOMElems() { 23 | this.DOM = {contentEl : document.querySelector('.content')}; 24 | this.DOM.intro = this.DOM.contentEl.querySelector('.content__intro'); 25 | this.DOM.introImg = this.DOM.contentEl.querySelector('.content__intro-img'); 26 | this.DOM.introImgWrap = this.DOM.contentEl.querySelector('.content__intro-imgWrap'); 27 | this.DOM.meta = this.DOM.contentEl.querySelector('.content__meta'); 28 | this.DOM.metaTitle = this.DOM.contentEl.querySelector('.content__meta-title'); 29 | this.DOM.titleBlock = this.DOM.contentEl.querySelector('.content__title > .anim-block'); 30 | this.DOM.subtitleBlock = this.DOM.contentEl.querySelector('.content__subtitle > .anim-block'); 31 | } 32 | layout() { 33 | // Hide main title and subtitle 34 | gsap.set(this.DOM.titleBlock, {y: '100%'}); 35 | gsap.set(this.DOM.subtitleBlock, {y: '100%'}); 36 | 37 | const bodyComputedStyle = getComputedStyle(document.body); 38 | 39 | // Get the image settings from the variables set in the stylesheet 40 | let imageSettings = { 41 | imageWidthStart: winsize.width, 42 | imageHeightStart: parseFloat(bodyComputedStyle.getPropertyValue('--image-height-start')), 43 | imageWidthEnd: parseFloat(bodyComputedStyle.getPropertyValue('--image-width-end')), 44 | imageHeightEnd: parseFloat(bodyComputedStyle.getPropertyValue('--image-height-end')), 45 | metaOffset: parseFloat(bodyComputedStyle.getPropertyValue('--meta-offset')) 46 | }; 47 | 48 | // Set the scale value from the initial and the target dimensions 49 | // and move the intro image wrap to the center of the screen. 50 | // We want to set the full width image to be the size of a thumbnail. 51 | // For that we scale down the outer wrapper and set a counter scale to 52 | // the inner image, followed by another scale so that the inner image 53 | // becomes the size of the thumbnail, too. 54 | let introRect = this.DOM.intro.getBoundingClientRect(); 55 | let introTransform = { 56 | scaleX: imageSettings.imageWidthEnd / imageSettings.imageWidthStart, 57 | scaleY: imageSettings.imageHeightEnd / imageSettings.imageHeightStart, 58 | y: (winsize.height/2 - introRect.top) - introRect.height/2 59 | }; 60 | gsap.set(this.DOM.intro, { 61 | y: introTransform.y, 62 | scaleX: introTransform.scaleX, 63 | scaleY: introTransform.scaleY 64 | }); 65 | 66 | // Apply the counter scale and again the target dimension scale to the inner image 67 | gsap.set(this.DOM.introImg, { 68 | scaleX: 1/introTransform.scaleX * imageSettings.imageWidthEnd / this.DOM.introImg.clientWidth, 69 | scaleY: 1/introTransform.scaleY * imageSettings.imageHeightEnd / this.DOM.introImg.clientHeight 70 | }); 71 | 72 | // Calculate new sizes/positions (after transform) 73 | introRect = this.DOM.intro.getBoundingClientRect(); 74 | // content__meta size/position 75 | const metaRect = this.DOM.meta.getBoundingClientRect(); 76 | let metaTransform = { 77 | x: introRect.left - metaRect.left, 78 | y: introRect.top - metaRect.top 79 | }; 80 | gsap.set(this.DOM.meta, { 81 | x: metaTransform.x, 82 | y: metaTransform.y - imageSettings.metaOffset 83 | }); 84 | 85 | // Show the meta title element 86 | gsap.set(this.DOM.metaTitle, {opacity: 1}); 87 | } 88 | initEvents() { 89 | this.onClickIntroFn = () => this.onClickIntro(); 90 | this.onMouseenterIntroFn = () => this.onMouseenterIntro(); 91 | this.onMouseleaveIntroFn = () => this.onMouseleaveIntro(); 92 | this.onResizeFn = () => this.onResize(); 93 | this.DOM.intro.addEventListener('click', this.onClickIntroFn); 94 | this.DOM.intro.addEventListener('mouseenter', this.onMouseenterIntroFn); 95 | this.DOM.intro.addEventListener('mouseleave', this.onMouseleaveIntroFn); 96 | window.addEventListener('resize', this.onResizeFn); 97 | } 98 | onResize() { 99 | if ( !this.config.isThumbView ) return false; 100 | // Reset transforms 101 | this.DOM.intro.style.transform = 'none'; 102 | this.DOM.introImg.style.transform ='none'; 103 | this.DOM.meta.style.transform = 'none'; 104 | // Re layout 105 | this.layout(); 106 | } 107 | onClickIntro() { 108 | // For demo purposes, remove the click event once clicked 109 | // We are just showcasing the effect 110 | // In a real case scenario there would be a back to slideshow button and the click event wouldn't need to be removed 111 | this.DOM.intro.removeEventListener('click', this.onClickIntroFn); 112 | this.config.isThumbView = false; 113 | 114 | // Animate all the elements (image, title, subtitle, meta) 115 | gsap 116 | .timeline({ 117 | onStart: () => this.emit('start'), 118 | onComplete: () => this.emit('end') 119 | }) 120 | .to(this.DOM.intro, { 121 | duration: 1, 122 | ease: 'expo.inOut', 123 | y: 0, 124 | scaleX: 1, 125 | scaleY: 1, 126 | onComplete: () => { 127 | this.DOM.contentEl.classList.add('content--scroll'); 128 | this.emit('scrollready'); 129 | } 130 | }) 131 | .to(this.DOM.introImg, { 132 | duration: 1, 133 | ease: 'expo.inOut', 134 | scaleX: 1, 135 | scaleY: 1 136 | }, 0) 137 | .to(this.DOM.introImgWrap, { 138 | duration: 1, 139 | ease: 'expo.inOut', 140 | scale: 1 141 | }, 0) 142 | .to(this.DOM.metaTitle, { 143 | duration: 0.6, 144 | ease: 'expo.inOut', 145 | opacity: 0 146 | }, 0) 147 | .to(this.DOM.meta, { 148 | duration: 1, 149 | ease: 'expo.inOut', 150 | x: 0, 151 | y: 0 152 | }, 0) 153 | .to([this.DOM.titleBlock, this.DOM.subtitleBlock], { 154 | duration: 0.8, 155 | ease: 'expo', 156 | startAt: {rotation: 3}, 157 | y: '0%', 158 | rotation: 0, 159 | stagger: 0.3 160 | }, 0.7); 161 | } 162 | // On mouseenter animate the scale of the intro image wrapper 163 | // This wrapper is necessary so we don't have to play with the image itself (which has already a transform applied) 164 | onMouseenterIntro() { 165 | if ( !this.config.isThumbView ) return false; 166 | this.emit('enter'); 167 | 168 | gsap 169 | .to(this.DOM.introImgWrap, { 170 | duration: 1, 171 | ease: 'expo', 172 | scale: 1.2 173 | }); 174 | } 175 | // On mouseleave reset/animate the scale of the intro image wrapper 176 | onMouseleaveIntro() { 177 | if ( !this.config.isThumbView ) return false; 178 | this.emit('leave'); 179 | 180 | gsap 181 | .to(this.DOM.introImgWrap, { 182 | duration: 1, 183 | ease: 'expo', 184 | scale: 1 185 | }); 186 | } 187 | } -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | const imagesLoaded = require('imagesloaded'); 2 | 3 | // Linear interpolation 4 | const lerp = (a, b, n) => (1 - n) * a + n * b; 5 | 6 | // Gets the mouse position 7 | const getMousePos = e => { 8 | return { 9 | x : e.clientX, 10 | y : e.clientY 11 | }; 12 | }; 13 | 14 | const calcWinsize = () => { 15 | return {width: window.innerWidth, height: window.innerHeight}; 16 | }; 17 | 18 | // Preload images 19 | const preloadImages = (selector = 'img') => { 20 | return new Promise((resolve) => { 21 | imagesLoaded(document.querySelectorAll(selector), resolve); 22 | }); 23 | }; 24 | 25 | // Preload images 26 | const preloadFonts = (id) => { 27 | return new Promise((resolve) => { 28 | WebFont.load({ 29 | typekit: { 30 | id: id 31 | }, 32 | active: resolve 33 | }); 34 | }); 35 | }; 36 | 37 | export { 38 | lerp, 39 | getMousePos, 40 | calcWinsize, 41 | preloadImages, 42 | preloadFonts 43 | }; 44 | --------------------------------------------------------------------------------