src/NormalParallax.js
import { noop, getElements } from './modules/utility'
/**
* Parallax library
*/
export default class NormalParallax {
/**
* @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
* @param {number} [options.speed=0.1] - Moving speed (-Infinity to Infinity)
* @param {number} [options.speedSp=options.speed] - Moving speed in SP display (default is same as PC)
* @param {function} [options.isSP=noop] - Function to determine whether SP display or not
*/
constructor (target, options = {}) {
this._els = getElements(target)
if (this._els.length === 0) {
this._disabled = true
return
}
const {
speed = 0.1,
speedSp = speed,
isSP = noop,
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._speedPc = speed
this._speedSp = speedSp
this._isSP = isSP
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._isSpCurrent = this._isSP()
this._speed = this._isSpCurrent ? this._speedSp : this._speedPc
this._speed *= window.innerWidth / window.innerHeight
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._centerViewport = this._scrollTop + this._windowHeight / 2
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) {
// Do not parallax if it is specified to invalidate by SP
if (this._isSpCurrent && el.dataset.sp === 'false') return
const bounding = el.getBoundingClientRect()
const top = bounding.top + scrollY
return {
el,
top,
center: top + bounding.height / 2,
speed: parseFloat(el.dataset.speed, 10) || this._speed,
inPos: top - this._windowHeight,
outPos: bounding.bottom + scrollY
}
}
/**
* Update the position of one element
*/
_updateElement (item) {
if (this._scrollTop > item.outPos) {
// After the element disappears in the upper direction
} else if (this._scrollTop > item.inPos) {
// After the element can be seen from below
const position =
(item.center - this._centerViewport) * item.speed
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`
*/