デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
SVG の <path> を t から毎回組み立て直します。ライブラリは使わず、ブラウザの描画のたびに呼ばれる関数で位相を進めるだけで動きます。周波数の違う 2 つの波を重ね、線ごとに位相をわずかにずらすと、束になって揺れながらうねって見えます。
時間の係数(1.5 と 3.2)を整数の比に保つと、ひと巻きで元の形に戻りループがつながります。線の色は、ローズから墨のスレートへ、線ごとに少しずつ移り変えています。色相だけでなく明るさも一緒に移すので、手前と奥の線が重なっても見分けがつきます。
折れ点の数を増やすほど線はなめらかになりますが、増やしすぎると毎フレームの計算が重くなります。線の本数と振れ幅は、置く場所の広さに合わせて調整してください。
つまみは本数・色・振れ幅・折れ点の数です。ヒーローの背景や、セクションの区切りにさりげない揺らぎを添えたいときに向いています。
コード
<section class="wave-hero">
<!-- 背景は飾りなので、読み上げからは外す -->
<svg class="wave-hero__lines" viewBox="0 0 1280 720" preserveAspectRatio="none" aria-hidden="true"></svg>
<div class="wave-hero__inner">
<p class="wave-hero__eyebrow">CREATIVE STUDIO</p>
<h1 class="wave-hero__title">伝えたいことに、<br>動きを添える。</h1>
<p class="wave-hero__lead">ここに説明文が入ります。</p>
<a class="wave-hero__button" href="/contact/">相談する</a>
</div>
</section>
.wave-hero {
--bg: #f2f1ed;
--ink: #262626;
--line-from: #c26a61; /* 最初の線の色(ローズ) */
--line-to: #3d5e73; /* 最後の線の色(墨のスレート) */
--gutter: clamp(24px, 7vw, 96px);
position: relative;
display: grid;
align-items: center;
min-height: min(90vh, 720px);
padding: 0 var(--gutter);
overflow: hidden;
color: var(--ink);
background-color: var(--bg);
}
.wave-hero__lines {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
}
.wave-hero__inner {
position: relative;
max-width: 34em;
}
.wave-hero__eyebrow {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.wave-hero__title {
margin: 24px 0 0;
font-size: clamp(32px, 8vw, 72px);
line-height: 1.05;
letter-spacing: -0.02em;
}
.wave-hero__lead {
margin: 24px 0 0;
font-size: 18px;
line-height: 1.8;
}
.wave-hero__button {
display: inline-grid;
place-items: center;
height: 48px;
margin-top: 32px;
padding: 0 32px;
border-radius: 999px;
background-color: var(--ink);
color: var(--bg);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-decoration: none;
}
.wave-hero__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
const DURATION = 5.4; // ひと巻きで元の模様に戻るまで(秒)
const LINES = 9;
const POINTS = 80; // 1 本あたりの折れ点。多いほど滑らか
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 線ごとに、CSS の --line-from から --line-to へ色を移す
function lineColor(i) {
return `color-mix(in srgb, var(--line-from), var(--line-to) ${((i / (LINES - 1)) * 100).toFixed(1)}%)`;
}
document.querySelectorAll(".wave-hero__lines").forEach((svg) => {
const [, , W, H] = svg.getAttribute("viewBox").split(" ").map(Number);
const paths = Array.from({ length: LINES }, (_, i) => {
const path = document.createElementNS(svg.namespaceURI, "path");
path.setAttribute("fill", "none");
path.style.stroke = lineColor(i);
path.setAttribute("stroke-width", "2.5");
path.setAttribute("stroke-linecap", "round");
svg.append(path);
return path;
});
const draw = (phase) => {
paths.forEach((path, i) => {
const base = H * 0.5 + (i - (LINES - 1) / 2) * 18;
let d = "";
for (let p = 0; p <= POINTS; p++) {
const x = (p / POINTS) * W;
const u = x / W;
// 周波数の違う 2 つの波を重ねる。時間の係数を整数にするとループがつながる
const y =
base +
Math.sin(u * Math.PI * 2 * 1.5 + phase + i * 0.35) * H * 0.12 +
Math.sin(u * Math.PI * 2 * 3.2 - phase * 2 + i * 0.6) * H * 0.03;
d += `${p === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`;
}
path.setAttribute("d", d);
});
};
if (reduceMotion) {
draw(0); // 動きを減らす設定では、1 枚だけ描いて止めておく
return;
}
const tick = (ms) => {
draw(((ms / 1000 / DURATION) * Math.PI * 2) % (Math.PI * 2));
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});