本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ヒーロー

ヒーローの背景で丸・三角・四角が浮かんでゆっくり回る

デモ

仕様

用途
ヒーロー
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

丸・三角・四角の小さな図形を並べ、それぞれをゆっくり上下に揺らしながら回しています。三角形は 120 度、四角形は 90 度回すと元の見た目にちょうど重なるので、その角度の倍数だけ回せば、どこで止めても形が破綻しません。

上下の揺れは図形ごとに周期や位相をずらしていて、全部がそろって動かないようにしています。影は図形の外側の要素に付けているので、内側で回転しても影の向きは変わりません。

位置や大きさは、画面の高さに対する比率で指定しています。こうしておくと、画面の大きさが変わっても計算をやり直す必要がありません。動きを減らす設定のときは、揺れも回転も止め、静かに並んだ見た目のままにしています。

図形の色や配置、揺れの大きさを変えれば、にぎやかさを自由に調整できます。明るく親しみやすい印象になるので、子ども向けサービスや教育、イベント系のヒーローに向いています。

コード

HTML
<section class="shape-hero">
  <!-- 背景は飾りなので、読み上げからは外す -->
  <div class="shape-hero__field" aria-hidden="true"></div>
  <div class="shape-hero__inner">
    <p class="shape-hero__eyebrow">Workshop</p>
    <h1 class="shape-hero__title">あそびながら、<br>まなぼう。</h1>
    <p class="shape-hero__lead">ここに説明文が入ります。</p>
    <a class="shape-hero__button" href="/workshops/">予約する</a>
  </div>
</section>

CSS
.shape-hero {
  --bg: #f2f1ed;
  --ink: #262626;
  /* 形の色。script.js はこの名前だけを参照し、値はここでだけ決める。色相より明るさを離す */
  --olive: #c8bf83;
  --rose: #b27771;
  --slate: #688297;
  --sage: #739374;
  --lavender: #877fa1;
  --deep: #4a5b66;
  --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);
}

.shape-hero__field {
  position: absolute;
  inset: 0;
  pointer-events: none;
  container-type: size; /* 子の cqw / cqh を、この箱の大きさから決める */
}

.shape-hero__shape {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--size);
  height: var(--size);
  /* 位置は中心を基準にし、浮遊は -1..1 の値(--sx / --sy)に振れ幅(--ax / --ay)を掛けて足す */
  transform: translate(-50%, -50%) translate(calc(var(--sx, 0) * var(--ax)), calc(var(--sy, 0) * var(--ay)));
}

/* 影は浮遊する外側に付けるので、回っても向きが変わらない */
.shape-hero__shape--fill { filter: drop-shadow(0 calc(var(--size) * 0.1) calc(var(--size) * 0.14) rgba(38, 38, 38, 0.14)); }

.shape-hero__shape svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
  transform: rotate(var(--rot, 0deg)); /* 回転は内側だけにかける */
}

.shape-hero__inner {
  position: relative; /* 背景の上に載せる */
  max-width: 34em;
}

.shape-hero__eyebrow {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.shape-hero__title {
  margin: 24px 0 0;
  font-size: clamp(40px, 8vw, 72px);
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.shape-hero__lead {
  margin: 24px 0 0;
  font-size: 18px;
  line-height: 1.8;
}

.shape-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;
}

.shape-hero__button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

JavaScript
import { gsap } from "gsap";

const SVG_NS = "http://www.w3.org/2000/svg";
const DURATION = 8; // ひと回りして同じ位置・角度に戻るまで(秒)
const SYMMETRY = { circle: 0, triangle: 120, square: 90 }; // この角度だけ回すと元の形に重なる
// x, y: 中心(画面比) / size: 大きさ(高さ比) / float: 浮遊の振れ幅(高さ比) / f: 上下の周波数
// spin: 1 ループで回る「対称角」の数(整数) / phase: 各形をずらす位相
// color: 色は CSS 変数の名前(値そのものは style.css に書く)
const SHAPES = [
  { type: "circle", x: 0.13, y: 0.26, size: 0.17, color: "--olive", fill: true, float: 0.03, f: 1, spin: 0, phase: 0 },
  { type: "triangle", x: 0.27, y: 0.74, size: 0.21, color: "--rose", fill: true, float: 0.035, f: 1, spin: 1, phase: 1.2 },
  { type: "square", x: 0.8, y: 0.27, size: 0.16, color: "--slate", fill: true, float: 0.025, f: 2, spin: -1, phase: 2 },
  { type: "circle", x: 0.86, y: 0.74, size: 0.12, color: "--sage", fill: false, float: 0.03, f: 1, spin: 0, phase: 2.8 },
  { type: "square", x: 0.5, y: 0.13, size: 0.08, color: "--lavender", fill: false, float: 0.02, f: 2, spin: 1, phase: 3.5 },
  { type: "triangle", x: 0.63, y: 0.84, size: 0.11, color: "--deep", fill: false, float: 0.025, f: 1, spin: -1, phase: 4.1 },
  { type: "circle", x: 0.4, y: 0.36, size: 0.05, color: "--slate", fill: true, float: 0.02, f: 2, spin: 0, phase: 4.8 },
  { type: "square", x: 0.07, y: 0.64, size: 0.07, color: "--lavender", fill: false, float: 0.02, f: 1, spin: 1, phase: 5.3 },
  { type: "circle", x: 0.95, y: 0.1, size: 0.06, color: "--rose", fill: true, float: 0.015, f: 2, spin: 0, phase: 0.6 },
  { type: "triangle", x: 0.21, y: 0.09, size: 0.08, color: "--sage", fill: true, float: 0.02, f: 1, spin: 1, phase: 1.9 },
  { type: "square", x: 0.66, y: 0.52, size: 0.07, color: "--olive", fill: true, float: 0.025, f: 1, spin: -1, phase: 3 },
  { type: "circle", x: 0.73, y: 0.4, size: 0.035, color: "--deep", fill: false, float: 0.02, f: 2, spin: 0, phase: 5.9 },
];
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

// 100 × 100 の座標で形を描く。三角は重心を (50, 50) に置くので、回しても対称に重なる
function shapeSvg({ type, color, fill }) {
  const svg = document.createElementNS(SVG_NS, "svg");
  svg.setAttribute("viewBox", "0 0 100 100");
  const node = document.createElementNS(SVG_NS, type === "circle" ? "circle" : type === "square" ? "rect" : "polygon");
  const geometry = {
    circle: { cx: 50, cy: 50, r: 46 },
    square: { x: 9, y: 9, width: 82, height: 82, rx: 14 },
    triangle: { points: "50,4 89.84,73 10.16,73" },
  }[type];
  const width = fill ? 8 : 4;
  node.style.cssText = `stroke-width:${width}px;stroke-linejoin:round;fill:${fill ? `var(${color})` : "none"};stroke:var(${color});`;
  if (!fill) node.setAttribute("vector-effect", "non-scaling-stroke");
  for (const [key, value] of Object.entries(geometry)) node.setAttribute(key, String(value));
  svg.append(node);
  return svg;
}

document.querySelectorAll(".shape-hero__field").forEach((field) => {
  const nodes = SHAPES.map((shape) => {
    const node = document.createElement("div");
    node.className = `shape-hero__shape${shape.fill ? " shape-hero__shape--fill" : ""}`;
    // 位置・大きさ・振れ幅は cqw / cqh にしておくと、画面の大きさが変わっても計算し直さずに済む
    node.style.cssText = `--x:${shape.x * 100}cqw;--y:${shape.y * 100}cqh;--size:${shape.size * 100}cqh;--ax:${shape.float * 40}cqh;--ay:${shape.float * 100}cqh;`;
    node.append(shapeSvg(shape));
    field.append(node);
    return node;
  });

  if (reduceMotion) return; // 揺れ・回転(--sx / --sy / --rot)を書かなければ、静かに並んだまま

  gsap.ticker.add((time) => {
    const angle = (time / DURATION) * Math.PI * 2;
    SHAPES.forEach((shape, i) => {
      const rot = shape.spin * SYMMETRY[shape.type] * (time / DURATION) + 8 * Math.sin(angle + shape.phase);
      nodes[i].style.setProperty("--sx", Math.sin(angle + shape.phase * 1.7).toFixed(4));
      nodes[i].style.setProperty("--sy", Math.sin(angle * shape.f + shape.phase).toFixed(4));
      nodes[i].style.setProperty("--rot", `${rot.toFixed(2)}deg`);
    });
  });
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。