デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
SVG の circle の半径を attr でトゥイーンして輪を広げ、線の太さを保ったまま波紋のように見せます。3本の輪を等間隔でずらして繰り返すので、輪が途切れずに次々と生まれ続けます。中心の点は輪が生まれる瞬間にいちばん大きくなるよう、同じ間隔で拍動させています。
1本の輪が広がって消えるまでは 2.4 秒、3本なので周期を3で割った間隔でずらすと、途切れずに出続けます。広がりには power2.out、消え方には power1.in と別のイージングを充てているので、生まれた直後は速く、消える間際はゆっくり薄れます。
半径は scale ではなく attr で変えています。拡大縮小だと線の太さまで一緒に伸び縮みしてしまうためです。輪と中心の点の色は CSS の変数ひとつで決めているので、地の色を変えたときはその変数を合わせるだけで済みます。
本数や広がる大きさ、繰り返す間隔を変えると、密度の違う波紋になります。読み込み中だけでなく、「接続中」「録音中」「ライブ」のような、終わりの見えない状態を示す表示にも使えます。
コード
<div class="pulse-loader" role="status">
<svg class="pulse-loader__svg" viewBox="-100 -100 200 200" aria-hidden="true">
<circle class="pulse-loader__ring" cx="0" cy="0" r="14"></circle>
<circle class="pulse-loader__ring" cx="0" cy="0" r="14"></circle>
<circle class="pulse-loader__ring" cx="0" cy="0" r="14"></circle>
<circle class="pulse-loader__core" cx="0" cy="0" r="14"></circle>
</svg>
<span class="pulse-loader__text">読み込み中</span>
</div>
.pulse-loader {
--ink: #262626; /* 輪と中心の点の色 */
--ground: #f2f1ed; /* 地の色 */
--size: 232px; /* SVG の一辺 */
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--ground);
}
.pulse-loader__svg {
display: block;
width: var(--size);
height: var(--size);
overflow: visible;
}
.pulse-loader__ring {
fill: none;
stroke: var(--ink);
stroke-width: 2.5;
}
.pulse-loader__core {
fill: var(--ink);
}
/* 読み上げにだけ届く文言。画面には出さない */
.pulse-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 COUNT = 3;
const PERIOD = 2.4; // 1 本の輪が広がって消えるまで(秒)
const EACH = PERIOD / COUNT; // 輪の間隔。周期を本数で割ると、途切れずに出続ける
const R0 = 14; // 輪の出はじめの半径(viewBox 単位。中心の点と同じ)
const R1 = reduceMotion ? 30 : 97; // 消えるときの半径。動きを減らす設定では広がりを小さくする
function createLoader(root) {
const rings = root.querySelectorAll(".pulse-loader__ring");
const core = root.querySelector(".pulse-loader__core");
const pulse = gsap
.timeline()
.fromTo(rings, { attr: { r: R0 } }, { attr: { r: R1 }, duration: PERIOD, ease: "power2.out", stagger: { each: EACH, repeat: -1 } }, 0)
.fromTo(rings, { opacity: 0.9 }, { opacity: 0, duration: PERIOD, ease: "power1.in", stagger: { each: EACH, repeat: -1 } }, 0)
// 輪が出る瞬間(EACH の倍数)に scale 1、その間に 0.78 まで縮む
.fromTo(core, { scale: 1 }, { scale: 0.78, duration: EACH / 2, ease: "sine.inOut", repeat: -1, yoyo: true }, 0);
return {
// 読み込みが終わったら呼ぶ。呼ぶまで波紋が広がり続ける
done() {
gsap.to(root, {
autoAlpha: 0,
duration: reduceMotion ? 0 : 0.3,
onComplete: () => {
pulse.kill();
root.remove();
},
});
},
};
}
// 例: fetch や画像の読み込みを待って loader.done()。「接続中」「録音中」「ライブ」の表示にも使える
export const loader = createLoader(document.querySelector(".pulse-loader"));