Web production note

 【更新日 :

【JavaScript】行ごとのスライドアップアニメーションを実装する

Category:
css / js

行ごとにテキストがスライドアップするアニメーションをJavaScriptで実装したサンプルです。
Web Animations APIとIntersectionObserverを使っています。

codepen

See the Pen Text slides up line by line Vanilla JS (Web Animations API) by web_nak (@web_walking_nak) on CodePen.

JavaScript

//行ごとにスライドアップアニメーションさせる
const slideUpRowTexts = document.querySelectorAll('.js-slide-up-row');
if(slideUpRowTexts.length > 0) {

  //スライドアップアニメーション関数
  const textSlideUpAutoAnime = (target) => {
    //行ごとにアニメーション
    const texts = target.querySelectorAll('.js-slide-up-row__text');
    //textsを配列に変換
    const animations = [...texts].map((text, index) => {
      //Web Animations APIでスライドアップアニメーション
      return text.animate(
        {
          transform: 'translateY(0)',
        },
        {
          fill: 'forwards', //アニメーション後に値を維持する
          duration: 1000, //アニメーション時間
          easing: 'cubic-bezier(0.33, 1, 0.68, 1)', //イージング
          delay: index * 180 //行ごとの遅延時間
        }
      ).finished;
    });

    //アニメーション終了後に元のテキストに戻す
    Promise.all(animations).then(() => {
      const baseText = document.querySelector('.js-slide-up-row__base');
      if(baseText) {
        target.innerHTML = baseText.innerHTML;
      }
    });
  }

  /*
    ■observerセットアップ
      ・初回ページロード時:画面全体を検知
      ・ロード後:rootMarginの値を変更
  */
  let observer = new IntersectionObserver(entries => {
    //監視対象の総数を取得
    const total = entries.length - 1;
    entries.forEach((entry, index) => {
      const target = entry.target;

      //アニメーションフラグ設定
      target.isFinished = false;

      //要素が画面内にある場合はアニメーション発火
      if(entry.isIntersecting) {
        textSlideUpAutoAnime(target);
        target.isFinished = true;
      }

      //要素の監視を解除
      observer.unobserve(target);

      //一通り監視し終わったら スクロール用のobserverをセットアップ
      if(total === index) {
        let observerScroll = new IntersectionObserver(entries => {
          entries.forEach(entry => {
            const target = entry.target;

            //要素が画面内にある場合はアニメーション発火
            if(entry.isIntersecting) {
              textSlideUpAutoAnime(target);
              //要素の監視を解除
              observerScroll.unobserve(target);
            }
          });

        //検知範囲を設定
        }, {rootMargin: '-20% 0px -20% 0px'});

        //アニメーションが終了していない要素のみ監視対象とする
        slideUpRowTexts.forEach(slideUpRowText => {
          if(!slideUpRowText.isFinished) {
            observerScroll.observe(slideUpRowText);
          }
        });
      }
    });
  });

  const setUpText = (slideUpRowText, baseText) => {
    //表示領域確保用の透明テキスト
    let html = '<span class="js-slide-up-row__base" aria-hidden="true">' + baseText + '</span><span class="u-visually-hidden">' + baseText + '</span>';

    //表示領域全体の高さを取得
    const textHeight = slideUpRowText.clientHeight;

    //1行の高さを取得
    const styles = getComputedStyle(slideUpRowText);
    let lineHeight = styles.lineHeight;
    if(lineHeight === 'normal') {
      //line-heightが未設定だった場合は1行の高さをチェックする
      slideUpRowText.insertAdjacentHTML('beforeend', '<span class="js-slide-up-row__checker" aria-hidden="true"> </span>');
      lineHeight = slideUpRowText.querySelector('.js-slide-up-row__checker').clientHeight;
    } else {
      lineHeight = parseFloat(lineHeight);
    }

    //現在何行なのか調べる
    const row =  textHeight / lineHeight;

    //アニメーション用 位置変更設定
    let translateY = lineHeight;
    //アニメーションが終了していたら位置変更しない
    if(slideUpRowText.isFinished) {
      translateY = 0;
    }
    //行ごとにclip-pathを設定する
    for (let i = 0; i < row; i++) {
      const insetTop = lineHeight * i;
      let insetBottom = textHeight - (lineHeight * (i + 1));
      if(insetBottom < 0) {
        insetBottom = 0;
      }
      html += '<span class="js-slide-up-row__line" aria-hidden="true" style="clip-path: inset(' + insetTop + 'px 0 ' + insetBottom + 'px)"><span class="js-slide-up-row__text" style="clip-path: inset(' + insetTop + 'px 0 ' + insetBottom + 'px);transform: translateY(' + translateY + 'px)">' + baseText + '</span></span>';
    }

    //中身をリセット
    slideUpRowText.textContent = '';
    //定義したHTMLを反映
    slideUpRowText.insertAdjacentHTML('beforeend', html);
    //セットアップ完了classの追加
    slideUpRowText.classList.add('is-setup');
  }

  //初回セットアップ
  slideUpRowTexts.forEach(slideUpRowText => {
    //ベーステキストの取得
    const baseText = slideUpRowText.innerHTML;

    //アニメーション用マークアップ
    setUpText(slideUpRowText,baseText);

    //observer初回監視開始
    observer.observe(slideUpRowText);

    /*
      レスポンシブ対応
    */
    //表示エリアリサイズ監視 ResizeObserver
    const resizeObserver = new ResizeObserver(() => {
      //アニメーション用マークアップ再定義
      setUpText(slideUpRowText, baseText);
    });
    //リサイズ監視開始
    resizeObserver.observe(slideUpRowText,slideUpRowText);
  });
}

HTML

アニメーションさせたいテキスト要素に .js-slide-up-row を追加します。

<p class="js-slide-up-row">
  行ごとにスライドアップするテキストアニメーションです。
</p>

CSS

.js-slide-up-row {
  position: relative;
  opacity: 0;
  width: fit-content;
}
.js-slide-up-row.is-setup {
  opacity: 1;
}
.js-slide-up-row__base {
  opacity: 0;
}
.js-slide-up-row__line,
.js-slide-up-row__checker {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  pointer-events: none;
}
.js-slide-up-row__text {
  display: block;
}
.u-visually-hidden {
  overflow: hidden;
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}