Created
November 23, 2017 10:28
-
-
Save Ahrengot/4ba4977beb4d72fa6c9e36cce3db434f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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