デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
色の違う面を順に見せながら、立方体が前へ、横へと交互に90度ずつ転がり続けます。ひと転がりは0.42秒、次に転がり出すまでの間を含めて0.6秒でひとサイクル。前と横を交互に6回転がすと、立方体はちょうど元の向きに戻ります。
転がった回数は、ひとつの数値だけで持つことにしました。描くたびに、その値からいま転がっている途中の回転を求め、それまでに転がり終えた回転を順に積み重ねて向きを決めます。毎回はじめからたどり直すので、何度転がしたあとでも続きから動かせます。一辺の大きさと転がる速さは、CSS とスクリプトの先頭で変えられます。
転がる途中の立方体は辺を軸に傾き、斜めに立った瞬間に中心がいちばん高くなります。その分だけ立方体を持ち上げ、床の影を小さく薄くして、床から浮き上がったように見せています。止めるための関数も用意してあり、読み込みが終わったら呼んで止めます。OS で動きを抑える設定にしている人には、転がす代わりに、明るさをゆっくり点滅させるだけにしました。
コード
<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>
.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;
}
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"));