デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- MorphSVGPlugin
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
輪郭のやわらかな丸い図形(ブロブ)が、ゆっくり回りながら少しずつ別の形へ移り、色も一緒に移り変わっていきます。形は3つで、MorphSVGPlugin で順に変形させ、3つ目のあとは最初の形へ戻る変形でつないであります。1周の終わりと次の始まりが同じ形になるので、何周見ていても継ぎ目は見えません。
それぞれの形が受け持つ時間のうち、はじめのしばらくは形を保ったまま、残りの時間で次の形へ移ります。入ってすぐ変形し始めないぶん、形の変化が唐突にならず、落ち着いて見えます。変形には sine.inOut をかけ、回転は一定の速さのまま回し続けることにしました。速さの付き方が違うふたつの動きが重なって、ただ回っているだけよりも込み入った動きに見えます。
形を足すときも、最後の形を最初の形へつなげば、同じように回り続けます。背景はオフホワイトのままにしておくと、ブロブの色がよく映えます。ヒーローの装飾やローディングの背景のように、目は引きたいけれど主役にはしたくない場所なら、この控えめな動きがちょうど合います。
コード
<section class="blob-hero">
<!-- 絵は飾りなので、読み上げからは外す -->
<svg class="blob-hero__art" viewBox="0 0 600 600" aria-hidden="true">
<path class="blob-hero__shape"></path>
</svg>
<div class="blob-hero__copy">
<p class="blob-hero__eyebrow">Studio</p>
<h1 class="blob-hero__title">かたちは、<wbr>変わりつづける。</h1>
<p class="blob-hero__lead">ここに説明文が入ります。</p>
</div>
</section>
.blob-hero {
--bg: #f2f1ed;
--ink: #262626;
--blob-1: #b27771; /* ブロブの色(形の順)。明るさを離すと、形が移るのが見分けやすい */
--blob-2: #98b1c4;
--blob-3: #4a5b66;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: center;
gap: clamp(24px, 6vw, 64px);
padding: 96px clamp(24px, 7vw, 96px);
background-color: var(--bg);
color: var(--ink);
}
.blob-hero__art {
display: block;
width: 100%;
max-width: 420px;
}
.blob-hero__eyebrow {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.blob-hero__title {
margin: 24px 0 0;
font-size: clamp(32px, 5vw, 56px);
line-height: 1.1;
letter-spacing: -0.02em;
word-break: keep-all; /* 和文を語の途中で折らず、<wbr> の所で折り返す */
}
.blob-hero__lead {
margin: 24px 0 0;
max-width: 30em;
font-size: 18px;
line-height: 1.8;
}
@media (max-width: 640px) {
.blob-hero { grid-template-columns: minmax(0, 1fr); }
}
import { gsap } from "gsap";
import { MorphSVGPlugin } from "gsap/MorphSVGPlugin";
gsap.registerPlugin(MorphSVGPlugin);
const STEP = 1.2; // 1 つの形にとどまってから次へ移るまでの時間(秒)
// 3 つのブロブ形。前半は形を保ち、後半で次の形へ移る。色は CSS の --blob-1〜3
const SHAPES = [
"M300,90 C420,90 510,170 510,300 C510,430 420,510 300,510 C180,510 90,430 90,300 C90,170 180,90 300,90 Z",
"M300,70 C450,110 540,200 500,330 C460,470 370,540 260,500 C130,460 60,380 90,250 C120,130 190,40 300,70 Z",
"M320,110 C430,60 540,160 520,280 C500,420 430,520 300,520 C160,520 70,420 110,300 C140,200 200,150 320,110 Z",
];
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".blob-hero__shape").forEach((blob) => {
const css = getComputedStyle(blob);
const fills = SHAPES.map((_, i) => css.getPropertyValue(`--blob-${i + 1}`).trim());
blob.setAttribute("d", SHAPES[0]);
blob.setAttribute("fill", fills[0]);
if (reduceMotion) return; // 動きを減らす設定では、最初の形のまま止めておく
const tl = gsap.timeline({ repeat: -1 });
tl.fromTo(blob, { rotation: 0, svgOrigin: "300 300" }, { rotation: 360, svgOrigin: "300 300", duration: SHAPES.length * STEP, ease: "none" }, 0);
SHAPES.forEach((_, i) => {
const next = (i + 1) % SHAPES.length;
tl.to(blob, { morphSVG: SHAPES[next], fill: fills[next], duration: STEP * 0.6, ease: "sine.inOut" }, i * STEP + STEP * 0.4);
});
});