デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
中心の玉から 4 つの小さな玉が糸を引くように離れ、また吸い込まれます。粘りの正体は SVG フィルターで、ぼかした形をアルファ値で強調し直すと、近づいた玉どうしのぼかしがひとつの形として残り、水滴がつながったように見えます。
向かい合う 2 組の玉を、片方が離れるあいだもう片方が戻る形にずらしていて、離れきるまで 1.2 秒、全体はそのあいだにゆっくり半回転します。形が点対称なので、1 ループで半回転させると元の並びに戻り、継ぎ目なくつながります。
ぼかしを強くしすぎると輪郭がにじみすぎて玉の形が崩れるので、ぼかしは小さめにして、代わりにアルファ値の強調を効かせて遠くの玉までつなげています。動きを減らす設定では全体の回転を止め、離れる幅も小さくして、大きな動きを見せないようにしています。
ぼかしと強調の度合いを上げれば、もっと離れた玉どうしも糸を引くようになります。終わりの見当がつかない待ち時間の、やわらかい印象を出したい場面に向いています。
コード
<div class="gooey-loader" role="status">
<svg class="gooey-loader__svg" viewBox="-100 -100 200 200" aria-hidden="true">
<defs>
<filter id="gooey-loader-goo" filterUnits="userSpaceOnUse" x="-100" y="-100" width="200" height="200" color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="3.8" result="blur" />
<feColorMatrix in="blur" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -5" />
</filter>
</defs>
<g class="gooey-loader__spin" filter="url(#gooey-loader-goo)">
<g transform="rotate(0)"><circle class="gooey-loader__drop" cx="0" cy="0" r="12" /></g>
<g transform="rotate(90)"><circle class="gooey-loader__drop" cx="0" cy="0" r="12" /></g>
<g transform="rotate(180)"><circle class="gooey-loader__drop" cx="0" cy="0" r="12" /></g>
<g transform="rotate(270)"><circle class="gooey-loader__drop" cx="0" cy="0" r="12" /></g>
<circle class="gooey-loader__core" cx="0" cy="0" r="17" />
</g>
</svg>
<span class="gooey-loader__text">読み込み中</span>
</div>
.gooey-loader {
--size: 200px; /* SVG の一辺 */
--ground: #F2F1ED; /* 地の色(Paper) */
--drop: #4A5B66; /* 玉の色(墨のスレート) */
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--ground);
}
.gooey-loader__svg {
width: var(--size);
height: var(--size);
overflow: visible;
}
.gooey-loader__spin {
fill: var(--drop);
}
.gooey-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) {
const spin = root.querySelector(".gooey-loader__spin");
const drops = root.querySelectorAll(".gooey-loader__drop");
// 向かい合う 2 組を、片方が離れるあいだもう片方が戻る形にずらす(from / to を逆にした yoyo)
const goo = gsap.timeline({ repeat: -1 });
if (reduceMotion) {
// 全体の回転を止め、離れる幅も小さくする
goo
.fromTo([drops[0], drops[2]], { x: 0 }, { x: 12, duration: 1.2, ease: "power2.inOut", repeat: 1, yoyo: true }, 0)
.fromTo([drops[1], drops[3]], { x: 12 }, { x: 0, duration: 1.2, ease: "power2.inOut", repeat: 1, yoyo: true }, 0);
} else {
goo
.fromTo(spin, { rotation: 0, svgOrigin: "0 0" }, { rotation: 180, svgOrigin: "0 0", duration: 2.4, ease: "none" }, 0)
.fromTo([drops[0], drops[2]], { x: 0 }, { x: 50, duration: 1.2, ease: "power2.inOut", repeat: 1, yoyo: true }, 0)
.fromTo([drops[1], drops[3]], { x: 50 }, { x: 0, duration: 1.2, ease: "power2.inOut", repeat: 1, yoyo: true }, 0);
}
return {
// 読み込みが終わったら呼ぶ
done() {
gsap.to(root, {
autoAlpha: 0,
duration: reduceMotion ? 0 : 0.3,
onComplete: () => {
goo.kill();
root.remove();
},
});
},
};
}
export const loader = createLoader(document.querySelector(".gooey-loader"));