├── .gitignore
├── README.md
├── css
└── base.css
├── favicon.ico
├── img
├── 1.jpg
├── 2.jpg
├── 3.jpg
├── 4.jpg
├── 5.jpg
├── 6.jpg
├── 7.jpg
├── 8.jpg
└── 9.jpg
├── index.html
└── js
├── demo.js
└── imagesloaded.pkgd.min.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Smooth scrolling and inner image animation
2 |
3 | Demo for the [tutorial](https://tympanus.net/codrops/?p=41030) on how to add smooth page scrolling with an inner image animation.
4 |
5 | 
6 |
7 | [Article on Codrops](https://tympanus.net/codrops/?p=41030)
8 |
9 | [Demo](http://tympanus.net/Tutorials/SmoothScrollAnimations/)
10 |
11 | ## Credits
12 |
13 | - [imagesLoaded](https://imagesloaded.desandro.com/) by Dave DeSandro
14 | - Images from [Unsplash.com](https://unsplash.com/)
15 |
16 | ## License
17 | This resource can be used freely if integrated or build upon in personal or commercial projects such as websites, web apps and web templates intended for sale. It is not allowed to take the resource "as-is" and sell it, redistribute, re-publish it, or sell "pluginized" versions of it. Free plugins built using this resource should have a visible mention and link to the original work. Always consider the licenses of all included libraries, scripts and images used.
18 |
19 | ## Misc
20 |
21 | Follow Codrops: [Twitter](http://www.twitter.com/codrops), [Facebook](http://www.facebook.com/codrops), [Google+](https://plus.google.com/101095823814290637419), [GitHub](https://github.com/codrops), [Pinterest](http://www.pinterest.com/codrops/), [Instagram](https://www.instagram.com/codropsss/)
22 |
23 |
24 | [© Codrops 2019](http://www.codrops.com)
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/css/base.css:
--------------------------------------------------------------------------------
1 | *,
2 | *::after,
3 | *::before {
4 | box-sizing: border-box;
5 | }
6 |
7 | :root {
8 | font-size: 16px;
9 | }
10 |
11 | body {
12 | margin: 0;
13 | --color-text: #1c1b1b;
14 | --color-bg: #efe6e1;
15 | --color-link: #b07656;
16 | --color-link-hover: #000;
17 | --color-deco: #e4dbd6;
18 | color: var(--color-text);
19 | background-color: var(--color-bg);
20 | font-family: paralucent, sans-serif;
21 | font-family: quiroh, sans-serif;
22 | font-family: mr-eaves-xl-sans, sans-serif;
23 | }
24 |
25 | /* Page Loader */
26 | .js .loading::before {
27 | content: '';
28 | position: fixed;
29 | z-index: 100000;
30 | top: 0;
31 | left: 0;
32 | width: 100%;
33 | height: 100%;
34 | background: var(--color-bg);
35 | }
36 |
37 | .js .loading::after {
38 | content: '';
39 | position: fixed;
40 | z-index: 100000;
41 | top: 50%;
42 | left: 50%;
43 | width: 60px;
44 | height: 60px;
45 | margin: -30px 0 0 -30px;
46 | pointer-events: none;
47 | border-radius: 50%;
48 | opacity: 0.4;
49 | background: var(--color-link);
50 | animation: loaderAnim 0.7s linear infinite alternate forwards;
51 | }
52 |
53 | @keyframes loaderAnim {
54 | to {
55 | opacity: 1;
56 | transform: scale3d(0.5,0.5,1);
57 | }
58 | }
59 |
60 | a {
61 | text-decoration: none;
62 | color: var(--color-link);
63 | outline: none;
64 | }
65 |
66 | a:hover,
67 | a:focus {
68 | color: var(--color-link-hover);
69 | outline: none;
70 | }
71 |
72 | [data-scroll] {
73 | will-change: transform;
74 | }
75 |
76 | .frame {
77 | padding: 2.5rem 3rem;
78 | position: absolute;
79 | z-index: 10;
80 | }
81 |
82 | .frame__title {
83 | font-size: 1rem;
84 | margin: 0 0 2.5rem;
85 | }
86 |
87 | .frame__links {
88 | display: flex;
89 | flex-direction: column;
90 | align-items: flex-start;
91 | }
92 |
93 | .frame a {
94 | margin: 0.25rem 0;
95 | text-transform: lowercase;
96 | }
97 |
98 | .frame__github,
99 | .frame__links a:not(:last-child),
100 | .frame__demos a:not(:last-child) {
101 | margin-right: 1rem;
102 | }
103 |
104 | .frame__demos {
105 | margin: 1rem 0;
106 | }
107 |
108 | .frame__demo--current,
109 | .frame__demo--current:hover {
110 | color: var(--color-text);
111 | }
112 |
113 | .content {
114 | display: flex;
115 | flex-direction: column;
116 | position: relative;
117 | align-items: center;
118 | padding: 12rem 0;
119 | counter-reset: figure;
120 | }
121 |
122 | .item {
123 | margin: 10vh auto;
124 | max-width: 100%;
125 | position: relative;
126 | will-change: transform;
127 | }
128 |
129 | .item::before {
130 | counter-increment: figure;
131 | content: counter(figure, decimal-leading-zero);
132 | position: absolute;
133 | font-family: paralucent, sans-serif;
134 | font-size: 10rem;
135 | color: var(--color-deco);
136 | bottom: calc(100% - 3rem);
137 | }
138 |
139 | .item:nth-child(even)::before {
140 | right: 0;
141 | }
142 |
143 | .item__img-wrap {
144 | --aspect-ratio: 1/1.5;
145 | overflow: hidden;
146 | width: 500px;
147 | margin: 0 auto;
148 | padding-bottom: calc(100% / (var(--aspect-ratio)));
149 | max-width: calc(100% - 2rem);
150 | will-change: transform;
151 | }
152 |
153 | .item:first-child .item__img-wrap {
154 | --aspect-ratio: 8/10;
155 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/1.jpg);
156 | }
157 |
158 | .item:nth-child(2) .item__img-wrap {
159 | width: 1000px;
160 | --aspect-ratio: 120/76;
161 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/2.jpg);
162 | }
163 |
164 | .item:nth-child(3) .item__img-wrap {
165 | --aspect-ratio: 60/75;
166 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/3.jpg);
167 | }
168 |
169 | .item:nth-child(4) .item__img-wrap {
170 | width: 800px;
171 | --aspect-ratio: 900/505;
172 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/4.jpg);
173 | }
174 |
175 | .item:nth-child(5) .item__img-wrap {
176 | --aspect-ratio: 6/8;
177 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/5.jpg);
178 | }
179 |
180 | .item:nth-child(6) .item__img-wrap {
181 | width: calc(100vw - 2rem);
182 | --aspect-ratio: 1500/844;
183 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/6.jpg);
184 | }
185 |
186 | .item:nth-child(7) .item__img-wrap {
187 | width: 900px;
188 | --aspect-ratio: 1000/749;
189 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/7.jpg);
190 | }
191 |
192 | .item:nth-child(8) .item__img-wrap {
193 | width: 900px;
194 | --aspect-ratio: 1000/562;
195 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/8.jpg);
196 | }
197 |
198 | .item:nth-child(9) .item__img-wrap {
199 | --aspect-ratio: 60/75;
200 | --image: url(https://tympanus.net/Tutorials/SmoothScrollAnimations/img/9.jpg);
201 | }
202 |
203 | .item__img {
204 | --overflow: 40px;
205 | height: calc(100% + (2 * var(--overflow)));
206 | top: calc( -1 * var(--overflow));
207 | width: 100%;
208 | position: absolute;
209 | background-image: var(--image);
210 | background-size: cover;
211 | background-position: 50% 0%;
212 | will-change: transform;
213 | }
214 |
215 | .item__img--t1 {
216 | --overflow: 60px;
217 | }
218 |
219 | .item__img--t2 {
220 | --overflow: 80px;
221 | }
222 |
223 | .item__img--t3 {
224 | --overflow: 120px;
225 | }
226 |
227 | .item__caption {
228 | padding: 2rem 1rem;
229 | }
230 |
231 | .item__caption-title {
232 | font-family: paralucent, sans-serif;
233 | font-weight: 400;
234 | font-size: 3rem;
235 | margin: 0;
236 | }
237 |
238 | .item__caption-copy {
239 | margin: 0;
240 | }
241 |
242 | .item__caption-copy::before {
243 | content: '__';
244 | line-height: 1;
245 | color: var(--color-link);
246 | font-weight: 700;
247 | font-size: 3rem;
248 | margin: 0 0 1rem;
249 | display: block;
250 | }
251 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/favicon.ico
--------------------------------------------------------------------------------
/img/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/1.jpg
--------------------------------------------------------------------------------
/img/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/2.jpg
--------------------------------------------------------------------------------
/img/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/3.jpg
--------------------------------------------------------------------------------
/img/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/4.jpg
--------------------------------------------------------------------------------
/img/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/5.jpg
--------------------------------------------------------------------------------
/img/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/6.jpg
--------------------------------------------------------------------------------
/img/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/7.jpg
--------------------------------------------------------------------------------
/img/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/8.jpg
--------------------------------------------------------------------------------
/img/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codrops/SmoothScrollAnimations/06743813ac0a13bf94e021c56baa4419097e99fb/img/9.jpg
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Smooth scrolling and inner image animation | Codrops
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Smooth scrolling and inner image animation
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
Central view
33 |
Great turbulent clouds emerged into consciousness citizens.
34 |
35 |
36 |
37 |
38 |
39 |
Lost in time
40 |
Brain is the seed of intelligence the sky calls to us a very small stage.
41 |
42 |
43 |
44 |
45 |
46 |
Ready to land
47 |
Cosmos encyclopaedia galactica a billion trillion culture cosmic ocean.
48 |
49 |
50 |
51 |
52 |
53 |
All equal
54 |
Network of wormholes dream of the mind's eye finite but unbounded concept.
55 |
56 |
57 |
58 |
59 |
60 |
Connections
61 |
Two ghostly white figures in coveralls and helmets are softly dancing vastness.
62 |
63 |
64 |
65 |
66 |
67 |
The state of divergence
68 |
Vastness is bearable only through love invent the universe vanquish.
69 |
70 |
71 |
72 |
73 |
74 |
Open perspective
75 |
The only home we've ever known concept of the number one.
76 |
77 |
78 |
79 |
80 |
81 |
Discovery of shapes
82 |
Decipherment explorations tesseract as a patch of light.
83 |
84 |
85 |
86 |
87 |
88 |
The Observer
89 |
Astonishment muse about dispassionate extraterrestrial observer.
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/js/demo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * demo.js
3 | * http://www.codrops.com
4 | *
5 | * Licensed under the MIT license.
6 | * http://www.opensource.org/licenses/mit-license.php
7 | *
8 | * Copyright 2019, Codrops
9 | * http://www.codrops.com
10 | */
11 | {
12 | // helper functions
13 | const MathUtils = {
14 | // map number x from range [a, b] to [c, d]
15 | map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
16 | // linear interpolation
17 | lerp: (a, b, n) => (1 - n) * a + n * b
18 | };
19 |
20 | // body element
21 | const body = document.body;
22 |
23 | // calculate the viewport size
24 | let winsize;
25 | const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight};
26 | calcWinsize();
27 | // and recalculate on resize
28 | window.addEventListener('resize', calcWinsize);
29 |
30 | // scroll position and update function
31 | let docScroll;
32 | const getPageYScroll = () => docScroll = window.pageYOffset || document.documentElement.scrollTop;
33 | window.addEventListener('scroll', getPageYScroll);
34 |
35 | // Item
36 | class Item {
37 | constructor(el) {
38 | // the .item element
39 | this.DOM = {el: el};
40 | // the inner image
41 | this.DOM.image = this.DOM.el.querySelector('.item__img');
42 | this.renderedStyles = {
43 | // here we define which property will change as we scroll the page and the items is inside the viewport
44 | // in this case we will be translating the image on the y-axis
45 | // we interpolate between the previous and current value to achieve a smooth effect
46 | innerTranslationY: {
47 | // interpolated value
48 | previous: 0,
49 | // current value
50 | current: 0,
51 | // amount to interpolate
52 | ease: 0.1,
53 | // the maximum value to translate the image is set in a CSS variable (--overflow)
54 | maxValue: parseInt(getComputedStyle(this.DOM.image).getPropertyValue('--overflow'), 10),
55 | // current value setter
56 | // the value of the translation will be:
57 | // when the item's top value (relative to the viewport) equals the window's height (items just came into the viewport) the translation = minimum value (- maximum value)
58 | // when the item's top value (relative to the viewport) equals "-item's height" (item just exited the viewport) the translation = maximum value
59 | setValue: () => {
60 | const maxValue = this.renderedStyles.innerTranslationY.maxValue;
61 | const minValue = -1 * maxValue;
62 | return Math.max(Math.min(MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, minValue, maxValue), maxValue), minValue)
63 | }
64 | }
65 | };
66 | // set the initial values
67 | this.update();
68 | // use the IntersectionObserver API to check when the element is inside the viewport
69 | // only then the element translation will be updated
70 | this.observer = new IntersectionObserver((entries) => {
71 | entries.forEach(entry => this.isVisible = entry.intersectionRatio > 0);
72 | });
73 | this.observer.observe(this.DOM.el);
74 | // init/bind events
75 | this.initEvents();
76 | }
77 | update() {
78 | // gets the item's height and top (relative to the document)
79 | this.getSize();
80 | // sets the initial value (no interpolation)
81 | for (const key in this.renderedStyles ) {
82 | this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
83 | }
84 | // translate the image
85 | this.layout();
86 | }
87 | getSize() {
88 | const rect = this.DOM.el.getBoundingClientRect();
89 | this.props = {
90 | // item's height
91 | height: rect.height,
92 | // offset top relative to the document
93 | top: docScroll + rect.top
94 | }
95 | }
96 | initEvents() {
97 | window.addEventListener('resize', () => this.resize());
98 | }
99 | resize() {
100 | // on resize rest sizes and update the translation value
101 | this.update();
102 | }
103 | render() {
104 | // update the current and interpolated values
105 | for (const key in this.renderedStyles ) {
106 | this.renderedStyles[key].current = this.renderedStyles[key].setValue();
107 | this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
108 | }
109 | // and translates the image
110 | this.layout();
111 | }
112 | layout() {
113 | // translates the image
114 | this.DOM.image.style.transform = `translate3d(0,${this.renderedStyles.innerTranslationY.previous}px,0)`;
115 | }
116 | }
117 |
118 | // SmoothScroll
119 | class SmoothScroll {
120 | constructor() {
121 | // the element
122 | this.DOM = {main: document.querySelector('main')};
123 | // the scrollable element
124 | // we translate this element when scrolling (y-axis)
125 | this.DOM.scrollable = this.DOM.main.querySelector('div[data-scroll]');
126 | // the items on the page
127 | this.items = [];
128 | [...this.DOM.main.querySelectorAll('.content > .item')].forEach(item => this.items.push(new Item(item)));
129 | // here we define which property will change as we scroll the page
130 | // in this case we will be translating on the y-axis
131 | // we interpolate between the previous and current value to achieve the smooth scrolling effect
132 | this.renderedStyles = {
133 | translationY: {
134 | // interpolated value
135 | previous: 0,
136 | // current value
137 | current: 0,
138 | // amount to interpolate
139 | ease: 0.1,
140 | // current value setter
141 | // in this case the value of the translation will be the same like the document scroll
142 | setValue: () => docScroll
143 | }
144 | };
145 | // set the body's height
146 | this.setSize();
147 | // set the initial values
148 | this.update();
149 | // the element's style needs to be modified
150 | this.style();
151 | // init/bind events
152 | this.initEvents();
153 | // start the render loop
154 | requestAnimationFrame(() => this.render());
155 | }
156 | update() {
157 | // sets the initial value (no interpolation) - translate the scroll value
158 | for (const key in this.renderedStyles ) {
159 | this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
160 | }
161 | // translate the scrollable element
162 | this.layout();
163 | }
164 | layout() {
165 | // translates the scrollable element
166 | this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0)`;
167 | }
168 | setSize() {
169 | // set the heigh of the body in order to keep the scrollbar on the page
170 | body.style.height = `${this.DOM.scrollable.scrollHeight}px`;
171 | }
172 | style() {
173 | // the needs to "stick" to the screen and not scroll
174 | // for that we set it to position fixed and overflow hidden
175 | this.DOM.main.style.position = 'fixed';
176 | this.DOM.main.style.width = this.DOM.main.style.height = '100%';
177 | this.DOM.main.style.top = this.DOM.main.style.left = 0;
178 | this.DOM.main.style.overflow = 'hidden';
179 | }
180 | initEvents() {
181 | // on resize reset the body's height
182 | window.addEventListener('resize', () => this.setSize());
183 | }
184 | render() {
185 | // update the current and interpolated values
186 | for (const key in this.renderedStyles ) {
187 | this.renderedStyles[key].current = this.renderedStyles[key].setValue();
188 | this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
189 | }
190 | // and translate the scrollable element
191 | this.layout();
192 |
193 | // for every item
194 | for (const item of this.items) {
195 | // if the item is inside the viewport call it's render function
196 | // this will update the item's inner image translation, based on the document scroll value and the item's position on the viewport
197 | if ( item.isVisible ) {
198 | item.render();
199 | }
200 | }
201 |
202 | // loop..
203 | requestAnimationFrame(() => this.render());
204 | }
205 | }
206 |
207 | /***********************************/
208 | /********** Preload stuff **********/
209 |
210 | // Preload images
211 | const preloadImages = () => {
212 | return new Promise((resolve, reject) => {
213 | imagesLoaded(document.querySelectorAll('.item__img'), {background: true}, resolve);
214 | });
215 | };
216 |
217 | // And then..
218 | preloadImages().then(() => {
219 | // Remove the loader
220 | document.body.classList.remove('loading');
221 | // Get the scroll position
222 | getPageYScroll();
223 | // Initialize the Smooth Scrolling
224 | new SmoothScroll();
225 | });
226 | }
--------------------------------------------------------------------------------
/js/imagesloaded.pkgd.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * imagesLoaded PACKAGED v4.1.1
3 | * JavaScript is all like "You images are done yet or what?"
4 | * MIT License
5 | */
6 |
7 | !function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},n=i[t]=i[t]||[];return-1==n.indexOf(e)&&n.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},n=i[t]=i[t]||{};return n[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=i.indexOf(e);return-1!=n&&i.splice(n,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var n=0,o=i[n];e=e||[];for(var r=this._onceEvents&&this._onceEvents[t];o;){var s=r&&r[o];s&&(this.off(t,o),delete r[o]),o.apply(this,e),n+=s?0:1,o=i[n]}return this}},t}),function(t,e){"use strict";"function"==typeof define&&define.amd?define(["ev-emitter/ev-emitter"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("ev-emitter")):t.imagesLoaded=e(t,t.EvEmitter)}(window,function(t,e){function i(t,e){for(var i in e)t[i]=e[i];return t}function n(t){var e=[];if(Array.isArray(t))e=t;else if("number"==typeof t.length)for(var i=0;i