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

ヒーローの背景で同心円のリングと点が別々の速さで回る

デモ

仕様

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

しくみと調整

同心円のリングと点を、SVGのグループ要素ごと rotate で回すだけの背景です。ライブラリは使わず、リングと点、点の後ろに引く尾を1つのグループにまとめ、角度をそのまま属性に指定して回しています。

リングごとに半径・周期・向きを変え、1周期でそろって出発点に戻るよう、周る回数をすべて整数にしています。破線は円周を分割数で割った長さにしているので、何周しても継ぎ目が出ません。尾は長さの違う弧を重ねて、点に近いほど濃くなるようにしています。

コンテナの大きさが変わるたびに viewBox を測り直して、リングと点の位置を作り直します。動きを減らす設定では、回転を止めて1コマだけを見せます。

半径や周期、色を変えるだけで、軌道の込み具合や速さの印象を変えられます。宇宙・通信・グローバル展開など、広がりを感じさせたいヒーローの背景に向いています。

コード

HTML
<section class="orbit-hero">
  <!-- リングは飾りなので、JS が組み立てて読み上げから外す -->
  <svg class="orbit-hero__rings" aria-hidden="true"></svg>
  <div class="orbit-hero__inner">
    <p class="orbit-hero__eyebrow">NETWORK</p>
    <h1 class="orbit-hero__title">いつでも、<br>つながる。</h1>
    <p class="orbit-hero__lead">ここに説明文が入ります。</p>
    <a class="orbit-hero__button" href="/network/">ネットワークを見る</a>
  </div>
</section>

CSS
.orbit-hero {
  --paper: #ffffff;
  --ink: #111111;
  --ring: #ffffff; /* リングと点の色 */
  --accent: #d2b999; /* 強調する点の色。グレージュ */
  --gutter: clamp(24px, 7vw, 96px);
  position: relative;
  display: grid;
  align-items: center;
  min-height: min(90vh, 640px);
  padding: 0 var(--gutter);
  overflow: hidden;
  color: var(--paper);
  background: radial-gradient(circle at 62% 52%, #5a6f80 0%, #4a5b66 45%, #2e3940 100%);
}

.orbit-hero__rings {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.orbit-hero__inner {
  position: relative; /* リングの上に載せる */
  max-width: 28em;
}

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

.orbit-hero__title {
  margin: 24px 0 0;
  font-size: clamp(36px, 7vw, 60px);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

.orbit-hero__lead {
  margin: 24px 0 0;
  font-size: 18px;
  line-height: 1.8;
  word-break: auto-phrase; /* 語の途中で折り返さない */
}

.orbit-hero__button {
  display: inline-grid;
  place-items: center;
  height: 48px;
  margin-top: 32px;
  padding: 0 32px;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-decoration: none;
}

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

JavaScript
import { gsap } from "gsap";

const SVG_NS = "http://www.w3.org/2000/svg";
const DURATION = 24; // 全部のリングがそろって出発点に戻るまでの秒数
const CENTER = [0.62, 0.52]; // 回転の中心(コンテナに対する比)
const TAIL = 42; // 点の後ろに引く尾の長さ(度)
// radius: 高さに対する半径 / laps: 1 周期で回る周数(整数。負は反時計回り)/ width: 線の太さ(px)
// dash: [円周の分割数, 線の割合](null は実線)/ dots: [角度(度), 半径(高さ比), "fill" | "ring" | "accent"]
const RINGS = [
  { radius: 0.1, laps: 4, width: 1.5, opacity: 0.85, dash: null, dots: [[0, 0.011, "fill"]] },
  { radius: 0.18, laps: -3, width: 1, opacity: 0.5, dash: [48, 0.45], dots: [[140, 0.009, "fill"], [320, 0.014, "ring"]] },
  { radius: 0.27, laps: 2, width: 1.5, opacity: 0.7, dash: null, dots: [[60, 0.018, "accent"]] },
  { radius: 0.36, laps: -1, width: 7, opacity: 0.3, dash: [120, 0.12], dots: [] },
  { radius: 0.46, laps: 1, width: 1.2, opacity: 0.6, dash: null, dots: [[200, 0.012, "fill"], [20, 0.022, "ring"]] },
  { radius: 0.58, laps: -1, width: 1, opacity: 0.38, dash: [72, 0.6], dots: [[300, 0.014, "fill"]] },
  { radius: 0.72, laps: 1, width: 1, opacity: 0.3, dash: null, dots: [[110, 0.02, "accent"], [250, 0.009, "fill"]] },
  { radius: 0.9, laps: -1, width: 1, opacity: 0.2, dash: [120, 0.3], dots: [] },
];
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

function el(name, attrs) {
  const node = document.createElementNS(SVG_NS, name);
  for (const key in attrs) node.setAttribute(key, attrs[key]);
  return node;
}

document.querySelectorAll(".orbit-hero__rings").forEach((svg, index) => {
  // リングと点の色は CSS 変数から読む
  const style = getComputedStyle(svg);
  const ink = style.getPropertyValue("--ring").trim();
  const accent = style.getPropertyValue("--accent").trim();
  let cx, cy, groups, halo;

  // リング 1 本と、その上の点・尾をひとつの <g> にまとめ、<g> ごと回す
  function layout() {
    const w = svg.clientWidth;
    const h = svg.clientHeight;
    svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
    svg.textContent = "";
    cx = w * CENTER[0];
    cy = h * CENTER[1];

    groups = RINGS.map((ring) => {
      const r = ring.radius * h;
      const circumference = 2 * Math.PI * r;
      const group = el("g", {});
      const line = el("circle", { cx, cy, r, fill: "none", stroke: ink, "stroke-width": ring.width, "stroke-opacity": ring.opacity });
      if (ring.dash) {
        const period = circumference / ring.dash[0];
        line.setAttribute("stroke-dasharray", `${period * ring.dash[1]} ${period * (1 - ring.dash[1])}`);
      }
      group.append(line);

      for (const [angle, size, style] of ring.dots) {
        const color = style === "accent" ? accent : ink;
        if (style !== "ring") {
          // 尾: 長さの違う弧を 4 本重ね、点に近いほど濃くする
          for (let k = 1; k <= 4; k++) {
            const span = TAIL * (k / 4);
            const start = ring.laps > 0 ? angle - span : angle;
            const dash = `${(span / 360) * circumference} ${circumference}`;
            group.append(el("circle", { cx, cy, r, fill: "none", stroke: color, "stroke-width": 2, "stroke-opacity": 0.12, "stroke-dasharray": dash, transform: `rotate(${start} ${cx} ${cy})` }));
          }
        }
        const x = cx + r * Math.cos((angle * Math.PI) / 180);
        const y = cy + r * Math.sin((angle * Math.PI) / 180);
        if (style === "accent") group.append(el("circle", { cx: x, cy: y, r: size * h * 2.4, fill: accent, "fill-opacity": 0.22 }));
        group.append(
          style === "ring"
            ? el("circle", { cx: x, cy: y, r: size * h, fill: "none", stroke: ink, "stroke-width": 1.5 })
            : el("circle", { cx: x, cy: y, r: size * h, fill: color })
        );
      }
      svg.append(group);
      return { node: group, laps: ring.laps };
    });

    // 中心の光は、中心ほど濃い放射グラデーション(id は svg ごとに変えて、同じページに 2 つ置いても混ざらない)
    const glow = el("radialGradient", { id: `orbit-halo-${index}` });
    glow.append(el("stop", { offset: 0, "stop-color": ink, "stop-opacity": 0.55 }), el("stop", { offset: 1, "stop-color": ink, "stop-opacity": 0 }));
    halo = el("circle", { cx, cy, r: h * 0.12, fill: `url(#orbit-halo-${index})` });
    svg.append(glow);
    svg.append(halo, el("circle", { cx, cy, r: h * 0.028, fill: ink }));
    spin(reduceMotion ? 0 : gsap.ticker.time % DURATION);
  }

  function spin(t) {
    const turn = (t / DURATION) * 360;
    for (const { node, laps } of groups) node.setAttribute("transform", `rotate(${(laps * turn) % 360} ${cx} ${cy})`);
    halo.setAttribute("opacity", 0.45 + 0.35 * (0.5 + 0.5 * Math.sin((t / DURATION) * Math.PI * 2 * 6)));
  }

  new ResizeObserver(layout).observe(svg);
  layout();

  // 動きを減らす設定では、リングを止めて 1 コマだけ見せる
  if (reduceMotion) return;
  gsap.ticker.add((time) => spin(time % DURATION));
});

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

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