MENU CLOSE

【CSS + JS】
テキストの無限ループアニメーション

テキストの無限ループアニメーション【CSS + JS】
テキストの無限ループアニメーション【CSS + JS】

今回は、配置したテキストを画面の端から端まで無限ループで流し続けるアニメーションの実装方法をメモ。
特に難しい内容ではないですが、何度か実装する機会があったのでブログに書いておくことにしました。

アニメーションのデモ

デモサイトはこちら。

テキストが無限に流れ続けるアニメーションのデモ

HTML

<div class="c-text">
  <div class="c-text__item">NOTES BY SHARESL</div>
  <div class="c-text__item">NOTES BY SHARESL</div>
</div>

サンプルとして「NOTES BY SHARESL」という文字列を無限ループさせるため、上記のようなHTMLを用意します。ポイントはループするテキストは複数用意すること。ここでは2つ同じ要素を用意していますが、もっと短い単語の場合は、テキストを横並びにしたときに表示したいエリアの横幅(ここでは画面の横幅)を超えるように複数設置する必要があります。

CSS

.c-text {
  overflow: hidden;
  display: flex;
  width: 100vw;
  margin-inline: calc(50% - 50vw);
}

.c-text__item {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 120px;

  &:nth-child(odd) {
    animation: MoveLeft 24s -12s infinite linear; //24秒かけて-12秒後に無限ループさせる
  }

  &:nth-child(even) {
    animation: MoveLeft2 24s infinite linear; //24秒かけて無限ループさせる
  }
}

ラッパー要素にdisplay: flex;、テキスト要素には改行したり潰れたりしないようにflex-shrink: 0;white-space: nowrap;を指定してテキストを横並びにします。はみ出したテキストは表示しないようにラッパー要素にはoverflow: hidden;も指定します。

nth-child(odd)nth-child(even)で奇数番目・偶数番目の要素にそれぞれアニメーションを設定します。

流れるアニメーションの動きは下記のkeyframesを使います。

@keyframes MoveLeft {
  from {
    transform: translateX(100%);
  }
  to {
    transform: translateX(-100%);
  }
}

@keyframes MoveLeft2 {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-200%);
  }
}

ここでのポイントは複数用意した要素のアニメーションをずらすことです。奇数番目と偶数番目でアニメーションの開始位置と開始時間をずらすことで、見切れずに無限に流れ続けるアニメーションを作ることができます。

テキストの無限ループアニメーションのデモ(CSSのみ)

ここまででとりあえず動くものを作ることはできました。

あとはJSで調整していきます。
具体的には、下記の内容になります。

  • 画面サイズや文字の長さによってアニメーションの早さが変わってしまうことを防ぐ
  • 無限ループしたい文字列を計算して必要な分だけコピーして自動追加

CSSでもある程度は対応可能なのであくまで参考程度の情報になりますが、メモしておきます。

JSで調整

画面サイズや文字の長さによってアニメーションの早さが変わってしまうことを防ぐ

ここまでのサンプルでは「NOTES BY SHARESL」という文字だけで作っていたので気になりませんでしたが、他の文字列を使ったテキストアニメーションを追加した場合、同じCSSを使い回すと文字列の長さによってアニメーションの速度が変わってきます。

デモで見るとわかりやすいのでご覧ください。

複数の異なる長さの文章を交互にアニメーション(JS調整なし)

また、スマホとPCでも文字の大きさが変わる影響で流れる早さが変わってきます。

そういった場合に、アニメーションの早さを統一するようにJSを書いて調整してみました。

複数の異なる長さの文章を交互にアニメーション(JS調整あり)

上記のデモの内容を見ていきます。
まずCSS変数を使いたいので、CSSを下記のように調整します。

.c-text__item {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 120px;

  &:nth-child(odd) {
    animation: MoveLeft var(--tick-duration, 24s) var(--tick-delay, -12s) infinite linear;
  }

  &:nth-child(even) {
    animation: MoveLeft2 var(--tick-duration, 24s) infinite linear;
  }
}

これで--tick-duration--tick-delayの変数に値がある場合は変数の値が優先されるようになりました。

次にこの変数をJSから設定するためにHTMLにクラスを追加します。

<div class="c-text js-tick">
  <div class="c-text__item js-tick-item">NOTES BY SHARESL</div>
  <div class="c-text__item js-tick-item">NOTES BY SHARESL</div>
</div>

次に--tick-duration--tick-delayをJSから設定します。

//アニメーションの速度を計算してCSS変数に
function calculateLoopAnimationSpeed() {
  const targets = document.querySelectorAll('.js-tick');
  if (!targets.length) {
    return;
  }

  const distance = window.innerWidth;
  const mql = window.matchMedia('(min-width: 801px)');
  const time = mql.matches ? 18 : 9; //ここで時間を調整
  const speed = distance / time;

  targets.forEach((target) => {
    const tickElems = target.querySelectorAll('.js-tick-item');
    if (!tickElems.length) {
      return;
    }

    const total = tickElems.length - 1;

    tickElems.forEach((el, i) => {
      const elWidth = el.clientWidth;
      const elTime = Math.floor(elWidth / speed);
      el.style.setProperty('--tick-duration', `${elTime}s`);
      el.style.setProperty('--tick-delay', `${elTime / -2}s`);
    });
  });
}

テキスト要素にstyle.setProperty()を使って計算したテキストアニメーションの速度を設定します。

計算方法は、下記のような感じです。

  • 早さ = 動かす距離 ÷ 時間
  • --tick-duration = テキストの横幅 ÷ 早さ
  • --tick-delay = --tick-duration の半分

時間のところを任意で調整してあげれば、テキストアニメーションを複数設置して文字の長さがバラバラの場合でも、テキストアニメーションのスピードを一律にコントロールすることができます。ここでは800pxのブレイクポイントを設定して、801px以上は18秒、それ以下は半分の9秒に設定しました。

次に、画面リサイズによってCSS変数を再計算するようにしてあげます。

function resizeRefresh() {
  const target = document.body;
  const resizeObserver = new ResizeObserver((entries) => {
    entries.forEach((entry) => {
      calculateLoopAnimationSpeed();
    });
  });
  resizeObserver.observe(target);
}

これはResizeObserverで簡単に書けますね。

無限ループしたい文字列を計算して必要な分だけコピーして自動追加

動かしたいテキストを手動で複数追加するのはなんかめんどくさいのでJSで自動追加します。

function copyText() {
  const targets = document.querySelectorAll('.js-tick');
  if (!targets.length) {
    return;
  }

  targets.forEach((target) => {
    const tickElems = target.querySelectorAll('.js-tick-item');
    if (!tickElems.length) {
      return;
    }

    let length = 0;
    tickElems.forEach((el) => {
      length += el.clientWidth;
      el.insertAdjacentHTML('afterend', el.outerHTML);
      if (length > window.innerWidth) {
        return;
      }
    });
  });
}

要素が画面幅を超えるまで自動的にコピーして追加します。el.insertAdjacentHTML('afterend', el.outerHTML);って、もっと効率良い書き方ありそうですがここではとりあえずこれで。。。

まとめると完成系は下記になります。

class Main {
  constructor() {
    this.init();
  }

  init() {
    this.copyText();
    this.calculateLoopAnimationSpeed();
    this.resizeRefresh();
  }

  //リサイズ時にアニメーションの速度を再計算
  resizeRefresh() {
    const target = document.body;
    const resizeObserver = new ResizeObserver((entries) => {
      entries.forEach((entry) => {
        this.calculateLoopAnimationSpeed();
      });
    });
    resizeObserver.observe(target);
  }

  //アニメーションの速度を計算してCSS変数に
  calculateLoopAnimationSpeed() {
    const targets = document.querySelectorAll('.js-tick');
    if (!targets.length) {
      return;
    }

    const distance = window.innerWidth;
    const mql = window.matchMedia('(min-width: 801px)');
    const time = mql.matches ? 18 : 9;
    const speed = distance / time;

    targets.forEach((target) => {
      const tickElems = target.querySelectorAll('.js-tick-item');
      if (!tickElems.length) {
        return;
      }

      const total = tickElems.length - 1;

      tickElems.forEach((el, i) => {
        const elWidth = el.clientWidth;
        const elTime = Math.floor(elWidth / speed);
        el.style.setProperty('--tick-duration', `${elTime}s`);
        el.style.setProperty('--tick-delay', `${elTime / -2}s`);

        if (i === total) {
          el.parentNode.classList.remove('no-tick');
        }
      });
    });
  }

  //テキストをコピーする
  copyText() {
    const targets = document.querySelectorAll('.js-tick');
    if (!targets.length) {
      return;
    }

    targets.forEach((target) => {
      const tickElems = target.querySelectorAll('.js-tick-item');
      if (!tickElems.length) {
        return;
      }

      let length = 0;
      tickElems.forEach((el) => {
        length += el.clientWidth;
        el.insertAdjacentHTML('afterend', el.outerHTML);
        if (length > window.innerWidth) {
          return;
        }
      });
    });
  }
}

new Main();

最終的にClassでまとめました。また、テキスト要素を複数コピーすると少しずつアニメーションの開始がずれる可能性があるので、.no-tickというクラスを最初に付けておいて、.no-tickが消えた場合にアニメーションをスタートする、という処理をcalculateLoopAnimationSpeed関数に追加しています。

<div class="c-text no-tick js-tick">
  <div class="c-text__item js-tick-item">NOTES BY SHARESL</div>
</div>
.c-text__item {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 120px;

  &:nth-child(odd) {
    .c-text:not(.no-tick) & {
      animation: MoveLeft var(--tick-duration, 24s) var(--tick-delay, -12s) infinite linear;
    }
  }

  &:nth-child(even) {
    .c-text:not(.no-tick) & {
      animation: MoveLeft2 var(--tick-duration, 24s) infinite linear;
    }
  }
}

これで完成です。

複数の異なる長さの文章を交互にアニメーション(JS調整あり)

まとめ

テキストの無限ループアニメーションについて書きました。

一度やってみれば簡単ですが、こういうのは忘れがちでしてほとんど自分用のメモです。

無限ループはSwiperなどで代用することも可能ですが、ループの最後でカクついたりしたのでCSSアニメーションで書く方が個人的には好きです。この記事ではテキストに限定しましたが、もちろん画像でもほとんど同じ書き方で使えます。

記事一覧

RELATED

shopify
inoue
inoue
TIPS

【Shopify】カートへの遷移を飛ばしてチェックアウトに進むボタンを設置する

PhotoSwipe v5
inoue
inoue
TIPS

【JS】PhotoSwipe v5を使って画像をポップアップ表示する

AVIF [ gulpでjpg・pngからAVIF画像を生成 ]
inoue
inoue
TIPS

gulpでjpg・pngからAVIF画像を生成する

MutationObserverでDOMを監視[PhotoSwipe(v5)+Swiper(v8)連携]
inoue
inoue
TIPS

【JS】MutationObserverでDOMを監視[PhotoSwipe(v5)+Swiper(v8)連携]

Google Sheets API スプレッドシートのデータをJSONで取得する
inoue
inoue
TIPS

【Google Sheets API】 スプレッドシートのデータをJSONで取得する

Google Business Profile API [ PHPでクチコミ一覧取得 ]
inoue
inoue
TIPS

【PHP】Google Business Profile APIを使ってクチコミを取得する

NEW POSTS

GA4連携人気記事ランキング機能を自作プラグイン化してみ【WordPress】
inoue
inoue
TRY

【WordPress】GA4連携の人気記事ランキング機能を自作プラグイン化してみた

netlify UPDATE BUILD IMAGE [ビルドイメージの更新]
inoue
inoue
TRY

【netlify】ビルドイメージを更新 [ Ubuntu Xenial 16.04 → Ubuntu Focal 20.04 ]

screendot screenshot API
inoue
inoue
TRY

スクリーンショットのAPI「screendot」を使ってみた

window.matchMedia hoverを判定
inoue
inoue
TIPS

【window.matchMedia】メディアクエリでhoverが使えるデバイスを判定

lax.js
inoue
inoue
TIPS

lax.jsの使い方【スクロール連動アニメーションの実装】

Dart Sass + Gulp
inoue
inoue
TRY

DartSassがなかなか辛かったのでGulpを修正してみた

ブログ記事一覧
  • HOME
  • TIPS
  • 【CSS + JS】テキストの無限ループアニメーション