1
3

More than 1 year has passed since last update.

【JavaScript】数値のカウントアップ・ダウン

Last updated at Posted at 2022-01-31

採用サイトでよく見かけるようになった、スクロールで数値がカウントアップ表示されるアレ。

  • プラグインを利用したくない
  • HTMLのdata属性でオプションを設定したい

というわけで、自作してみました!

仕様

全般

  • 文書構造を考慮し、HTMLにはカウント後の数値を設定する
  • カウントしたい要素にCSSで opacity: 0; を設定する
    • JavaScript でカウント後からカウント開始数へと変更しているため読み込み時のチラツキ対策
  • カウントしたい要素にclass(js-counter)を設定する
  • 画面と数値が交差した時にclass(is-inview)が付与されたらカウント開始

オプション

カウントのオプションはHTMLのdata属性に設定します。

data属性 デフォルト 設定内容
data-count-start 0 カウント開始数
data-count-duration 2000 アニメーション秒数(ミリ秒)
data-count-easing なし イージング名

イージングについては止まる時に適用したいので、以下の4つが設定可能です。

  • easeOutQuad
  • easeOutCubic
  • easeOutQuart
  • easeOutQuint

JavaScript

Counter

JS
class Counter {
  #defaults
  #options
  #easings
  #counterList
  constructor(config) {
    this.#defaults = {
      selector: '.js-counter',
      duration: 2000,
      easing: null,
      inviewClass: 'is-inview'
    }
    this.#options = { ...this.#defaults, ...config }
    this.#easings = {
      easeOutQuad: (t) => t * (2 - t),
      easeOutCubic: (t) => --t * t * t + 1,
      easeOutQuart: (t) => 1 - --t * t * t * t,
      easeOutQuint: (t) => 1 + --t * t * t * t * t
    }
    this.#counterList = document.querySelectorAll(this.#options.selector)
    this.#init()
  }

  #init() {
    for (const el of this.#counterList) {
      this.#setStartNumber(el)
      this.#observeNumber(el)
    }
  }

  // カウント開始数を設定
  #setStartNumber(el) {
    const start = Number(el.dataset.countStart) || 0
    el.dataset.countEnd = el.textContent
    el.textContent = start
    el.style.opacity = 1
  }

  // 数値のclass属性を監視
  #observeNumber(el) {
    // オブザーバーの作成
    const observer = new MutationObserver(() => {
      // 画面と数値が交差した時のclassが付与されたら
      if (el.classList.contains(this.#options.inviewClass)) {
        this.countNumber(el)
      }
    })
    // 監視の開始
    observer.observe(el, {
      attributes: true,
      attributeFilter: ['class']
    })
  }

  // 数値のカウント
  countNumber(el) {
    const start = Number(el.dataset.countStart) || 0
    const endStr = el.dataset.countEnd
    const end = parseFloat(endStr)
    const duration = Number(el.dataset.countDuration) || this.#options.duration
    const decimal = endStr.split('.')[1]
    const digits = typeof decimal === 'undefined' ? 0 : decimal.length
    const easing = el.dataset.countEasing
      ? this.#easings[el.dataset.countEasing]
      : this.#easings[this.#options.easing]
    let startTime = null
    // ブラウザのフレームを更新するタイミング(16ms)毎に実行する関数
    const countAnimation = (currentTime) => {
      if (!startTime) startTime = currentTime
      let progress = Math.min((currentTime - startTime) / duration, 1)

      // イージングの設定
      if (easing) {
        progress = easing(progress)
      }

      // htmlに数値を設定
      el.textContent = digits
        ? (progress * (end - start) + start).toFixed(digits)
        : Math.floor(progress * (end - start) + start)

      // カウントアニメーションを終了
      if (progress < 1) {
        requestAnimationFrame(countAnimation)
      }
    }
    requestAnimationFrame(countAnimation)
  }
}

Inview

JS
class Inview {
  #defaults
  #options
  #inviewList
  constructor(config) {
    this.#defaults = {
      selector: '.js-inview',
      inviewClass: 'is-inview'
    }
    this.#options = { ...this.#defaults, ...config }
    this.#inviewList = document.querySelectorAll(this.#options.selector)
    this.#init()
  }

  #init() {
    for (const el of this.#inviewList) {
      const options = {
        rootMargin: el.dataset.inviewRootMargin || '0px'
      }
      const callback = (entries) => {
        for (const entry of entries) {
          const isInviewToggle = el.dataset.inviewToggle === ''
          // 画面と要素が交差
          if (entry.isIntersecting) {
            el.classList.add(this.#options.inviewClass)
            // target.dataset.inviewToggle属性がない場合
            // 表示後は監視を解除
            if (!isInviewToggle) {
              observer.unobserve(el)
            }
          }
          // data-inview-toggle属性がある場合
          // 画面内から消えたら要素のclassを削除
          if (!entry.isIntersecting && isInviewToggle) {
            el.classList.remove(this.#options.inviewClass)
          }
        }
      }
      const observer = new IntersectionObserver(callback, options)
      observer.observe(el)
    }
  }
}

DEMO

See the Pen Number Count Up Down by Takuya Mori (@taqumo) on CodePen.

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3