デモ
デモを読み込めませんでした。
仕様
- 用途
- ローディング
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- MorphSVGPlugin
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
1本のパスを MorphSVGPlugin で丸・三角・四角と順に変形し、塗りの色も同じトゥイーンで一緒に移します。角の丸みは塗りと同じ色の太い線で付けているので、形そのものはとがったままで構いません。
回す角度は、形が変わり終えたときに元の向きに見える量にしています。丸→三角→四角の3段階で合計 360° になるので、ループの継ぎ目がつながります。形が変わるあいだは軽く浮き上がり、影も一緒に小さく薄くなります。
変形の中心は、それぞれの形の対称の中心に置く必要があります。ずらすと、回したときに次の形が元の位置からはみ出して重なりません。
形の並びや回す角度、浮き上がる高さを変えると、違うリズムの起動画面になります。デザインツールやクリエイティブ系サービスの起動画面に向いています。
コード
<div class="morph-loader" role="status">
<svg class="morph-loader__svg" viewBox="-150 -150 300 300" aria-hidden="true">
<ellipse class="morph-loader__shadow" cx="0" cy="128" rx="66" ry="9"></ellipse>
<path class="morph-loader__shape"
d="M0,-80 C44.18,-80 80,-44.18 80,0 C80,44.18 44.18,80 0,80 C-44.18,80 -80,44.18 -80,0 C-80,-44.18 -44.18,-80 0,-80 Z"></path>
</svg>
<span class="morph-loader__text">読み込み中</span>
</div>
.morph-loader {
--ground: #f2f1ed; /* 地の色 */
--ink: #262626; /* 影の色 */
--shape-1: #b27771; /* 丸(くすみローズ) */
--shape-2: #a28767; /* 三角(グレージュ) */
--shape-3: #4a5b66; /* 四角(墨のスレート) */
--size: 300px;
position: fixed;
inset: 0;
z-index: 10;
display: grid;
place-items: center;
background: var(--ground);
}
.morph-loader__svg {
display: block;
width: var(--size);
height: var(--size);
overflow: visible;
}
.morph-loader__shadow {
fill: var(--ink);
opacity: 0.16;
}
.morph-loader__shape {
stroke-width: 16;
stroke-linejoin: round;
}
/* 読み上げにだけ届く文言。画面には出さない */
.morph-loader__text {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
import { gsap } from "gsap";
import { MorphSVGPlugin } from "gsap/MorphSVGPlugin";
gsap.registerPlugin(MorphSVGPlugin);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 形はどれも、回す中心(対称の中心)を原点に置く。そうしないと、回したあと元の向きに重ならない
const TRIANGLE = "M0,-94 L81.41,47 L-81.41,47 Z";
const SQUARE = "M0,-70 L70,-70 L70,70 L-70,70 L-70,-70 Z";
const CIRCLE = "M0,-80 C44.18,-80 80,-44.18 80,0 C80,44.18 44.18,80 0,80 C-44.18,80 -80,44.18 -80,0 C-80,-44.18 -44.18,-80 0,-80 Z";
// turn: 次の形へ移るときに回す角度。合計 360° で丸に戻り、ループがつながる
const SHAPES = [{ d: CIRCLE, turn: 120 }, { d: TRIANGLE, turn: 150 }, { d: SQUARE, turn: 90 }];
const STEP = 1; // 1 つの形にかける時間(秒)
const HOLD = 0.35; // 形を保つ時間。残りで次の形へ移る
const LIFT = 26; // 形を変える間に浮く高さ(viewBox 単位)
function createLoader(root) {
const shape = root.querySelector(".morph-loader__shape");
const shadow = root.querySelector(".morph-loader__shadow");
// 色は CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
const css = getComputedStyle(root);
const colors = [1, 2, 3].map((n) => css.getPropertyValue(`--shape-${n}`).trim());
shape.setAttribute("fill", colors[0]);
shape.setAttribute("stroke", colors[0]);
let tl;
if (reduceMotion) {
// 動きを減らす設定では形を変えず、明るさだけをゆっくり点滅させる
tl = gsap.to(root, { opacity: 0.6, duration: 0.9, ease: "sine.inOut", repeat: -1, yoyo: true });
} else {
tl = gsap.timeline({ repeat: -1 });
SHAPES.forEach((current, i) => {
const next = (i + 1) % SHAPES.length;
const dur = STEP - HOLD;
tl.to(shape, { morphSVG: SHAPES[next].d, fill: colors[next], stroke: colors[next], duration: dur, ease: "power2.inOut" }, `+=${HOLD}`)
.to(shape, { rotation: `+=${current.turn}`, svgOrigin: "0 0", duration: dur, ease: "back.inOut(1.3)" }, "<")
.to(shape, {
y: -LIFT, duration: dur / 2, ease: "sine.inOut", yoyo: true, repeat: 1,
onUpdate: () => {
const k = 1 + gsap.getProperty(shape, "y") / (LIFT * 4); // 浮くと影が小さく薄くなる
shadow.setAttribute("rx", (66 * k).toFixed(2));
shadow.setAttribute("opacity", (0.16 * k).toFixed(3));
},
}, "<");
});
}
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(".morph-loader"));