本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

ローディング

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

デモ

仕様

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

しくみと調整

色の違う面を順に見せながら、立方体が前へ、横へと交互に90度ずつ転がり続けます。ひと転がりは0.42秒、次に転がり出すまでの間を含めて0.6秒でひとサイクル。前と横を交互に6回転がすと、立方体はちょうど元の向きに戻ります。

転がった回数は、ひとつの数値だけで持つことにしました。描くたびに、その値からいま転がっている途中の回転を求め、それまでに転がり終えた回転を順に積み重ねて向きを決めます。毎回はじめからたどり直すので、何度転がしたあとでも続きから動かせます。一辺の大きさと転がる速さは、CSS とスクリプトの先頭で変えられます。

転がる途中の立方体は辺を軸に傾き、斜めに立った瞬間に中心がいちばん高くなります。その分だけ立方体を持ち上げ、床の影を小さく薄くして、床から浮き上がったように見せています。止めるための関数も用意してあり、読み込みが終わったら呼んで止めます。OS で動きを抑える設定にしている人には、転がす代わりに、明るさをゆっくり点滅させるだけにしました。

コード

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 サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。