Skip to content

Instantly share code, notes, and snippets.

@osied
Created August 2, 2020 21:37
Show Gist options
  • Save osied/287b86de81c5e9338ca76e46bdb0ce6d to your computer and use it in GitHub Desktop.
Save osied/287b86de81c5e9338ca76e46bdb0ce6d to your computer and use it in GitHub Desktop.
/**
* demo.js
* http://www.codrops.com
*
* Licensed under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*
* Copyright 2019, Codrops
* http://www.codrops.com
*/
{
// helper functions
const MathUtils = {
// map number x from range [a, b] to [c, d]
map: (x, a, b, c, d) => (x - a) * (d - c) / (b - a) + c,
// linear interpolation
lerp: (a, b, n) => (1 - n) * a + n * b,
// Random float
getRandomFloat: (min, max) => (Math.random() * (max - min) + min).toFixed(2)
};
// body element
const body = document.body;
// calculate the viewport size
let winsize;
const calcWinsize = () => winsize = {width: window.innerWidth, height: window.innerHeight};
calcWinsize();
// and recalculate on resize
window.addEventListener('resize', calcWinsize);
// scroll position
let docScroll;
// for scroll speed calculation
let lastScroll;
let scrollingSpeed = 0;
// scroll position update function
const getPageYScroll = () => docScroll = window.pageYOffset || document.documentElement.scrollTop;
window.addEventListener('scroll', getPageYScroll);
// Item
class Item {
constructor(el) {
// the .item element
this.DOM = {el: el};
// the inner image
this.DOM.image = this.DOM.el.querySelector('.content__item-img');
this.DOM.imageWrapper = this.DOM.image.parentNode;
this.DOM.title = this.DOM.el.querySelector('.content__item-title');
this.renderedStyles = {
// here we define which property will change as we scroll the page and the item is inside the viewport
// in this case we will be:
// - scaling the inner image
// - translating the item's title
// we interpolate between the previous and current value to achieve a smooth effect
imageScale: {
// interpolated value
previous: 0,
// current value
current: 0,
// amount to interpolate
ease: 0.1,
// current value setter
setValue: () => {
const toValue = 1.5;
const fromValue = 1;
const val = MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, fromValue, toValue);
return Math.max(Math.min(val, toValue), fromValue);
}
},
titleTranslationY: {
previous: 0,
current: 0,
ease: 0.1,
fromValue: Number(MathUtils.getRandomFloat(30,400)),
setValue: () => {
const fromValue = this.renderedStyles.titleTranslationY.fromValue;
const toValue = -1*fromValue;
const val = MathUtils.map(this.props.top - docScroll, winsize.height, -1 * this.props.height, fromValue, toValue);
return fromValue < 0 ? Math.min(Math.max(val, fromValue), toValue) : Math.max(Math.min(val, fromValue), toValue);
}
}
};
// gets the item's height and top (relative to the document)
this.getSize();
// set the initial values
this.update();
// use the IntersectionObserver API to check when the element is inside the viewport
// only then the element styles will be updated
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => this.isVisible = entry.intersectionRatio > 0);
});
this.observer.observe(this.DOM.el);
// init/bind events
this.initEvents();
}
update() {
// sets the initial value (no interpolation)
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
}
// apply changes/styles
this.layout();
}
getSize() {
const rect = this.DOM.el.getBoundingClientRect();
this.props = {
// item's height
height: rect.height,
// offset top relative to the document
top: docScroll + rect.top
}
}
initEvents() {
window.addEventListener('resize', () => this.resize());
}
resize() {
// gets the item's height and top (relative to the document)
this.getSize();
// on resize reset sizes and update styles
this.update();
}
render() {
// update the current and interpolated values
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].setValue();
this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
}
// and apply changes
this.layout();
}
layout() {
// scale the image
this.DOM.image.style.transform = `scale3d(${this.renderedStyles.imageScale.previous},${this.renderedStyles.imageScale.previous},1)`;
// translate the title
this.DOM.title.style.transform = `translate3d(0,${this.renderedStyles.titleTranslationY.previous}px,0)`;
}
}
// SmoothScroll
class SmoothScroll {
constructor() {
// the <main> element
this.DOM = {main: document.querySelector('main')};
// the scrollable element
// we translate this element when scrolling (y-axis)
this.DOM.scrollable = this.DOM.main.querySelector('div[data-scroll]');
// the items on the page
this.items = [];
this.DOM.content = this.DOM.main.querySelector('.content');
[...this.DOM.content.querySelectorAll('.content__item')].forEach(item => this.items.push(new Item(item)));
// here we define which property will change as we scroll the page
// in this case we will be translating on the y-axis
// we interpolate between the previous and current value to achieve the smooth scrolling effect
this.renderedStyles = {
translationY: {
// interpolated value
previous: 0,
// current value
current: 0,
// amount to interpolate
ease: 0.1,
// current value setter
// in this case the value of the translation will be the same like the document scroll
setValue: () => docScroll
}
};
// set the body's height
this.setSize();
// set the initial values
this.update();
// the <main> element's style needs to be modified
this.style();
// init/bind events
this.initEvents();
// start the render loop
requestAnimationFrame(() => this.render());
}
update() {
// sets the initial value (no interpolation) - translate the scroll value
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].previous = this.renderedStyles[key].setValue();
}
// translate the scrollable element
this.layout();
}
layout() {
this.DOM.scrollable.style.transform = `translate3d(0,${-1*this.renderedStyles.translationY.previous}px,0)`;
}
setSize() {
// set the heigh of the body in order to keep the scrollbar on the page
body.style.height = `${this.DOM.scrollable.scrollHeight}px`;
}
style() {
// the <main> needs to "stick" to the screen and not scroll
// for that we set it to position fixed and overflow hidden
this.DOM.main.style.position = 'fixed';
this.DOM.main.style.width = this.DOM.main.style.height = '100%';
this.DOM.main.style.top = this.DOM.main.style.left = 0;
this.DOM.main.style.overflow = 'hidden';
}
initEvents() {
// on resize reset the body's height
window.addEventListener('resize', () => this.setSize());
}
render() {
// Get scrolling speed
// Update lastScroll
scrollingSpeed = Math.abs(docScroll - lastScroll);
lastScroll = docScroll;
// update the current and interpolated values
for (const key in this.renderedStyles ) {
this.renderedStyles[key].current = this.renderedStyles[key].setValue();
this.renderedStyles[key].previous = MathUtils.lerp(this.renderedStyles[key].previous, this.renderedStyles[key].current, this.renderedStyles[key].ease);
}
// and translate the scrollable element
this.layout();
// for every item
for (const item of this.items) {
// if the item is inside the viewport call it's render function
// this will update item's styles, based on the document scroll value and the item's position on the viewport
if ( item.isVisible ) {
if ( item.insideViewport ) {
item.render();
}
else {
item.insideViewport = true;
item.update();
}
}
else {
item.insideViewport = false;
}
}
// loop..
requestAnimationFrame(() => this.render());
}
}
/***********************************/
/********** Preload stuff **********/
// Preload images
const preloadImages = () => {
return new Promise((resolve, reject) => {
imagesLoaded(document.querySelectorAll('.content__item-img'), {background: true}, resolve);
});
};
// And then..
preloadImages().then(() => {
// Remove the loader
document.body.classList.remove('loading');
// Get the scroll position and update the lastScroll variable
getPageYScroll();
lastScroll = docScroll;
// Initialize the Smooth Scrolling
new SmoothScroll();
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment