簡単でブラウザにエコなスクロール連動型テキストアニメーションを実装してみた

さかっちょのプロフィール画像
エンジニアさかっちょ

今回は、テイストを変えて様々な箇所でよく使う「一文字ずつ登場させるアニメーション」の基礎をソースコードとともに解説していきたいと思います。まず、完成版はこんな感じです。スクロールしてみてください!

See the Pen 1文字ずつ登場するアニメーション by Takuro Sakai (@sakaccho) on CodePen.

※解説のために.js-observe-targetを2回も変数に代入していますが、適宜変更してください。

以下の3つのパートに分け、解説していきます。

  1. JSで一文字ずつタグで括る
  2. SCSSを書く
  3. JSで「要素が見えたらクラスを付与する」処理を書く

1. JSで一文字ずつ<span>タグで括る

これはHTMLの段階で手書きで<span>タグで一文字ずつ括ってしまっても良いです。というより、そのほうがパフォーマンス的には○です。「動的に変わるので構造上難しい」とか「いちいち手書きでなんかやってらんないよ!」という方は以下のJSで自動で一文字ずつ括るようにしてください。

<div class="text js-observe-target">テキスト 1</div>

上記のような要素があったとします。JSは以下のようになります。

const text = document.querySelectorAll('.js-observe-target');
// .text要素の数だけループ処理
text.forEach(element => {
  let html = '';
  
  const letters = element.textContent.split(''); // 1文字ずつ分解して配列へ

  // 一文字ずつ回して<span>タグで括る処理
  letters.forEach(letter => {
    const str = letter.replace(/\s|&nbsp;/g, '&nbsp;');
    html += '<span>' + str + '</span>';
  });
  element.innerHTML = html; // .text要素内を置き換える
});

簡単に処理の中身を解説します。

  1. .textクラスを持った要素だけループします
  2. split('')で一文字ずつ分解してlettersという配列に入れます
  3. letters = ['テ', 'キ', 'ス', 'ト', ' ', '1']となっているのでこれをループします
  4. ループの中で当該文字が半スペの場合は非表示になってしまわぬように&nbsp;に置換します
  5. <span>テ</span>となるようにしてhtmlという変数に足していきます
  6. 最後にinnerHTML.text要素に返します

これで以下のようにテキストが分解されたはずです。

<div class="text js-observe-target">
  <span>テ</span>
  <span>キ</span>
  <span>ス</span>
  <span>ト</span>
  <span>&nbsp;</span>
  <span>1</span>
</div>

2. SCSSを書く

HTMLの準備ができたところでスタイルをあてていきましょう。今回はSCSSを使います。ざっと以下のスタイル実装が必要となります。

  • 文字がスワイプで登場するモーションをつける
  • <span>ごとに遅延させる

文字がスワイプで登場するモーションをつける

まずは文字が「見えない状態」を作ります。今回はclip-pathプロパティのinset(X)を用います。これはclip-path: inset(上辺 右辺 下辺 左辺);というように各辺から%で指定することで表示領域をコントロールできるものです。

clip-pathのinset()の仕組み

上の図をみてもらうとわかりやすいですが、inset(0 0 0 0)とすると四角形の全域が表示され、inset(0 0 50% 0)とすると下辺から50%の位置まで表示領域が狭められた状態になります。つまりinset(0 0 100% 0)とすれば表示領域は見えなくなります。また、これはあくまで表示領域の設定なので内部のコンテンツが潰れたりすることはありません。

ここでは<span>タグごとにinset(0 0 100% 0)にして文字が見えないようにしておきましょう。

.text {
  span {
    clip-path: inset(0 0 100% 0);
  }
}

さて、このclip-pathtransitionがかけられるので、これを利用してアニメーションを実装します。今回は1文字ずつ少しだけ登場アニメーションを遅延させたいので、ループを使って以下のように1文字ごとに0.06秒遅延するtransition-delayもつけます。また、.text-visibleというクラスが付与されたら文字が現れるようにしておきましょう。

.text {
  span {
    clip-path: inset(0 0 100% 0);
    transition: all cubic-bezier(0.215, 0.61, 0.355, 1) 1.0s

    @for $i from 1 through 6 {
      &:nth-child(#{$i}) {
        $delay: $i * 0.06 + s;
        transition-delay: $delay;
      }
    }
  }
  &.-visible {
    span {
      clip-path: inset(0 0 0 0);
    }
  }
}

繰り返し部分は、実際のCSSでは以下のように出力されます。

.text span:nth-child(1) {
  transition-delay: .06s
}

.text span:nth-child(2) {
  transition-delay: .12s
}

.text span:nth-child(3) {
  transition-delay: .18s
}

3. JSで「要素が見えたらクラスを付与する」処理を書く

「スクロールイベントでスクロール量を取得し、アニメーションさせたい要素の位置から計算してクラスを付与する」という実装方法もありますが今回は交差オブザーバーAPI(Intersection Observer API)を用いて実装します。こちらのほうがパフォーマンスも良いとのことです。

交差オブザーバーAPIですが、その名の通りブラウザで見えている領域とターゲット要素が交差したら、つまり要素が見えたら何かしらの処理を行う、といったことができる仕組みです。今回実現したい処理に必要なコードは以下のとおりです。

const setObserver = () => {
  // ④スクロールの度に実行される関数
  const intersectionCallback = (entries) => {
    entries.forEach( (entry) => {
      if (entry.isIntersecting) {
        // entry.targetが領域に入ったら(=見えたら)特定の処理を実行
        entry.target.classList.add('-visible');
      }
    });
  }

  // ①交差判定となるボーダーの設定&インスタンス生成
  const options = {
    rootMargin: '0px 0px -200px 0px',
  }
  const observer = new IntersectionObserver(intersectionCallback, options);

  // ②監視対象となる要素
  const targets = document.querySelectorAll('.js-observe-target');

  // ③「②」の要素を監視対象として登録
  targets.forEach( (target) => {
    observer.observe(target);
  });
}

setObserver();

①交差判定となるボーダーの設定&インスタンス生成

ここで設定しているオプションのrootMarginという考え方が少し癖ありです。これまでのscrollTopを取得する方法だとブラウザビューの上辺か、ブラウザ高さを足したりして下辺が要素を通過したら・・・のような処理をよく作っていましたが、rootMarginはブラウザビューの上下左右4辺からの距離を指定して境界を設定できます。

rootMarginの仕組み

rootMargin: '上 右 下 左'という風に設定するので、上記のrootMargin: '0px 0px -200px 0px'は「下辺から200pxの位置」を境界線にするということですね。

②監視対象となる要素

境界線に交差したかを監視したい要素を取得します。ふつうにquerySelectorAllで取得しときます。

③「②」の要素を監視対象として登録

②で取得した要素が複数なので、forEachでまわして各要素を監視するように登録します。

④スクロールの度に実行される関数

あとは交差した要素だけentry.isIntersecting = trueとなるので、交差したときに行いたい処理をここに書いておきます。今回は交差した要素に-visibleクラスを付与しています。

rootMarginで設定した範囲と交差するとentry.isIntersecting = trueとなる

おわりに

可視の判定処理は基本的に今回紹介したIntersection Observerがパフォーマンスもよく、実装も楽なのでよいのかなと思います。あとはイージング変えたり、時間をいじってテイストを変更してみてください。

こんな記事も読まれています

【2023年】ハンバーガーメニューの作り方決定版【コピペ可能】
【2023年】ハンバーガーメニューの作り方決定版【コピペ可能】
かんののプロフィール画像
かんの
ウェブアプリ開発にTailwind CSSを導入してみた
ウェブアプリ開発にTailwind CSSを導入してみた
金 伯冠のプロフィール画像
金 伯冠
俺流フロントエンドのディレクトリ構成と設計の考え方
俺流フロントエンドのディレクトリ構成と設計の考え方
アマノのプロフィール画像
アマノ
ChatGPTの活用方法(CSS編)
ChatGPTの活用方法(CSS編)
せおのプロフィール画像
せお
上に戻る