Home Reference Source Repository

src/BackgroundParallax.js

  1. import { noop, getElements } from './modules/utility'
  2.  
  3. /**
  4. * Automatically calculate the moving distance from the height of the parent element
  5. */
  6. export default class BackgroundParallax {
  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. */
  15. constructor (target, options = {}) {
  16. this._els = getElements(target)
  17. if (this._els.length === 0) {
  18. this._disabled = true
  19. return
  20. }
  21.  
  22. const {
  23. onResize = noop,
  24. onScroll = noop,
  25. isRound = false,
  26. autoRun = true
  27. } = options
  28.  
  29. this._optionOnResize = onResize
  30. this._optionOnScroll = onScroll
  31. this._fTansform = `_getTransform${isRound ? 'Round' : ''}`
  32. this._scrollTarget = document.scrollingElement || document.documentElement
  33.  
  34. this._onResize = () => {
  35. this.update()
  36. this._optionOnResize(this._windowHeight)
  37. }
  38. window.addEventListener('resize', this._onResize)
  39.  
  40. this._onLoad = () => {
  41. this.update()
  42. }
  43. window.addEventListener('load', this._onLoad)
  44.  
  45. autoRun && this.run()
  46. }
  47.  
  48. /**
  49. * Cache various values
  50. */
  51. _cache () {
  52. this._windowHeight = window.innerHeight
  53. const scrollY = window.scrollY || window.pageYOffset
  54.  
  55. this._items = this._els
  56. .map(el => {
  57. el.style.transform = 'none'
  58. const willChange = window.getComputedStyle(el).willChange
  59. el.style.willChange = 'transform'
  60. if (!(willChange === 'auto' || willChange === 'transform')) el.style.willChange += ', ' + willChange
  61.  
  62. return this._cacheElementPos(el, scrollY)
  63. })
  64. .filter(item => item)
  65. }
  66.  
  67. /**
  68. * Each frame of animation
  69. */
  70. _tick () {
  71. const scrollTop = this._scrollTarget.scrollTop
  72. if (scrollTop !== this._scrollTop) {
  73. // When the scroll position changes
  74. this._scrollTop = scrollTop
  75. this._update()
  76. }
  77.  
  78. this._animationFrameId = requestAnimationFrame(() => { this._tick() })
  79. }
  80.  
  81. /**
  82. * Update the position of each element
  83. */
  84. _update () {
  85. this._items.forEach(item => this._updateElement(item))
  86.  
  87. this._optionOnScroll(this._scrollTop)
  88. }
  89.  
  90. /**
  91. * Return the value of transform
  92. */
  93. _getTransform (position) {
  94. return `translate3d(0, ${position}px, 0)`
  95. }
  96.  
  97. /**
  98. * Return the value of transform
  99. * In order to avoid problems such as bleeding, convert the number to an integers
  100. */
  101. _getTransformRound (position) {
  102. return `translate3d(0, ${Math.round(position)}px, 0)`
  103. }
  104.  
  105. /**
  106. * Cache various values of one element
  107. */
  108. _cacheElementPos (el, scrollY) {
  109. const bounding = el.getBoundingClientRect()
  110. const boundingParent = el.parentNode.getBoundingClientRect()
  111. const top = boundingParent.top + scrollY
  112. const inPos = top - this._windowHeight
  113. const outPos = boundingParent.bottom + scrollY
  114.  
  115. return {
  116. el,
  117. max: boundingParent.height - bounding.height,
  118. inPos,
  119. outPos,
  120. distance: outPos - inPos,
  121. offset: boundingParent.top - bounding.top
  122. }
  123. }
  124.  
  125. /**
  126. * Update the position of one element
  127. */
  128. _updateElement (item) {
  129. if (this._scrollTop > item.outPos) {
  130. // After the element disappears in the upper direction
  131. } else {
  132. const diff = this._scrollTop - item.inPos
  133. if (diff > 0) {
  134. // After the element can be seen from below
  135. const rate = diff / item.distance
  136. const position = item.offset + item.max - item.max * rate // max - (max - min) * rate
  137.  
  138. item.el.style.transform = this[this._fTansform](position)
  139. }
  140. }
  141. }
  142.  
  143. /**
  144. * Run animation
  145. */
  146. run () {
  147. if (this._disabled) return
  148.  
  149. this._cache()
  150. this._animationFrameId = requestAnimationFrame(() => { this._tick() })
  151. }
  152.  
  153. /**
  154. * Update cache and position
  155. */
  156. update () {
  157. if (this._disabled) return
  158.  
  159. this._cache()
  160. this._update()
  161. }
  162.  
  163. /**
  164. * Destroy instance
  165. */
  166. destroy () {
  167. if (!this._els) return
  168.  
  169. cancelAnimationFrame(this._animationFrameId)
  170.  
  171. this._els.forEach(el => {
  172. el.style.transform = null
  173. el.style.willChange = null
  174. })
  175.  
  176. window.removeEventListener('resize', this._onResize)
  177. window.removeEventListener('load', this._onLoad)
  178. }
  179. }
  180.  
  181. /**
  182. * @typedef {function} onResize
  183. * @param {number} windowHeight - `window.innerHeight`
  184. */
  185.  
  186. /**
  187. * @typedef {function} onScroll
  188. * @param {number} scrollTop - `document.scrollingElement.scrollTop`
  189. */