← back to Dw Theme Boost Fix

assets/element.image.parallax.js

110 lines

class ParallaxImage extends HTMLElement {
  connectedCallback() {
    this.abortController = new AbortController()
    this.parallaxImage = this.querySelector('[data-parallax-image]')
    this.windowInnerHeight = window.innerHeight
    this.isActive = false
    this.timeout = null
    this.directionMap = {
      right: 0,
      top: 90,
      left: 180,
      bottom: 270
    }
    this.directionMultipliers = {
      0: [1, 0],
      90: [0, -1],
      180: [-1, 0],
      270: [0, 1]
    }


    window.addEventListener('scroll', this.scrollHandler.bind(this), { signal: this.abortController.signal })

    this.init()
  }

  disconnectedCallback() {
    this.abortController.abort()
  }

  getParallaxInfo() {
    const { width, height, top } = this.parallaxImage.getBoundingClientRect()
    let element = this.parallaxImage
    let multipliers
    let { angle, movement } = element.dataset

    let movementPixels =
      angle === 'top'
        ? Math.ceil(height * (parseFloat(movement) / 100))
        : Math.ceil(width * (parseFloat(movement) / 100))

    // angle has shorthands "top", "left", "bottom" and "right"
    // nullish coalescing. using `||` here would fail for `0`
    angle = this.directionMap[angle] ?? parseFloat(angle)

    // fallback if undefined
    // NaN is the only value that doesn't equal itself
    if (angle !== angle) angle = 270 // move to bottom (default parallax effect)
    if (movementPixels !== movementPixels) movementPixels = 100 // 100px

    // check if angle is located in top half and/or left half
    angle %= 360
    if (angle < 0) angle += 360

    const toLeft = angle > 90 && angle < 270
    const toTop = angle < 180

    element.style[toLeft ? 'left' : 'right'] = 0
    element.style[toTop ? 'top' : 'bottom'] = 0

    // if it's not a perfectly horizontal or vertical movement, get cos and sin
    if (angle % 90) {
      const radians = (angle * Math.PI) / 180
      multipliers = [Math.cos(radians), Math.sin(radians) * -1] // only sin has to be inverted
    } else {
      multipliers = this.directionMultipliers[angle]
    }

    // increase width and height according to movement and multipliers
    if (multipliers[0]) element.style.width = `calc(100% + ${movementPixels * Math.abs(multipliers[0])}px)`
    if (multipliers[1]) element.style.height = `calc(100% + ${movementPixels * Math.abs(multipliers[1])}px)`

    return {
      element,
      movementPixels,
      multipliers,
      top,
      height
    }
  }

  init() {
    const { element, movementPixels, multipliers, top, height } = this.getParallaxInfo()

    const scrolledInContainer = this.windowInnerHeight - top
    const scrollArea = this.windowInnerHeight + height
    const progress = scrolledInContainer / scrollArea

    if (progress > -0.1 && progress < 1.1) {
      const position = Math.min(Math.max(progress, 0), 1) * movementPixels
      element.style.transform = `translate3d(${position * multipliers[0]}px, ${position * multipliers[1]}px, 0)`
    }

    if (this.isActive) requestAnimationFrame(this.init.bind(this))
  }

  scrollHandler() {
    if (this.isActive) {
      clearTimeout(this.timeout)
    } else {
      this.isActive = true
      requestAnimationFrame(this.init.bind(this))
    }

    this.timeout = setTimeout(() => (this.isActive = false), 20)
  }
}

customElements.define('parallax-image', ParallaxImage)