デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
四角いタイルを、縦の軸で半回転させたあと、続けて横の軸で半回転させます。どちらも半回転しか使わないので、表と裏の2面だけで裏返る動きを表せます。
1回裏返るのに 0.675 秒、裏返ったあとの間は 0.135 秒です。見る角度を正面から少し傾けているのは、真正面だと止まっている間はただの四角にしか見えないためです。
奥行きは親の要素に perspective、タイルと面には3D変形を保つ指定を添えます。裏の面は backface-visibility で隠しますが、表と裏の四隅の丸みをそろえておかないと、裏返ったときに向きが変わって見えます。
表と裏の色の差を大きくするほど、裏返る動きが読みやすくなります。読み込み中の全画面表示に向いていて、色や大きさを変えるだけでブランドに合わせられます。
コード
<div class="flip-loader" role="status">
<div class="flip-loader__view">
<div class="flip-loader__tile">
<div class="flip-loader__face flip-loader__face--front"></div>
<div class="flip-loader__face flip-loader__face--back"></div>
</div>
</div>
<span class="flip-loader__text">読み込み中</span>
</div>
.flip-loader {
--ground: #f2f1ed; /* 地の色 */
--front: #A28767; /* 表(中身の色。グレージュ) */
--back: #262626; /* 裏 */
--size: 96px; /* タイルの一辺 */
--radius: 14px; /* 角の丸み */
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--ground);
perspective: calc(var(--size) * 5);
}
.flip-loader__view {
transform-style: preserve-3d;
transform: rotateX(26deg) rotateY(-24deg);
}
.flip-loader__tile {
position: relative;
width: var(--size);
height: var(--size);
transform-style: preserve-3d;
}
.flip-loader__face {
position: absolute;
inset: 0;
border-radius: var(--radius);
backface-visibility: hidden;
}
.flip-loader__face--front { background: var(--front); }
.flip-loader__face--back { background: var(--back); transform: rotateY(180deg); }
/* 読み上げにだけ届く文言。画面には出さない */
.flip-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 FLIP = 0.675; // 1 回裏返る時間(秒)
const GAP = 0.135; // 裏返ったあとに止まる時間(秒)
function createLoader(root) {
const tile = root.querySelector(".flip-loader__tile");
let tl;
if (reduceMotion) {
// 動きを減らす設定では裏返さず、明るさだけをゆっくり点滅させる
tl = gsap.to(root, { opacity: 0.6, duration: 0.9, ease: "sine.inOut", repeat: -1, yoyo: true });
} else {
// rotationX を -180 → 続けて rotationY を -180。点対称の四角なので 1 ループで元に戻る
tl = gsap
.timeline({ repeat: -1, repeatDelay: GAP })
.fromTo(tile, { rotationX: 0, rotationY: 0 }, { rotationX: -180, duration: FLIP, ease: "power2.inOut" })
.fromTo(tile, { rotationY: 0 }, { rotationY: -180, duration: FLIP, ease: "power2.inOut" }, `+=${GAP}`);
}
return {
// 読み込みが終わったら呼ぶ
done() {
gsap.to(root, {
autoAlpha: 0,
duration: reduceMotion ? 0 : 0.27,
onComplete: () => {
tl.kill();
root.remove();
},
});
},
};
}
// 例: 読み込み中の全画面表示に。終わったら loader.done()
export const loader = createLoader(document.querySelector(".flip-loader"));