デモ
デモを読み込めませんでした。
仕様
- 用途
- ヒーロー
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
SVG のトルシェ・タイル(四分円の弧を 2 本持つタイル)を格子状に敷き詰め、各タイルの transform 属性の rotate を t から直接書き換えて回します。弧は辺の中点で隣のタイルの弧とつながるので、どの向きの組み合わせでも途切れない線の迷路になり、回っている途中だけ線が切れます。
1 ループのあいだに 90° の波を 2 回通します。1 回目は左上から斜めに、2 回目は右側の一点から輪になって広がり、合計 180° 回ると元の模様に戻ります(このタイルは 180° 回すと元と同じ形になるため)。back.inOut の ease で少し行き過ぎてから止まるので、タイルが弾んで見えます。
タイルの大きさと位置は整数 px にそろえます。端数の座標にすると、回転のたびに縁の画素がちらつきます。GSAP の stagger に grid を渡すと、格子上の位置から波の順番を自動で計算してくれます。
つまみはタイルの大きさ・線の太さ・波が始まる時刻と広がる速さ・波の起点です。地と線の色を差し替えれば、ブランドやイベントごとに配色を変えられます。ヒーローの背景の模様や、スタジオ・イベントのジェネラティブなアイデンティティに向いています。
コード
<section class="tile-hero">
<!-- 背景は飾りなので、読み上げからは外す -->
<svg class="tile-hero__pattern" viewBox="0 0 960 540" preserveAspectRatio="xMidYMid slice" aria-hidden="true"></svg>
<div class="tile-hero__inner">
<p class="tile-hero__eyebrow">DESIGN STUDIO</p>
<h1 class="tile-hero__title">動き続ける模様を、<br>ブランドの顔に。</h1>
<p class="tile-hero__lead">ここに説明文が入ります。</p>
<a class="tile-hero__button" href="/work/">システムを見る</a>
</div>
</section>
.tile-hero {
--bg: #688297; /* 模様の地(スレート) */
--line: #e6dccb; /* 模様の線(淡いグレージュ) */
--ink: #ffffff;
--on-ink: #111111; /* 白い主ボタンの文字 */
--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);
}
.tile-hero__pattern {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
}
/* 模様の上・文字の下の幕。文字のある左ほど濃くする */
.tile-hero::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgb(0 0 0 / 0.6) 0%, rgb(0 0 0 / 0.3) 100%);
}
.tile-hero__inner {
position: relative;
max-width: 34em;
}
.tile-hero__eyebrow {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.tile-hero__title {
margin: 24px 0 0;
font-size: clamp(32px, 8vw, 60px);
line-height: 1.05;
letter-spacing: -0.02em;
}
.tile-hero__lead {
margin: 24px 0 0;
font-size: 18px;
line-height: 1.8;
}
.tile-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(--on-ink);
font-size: 15px;
font-weight: 600;
letter-spacing: 0.04em;
text-decoration: none;
}
.tile-hero__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
import { gsap } from "gsap";
const TILE = 60; // タイルの大きさ(px)
const TURN = 0.9; // 1 枚が 90° 回る時間(秒)
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".tile-hero__pattern").forEach((svg) => {
const hero = svg.closest(".tile-hero");
const line = getComputedStyle(hero).getPropertyValue("--line").trim();
const [, , W, H] = svg.getAttribute("viewBox").split(" ").map(Number);
const cols = Math.ceil(W / TILE);
const rows = Math.ceil(H / TILE);
const h = TILE / 2;
const tiles = [];
for (let j = 0; j < rows; j++) {
for (let i = 0; i < cols; i++) {
const x = i * TILE;
const y = j * TILE;
const path = document.createElementNS(svg.namespaceURI, "path");
// 左上と右下の角を中心にした四分円。どちらも辺の中点で終わるので隣のタイルの弧とつながる
path.setAttribute("d", `M${x + h} ${y}A${h} ${h} 0 0 1 ${x} ${y + h}M${x + h} ${y + TILE}A${h} ${h} 0 0 1 ${x + TILE} ${y + h}`);
path.setAttribute("fill", "none");
path.setAttribute("stroke", line);
path.setAttribute("stroke-width", String(Math.round(TILE * 0.3)));
path.dataset.center = `${x + h} ${y + h}`;
if (Math.random() < 0.5) path.setAttribute("transform", `rotate(90 ${x + h} ${y + h})`);
svg.append(path);
tiles.push(path);
}
}
if (reduceMotion) return; // 動きを減らす設定では、組み上がった模様のまま止めておく
gsap
.timeline({ repeat: -1, repeatDelay: 0.8 })
.to(tiles, {
rotation: "+=90",
svgOrigin: (i, el) => el.dataset.center,
duration: TURN,
ease: "back.inOut(1.4)",
stagger: { grid: [rows, cols], from: "start", amount: 2.2 },
})
.to(
tiles,
{
rotation: "+=90",
svgOrigin: (i, el) => el.dataset.center,
duration: TURN,
ease: "back.inOut(1.4)",
stagger: { grid: [rows, cols], from: [0.74, 0.5], amount: 2.4 },
},
"+=0.9"
);
});