Skip to content

Instantly share code, notes, and snippets.

@Ahrengot
Created November 23, 2017 10:28
Show Gist options
  • Save Ahrengot/4ba4977beb4d72fa6c9e36cce3db434f to your computer and use it in GitHub Desktop.
Save Ahrengot/4ba4977beb4d72fa6c9e36cce3db434f to your computer and use it in GitHub Desktop.
import _ from 'underscore';
import ImageLoader from '../util/image-loader';
const getScreenSize = width => {
const sizeBreakpoint = 900;
return width > sizeBreakpoint ? 'lg' : 'sm';
}
/**
* Returns scroll progress for screen viewport vs
* component on page.
*
* If screen viewport is above the component (i.e. the component
* is placed halfway down the page) the progress value
* will return a negative value.
*
* When screenBottom <= componentTop the result is -1
* When screenTop === componentTop the result is 0.
* When screenTop >= componentBottom the result is 1
* When the users viewport is scrolling over the component the result is somewhere between 0-1.
*/
const getScrollProgress = (componentTop, componentBottom, screenTop, screenBottom) => {
// Entire component is below the viewport
if ( componentTop >= screenBottom ) {
return -1;
}
// Entire component is above the viewport
else if ( screenTop >= componentBottom ) {
return 1;
}
// A little silly to have this as an explicit condition,
// but given that it's often the case, this will be a slight performance
// boost in most cases.
else if (screenTop === componentTop) {
return 0;
}
// Component is somewhere within the viewport.
else {
const screenHeight = screenBottom - screenTop;
const componentHeight = componentBottom - componentTop;
const progress = (() => {
if (screenTop <= componentTop) {
const result = -1 + ((screenBottom - componentTop) / screenHeight);
return result;
} else {
return (screenTop - componentTop) / componentHeight;
}
})();
return progress;
}
}
const getTransform = (prog, moveMax, scaleMax) => {
const scaleVal = Math.max(1, 1 + (prog * (scaleMax - 1)));
const moveVal = prog * moveMax;
return `translate(0, ${moveVal}vmin) scale(${scaleVal})`;
}
const getConfig = (images, id) => {
const img = _.findWhere(images, { id });
if ( img ) {
return img.config;
} else {
return {
moveMax: 0,
scaleMax: 1
}
}
}
class ParallaxHero {
constructor(containerEl, config) {
this.config = config;
this.containerEl = containerEl;
this.state = this.getInitialState();
this.onScroll = this.onScroll.bind(this);
this.onResize = this.onResize.bind(this);
window.addEventListener('scroll', this.onScroll);
window.addEventListener('resize', this.onResize);
this.loader = new ImageLoader();
this.loader.on('progress', this.onLoadProgress, this);
this.loader.on('complete', this.onImagesLoaded, this);
this.loader.load(this.getImages());
this.render();
}
getInitialState() {
const containerBounds = this.containerEl.getBoundingClientRect();
const scrollY = (window.pageYOffset || document.documentElement.scrollTop);
const { innerHeight, innerWidth } = window;
return {
screen: {
width: innerWidth,
height: innerHeight,
size: getScreenSize(innerWidth)
},
container: {
width: containerBounds.width,
height: containerBounds.height,
x: containerBounds.x,
y: containerBounds.y + scrollY
},
scroll: {
y: scrollY,
progress: getScrollProgress(containerBounds.y, containerBounds.y + containerBounds.height, scrollY, scrollY + innerHeight)
},
loadProgress: 0,
images: []
};
}
getImages() {
if ( !this.config.images ) {
// eslint-disable-next-line no-console
return console.error("No images provided in ", this.config);
}
return this.config.images.map( img => {
return {
id: img.id,
src: img.urls[this.state.screen.size]
}
});
}
setState(newState, cb = null) {
this.state = {
...this.state,
...newState,
};
if ( cb !== null ) {
cb();
}
if ( !this._pendingReRender ) {
requestAnimationFrame(() => {
this.render();
});
}
this._pendingReRender = true;
return this.state;
}
onScroll() {
const scrollY = (window.pageYOffset || document.documentElement.scrollTop);
const { y, height } = this.state.container;
this.setState({
scroll: {
y: scrollY,
progress: getScrollProgress(y, y + height, scrollY, scrollY + this.state.screen.height)
}
});
}
onResize() {
const { x, y, width, height } = this.containerEl.getBoundingClientRect();
const { innerHeight, innerWidth } = window;
this.setState({
screen: {
width: innerWidth,
height: innerHeight,
size: getScreenSize(innerWidth)
},
container: {
width,
height,
x,
y: y + this.state.scroll.y
},
scroll: {
y: this.state.scroll.y,
progress: getScrollProgress(y, y + height, this.state.scroll.y, this.state.scroll.y + innerHeight)
}
});
}
onLoadProgress(e) {
this.setState({ loadProgress: e.progress });
}
onImagesLoaded(loadQueue) {
// Hide preloader if it exists
const preloader = this.containerEl.querySelector('.preloader');
if ( preloader ) {
preloader.parentNode.removeChild(preloader);
}
const ids = _.pluck(this.config.images, 'id');
let images = [];
_.each(ids, id => {
const img = loadQueue._loadedResults[id];
if ( img ) {
this.containerEl.appendChild(img);
images.push({
id,
el: img
});
}
}, this);
const { y, height } = this.containerEl.getBoundingClientRect();
const containerY = y + this.state.scroll.y;
this.setState({
images: images,
container: {
...this.state.container,
height: height,
y: containerY,
},
scroll: {
y: this.state.scroll.y,
progress: getScrollProgress(containerY, containerY + height, this.state.scroll.y, this.state.scroll.y + this.state.screen.height)
}
});
}
render() {
if ( this.state.images.length && this.state.loadProgress >= 1 ) {
// Apply parallax transformations
_.each(this.state.images.slice(1), img => {
const { moveMax, scaleMax } = getConfig(this.config.images, img.id);
img.el.style.transform = getTransform(this.state.scroll.progress, moveMax, scaleMax);
}, this);
}
this._pendingReRender = false;
}
destroy() {
window.removeEventListener('scroll', this.onScroll);
window.removeEventListener('resize', this.onResize);
if ( this.loader ) {
this.loader.off('complete', this.onImagesLoaded, this);
this.loader.destroy();
this.loader = null;
}
this._pendingReRender = false;
}
}
export default ParallaxHero;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment