デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
面ごとに色の違う立方体を、前へ・横へと交互に90度ずつ転がします。転がった回数をひとつの値として持ち、そこから今の回転と転がり終えた回転を積み上げて描くので、何度転がしても続きから動かせます。
1回転がるのに0.42秒、転がってから次に転がり出すまでの間を合わせて0.6秒を1セットとしています。前後の軸を交互に6回転がすと、ちょうど元の向きに戻ります。転がっている途中は角で立つぶん立方体を持ち上げ、影を小さく薄くして、地面から浮く感じを出します。
動きを減らす設定では立方体を転がさず、明るさをゆっくり点滅させるだけにします。転がったまま読み込みが終わらないと止まらないので、読み込みが終わったら止める合図を受け取る口を、あらかじめ用意しています。
立方体の一辺の大きさと転がる速さは、CSSとスクリプトの先頭で変えられます。処理や生成にしばらく時間がかかる場面の、目を引く待ち時間の表示に向いています。
コード
<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"));