如何用基本的JavaScript制作一个滚动触发的动画

357 阅读5分钟

在网站上做一点动画可以增加一些亮点,给用户留下深刻印象,并引起他们的注意。你可以让它们运行,不管它们在页面上的什么位置,当页面加载时立即运行。但如果你的网站相当长,所以用户花了一些时间向下滚动到那个元素呢?他们可能会错过它。

你可以让它们一直运行,但也许动画的设计最好是让你肯定能看到它的开始。诀窍是在用户向下滚动到那个元素时开始动画--滚动触发的动画,如果你愿意的话。

为了解决这个问题,我们使用滚动触发器。当用户向下滚动到任何特定元素时,我们可以使用该事件来做一些事情。它可以是任何东西,甚至是一个动画的开始。它甚至可以是滚动触发的图片的懒惰加载或懒惰加载整个评论部分。这样一来,我们就不会强迫用户在初始页面加载时下载不在视口中的元素。许多用户可能根本就不会向下滚动,所以我们真的为他们(和我们)节省了带宽和加载时间。

滚动触发器是非常有用的。有很多库可以用来实现它们,比如Greensock的流行的ScrollTrigger插件。但你不必使用第三方库,特别是对于相当简单的想法。事实上,你可以只用一小撮虚无缥缈的JavaScript来自己实现它。这就是我们在这篇文章中要做的。

下面是我们如何制作我们的滚动触发事件的方法

  • 创建一个叫做scrollTrigger 的函数,我们可以应用于某些元素
  • 当一个元素进入视口时,在该元素上应用一个.active
  • 用CSS对该.active 类进行动画处理

有的时候,仅仅添加一个.active 类是不够的。例如,我们可能想执行一个自定义函数。这意味着我们应该能够传递一个自定义函数,在元素可见时执行。像这样。

scrollTrigger('.loader', {
  cb: function(el) {
    el.innerText = 'Loading ...'
    loadContent()
  }
})

我们还将尝试处理旧的不支持的浏览器的滚动触发器。

但首先,IntersectionObserver API

我们要使用的主要JavaScript功能是交叉观察者。这个API提供了一种异步观察目标元素相交处变化的方法--而且它以一种比观察scroll 事件更有效的方式进行观察。我们将使用IntersectionObserver 来监视滚动达到页面上某些元素可见的时候。

让我们开始构建滚动触发器

我们要创建一个叫做scrollTrigger 的函数,这个函数应该接受一个选择器作为它的参数。

function scrollTrigger(selector) {
  // Multiple element can have same class/selector,
  // so we are using querySelectorAll
  let els = document.querySelectorAll(selector)
  // The above `querySelectorAll` returns a nodeList,
  // so we are converting it to an array
  els = Array.from(els)
  // Now we are iterating over the elements array
  els.forEach(el => {
    // `addObserver function` will attach the IntersectionObserver to the element
    // We will create this function next
    addObserver(el)
  })
}
// Example usage
scrollTrigger('.scroll-reveal')

现在让我们创建addObserver ,这个函数要用IntersectionObserver 附加到元素上。

function scrollTrigger(selector){
  let els = document.querySelectorAll(selector)
  els = Array.from(els)
  els.forEach(el => {
    addObserver(el)
  })
}
function addObserver(el){
    // We are creating a new IntersectionObserver instance
    let observer = new IntersectionObserver((entries, observer) => { // This takes a callback function that receives two arguments: the elements list and the observer instance.
      entries.forEach(entry => {
        // `entry.isIntersecting` will be true if the element is visible
      if(entry.isIntersecting) {
        entry.target.classList.add('active')
        // We are removing the observer from the element after adding the active class
        observer.unobserve(entry.target)
      }
    })
  })
  // Adding the observer to the element
  observer.observe(el)
}
// Example usage
scrollTrigger('.scroll-reveal')

如果我们这样做并滚动到一个有.scroll-reveal 类的元素,一个.active 类就会被添加到该元素上。但是请注意,只要该元素的任何小部分是可见的,active 类就会被添加。

Animated screenshot of the scroll-triggered animation with the code for it to the left and DevTools open on the right.

但这可能是矫枉过正。相反,我们可能希望.active 类在元素的较大部分可见时被添加。好吧,幸好,IntersectionObserver 接受一些选项作为它的第二个参数。让我们把这些应用到我们的scrollTrigger 函数。

// Receiving options as an object
// If the user doesn't pass any options, the default will be `{}`
function scrollTrigger(selector, options = {}) {
  let els = document.querySelectorAll(selector)
  els = Array.from(els)
  els.forEach(el => {
    // Passing the options object to the addObserver function
    addObserver(el, options)
  })
}
// Receiving options passed from the scrollTrigger function
function addObserver(el, options) {
  let observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if(entry.isIntersecting) {
        entry.target.classList.add('active')
        observer.unobserve(entry.target)
      }
    })
  }, options) // Passing the options object to the observer
  observer.observe(el)
}
// Example usage 1:
// scrollTrigger('.scroll-reveal')
// Example usage 2:
scrollTrigger('.scroll-reveal', {
  rootMargin: '-200px'
})

就这样,我们的前两个议程项目得到了满足

让我们继续第三项--当我们滚动到一个目标元素时增加执行回调函数的能力。具体来说,让我们在我们的选项对象中把回调函数作为cb

function scrollTrigger(selector, options = {}) {
  let els = document.querySelectorAll(selector)
  els = Array.from(els)
  els.forEach(el => {
    addObserver(el, options)
  })
}
function addObserver(el, options){
  let observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if(entry.isIntersecting){
        if(options.cb) {
          // If we've passed a callback function, we'll call it
          options.cb(el)
        } else{
          // If we haven't, we'll just add the active class
          entry.target.classList.add('active')
        }
        observer.unobserve(entry.target)
      }
    })
  }, options)
  observer.observe(el)
}
// Example usage:
scrollTrigger('.loader', {
  rootMargin: '-200px',
  cb: function(el){
    el.innerText = 'Loading...'
    // Done loading
    setTimeout(() => {
      el.innerText = 'Task Complete!'
    }, 1000)
  }
})

An updated animated screenshot of the same scroll-triggered animation. As boxes enter the screen from the bottom, a they rotate. A "loading" message that changes to "finished loading" message is the last element to scroll into view. The code is open to the left of the animation.

很好!还有最后一件事我们需要注意:传统的浏览器支持。某些浏览器可能不支持IntersectionObserver ,所以让我们在我们的addObserver 函数中处理这种情况。

function scrollTrigger(selector, options = {}) {
  let els = document.querySelectorAll(selector)
  els = Array.from(els)
  els.forEach(el => {
    addObserver(el, options)
  })
}
function addObserver(el, options) {
  // Check if `IntersectionObserver` is supported
  if(!('IntersectionObserver' in window)) {
    // Simple fallback
    // The animation/callback will be called immediately so
    // the scroll animation doesn't happen on unsupported browsers
    if(options.cb){
      options.cb(el)
    } else{
      entry.target.classList.add('active')
    }
    // We don't need to execute the rest of the code
    return
  }
  let observer = new IntersectionObserver((entries, observer) =>; {
    entries.forEach(entry => {
      if(entry.isIntersecting) {
        if(options.cb) {
          options.cb(el)
        } else{
          entry.target.classList.add('active')
        }
        observer.unobserve(entry.target)
      }
    })
  }, options)
  observer.observe(el)
}
// Example usages:
scrollTrigger('.intro-text')
scrollTrigger('.scroll-reveal', {
  rootMargin: '-200px',
})
scrollTrigger('.loader', {
  rootMargin: '-200px',
  cb: function(el){
    el.innerText = 'Loading...'
    setTimeout(() => {
      el.innerText = 'Task Complete!'
    }, 1000)
  }
})

这个小旅程就到此结束了!希望你喜欢并学到了一些东西。我希望你喜欢它,并在这个过程中学到一些新东西。