Skip to content

Instantly share code, notes, and snippets.

@Jeremboo
Last active August 30, 2017 13:36
Show Gist options
  • Save Jeremboo/158265d376fe0bf61316ec6a32eb4f15 to your computer and use it in GitHub Desktop.
Save Jeremboo/158265d376fe0bf61316ec6a32eb4f15 to your computer and use it in GitHub Desktop.
/**
* @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