本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ローディング

ローディング中に立方体が転がるように回る

デモ

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

面ごとに色の違う立方体を、前へ・横へと交互に90度ずつ転がします。転がった回数をひとつの値として持ち、そこから今の回転と転がり終えた回転を積み上げて描くので、何度転がしても続きから動かせます。

1回転がるのに0.42秒、転がってから次に転がり出すまでの間を合わせて0.6秒を1セットとしています。前後の軸を交互に6回転がすと、ちょうど元の向きに戻ります。転がっている途中は角で立つぶん立方体を持ち上げ、影を小さく薄くして、地面から浮く感じを出します。

動きを減らす設定では立方体を転がさず、明るさをゆっくり点滅させるだけにします。転がったまま読み込みが終わらないと止まらないので、読み込みが終わったら止める合図を受け取る口を、あらかじめ用意しています。

立方体の一辺の大きさと転がる速さは、CSSとスクリプトの先頭で変えられます。処理や生成にしばらく時間がかかる場面の、目を引く待ち時間の表示に向いています。

コード

HTML
<div class="cube-loader" role="status">
  <div class="cube-loader__scene">
    <div class="cube-loader__shadow"></div>
    <div class="cube-loader__cube">
      <span class="cube-loader__face cube-loader__face--front"></span>
      <span class="cube-loader__face cube-loader__face--right"></span>
      <span class="cube-loader__face cube-loader__face--back"></span>
      <span class="cube-loader__face cube-loader__face--left"></span>
      <span class="cube-loader__face cube-loader__face--top"></span>
      <span class="cube-loader__face cube-loader__face--bottom"></span>
    </div>
  </div>
  <span class="cube-loader__text">読み込み中</span>
</div>

CSS
.cube-loader {
  --ground: #111111;  /* 地の色 */
  --size: 72px;        /* 立方体の一辺 */
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: var(--ground);
  perspective: calc(var(--size) * 8);
}

.cube-loader__scene {
  position: relative;
  width: var(--size);
  height: var(--size);
  transform-style: preserve-3d;
  transform: rotateX(-26deg) rotateY(38deg);
}

.cube-loader__shadow {
  position: absolute;
  left: calc(var(--size) * -0.3);
  top: calc(var(--size) * -0.3);
  width: calc(var(--size) * 1.6);
  height: calc(var(--size) * 1.6);
  border-radius: 50%;
  background: radial-gradient(closest-side, #000000aa, #00000000);
  transform: translateY(calc(var(--size) / 2)) rotateX(90deg);
}

.cube-loader__cube {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
}

.cube-loader__face {
  position: absolute;
  inset: 0;
  box-shadow: inset 0 0 0 2px #00000014;
}

/* 面ごとの色。向かい合う面を同じ明るさの段(前後が淡い・左右が中間・上下が濃い)にし、見えている 3 面を明るさで見分ける */
.cube-loader__face--front { background: #d2b999; transform: rotateY(0deg) translateZ(calc(var(--size) / 2)); }
.cube-loader__face--right { background: #b27771; transform: rotateY(90deg) translateZ(calc(var(--size) / 2)); }
.cube-loader__face--back { background: #c8bf83; transform: rotateY(180deg) translateZ(calc(var(--size) / 2)); }
.cube-loader__face--left { background: #739374; transform: rotateY(-90deg) translateZ(calc(var(--size) / 2)); }
.cube-loader__face--top { background: #4a5b66; transform: rotateX(90deg) translateZ(calc(var(--size) / 2)); }
.cube-loader__face--bottom { background: #605a75; transform: rotateX(-90deg) translateZ(calc(var(--size) / 2)); }

/* 読み上げにだけ届く文言。画面には出さない */
.cube-loader__text {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const STEPS = 6; // 1 ループで転がる回数(6 回で元の向きに戻る)
const ROLL = 0.42; // 1 回転がる時間(秒)
const STEP = 0.6; // 1 回ぶんの長さ(転がる + 止まる)

// k 回目に転がる向き。偶数回は X 軸(前へ)、奇数回は Z 軸(横へ)
function rotate(k, angle) {
  return k % 2 === 0 ? `rotateX(${-angle}deg)` : `rotateZ(${angle}deg)`;
}

function createLoader(root) {
  const cube = root.querySelector(".cube-loader__cube");
  const shadow = root.querySelector(".cube-loader__shadow");
  const size = cube.offsetWidth;
  const lift = size * ((Math.SQRT2 - 1) / 2); // 角で立ったときに中心が上がる高さ
  const state = { roll: 0 };

  function draw() {
    const n = Math.min(Math.floor(state.roll), STEPS - 1);
    const p = state.roll - n;
    const up = Math.sin(Math.PI * p);
    let transform = `translate3d(0, ${-lift * up}px, 0) ${rotate(n, 90 * p)}`;
    for (let k = n - 1; k >= 0; k--) transform += ` ${rotate(k, 90)}`;
    cube.style.transform = transform;
    shadow.style.transform = `translateY(${size / 2}px) rotateX(90deg) scale(${1 - 0.3 * up})`;
    shadow.style.opacity = String(1 - 0.45 * up);
  }

  let tl;
  if (reduceMotion) {
    // 動きを減らす設定では転がさず、明るさだけをゆっくり点滅させる
    tl = gsap.to(root, { opacity: 0.6, duration: 0.9, ease: "sine.inOut", repeat: -1, yoyo: true });
  } else {
    tl = gsap.timeline({ repeat: -1, onUpdate: draw });
    for (let k = 0; k < STEPS; k++) {
      tl.fromTo(state, { roll: k }, { roll: k + 1, duration: ROLL, ease: "power2.inOut", immediateRender: k === 0 }, k * STEP + (STEP - ROLL) / 2);
    }
  }

  return {
    // 読み込みが終わったら呼ぶ。呼ぶまで回り続ける
    done() {
      gsap.to(root, {
        autoAlpha: 0,
        duration: reduceMotion ? 0 : 0.3,
        onComplete: () => {
          tl.kill();
          root.remove();
        },
      });
    },
  };
}

// 例: fetch や画像の読み込みを待って loader.done()
export const loader = createLoader(document.querySelector(".cube-loader"));

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。