Home Reference Source Repository

src/NormalParallax.js

  1. import { noop, getElements } from './modules/utility'
  2.  
  3. /**
  4. * Parallax library
  5. */
  6. export default class NormalParallax {
  7. /**
  8. * @param {string|NodeList|Element|Element[]} target - Target elements (selector or element object)
  9. * @param {Object} [options={}]
  10. * @param {onResize} [options.onResize=noop] - Resize event handler
  11. * @param {onScroll} [options.onScroll=noop] - Scroll event handler
  12. * @param {boolean} [options.isRound=false] - Whether transform style value is rounded or not
  13. * @param {boolean} [options.autoRun=true] - Whether to run automatically
  14. * @param {number} [options.speed=0.1] - Moving speed (-Infinity to Infinity)
  15. * @param {number} [options.speedSp=options.speed] - Moving speed in SP display (default is same as PC)
  16. * @param {function} [options.isSP=noop] - Function to determine whether SP display or not
  17. */
  18. constructor (target, options = {}) {
  19. this._els = getElements(target)
  20. if (this._els.length === 0) {
  21. this._disabled = true
  22. return
  23. }
  24.  
  25. const {
  26. speed = 0.1,
  27. speedSp = speed,
  28. isSP = noop,
  29. onResize = noop,
  30. onScroll = noop,
  31. isRound = false,
  32. autoRun = true
  33. } = options
  34.  
  35. this._optionOnResize = onResize
  36. this._optionOnScroll = onScroll
  37. this._fTansform = `_getTransform${isRound ? 'Round' : ''}`
  38. this._scrollTarget = document.scrollingElement || document.documentElement
  39. this._speedPc = speed
  40. this._speedSp = speedSp
  41. this._isSP = isSP
  42.  
  43. this._onResize = () => {
  44. this.update()
  45. this._optionOnResize(this._windowHeight)
  46. }
  47. window.addEventListener('resize', this._onResize)
  48.  
  49. this._onLoad = () => {
  50. this.update()
  51. }
  52. window.addEventListener('load', this._onLoad)
  53.  
  54. autoRun && this.run()
  55. }
  56.  
  57. /**
  58. * Cache various values
  59. */
  60. _cache () {
  61. this._isSpCurrent = this._isSP()
  62. this._speed = this._isSpCurrent ? this._speedSp : this._speedPc
  63. this._speed *= window.innerWidth / window.innerHeight
  64. this._windowHeight = window.innerHeight
  65. const scrollY = window.scrollY || window.pageYOffset
  66.  
  67. this._items = this._els
  68. .map(el => {
  69. el.style.transform = 'none'
  70. const willChange = window.getComputedStyle(el).willChange
  71. el.style.willChange = 'transform'
  72. if (!(willChange === 'auto' || willChange === 'transform')) el.style.willChange += ', ' + willChange
  73.  
  74. return this._cacheElementPos(el, scrollY)
  75. })
  76. .filter(item => item)
  77. }
  78.  
  79. /**
  80. * Each frame of animation
  81. */
  82. _tick () {
  83. const scrollTop = this._scrollTarget.scrollTop
  84. if (scrollTop !== this._scrollTop) {
  85. // When the scroll position changes
  86. this._scrollTop = scrollTop
  87. this._update()
  88. }
  89.  
  90. this._animationFrameId = requestAnimationFrame(() => { this._tick() })
  91. }
  92.  
  93. /**
  94. * Update the position of each element
  95. */
  96. _update () {
  97. this._centerViewport = this._scrollTop + this._windowHeight / 2
  98.  
  99. this._items.forEach(item => this._updateElement(item))
  100.  
  101. this._optionOnScroll(this._scrollTop)
  102. }
  103.  
  104. /**
  105. * Return the value of transform
  106. */
  107. _getTransform (position) {
  108. return `translate3d(0, ${position}px, 0)`
  109. }
  110.  
  111. /**
  112. * Return the value of transform
  113. * In order to avoid problems such as bleeding, convert the number to an integers
  114. */
  115. _getTransformRound (position) {
  116. return `translate3d(0, ${Math.round(position)}px, 0)`
  117. }
  118.  
  119. /**
  120. * Cache various values of one element
  121. */
  122. _cacheElementPos (el, scrollY) {
  123. // Do not parallax if it is specified to invalidate by SP
  124. if (this._isSpCurrent && el.dataset.sp === 'false') return
  125.  
  126. const bounding = el.getBoundingClientRect()
  127. const top = bounding.top + scrollY
  128.  
  129. return {
  130. el,
  131. top,
  132. center: top + bounding.height / 2,
  133. speed: parseFloat(el.dataset.speed, 10) || this._speed,
  134. inPos: top - this._windowHeight,
  135. outPos: bounding.bottom + scrollY
  136. }
  137. }
  138.  
  139. /**
  140. * Update the position of one element
  141. */
  142. _updateElement (item) {
  143. if (this._scrollTop > item.outPos) {
  144. // After the element disappears in the upper direction
  145. } else if (this._scrollTop > item.inPos) {
  146. // After the element can be seen from below
  147. const position =
  148. (item.center - this._centerViewport) * item.speed
  149. item.el.style.transform = this[this._fTansform](position)
  150. }
  151. }
  152.  
  153. /**
  154. * Run animation
  155. */
  156. run () {
  157. if (this._disabled) return
  158.  
  159. this._cache()
  160. this._animationFrameId = requestAnimationFrame(() => { this._tick() })
  161. }
  162.  
  163. /**
  164. * Update cache and position
  165. */
  166. update () {
  167. if (this._disabled) return
  168.  
  169. this._cache()
  170. this._update()
  171. }
  172.  
  173. /**
  174. * Destroy instance
  175. */
  176. destroy () {
  177. if (!this._els) return
  178.  
  179. cancelAnimationFrame(this._animationFrameId)
  180.  
  181. this._els.forEach(el => {
  182. el.style.transform = null
  183. el.style.willChange = null
  184. })
  185.  
  186. window.removeEventListener('resize', this._onResize)
  187. window.removeEventListener('load', this._onLoad)
  188. }
  189. }
  190.  
  191. /**
  192. * @typedef {function} onResize
  193. * @param {number} windowHeight - `window.innerHeight`
  194. */
  195.  
  196. /**
  197. * @typedef {function} onScroll
  198. * @param {number} scrollTop - `document.scrollingElement.scrollTop`
  199. */