// ------------------------------------------ // Rellax.js - v1.0.0 // Buttery smooth parallax library // Copyright (c) 2016 Moe Amaya (@moeamaya) // MIT license // // Thanks to Paraxify.js and Jaime Cabllero // for parallax concepts // ------------------------------------------ import { getCurrentScreen } from '../helpers/current-screen' import innerHeight from './ios-inner-height' // Ahh a pure function, gets new transform value // based on scrollPostion and speed // Allow for decimal pixel values const updatePosition = (percentage, speed) => speed * (100 * (1 - percentage)) // We want to cache the parallax blocks' // values: base, top, height, speed // el: is dom object, return: el cache values const createBlock = ({ el = null, speed = null, fitInsideContainer = null, isVisible = false, shouldSetHeightToIncrease = true, parallaxBehavior = 'desktop:tablet:mobile', }) => { // Optional individual block speed as data attr, otherwise global speed // Check if has percentage attr, and limit speed to 5, else limit it to 10 // The function is named clamp speed = speed <= -5 ? -5 : speed >= 5 ? 5 : speed // We need to guess the position the background will be, when the section // will reach the top of the viewport. This calculation will be based on the // speed for sure if (fitInsideContainer && shouldSetHeightToIncrease) { let heightWeWantToIncrease = 0 if (speed > 0) { heightWeWantToIncrease = updatePosition(0.5, speed) } else { heightWeWantToIncrease = updatePosition( innerHeight() / (fitInsideContainer.clientHeight + innerHeight()), speed ) - updatePosition(0.5, speed) } heightWeWantToIncrease = Math.abs(heightWeWantToIncrease) * 2 if (!isVisible) { el.parentNode.removeAttribute('style') } else { el.parentNode.style.height = `calc(100% + ${heightWeWantToIncrease}px)` } } // initializing at scrollY = 0 (top of browser) // ensures elements are positioned based on HTML layout. let { top, height } = nullifyTransforms( fitInsideContainer ? fitInsideContainer : el ) var blockTop = pageYOffset + top return { parallaxBehavior, shouldSetHeightToIncrease, fitInsideContainer, el, top: blockTop, height, speed, isVisible, } } function elementInViewport(el) { var rect = el.getBoundingClientRect() return ( rect.bottom > -450 && rect.top - 450 < (innerHeight() || document.documentElement .clientHeight) /* or $(window).height() */ ) } function nullifyTransforms(el) { if (!el) return null //add sanity checks and default values let { top, left, right, width, height } = el.getBoundingClientRect() let transformArr = window .getComputedStyle(el) .transform.split(/\(|,|\)/) .slice(1, -1) .map((v) => parseFloat(v)) if (transformArr.length != 6) { return el.getBoundingClientRect() } // 2D matrix // need some math to apply inverse of matrix // That is the matrix of the transformation of the element var t = transformArr let det = t[0] * t[3] - t[1] * t[2] /*if (transformArr.length > 6)*/ //3D matrix //haven't done the calculation to apply inverse of 4x4 matrix return { width: width / t[0], height: height / t[3], left: (left * t[3] - top * t[2] + t[2] * t[5] - t[4] * t[3]) / det, right: (right * t[3] - top * t[2] + t[2] * t[5] - t[4] * t[3]) / det, top: (-left * t[1] + top * t[0] + t[4] * t[1] - t[0] * t[5]) / det, } } export class Rellax { constructor() { this.blocks = [] this.oldPosY = false this.intersectionObserver = new IntersectionObserver( (entries) => { entries.map( ({ target: el, isIntersecting, intersectionRatio }) => { let blocks = this.blocks.filter( ({ fitInsideContainer, el: blockEl }) => blockEl.closest('svg') ? blockEl.closest('svg') === el : fitInsideContainer === el || blockEl === el ) let hasNewBlock = false this.blocks = this.blocks.map((block) => { const isThisBlock = block.el.closest('svg') ? block.el.closest('svg') === el : block.fitInsideContainer === el || block.el === el if (!isThisBlock) { return block } hasNewBlock = true return createBlock({ ...block, isVisible: isIntersecting && block.parallaxBehavior.indexOf( getCurrentScreen({ withTablet: true }) ) > -1, }) }) if (hasNewBlock) { this.oldPosY = false this.animate() } } ) }, { rootMargin: '450px', } ) // Start the loop this.update() // The loop does nothing if the scrollPosition did not change // so call animate to make sure every element has their transforms this.animate() } removeEl({ el }) { el.removeAttribute('style') this.blocks = this.blocks.filter(({ el: e }) => e !== el) } addEl({ el, speed, fitInsideContainer = null, shouldSetHeightToIncrease = true, parallaxBehavior = 'desktop:tablet:mobile', }) { if (fitInsideContainer) { this.intersectionObserver.observe(fitInsideContainer) } else { this.intersectionObserver.observe( el.closest('svg') ? el.closest('svg') : el ) } this.blocks.push( createBlock({ el, speed, fitInsideContainer, isVisible: elementInViewport( fitInsideContainer ? fitInsideContainer : el ) && parallaxBehavior.indexOf( getCurrentScreen({ withTablet: true }) ) > -1, shouldSetHeightToIncrease, parallaxBehavior, }) ) } update() { if (!this.oldPosY && this.oldPosY !== 0) { this.animate() } if (this.setPosition()) { this.animate() } requestAnimationFrame(this.update.bind(this)) } setPosition() { if (this.blocks.length === 0) return false let old = this.oldPosY this.oldPosY = pageYOffset return old != pageYOffset } animate() { this.blocks.map((block) => { if (!block.isVisible) { block.el.removeAttribute('style') return } var percentage = (pageYOffset - block.top + innerHeight()) / (block.height + innerHeight()) let { top, height } = nullifyTransforms( block.fitInsideContainer ? block.fitInsideContainer : block.el ) if (!height) { height = ( block.fitInsideContainer ? block.fitInsideContainer : block.el ).getBoundingClientRect().height } const newPercentage = 1 - (top + (block.el.dataset.percentage && parseInt(block.el.dataset.percentage, 10) === 0 ? 0 : height / 2)) / innerHeight() // Subtracting initialize value, so element stays in same spot as HTML var position = updatePosition( block.fitInsideContainer ? percentage : newPercentage, block.speed ) - updatePosition( block.el.dataset.percentage ? parseInt(block.el.dataset.percentage, 10) : 0.5, block.speed ) // Move that element block.el.style.transform = `translate3d(0, ${position}px, 0)` }) } }