デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
中心の玉から小さな玉が糸を引くように離れていき、また吸い込まれます。玉どうしがつながって見えるのは SVG フィルターの働きで、まず形をぼかし、そのあとアルファ値を強く持ち上げて、輪郭をくっきりさせ直しています。近くにある玉はぼけた部分が重なって濃くなっているので、くっきりさせたときにひとつの形として残り、水滴がくっついたように見えます。
ぼかしを大きくすれば遠くの玉までつながりそうですが、それでは輪郭がにじみすぎて、玉が丸く見えなくなります。ぼかしは小さめにとどめ、アルファ値の強調のほうを強く効かせて、離れた玉まで糸が届くようにしました。どこまで糸を引かせるかはこの2つの度合いで決まり、両方を上げるほど、遠く離れた玉どうしもつながります。
4つの玉は向かい合う2組に分け、片方が離れていくあいだに、もう片方が戻ってくるようにずらしています。離れきるまでが1.2秒。離れて戻るまでの2.4秒で、全体がゆっくり半回転します。玉の並びは点対称なので、半回転した姿は回る前と同じになり、ループの切れ目が見えません。
コード
<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; /* 明るい背景色 */
--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"));