デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- DrawSVGPlugin
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
1本の弧を伸ばしたり縮めたりしながら回し続けます。長さの操作には DrawSVGPlugin を使っていて、円周を測らずに % の長さで弧の始まりと終わりを指定できます。
弧はいちばん縮んだときに全周の 8% 残るようにしていて、伸びる半分と縮む半分で power1.inOut のイージングをかけています。回転には、弧が伸縮するあいだに先へ進む分(28.8°)を足しているので、1周ごとに継ぎ目が出ません。
回転の軸は svgOrigin で円の中心に固定しています。既定の軸のままだと、弧の見える長さが変わるたびに軸がわずかにずれます。
輪の大きさや色、1周の時間を変えるだけで、送信中のボタンや読み込み中の全画面表示など、いろいろな場所で使い回せます。
コード
<div class="spin-loader" role="status">
<svg class="spin-loader__svg" viewBox="0 0 100 100" aria-hidden="true">
<circle class="spin-loader__track" cx="50" cy="50" r="40"></circle>
<circle class="spin-loader__arc" cx="50" cy="50" r="40"></circle>
</svg>
<span class="spin-loader__text">読み込み中</span>
</div>
.spin-loader {
--ground: #f2f1ed; /* 地の色 */
--line: rgba(38, 38, 38, 0.12); /* 溝の色 */
--arc: #b27771; /* 弧の色(中身の色。くすみローズ) */
--size: 144px; /* 輪の大きさ */
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--ground);
}
.spin-loader__svg {
display: block;
width: var(--size);
height: var(--size);
overflow: visible;
}
.spin-loader__track,
.spin-loader__arc {
fill: none;
stroke-width: 9;
stroke-linecap: round;
}
.spin-loader__track { stroke: var(--line); }
.spin-loader__arc { stroke: var(--arc); }
/* 読み上げにだけ届く文言。画面には出さない */
.spin-loader__text {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
import { gsap } from "gsap";
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
gsap.registerPlugin(DrawSVGPlugin);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const DURATION = 1.4; // 1 周の時間(秒)
const SEGMENT = 8; // 縮みきったときの弧の長さ(%)
function createLoader(root) {
const arc = root.querySelector(".spin-loader__arc");
let tl;
if (reduceMotion) {
// 動きを減らす設定では回さず、明るさだけをゆっくり点滅させる
gsap.set(arc, { drawSVG: "0% 30%" });
tl = gsap.to(root, { opacity: 0.5, duration: 0.9, ease: "sine.inOut", repeat: -1, yoyo: true });
} else {
// 弧は 8% ぶん先へ進んで縮むので、回転に 8% 分(28.8°)を足すと 1 周で継ぎ目が出ない
tl = gsap
.timeline({ repeat: -1 })
.fromTo(arc, { rotation: -90, svgOrigin: "50 50" }, { rotation: -90 + 360 + SEGMENT * 3.6, svgOrigin: "50 50", duration: DURATION, ease: "none" }, 0)
.fromTo(arc, { drawSVG: `0% ${SEGMENT}%` }, { drawSVG: "0% 78%", duration: DURATION / 2, ease: "power1.inOut" }, 0)
.to(arc, { drawSVG: `${100 - SEGMENT}% 100%`, duration: DURATION / 2, ease: "power1.inOut" }, DURATION / 2);
}
return {
// 読み込みが終わったら呼ぶ
done() {
gsap.to(root, {
autoAlpha: 0,
duration: reduceMotion ? 0 : 0.3,
onComplete: () => {
tl.kill();
root.remove();
},
});
},
};
}
// 例: 送信中のボタンや、読み込み中の全画面表示に。終わったら loader.done()
export const loader = createLoader(document.querySelector(".spin-loader"));