LIGのメルマガ、はじめました!
LIGのメルマガ、はじめました!

GSAPでこんなこともできる! スクロールでテキストが1文字ずつ出てくる会話風アニメーション

ぼこ

こんにちは。フロントエンドエンジニアになりたてのぼこです。

前回の記事で、JavaScriptアニメーションライブラリ「GSAP」を使った表現を紹介しました。

今回もGSAPを使って、少し変わった、でもどこかで使い道がありそうな表現を実装していこうと思います!

今回作るデモ

今回作るのはこんなアニメーションです。
テキストが一文字ずつ出てくるチャットのようなアニメーション

スクロールで徐々に吹き出しが現れ、中のテキストが1文字ずつ出てくるようなアニメーションです。スクロールに合わせて始まる処理と、1文字ずつ順番に出てくる処理を組み合わせることで、チャットのような表現が実装できます。

スクロールに連動する演出、1文字ずつ順番に出てくる演出はどちらも様々なサイトで見かける演出ですが、GSAPを使うと簡単に実装することができるのでぜひやってみてください。

ではでは解説に参りましょう〜〜。

GSAPについて

GSAPは複雑なアニメーションを実装しやすくしてくれるJavaScriptのアニメーションです。

導入方法や特徴などは前回の記事になるべく詳しく書きましたので、ぜひそちらを読んでみてください。

実装の解説

実際のソースコードはこちらになります(表示サイズの関係で見づらいかと思いますが、CodePenの枠にある0.5×のボタンを押すと見やすいかもです。最初から実行し直す場合はReturnを押してください)。

See the Pen
GSAP Chat Animation
by Bokoko33 (@bokoko33)
on CodePen.

HTML

まずは前回と同様に、headタグ内でGSAPを読み込みます。また、GSAPのプラグインであるScrollTriggerも合わせて読み込む必要があるので注意です。

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.1/ScrollTrigger.min.js"></script>

GSAPのインストールの詳細はこちら

そして以下がbodyタグの中身になります。

<div class="container">
  <div class="bubble-wrapper">
    <p class="bubble">
      あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら
    </p>
  </div>
  <!-- 省略 -->
  <div class="bubble-wrapper">
    <p class="bubble">
      しずかにあの年のイーハトーヴォの五月から十月までを書きつけましょう
    </p>
  </div>
</div>

コンテナの中に、吹き出しになる要素をたくさん並べています。

CSS

まず、今回はスクロールして吹き出しが画面に入ったらアニメーションするようにしたいので、コンテナに高さいっぱいのpaddingをつけます。

.container {
  width: 100%;
  max-width: 800px;
  margin: 0 auto;
  padding: 100vh 0 40px;
}

これで、最初は何も映らず、スクロールしたらコンテンツが現れるようにしています。

次に、左右交互に吹き出しを配置する部分についてです。

.bubble-wrapper {
  width: 100%;
  display: flex;
  justify-content: space-between;
}

.bubble-wrapper::after {
  display: block;
  content: '';
}

.bubble-wrapper:nth-of-type(2n) {
  flex-direction: row-reverse;
}

少し分かりにくいかもしれませんが、吹き出しの横に大きさのない擬似要素を配置しています。そして、吹き出しと擬似要素をflexで横並びにし、justify-content: space-betweenを使ってコンテナの端に配置します。

さらに、偶数番目のときだけflex-direction: row-reverseにすることで、交互に吹き出しを左右逆に配置しています(もう少し良いやり方があるかもしれませんが今回はこんな感じで……)。

そして、これと同様に偶数奇数を判定して、左右交互に色を変えたり、吹き出しの「ちょび」の部分をつけたりしています。

.bubble-wrapper:nth-of-type(2n) .bubble {
  background-color: #4c4c6d;
  color: white;
}

.bubble-wrapper:nth-of-type(2n + 1) .bubble {
  background-color: #e8f6ef;
  color: #222;
}

.bubble-wrapper:nth-of-type(2n) .bubble::after {
  background-color: #4c4c6d;
  clip-path: polygon(0 0, 0% 100%, 100% 50%);
  right: -16px;
}

.bubble-wrapper:nth-of-type(2n + 1) .bubble::after {
  background-color: #e8f6ef;
  clip-path: polygon(0 50%, 100% 0, 100% 100%);
  left: -16px;
}

JavaScript

本題のGSAPを使ったJavaScriptの部分です。

テキストの分割

まず、テキストを1文字ずつアニメーションさせる場合、テキストをすべて1文字ずつタグで囲む必要があります。つまりこんな感じです↓。

<span>テ</span><span>キ</span><span>ス</span><span>ト</span>

例えばこれが見出しのような短いテキストならいいですが、今回のように長めの文章を1文字ずつタグで囲うのはめちゃくちゃめんどくさいですよね。そのため、まずはこのテキストを1文字ずつ分割してタグで囲む処理をJavaScriptで自動化します。

まだここはGSAPは関係ありません。

const spanWrapText = (target) => {
  const nodes = [...target.childNodes]; // ノードリストを配列にする
  let returnText = ''; // 最終的に返すテキスト

  for (const node of nodes) {
    if (node.nodeType == 3) {
      //テキストの場合
      const text = node.textContent.replace(/\r?\n/g, ''); //テキストから改行コード削除
      const splitText = text.split(''); // 一文字ずつ分割
      for (const char of splitText) {
        returnText += `<span>${char}</span>`; // spanタグで挟んで連結
      }
    } else {
      //テキスト以外の場合
      //<br>などテキスト以外の要素をそのまま連結
      returnText += node.outerHTML;
    }
  }
  return returnText;
};

引数にHTMLの要素を受け取って、その中のテキストを1文字ずつspanタグで囲って返す関数になります。ノードリストというのは、そのHTMLタグの中にあるノード(タグやテキストなど)すべてが入った配列のようなものです。文字はもちろん、brタグなどもノードとして入ってきます。

それを

・テキストの場合→分割してspanで囲んで連結
・それ以外のタグなど→そのまま連結

というようにして文字を作り直します(今回の分割方法だと、HTMLの書き方によっては空のspanタグが生成されるかもしれませんが、見た目やアニメーションには問題ないのでこのままいきます)。

ちなみに、GSAPにもSplitTextというテキスト分割のプラグインがあるみたいですが、有料のようなので今回は割愛します。上記の僕のコードよりも多機能でめっちゃすごいらしいです。

テキスト分割の関数ができたら、これを使って全ての吹き出しの中身を分割します。

const bubbles = [...document.querySelectorAll('.bubble')];
for (const bubble of bubbles) {
  bubble.innerHTML = spanWrapText(bubble);

  // spanたちを配列にし、それをプロパティとして持っておく
  bubble.spans = bubble.querySelectorAll('span');

...

}

for文を使ってすべての吹き出し要素に対してテキスト分割を行い、1文字ずつspanタグで囲みます。また、あとで使いやすいよう、bubbleのプロパティとしてspan要素の配列を持っておきます。

アニメーションの作成

ここからがGSAPの記述部分になります。

まずは、アニメーションのタイムラインを作成します。

// scrollTriggerによって発火するTimelineを作成
const tl = gsap.timeline({
  scrollTrigger: {
    trigger: bubble, // 吹き出しをアニメーション発火のトリガーに
    start: 'top 90%', // 吹き出しの上部(TOP)が、画面の上から90%の位置を通過したらスタート
  },
});

今回のアニメーションは、「吹き出しが出現する→テキストが1文字ずつ表示される」といったように、複数のアニメーションを順番に行う必要があります。

そんなときに使えるのがGSAPのTimeline機能です。アニメーションAを行ったあとにアニメーションBを行う、またそれぞれのタイミングを細かく調整する、といったことが簡単にできちゃいます(具体的な書き方は後ほど)。

今回はスクロールして要素が画面に入ったらタイムラインアニメーションを実装するので、gsap.timeline()の引数にScrollTriggerの設定を入れています。

前回の記事のおさらいになりますが、triggerで「どの要素が画面内に入ったらアニメーションするか」を指定し、startで「triggerの要素が具体的に画面のどの部分に入ったらスタートするか」を設定しています。

次にTimelineの部分です。

tl.from(bubble, {
  opacity: 0,
  y: '10%',
}).from(bubble.spans, {
  opacity: 0,
  duration: 0.01,
  stagger: 0.03,
});

とってもシンプルですね。先ほど作成したtimelineオブジェクトに、「.(ドット)」で繋いでアニメーションを順番に書いていくだけです。

gsapのfrom関数は、動く前の状態を指定してアニメーションを表現する関数です。そのため今回は、

  1. 吹き出しが透明状態から表示され、かつ下からふわっと上がってくる
  2. テキストが透明状態から表示される

という動きになります。

ここで注目していただきたいのが、今回のメインであるstaggerプロパティです。このstaggerが、まさに「テキストを1文字ずつ表示」しています。

staggerを指定すると、それに並列する要素が順番にアニメーションをしてくれます。今回は吹き出しの中のspan要素に指定するので、spanに入っているテキストが0.03秒ごとにアニメーションすることになります。この1行だけで順番にアニメーションすることができてしまうので恐ろしいですよね。

以上で完成になります。

おわりに

今回は、GSAPの特徴的な機能であるTimeline、Stagger、ScrollTriggerを使ってアニメーションを作ってみました(最初にも書きましたが、ScrollTriggerはプラグインという扱いになるので別途読み込みが必要な点に注意です)。

それぞれの公式ドキュメントは以下になります。今回扱っていないような使い方や、面白いデモがあるのでぜひ見てみてください。
GSAP – Timeline
GSAP – Stagger
GSAP – ScrollTrigger

ではまた〜〜。

M o n g o