Last active
August 30, 2017 13:36
-
-
Save Jeremboo/158265d376fe0bf61316ec6a32eb4f15 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
/** | |
* @author jeremboo https://jeremieboulay.fr | |
* | |
* ScrollEngine.js to create parallax effect on your html page | |
* with the transform CSS property and an animationFrameRate. | |
* | |
* Usage: | |
* ------- | |
* | |
* // HTML | |
* // Build the html content around a unique div. | |
* <body> | |
* <div id="wrapper"> | |
* ... | |
* </div> | |
* </body> | |
* | |
* // JS | |
* // Add DOM an unique element into the scrollEngine : | |
* scrollEngine.add('#element img', { | |
* velocity: 0.2, | |
* }); | |
* ... | |
* | |
* // Then start the scrollEngine | |
* scrollEngine.start(document.getELementById('wrapper')); | |
* | |
* // Don't forget to start the loop frameRate | |
* loop.start(); | |
* | |
**/ | |
import loop from './loop'; | |
/** | |
* CLASS FOR EACH DOM ELEMENT ADDED INTO THE SCROLL ENGINE | |
*/ | |
class ScrollableElm { | |
constructor(elm, { padding = 0, velocity = 0, translate = 0, height = false, initialPosition = false } = {}) { | |
this.elm = elm; | |
this.initialPosition = initialPosition; | |
this.offsetHeight = height; | |
this.innerPosition = this.initialPosition % window.innerHeight; | |
this.velocity = velocity; | |
this.padding = padding; | |
this.baseTranslateX = translate; // Base of the translateX | |
this.isActive = false; | |
this.drag = 0; | |
this.translate = 0; | |
this.position = 0; | |
// Public | |
this.top = 0; | |
this.bottom = 0; | |
this.update(0); | |
} | |
active() { | |
this.isActive = true; | |
} | |
disable() { | |
this.isActive = false; | |
} | |
onResize() { | |
this.initialPosition = this.elm.offsetTop; | |
} | |
update(currentScrollPosition) { | |
// update element props | |
const offsetHeight = this.offsetHeight || this.elm.offsetHeight; | |
const initialPosition = this.initialPosition || this.elm.offsetTop; | |
// The shift due to the velocity | |
this.drag = ((currentScrollPosition + this.innerPosition) - initialPosition) * this.velocity; | |
// The translated property position | |
this.translate = (currentScrollPosition - this.drag) - this.baseTranslateX; | |
// The current visible top and bottom position of the element into the screen | |
this.position = initialPosition - this.translate; | |
this.top = this.position - this.padding; | |
this.bottom = this.position + offsetHeight + this.padding; | |
} | |
setDomPosition() { | |
this.elm.style.transform = `translate3d(0px, ${-this.translate}px, 0px)`; | |
} | |
} | |
/** | |
* SCROLL ENGINE WHO MANAGE PARALLAX | |
*/ | |
class ScrollEngine { | |
constructor() { | |
// Props only update on scroll | |
this.targetedPosition = 0; | |
// Props update into the loop | |
this.position = 0; // the current position of the scroll | |
this.force = 0; // the force of the scroll motion | |
this.top = 0; // the top of the scroll area | |
this.bottom = window.innerHeight; // the bottom of the scroll area | |
this.scrollDown = false; // the orientation of the scroll motion | |
this.elmWrapper = false; // the elmWrapper to scroll | |
this.elmSizeKeeper = false; // the div element to keep the size into the dom | |
this.elms = []; | |
this._update = this._update.bind(this); | |
this._updateScroll = this._updateScroll.bind(this); | |
this._onResize = this._onResize.bind(this); | |
} | |
/** | |
* START / STOP | |
*/ | |
start(elmWrapper) { | |
// Update html | |
this.elmWrapper = elmWrapper; | |
this.elmWrapper.style.position = 'fixed'; | |
this.elmWrapper.style.width = '100%'; | |
this.elmWrapper.style.top = 0; | |
this.elmWrapper.style.left = 0; | |
this.elmSizeKeeper = document.createElement('div'); | |
this.elmSizeKeeper.id = 'size-keeper'; | |
this.elmSizeKeeper.style.height = `${this.elmWrapper.offsetHeight}px`; | |
this.elmWrapper.parentNode.appendChild(this.elmSizeKeeper); | |
window.addEventListener('scroll', this._updateScroll); | |
window.addEventListener('resize', this._onResize); | |
// update | |
loop.add(this._update); | |
this._updateScroll(); | |
} | |
stop() { | |
window.removeEventListener('scroll', this._updateScroll); | |
window.removeEventListener('resize', this._onResize); | |
loop.remove(this._update); | |
} | |
/** | |
* Add an element to scroll | |
* @type {[type]} | |
*/ | |
add(querySelection, props) { | |
const elm = document.querySelector(querySelection); | |
if (!elm) { | |
console.log('ScrollEngine.add() ERROR: no DOM element found with this selector'); | |
return; | |
} | |
this.elms.push(new ScrollableElm(elm, props)); | |
} | |
remove(querySelection) { | |
// TODO | |
console.warning('TODO: test the removeComponent() function'); | |
const elm = document.querySelector(querySelection); | |
if (!elm) { | |
console.log('ScrollEngine.remove() ERROR: no DOM element found with this selector'); | |
return; | |
} | |
let i = 0; | |
let index = -1; | |
while (index === -1 && i < this.elms.length) { | |
if (this.elms[i].elm === elm) { | |
this.elms.splice(i, 1); | |
// TODO also remove to the scrolledElms | |
index = i; | |
} | |
i++; | |
} | |
} | |
/** | |
* ***************** | |
* UPDATE | |
* ***************** | |
*/ | |
/** | |
* Update the current scroll position. | |
* And update the scrollableComponents status. | |
*/ | |
_updateScroll(e) { | |
// Update the targeted view position | |
this.targetedPosition = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; | |
this._updateProps(); // Must be sure props are updated | |
// Update the status of each component | |
// to see if they are into the scroll area | |
let i; | |
for (i = this.elms.length - 1; i >= 0; i--) { | |
// this.elms[i].update(this.position); | |
if (this._isIntoScrollArea(this.elms[i]) && !this.elms[i].isActive) { | |
this.elms[i].active(); | |
} | |
} | |
} | |
/** | |
* Update the properties of the scroll motion | |
*/ | |
_updateProps() { | |
this.force = (this.targetedPosition - this.position) * 0.1; | |
// Update the viewport who will be visible | |
this.scrollDown = (this.force > 0); // positive === down | |
this.top = this.scrollDown ? this.position : this.targetedPosition; | |
this.bottom = (this.scrollDown ? this.targetedPosition : this.position) + window.innerHeight; | |
} | |
_update() { | |
// Update the size keeper | |
this.elmSizeKeeper.style.height = `${this.elmWrapper.offsetHeight}px`; | |
// TODO could be made with gpu.js | |
this._updateProps(); | |
this.position += this.force; | |
let i; | |
for (i = this.elms.length - 1; i >= 0; i--) { | |
const scrollableElm = this.elms[i]; | |
scrollableElm.update(this.position); | |
// Move | |
if (scrollableElm.isActive) { | |
scrollableElm.setDomPosition(); | |
if (Math.abs(this.force) < 0.1 || !this._isIntoScrollArea(scrollableElm)) { | |
scrollableElm.disable(); | |
} | |
} | |
} | |
} | |
_onResize() { | |
this.elmSizeKeeper.style.height = `${this.elmWrapper.offsetHeight}px`; | |
for (let i = this.elms.length - 1; i >= 0; i--) { | |
this.elms[i].onResize(); | |
} | |
} | |
/** | |
* ***************** | |
* TESTS | |
* ***************** | |
*/ | |
/** | |
* Test if a scrollable element is into the scroll area. | |
*/ | |
_isIntoScrollArea(scrollabelElm) { | |
return ( | |
(scrollabelElm.bottom + this.position) > this.top && | |
(scrollabelElm.top - this.position) < this.bottom | |
); | |
} | |
} | |
const scrollEngine = new ScrollEngine(); | |
export default scrollEngine; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment