デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
同じ円の上を回る4つのドットが、上では詰まって重なりかけ、下ではばらけて、追いかけ合うように見えます。中心を軸に回る見えない腕を作り、それぞれの先端にドットを付けて、腕ごと回しています。
回転には sine.inOut を使っていて、上のほうではゆっくり、下では速く回ります。腕は0.14秒ずつ開始をずらしてあるので、上では前のドットに後ろが追いついて間が詰まり、下では間が開きます。1周は1.6秒で、ちょうど元の角度に戻るので、繰り返しても継ぎ目は見えません。
イージングを強めると、上に来たときにドットがほぼ重なって止まる時間が延び、追いかけっこには見えなくなります。だから曲線は sine.inOut の穏やかなカーブにとどめました。
重なったときの前後は、HTML に書いた要素の順で決まり、後に書いたものほど上に来ます。色はこの並びに合わせて割り当てています。ドットの数や色を増やしても、ずらす間隔を均等にしておけば同じ形のままです。
コード
<div class="orbit-loader" role="status">
<div class="orbit-loader__ring">
<div class="orbit-loader__arm"><span class="orbit-loader__dot"></span></div>
<div class="orbit-loader__arm"><span class="orbit-loader__dot"></span></div>
<div class="orbit-loader__arm"><span class="orbit-loader__dot"></span></div>
<div class="orbit-loader__arm"><span class="orbit-loader__dot"></span></div>
</div>
<span class="orbit-loader__text">読み込み中</span>
</div>
.orbit-loader {
--size: 192px; /* 輪の直径 */
--dot: 36px; /* 点の直径 */
--ground: #111111; /* 背景色 */
--line: rgba(238, 236, 230, 0.14); /* 円周の線 */
--c1: #E6D3B3; /* 点の色(先頭 = 最前面)。先頭ほど明るくして、重なっても見分けられるようにする */
--c2: #A5C0A3;
--c3: #C78E8A;
--c4: #6A7D88;
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--ground);
}
.orbit-loader__ring {
position: relative;
width: var(--size);
height: var(--size);
border-radius: 50%;
box-shadow: inset 0 0 0 2px var(--line);
}
.orbit-loader__arm {
position: absolute;
inset: 0;
}
.orbit-loader__dot {
position: absolute;
left: 50%;
top: 0;
width: var(--dot);
height: var(--dot);
margin: calc(var(--dot) / -2) 0 0 calc(var(--dot) / -2);
border-radius: 50%;
background: var(--c4);
}
/* DOM は奥から手前の順に並べてある。手前(最後の子)ほど先頭の色にする */
.orbit-loader__arm:nth-child(2) .orbit-loader__dot { background: var(--c3); }
.orbit-loader__arm:nth-child(3) .orbit-loader__dot { background: var(--c2); }
.orbit-loader__arm:nth-child(4) .orbit-loader__dot { background: var(--c1); }
.orbit-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;
function createLoader(root) {
// DOM は奥から手前の順に並べてある。逆順にすると「先頭 = 最前面」の点から並ぶ
const arms = [...root.querySelectorAll(".orbit-loader__arm")].reverse();
// sine.inOut で上では遅く下では速く回るので、ずらした腕どうしが上で詰まり下で開いて追いかけっこに見える
const orbit = gsap.timeline({ repeat: -1 }).fromTo(
arms,
{ rotation: 0 },
{
rotation: reduceMotion ? 28 : 360, // 動きを減らす設定では振れ幅を小さくする
duration: reduceMotion ? 0.9 : 1.6,
ease: "sine.inOut",
stagger: { each: 0.14, repeat: -1, yoyo: reduceMotion },
}
);
return {
// 読み込みが終わったら呼ぶ。呼ぶまで回り続ける
done() {
gsap.to(root, {
autoAlpha: 0,
duration: reduceMotion ? 0 : 0.3,
onComplete: () => {
orbit.kill();
root.remove();
},
});
},
};
}
export const loader = createLoader(document.querySelector(".orbit-loader"));