301 lines
6.8 KiB
JavaScript
301 lines
6.8 KiB
JavaScript
// ------------------------------------------
|
|
// 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)`
|
|
})
|
|
}
|
|
}
|