src/BackgroundParallax.js
import { noop, getElements } from './modules/utility'
/**
* Automatically calculate the moving distance from the height of the parent element
*/
export default class BackgroundParallax {
/**
* @param {string|NodeList|Element|Element[]} target - Target elements (selector or element object)
* @param {Object} [options={}]
* @param {onResize} [options.onResize=noop] - Resize event handler
* @param {onScroll} [options.onScroll=noop] - Scroll event handler
* @param {boolean} [options.isRound=false] - Whether transform style value is rounded or not
* @param {boolean} [options.autoRun=true] - Whether to run automatically
*/
constructor (target, options = {}) {
this._els = getElements(target)
if (this._els.length === 0) {
this._disabled = true
return
}
const {
onResize = noop,
onScroll = noop,
isRound = false,
autoRun = true
} = options
this._optionOnResize = onResize
this._optionOnScroll = onScroll
this._fTansform = `_getTransform${isRound ? 'Round' : ''}`
this._scrollTarget = document.scrollingElement || document.documentElement
this._onResize = () => {
this.update()
this._optionOnResize(this._windowHeight)
}
window.addEventListener('resize', this._onResize)
this._onLoad = () => {
this.update()
}
window.addEventListener('load', this._onLoad)
autoRun && this.run()
}
/**
* Cache various values
*/
_cache () {
this._windowHeight = window.innerHeight
const scrollY = window.scrollY || window.pageYOffset
this._items = this._els
.map(el => {
el.style.transform = 'none'
const willChange = window.getComputedStyle(el).willChange
el.style.willChange = 'transform'
if (!(willChange === 'auto' || willChange === 'transform')) el.style.willChange += ', ' + willChange
return this._cacheElementPos(el, scrollY)
})
.filter(item => item)
}
/**
* Each frame of animation
*/
_tick () {
const scrollTop = this._scrollTarget.scrollTop
if (scrollTop !== this._scrollTop) {
// When the scroll position changes
this._scrollTop = scrollTop
this._update()
}
this._animationFrameId = requestAnimationFrame(() => { this._tick() })
}
/**
* Update the position of each element
*/
_update () {
this._items.forEach(item => this._updateElement(item))
this._optionOnScroll(this._scrollTop)
}
/**
* Return the value of transform
*/
_getTransform (position) {
return `translate3d(0, ${position}px, 0)`
}
/**
* Return the value of transform
* In order to avoid problems such as bleeding, convert the number to an integers
*/
_getTransformRound (position) {
return `translate3d(0, ${Math.round(position)}px, 0)`
}
/**
* Cache various values of one element
*/
_cacheElementPos (el, scrollY) {
const bounding = el.getBoundingClientRect()
const boundingParent = el.parentNode.getBoundingClientRect()
const top = boundingParent.top + scrollY
const inPos = top - this._windowHeight
const outPos = boundingParent.bottom + scrollY
return {
el,
max: boundingParent.height - bounding.height,
inPos,
outPos,
distance: outPos - inPos,
offset: boundingParent.top - bounding.top
}
}
/**
* Update the position of one element
*/
_updateElement (item) {
if (this._scrollTop > item.outPos) {
// After the element disappears in the upper direction
} else {
const diff = this._scrollTop - item.inPos
if (diff > 0) {
// After the element can be seen from below
const rate = diff / item.distance
const position = item.offset + item.max - item.max * rate // max - (max - min) * rate
item.el.style.transform = this[this._fTansform](position)
}
}
}
/**
* Run animation
*/
run () {
if (this._disabled) return
this._cache()
this._animationFrameId = requestAnimationFrame(() => { this._tick() })
}
/**
* Update cache and position
*/
update () {
if (this._disabled) return
this._cache()
this._update()
}
/**
* Destroy instance
*/
destroy () {
if (!this._els) return
cancelAnimationFrame(this._animationFrameId)
this._els.forEach(el => {
el.style.transform = null
el.style.willChange = null
})
window.removeEventListener('resize', this._onResize)
window.removeEventListener('load', this._onLoad)
}
}
/**
* @typedef {function} onResize
* @param {number} windowHeight - `window.innerHeight`
*/
/**
* @typedef {function} onScroll
* @param {number} scrollTop - `document.scrollingElement.scrollTop`
*/