Skip to content

Instantly share code, notes, and snippets.

@jesperlandberg
Created August 5, 2019 12:14
Show Gist options
  • Select an option

  • Save jesperlandberg/094a0a91b9619439dbe579ba7123be91 to your computer and use it in GitHub Desktop.

Select an option

Save jesperlandberg/094a0a91b9619439dbe579ba7123be91 to your computer and use it in GitHub Desktop.
import store from '../store'
import math from '../utils/math'
import EventBus from '../utils/EventBus'
import { Events as GlobalRAFEvents } from '../utils/GlobalRAF'
import { Events as GlobalResizeEvents } from '../utils/GlobalResize'
class Parallax {
constructor(elems) {
this.elems = elems
this.cache = null
this.state = {
threshold: 100,
isResizing: false,
}
this.init()
}
run = ({ current }) => {
this.state.current = current
this.animateElems()
}
animateElems() {
this.cache.forEach((cache) => {
const { height, top, bottom, tl, duration } = cache
const { isVisible, start, end } = this.isVisible(top, bottom)
if (isVisible || this.state.isResizing) {
const { progress } = this.intersectRatio(height, start, end, duration)
tl.progress(progress)
}
})
}
isVisible(top, bottom) {
const { current } = this.state
const start = top - current
const end = bottom - current
const isVisible = start < store.height && end > 0
return {
isVisible,
start,
end
}
}
intersectRatio = (height, top, bottom, duration) => {
let progress
const start = top - store.height
const end = (store.height + bottom + height) * duration
progress = Math.abs(start / end)
progress = math.norm(progress, 0, 1)
return {
progress
}
}
getCache() {
if (this.elems) {
this.cache = []
this.elems.forEach(el => {
if ((el.dataset.animateMobile === undefined && store.isDevice) ||
(el.dataset.animateFirefox === undefined && store.isFirefox)) return
const tl = new TimelineLite({ paused: true })
const from = JSON.parse(el.dataset.from)
const to = {...JSON.parse(el.dataset.to), ...{ ease: Linear.easeNone }}
tl.fromTo(el, 1, from, to)
tl.progress(1)
const { top, bottom, height } = el.getBoundingClientRect()
tl.progress(0)
this.cache.push({
el: el,
tl: tl,
top: top > store.height ? top : store.height,
bottom: bottom + (store.height / 2),
height: height,
duration: el.dataset.duration || 1,
})
})
}
}
updateCache() {
this.elems.forEach(elem => {
const { top, bottom } = elem.getBoundingClientRect()
Object.assign(elem, {
top: top > store.height ? top : store.height,
bottom: bottom,
height: bottom - top,
})
})
}
addListeners() {
EventBus.on(GlobalRAFEvents.TICK, this.run)
EventBus.on(GlobalResizeEvents.RESIZE, this.onResize)
}
removeListeners() {
EventBus.off(GlobalRAFEvents.TICK, this.run)
EventBus.off(GlobalResizeEvents.RESIZE, this.onResize)
}
onResize = () => {
this.state.isResizing = true
this.updateCache()
this.state.isResizing = false
}
destroy() {
this.removeListeners()
this.cache = null
this.elems = null
this.state = null
}
init() {
this.getCache()
this.addListeners()
}
}
export default Parallax
import store from '../store'
import math from '../utils/math'
import EventBus from '../utils/EventBus'
import { Events as GlobalRAFEvents } from '../utils/GlobalRAF'
import { Events as GlobalResizeEvents } from '../utils/GlobalResize'
class P {
constructor(elems) {
this.elems = elems || document.querySelectorAll('[data-parallax]')
this.cache = null
this.state = {
total: this.elems.length,
isResizing: false,
}
this.init()
}
run = ({ current }) => {
this.state.current = current
this.loopElems()
}
loopElems() {
this.cache.forEach(this.animateElem)
}
animateElem = ({
el,
y, scale, opacity, rotation,
isTop, isBottom,
top, bottom, height,
... cache
}) => {
const {
isVisible,
start,
end
} = this.isVisible(top, bottom)
if (isVisible || !cache.out) {
const progress = this.intersectRatio(height, start, end, isTop, isBottom)
let yTransform = y ? `translateY(${(y[1] * progress) - y[0]}px)` : ''
let scaleTransform = scale ? `scale(${(scale[1] * progress) + scale[0]})` : ''
let rotationTransform = rotation ? `rotate(${(rotation[1] * progress)}deg)` : ''
el.style.transform = `
${yTransform}
${scaleTransform}
${rotationTransform}
translate3d(0, 0, 0)
`
if (opacity) {
el.style.opacity = opacity - progress
}
if (!cache.out) {
cache.out = false
}
} else if (cache.out) {
cache.out = true
}
}
isVisible(top, bottom) {
const { current } = this.state
const start = top - current
const end = bottom - current
const isVisible = start < store.height && end > 0
return {
isVisible,
start,
end
}
}
intersectRatio = (height, start, end, isTop, isBottom) => {
const proogressStart = start - store.height
let progress
let progressEnd
if (isTop) {
progressEnd = store.height - (store.height - height) + end
} else if (isBottom) {
progressEnd = end + store.height - start
} else {
progressEnd = end + store.height + height
}
progress = Math.abs(proogressStart / progressEnd)
progress = math.norm(progress, 0, 1)
return progress
}
getCache() {
if (this.elems) {
this.cache = []
this.elems.forEach((el, index) => {
if ((el.dataset.parallaxMobile === undefined && store.isDevice) ||
(el.dataset.parallaxNoFirefox != undefined && store.isFirefox)) return
const rect = el.getBoundingClientRect()
const cache = {}
cache.el = el
cache.index = index
cache.out = true
let offsetTop = 0
let offsetBottom = 0
const object = JSON.parse(el.dataset.parallax)
const objectEntries = Object.entries(object)
// Set keys and props
for (const [key, value] of objectEntries) {
const props = JSON.parse(value)
if (key === 'yPercent') {
offsetTop = Math.abs(rect.height * props[0])
offsetBottom = rect.height * props[1]
const start = offsetTop
const end = rect.height * props[1] + Math.abs(start)
cache.y = [start, end]
} else if (key === 'scale') {
const start = props[0]
const end = props[1] - start
cache.scale = [start, end]
} else if (key === 'opacity') {
const start = props[0]
const end = props[1]
cache.opacity = [start, end]
} else if (key === 'rotation') {
const start = props[0]
const end = props[1]
cache.rotation = [start, end]
}
}
cache.isBottom = rect.bottom >= (store.scrollHeight)
cache.isTop = rect.top < store.height
// Top
if (cache.isTop) {
cache.top = store.height + offsetTop
} else {
cache.top = rect.top - offsetTop
}
// Bottom
if (cache.isTop) {
cache.bottom = rect.bottom
} else if (cache.isBottom && !cache.isTop) {
cache.bottom = rect.bottom - store.height
} else {
cache.bottom = rect.bottom + offsetBottom
}
// Height
if (cache.isBottom) {
cache.height = 0
} else {
cache.height = rect.height
}
el.style.willChange = 'transform'
this.cache.push(cache)
})
}
}
addListeners() {
EventBus.on(GlobalRAFEvents.TICK, this.run)
EventBus.on(GlobalResizeEvents.RESIZE, this.onResize)
}
removeListeners() {
EventBus.off(GlobalRAFEvents.TICK, this.run)
EventBus.off(GlobalResizeEvents.RESIZE, this.onResize)
}
onResize = () => {
this.state.isResizing = true
this.state.isResizing = false
}
destroy() {
this.removeListeners()
this.cache = null
this.elems = null
this.state = null
}
init() {
this.getCache()
if (this.cache.length != 0) this.addListeners()
}
}
export default P
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment