star back image
people4
電飾 電飾
moon
men
mask-image ローディングアニメーション

【mask-image】SVGでローディングのアニメーション数値付き

BLOG WEBログ
読了約:21分

ページの読み込みをしているようなローディングのアニメーションをやりたい。
そんな要件はありますでしょうか?

ローディングと言っても、いろんなのがありますよ。

そうですね。
今回はあらかじめ、こんな感じにしたいとサンプルページを教えていただけました。

こんな感じのサンプル
https://codesandbox.io/p/sandbox/loading-text-animation-gnl3h

0%~100%の数値がカウントアップされています。

入力テキストにワイプアニメーション効果が現れる、凡庸性の高いサンプルです。すごい!

「このまま使えるぞ」と思ったのですが。。

そのまま使うには難しかった

デザイン文字にはアウトラインが掛かっていて、その文字はデザイナーさんのこだわりカーニングが施されていることが判明です。

<span>A</span>とかで囲って。。テキスト間隔を。。

1文字づつ調整か?と思ったのですが、やる前から気が遠くなるの感じ、SVG画像にすることにした時の記事内容となります。

 そんな時「mask-image」を教えてもらいました。

mask-imageを使う

マスク状態にあるsvgに、疑似要素の色を差し込むイメージでしょうか。
よくわかりません。
わかりやすかったサイトのURLを貼り付けます。

参考:「mask-image」でSVGアイコンの色をCSSで変えよう
https://zenn.dev/kagan/articles/cf3332462262f1

参考:雲隠れ
https://lopan.jp/css-mask/cloud-mask/

mask-imageって結構前から聞いたことがあるような。。

IE以外で使えるようですね。

【共有】ソースコード

サンプル作成したソースコードを共有します。

SVG版サンプル(オプション1バージョン)
https://astrowave.jp/amnesia_record/loding_anim.php
↑リンク先ページで更新ボタンすると何回もアニメーションを確認できます。

要件は以下です。
・ただの5秒固定のアニメーションであること(データloadチェックは不要)
・ロゴ文字を違う色でワイプさせたい
・プログレスバーがほしい
・数値%のカウントアップがほしい

html

<div id="load">
  <div class="load_inner">
    <div id="root"></div>
  </div>
</div>

id=”root”のdivに、スクリプトが出力するソースが差し込まれる想定です。

css

#load {
  width: 100%;
  padding: 0;
  position: relative; /* fixed */
  top: 0;
  left: 0;
  opacity: 1;
  visibility: visible;
  transition: .3s;
  box-sizing: border-box;
  z-index: 2;
  background: #F6F7F2;
}
.load_inner {
  position: relative;
}
.flex-container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  height: 100vh;
}
.progress-wrapper {
  width: 560px;
  height: 29px;
  margin: 40px 0 0;
  display: flex;
}
.progress-anim {
  display: inline-block;
  width: 100%;
  height: 2px;
  background-color: #ddd;
  position: relative;
  animation: loading 5s linear;
}
.progress-anim::after {
  content: "";
  position: absolute;
  top: -10px;
  right: 0;
  width: 15px;
  height: 29px;
  background: #f6f7f2;
}
.box-text {
  font-family: "Noto Sans JP", "Hiragino Kaku Gothic ProN", Meiryo, sans-serif;
  font-weight: 500;
  font-size: 14px;
  line-height: 29px;
  text-align: center;
  color: #000;
  width: 0;
  margin: 0 0 0;
  padding: 0 0 0 0;
  position: relative;
  transform: translateY(-50%) translateX(0%);
}
.box-progress {
  width: 560px;
  height: 58px;
  background-color: #D0DEE6;
  -webkit-mask-image: url(./img/logo_gray.svg);
  mask-image: url(./img/logo_gray.svg);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
  -webkit-mask-size: contain;
  mask-size: contain;
  position: relative;
}
.box-progress::after {
  -webkit-transform-origin: left center;
  -ms-transform-origin: left center;
  transform-origin: left center
}
.box-progress::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: #BB161D;
  animation: loading 5s linear;
}
.fill-progress {
  width: 0%;
  height: 20px;
  background-color: #BB161D;
  transition: width 0.5s; /* アニメーション */
  position: relative;
  margin-right: 10px; /* 数字との間隔 */
}

@keyframes loading {
  0% {
    width: 0;
  }
}

mask-imageが使われています。

ほかに重要そうなことは、
svg文字とプログレスバーのサイズを同じくらいの幅にすること。
ほか、5秒の指定が3箇所あります。

javascript

<script>
// loading
document.addEventListener("DOMContentLoaded", function() {
  const root = document.getElementById('root');
  const flexContainer = document.createElement('div');
  flexContainer.className = 'flex-container';

  const boxProgress = document.createElement('h1');
  boxProgress.className = 'box-progress';
  boxProgress.setAttribute('data-text', 'ASTROWAVE');
  boxProgress.innerText = 'ASTROWAVE';

  const boxWrapper = document.createElement('div');
  boxWrapper.className = 'progress-wrapper';

  const boxText = document.createElement('span');
  boxText.className = 'box-text';
  boxText.innerText = '0%';

  const progressBarFill = document.createElement('span');
  progressBarFill.className = 'progress-anim';

  boxWrapper.appendChild(progressBarFill);
  boxWrapper.appendChild(boxText);

  flexContainer.appendChild(boxProgress);
  flexContainer.appendChild(boxWrapper);
  root.appendChild(flexContainer);

  let startTime = null;
  const totalDuration = 5000; // 5 seconds

  function animateProgress(timestamp) {
    if (!startTime) startTime = timestamp;
    const elapsed = timestamp - startTime;

    // Calculate percentage of completion
    const progress = Math.min(elapsed / totalDuration, 1); // 0 to 1
    const percentage = Math.floor(progress * 100);

    // Update text and progress bar width
    boxText.innerText = `${percentage}%`;

    if (elapsed < totalDuration) {
      requestAnimationFrame(animateProgress);
    } else {
      webStorage(); // Animation completed
    }
  }

  requestAnimationFrame(animateProgress);
});


const webStorage = function () {
  const loadElement = document.getElementById('load');
  if (sessionStorage.getItem('visit')) {
    loadElement.style.opacity = 0;
    loadElement.style.visibility = 'hidden';
  } else {
    sessionStorage.setItem('visit', 'true');
    setTimeout(() => {
      loadElement.style.opacity = 0;
      loadElement.style.visibility = 'hidden';

    }, 5000); // 5秒に設定
  }
}

// 初回訪問時、初期化される
document.addEventListener("DOMContentLoaded", function() {
  webStorage();
});
</script>

rootのdivに、3種類の要素を吐き出し(appendChild)てほしいのでソースが長くなってしましました。

もう少しコード整理できそうですが、すみません。
解説はchatGPTなどに放り込んでくださいませ。

【オプション1】初回訪問でも再読み込みでも見せる

上の共有ソースコードは、ローディングを初回訪問で1回見たら、次は見なくていいよ、という想定でした。

そういう想定があるなら最初に言ってください。

すみません。

以下は、初回とか関係なく「必ずローディングを見せたい」バージョンです。

const webStorage = function () {
  const loadElement = document.getElementById('load');

  // 初回訪問かどうかを問わず、loadingアニメーションを表示
  setTimeout(() => {
    loadElement.style.opacity = 0;
    loadElement.style.visibility = 'hidden';

  }, 5000); // 5秒に設定
}

// 初回訪問でも再読み込みでも、初期化
document.addEventListener("DOMContentLoaded", function() {
  webStorage();
});

webStorageのsessionStorage部分を削ります。

ローディングアニメーション画面を設置したい話は以上です。

【オプション2】ローディング後にswiperを合わせたい

ローディングが終わったらスライダーを表示させたい時はないですか?

あるような、ないような。

そんな時はswiperの初期化をfunctionで囲って関数で作成すると良いとAIに教えてもらいました。

javascript

function initializeSwiper() {
  const mySwiper = new Swiper('.swiper', {
    slidesPerView: 1,
    spaceBetween: 0,
    loop: true,
    loopAdditionalSlides: 1,
    speed: 1000,
    autoplay: {
      delay: 3000,
      disableOnInteraction: false,
    },
    pagination: {
      el: '.swiper-pagination',
      clickable: true,
    },
    navigation: {
      nextEl: '.swiper-button-next',
      prevEl: '.swiper-button-prev',
    },
    on: {
      init: function () {
        this.slides.forEach((slide, index) => {
          const formattedIndex = String(index + 1).padStart(2, '0');
          slide.classList.add(`index_${formattedIndex}`);
        });
      },
    },
  });
}

その関数をwebStorageのところに差し込む形となります。

const webStorage = function () {
const loadElement = document.getElementById('load');
if (sessionStorage.getItem('visit')) {
loadElement.style.opacity = 0;
loadElement.style.visibility = 'hidden';
initializeSwiper(); // swiper
} else {
sessionStorage.setItem('visit', 'true');
setTimeout(() => {
loadElement.style.opacity = 0;
loadElement.style.visibility = 'hidden';
initializeSwiper(); // swiper
}, 5000); // 5秒に設定
}
}

webStorageのタイミングでswiper初期化しましょう。

【その他】cssでチラリ表示を回避する

ローディングはモーダルのようなfixedの見せ方ではないですか?

</body>の上にソースコードを仕込むと、先にあるソース内容のレンダリングが一瞬、ローディングよりも早く表示が見える時があるのではないでしょうか。

そんな時はopacityのanimationで逃げるのもいいと思います。

.swiper {
  opacity: 0;
  animation: showSwiper 0.2s ease forwards;
}
@keyframes showSwiper {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

これは別にこの記事に必要ないかなと思った。けど入れました。

【AI】イラストを描いてもらった

今回の記事のキャッチ画像で使わせてもらいます「Memeplex.app」で作成した画像です。誰でもgoogleアカウントでログインして使えます。

この記事にピッタリなイラストのための考えたリクエストは、「複数のシステムタスクがローディング中の円グラフと、アバター2名の人間の姿を出したい。」です。

選んだモードは以下の3つです。

  • 特撮
  • サイバーパンク風
  • UnrealEngine5風

サイバーでパンクしている気がします。

参考サイト置き場

「mask-image」でSVGアイコンの色をCSSで変えよう!
https://zenn.dev/kagan/articles/cf3332462262f1

グラデーションもできる。

lopan:雲隠れ
https://lopan.jp/css-mask/cloud-mask/

lopanさんのサイト、かわいくて頻繁に見にいきます。

ワイプとは
https://kotobank.jp/word/%E3%83%AF%E3%82%A4%E3%83%97-10162