動く幾何学パターン

canvas で三角格子を描き、サイン波に沿って各点を明滅させる動的背景。テック系のLPやダッシュボードの背景に使えます。

#canvas#javascript#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk:動く三角格子を背景にした稼働ステータス画面 -->
<section class="fg-stage">
  <!-- ★主役:canvas で三角格子を明滅させる動的背景 -->
  <canvas class="fg-canvas" id="fgCanvas" aria-hidden="true"></canvas>

  <div class="fg-inner">
    <header class="fg-head">
      <span class="fg-badge"><i class="fg-pulse"></i>すべて正常稼働中</span>
      <h1 class="fg-title">システムステータス</h1>
    </header>

    <div class="fg-grid">
      <div class="fg-card">
        <span class="fg-card__label">API応答</span>
        <b class="fg-card__val">142<small>ms</small></b>
        <span class="fg-card__ok">▲ 良好</span>
      </div>
      <div class="fg-card">
        <span class="fg-card__label">稼働率(30日)</span>
        <b class="fg-card__val">99.98<small>%</small></b>
        <span class="fg-card__ok">▲ 安定</span>
      </div>
      <div class="fg-card">
        <span class="fg-card__label">処理中ジョブ</span>
        <b class="fg-card__val">3,402</b>
        <span class="fg-card__ok">▲ 順調</span>
      </div>
    </div>
  </div>
</section>
CSS
/* FlowDesk:紺地に動く三角格子。テック系ステータスダッシュボード */
* { box-sizing: border-box; margin: 0; padding: 0; }

.fg-stage {
  position: relative;
  min-height: 400px;
  height: 400px;
  overflow: hidden;
  background: radial-gradient(120% 120% at 50% -10%, #16284f 0%, #0f1b34 60%, #0a1226 100%);
  color: #fff;
  font-family: "Segoe UI", "Hiragino Sans", system-ui, sans-serif;
}

/* ★主役:明滅する三角格子を描く canvas */
.fg-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}

.fg-inner {
  position: relative;
  z-index: 1;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 34px;
}

.fg-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  align-self: flex-start;
  font-size: 12px;
  font-weight: 700;
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(79,124,255,0.16);
  border: 1px solid rgba(79,124,255,0.4);
  color: #aec4ff;
}
.fg-pulse {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #4ade80;
  box-shadow: 0 0 0 0 rgba(74,222,128,0.6);
  animation: fgPulse 1.8s ease-out infinite;
}
@keyframes fgPulse {
  0%   { box-shadow: 0 0 0 0 rgba(74,222,128,0.6); }
  100% { box-shadow: 0 0 0 8px rgba(74,222,128,0); }
}

.fg-title {
  margin-top: 14px;
  font-size: 30px;
  font-weight: 800;
  letter-spacing: 0.01em;
  text-shadow: 0 4px 22px rgba(0,0,0,0.5);
}

.fg-grid {
  margin-top: 22px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}
.fg-card {
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.14);
  border-radius: 14px;
  padding: 16px;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.fg-card__label { display: block; font-size: 11px; color: rgba(255,255,255,0.65); }
.fg-card__val {
  display: block;
  margin: 8px 0 6px;
  font-size: 28px;
  font-weight: 800;
  color: #fff;
}
.fg-card__val small { font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.6); margin-left: 2px; }
.fg-card__ok { font-size: 11px; font-weight: 700; color: #6ff0a8; }

@media (prefers-reduced-motion: reduce) {
  .fg-pulse { animation: none; }
}
JavaScript
// canvasに三角格子を描き、サイン波で各点を明滅させるテック背景
(() => {
  const canvas = document.getElementById("fgCanvas");
  if (!canvas) return; // null安全
  const ctx = canvas.getContext("2d");
  if (!ctx) return;

  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  let w = 0, h = 0, dpr = 1;
  const gap = 46; // 格子の間隔
  let cols = [], rows = 0;

  // サイズ調整(HiDPI対応)
  const resize = () => {
    dpr = Math.min(window.devicePixelRatio || 1, 2);
    w = canvas.clientWidth;
    h = canvas.clientHeight;
    canvas.width = Math.floor(w * dpr);
    canvas.height = Math.floor(h * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    rows = Math.ceil(h / gap) + 1;
    cols = Math.ceil(w / gap) + 1;
  };
  resize();
  window.addEventListener("resize", resize);

  // 1フレーム描画
  const draw = (t) => {
    const s = t / 1000;
    ctx.clearRect(0, 0, w, h);

    // 三角格子のライン
    ctx.lineWidth = 1;
    for (let r = 0; r < rows; r++) {
      const offset = (r % 2) * (gap / 2); // 行ごとに半マスずらし三角に
      const y = r * gap;
      for (let c = 0; c < cols; c++) {
        const x = c * gap + offset;
        // 明滅の明るさ(位置と時間で揺らす)
        const bright = (Math.sin(s * 1.4 + (x + y) * 0.012) + 1) / 2;
        const a = 0.06 + bright * 0.22;
        // 右と右下へ線を引く
        ctx.strokeStyle = `rgba(120,160,255,${a})`;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x + gap, y);
        ctx.moveTo(x, y);
        ctx.lineTo(x + gap / 2, y + gap);
        ctx.stroke();
        // 交点のドット(強く明滅)
        ctx.fillStyle = `rgba(160,190,255,${0.1 + bright * 0.5})`;
        ctx.beginPath();
        ctx.arc(x, y, 1.4, 0, Math.PI * 2);
        ctx.fill();
      }
    }
  };

  let raf = 0;
  const loop = (t) => { draw(t); raf = requestAnimationFrame(loop); };

  if (reduce) {
    draw(0); // 静止1枚
  } else {
    raf = requestAnimationFrame(loop);
    document.addEventListener("visibilitychange", () => {
      cancelAnimationFrame(raf);
      if (!document.hidden) raf = requestAnimationFrame(loop);
    });
  }
})();

コード

HTML
<!-- 動く幾何学パターン: canvas で三角格子を波のように動かす -->
<div class="geo-stage">
  <canvas id="geoCanvas" class="geo-canvas"></canvas>
  <div class="geo-overlay">
    <h1 class="geo-title">Geometric Motion</h1>
    <p class="geo-sub">canvas で描く三角格子が、サイン波に沿って明滅。テック系LPの背景に。</p>
  </div>
</div>
CSS
/* canvas を全面に敷き、その上にテキストを乗せる */
* { box-sizing: border-box; margin: 0; padding: 0; }

.geo-stage {
  position: relative;
  min-height: 360px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: #070b1c;
  font-family: "Segoe UI", "Hiragino Sans", system-ui, sans-serif;
}

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

.geo-overlay {
  position: relative;
  z-index: 2;
  text-align: center;
  color: #fff;
  padding: 0 24px;
  max-width: 500px;
  /* 中央を少し暗くして可読性を確保 */
}

.geo-overlay::before {
  content: "";
  position: absolute;
  inset: -40px -60px;
  background: radial-gradient(closest-side, rgba(7, 11, 28, 0.7), transparent);
  z-index: -1;
}

.geo-title {
  font-size: 42px;
  font-weight: 800;
  letter-spacing: 0.02em;
  background: linear-gradient(90deg, #5ef2ff, #7b88ff, #c479ff);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.geo-sub {
  margin-top: 14px;
  font-size: 14px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.82);
}
JavaScript
// 三角格子を canvas に描き、各頂点の明度をサイン波で動かす
(() => {
  const canvas = document.getElementById("geoCanvas");
  if (!canvas || !canvas.getContext) return; // null安全
  const ctx = canvas.getContext("2d");

  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  let dpr = Math.min(window.devicePixelRatio || 1, 2);
  let W = 0, H = 0;
  const GAP = 46; // 格子間隔(px)

  // リサイズ対応: 親要素サイズに合わせる
  const resize = () => {
    dpr = Math.min(window.devicePixelRatio || 1, 2); // モニタ移動にも追従
    const r = canvas.getBoundingClientRect();
    W = Math.max(1, r.width);
    H = Math.max(1, r.height);
    canvas.width = Math.floor(W * dpr);
    canvas.height = Math.floor(H * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  };
  resize();
  window.addEventListener("resize", resize);

  // 1フレーム描画
  const draw = (t) => {
    const s = t / 1000;
    ctx.clearRect(0, 0, W, H);

    const cols = Math.ceil(W / GAP) + 2;
    const rows = Math.ceil(H / GAP) + 2;

    for (let y = 0; y < rows; y++) {
      for (let x = 0; x < cols; x++) {
        // 行ごとに半マスずらして三角格子に
        const px = x * GAP + (y % 2 ? GAP / 2 : 0);
        const py = y * GAP;
        // 距離と時間で明滅
        const d = (px + py) * 0.012;
        const wave = (Math.sin(d - s * 1.4) + 1) / 2;
        const radius = 1.4 + wave * 2.6;
        const alpha = 0.12 + wave * 0.55;

        // 色相を位置でずらす
        const hue = 190 + (px * 0.05 + py * 0.08) % 90;
        ctx.beginPath();
        ctx.arc(px, py, radius, 0, Math.PI * 2);
        ctx.fillStyle = `hsla(${hue}, 90%, 68%, ${alpha})`;
        ctx.fill();

        // 右隣へ細い線
        if (x < cols - 1 && wave > 0.4) {
          ctx.beginPath();
          ctx.moveTo(px, py);
          ctx.lineTo(px + GAP, py);
          ctx.strokeStyle = `hsla(${hue}, 90%, 70%, ${alpha * 0.25})`;
          ctx.lineWidth = 1;
          ctx.stroke();
        }
      }
    }
  };

  let raf = 0;
  if (reduce) {
    draw(0); // 静止画として1回だけ
  } else {
    const loop = (t) => { draw(t); raf = requestAnimationFrame(loop); };
    raf = requestAnimationFrame(loop);
    document.addEventListener("visibilitychange", () => {
      cancelAnimationFrame(raf); // 二重ループ防止
      if (!document.hidden) raf = requestAnimationFrame(loop);
    });
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「動く幾何学パターン」の効果を追加してください。

# 追加してほしい効果
動く幾何学パターン(背景 & グラデーション)
canvas で三角格子を描き、サイン波に沿って各点を明滅させる動的背景。テック系のLPやダッシュボードの背景に使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 動く幾何学パターン: canvas で三角格子を波のように動かす -->
<div class="geo-stage">
  <canvas id="geoCanvas" class="geo-canvas"></canvas>
  <div class="geo-overlay">
    <h1 class="geo-title">Geometric Motion</h1>
    <p class="geo-sub">canvas で描く三角格子が、サイン波に沿って明滅。テック系LPの背景に。</p>
  </div>
</div>

【CSS】
/* canvas を全面に敷き、その上にテキストを乗せる */
* { box-sizing: border-box; margin: 0; padding: 0; }

.geo-stage {
  position: relative;
  min-height: 360px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background: #070b1c;
  font-family: "Segoe UI", "Hiragino Sans", system-ui, sans-serif;
}

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

.geo-overlay {
  position: relative;
  z-index: 2;
  text-align: center;
  color: #fff;
  padding: 0 24px;
  max-width: 500px;
  /* 中央を少し暗くして可読性を確保 */
}

.geo-overlay::before {
  content: "";
  position: absolute;
  inset: -40px -60px;
  background: radial-gradient(closest-side, rgba(7, 11, 28, 0.7), transparent);
  z-index: -1;
}

.geo-title {
  font-size: 42px;
  font-weight: 800;
  letter-spacing: 0.02em;
  background: linear-gradient(90deg, #5ef2ff, #7b88ff, #c479ff);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.geo-sub {
  margin-top: 14px;
  font-size: 14px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.82);
}

【JavaScript】
// 三角格子を canvas に描き、各頂点の明度をサイン波で動かす
(() => {
  const canvas = document.getElementById("geoCanvas");
  if (!canvas || !canvas.getContext) return; // null安全
  const ctx = canvas.getContext("2d");

  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  let dpr = Math.min(window.devicePixelRatio || 1, 2);
  let W = 0, H = 0;
  const GAP = 46; // 格子間隔(px)

  // リサイズ対応: 親要素サイズに合わせる
  const resize = () => {
    dpr = Math.min(window.devicePixelRatio || 1, 2); // モニタ移動にも追従
    const r = canvas.getBoundingClientRect();
    W = Math.max(1, r.width);
    H = Math.max(1, r.height);
    canvas.width = Math.floor(W * dpr);
    canvas.height = Math.floor(H * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  };
  resize();
  window.addEventListener("resize", resize);

  // 1フレーム描画
  const draw = (t) => {
    const s = t / 1000;
    ctx.clearRect(0, 0, W, H);

    const cols = Math.ceil(W / GAP) + 2;
    const rows = Math.ceil(H / GAP) + 2;

    for (let y = 0; y < rows; y++) {
      for (let x = 0; x < cols; x++) {
        // 行ごとに半マスずらして三角格子に
        const px = x * GAP + (y % 2 ? GAP / 2 : 0);
        const py = y * GAP;
        // 距離と時間で明滅
        const d = (px + py) * 0.012;
        const wave = (Math.sin(d - s * 1.4) + 1) / 2;
        const radius = 1.4 + wave * 2.6;
        const alpha = 0.12 + wave * 0.55;

        // 色相を位置でずらす
        const hue = 190 + (px * 0.05 + py * 0.08) % 90;
        ctx.beginPath();
        ctx.arc(px, py, radius, 0, Math.PI * 2);
        ctx.fillStyle = `hsla(${hue}, 90%, 68%, ${alpha})`;
        ctx.fill();

        // 右隣へ細い線
        if (x < cols - 1 && wave > 0.4) {
          ctx.beginPath();
          ctx.moveTo(px, py);
          ctx.lineTo(px + GAP, py);
          ctx.strokeStyle = `hsla(${hue}, 90%, 70%, ${alpha * 0.25})`;
          ctx.lineWidth = 1;
          ctx.stroke();
        }
      }
    }
  };

  let raf = 0;
  if (reduce) {
    draw(0); // 静止画として1回だけ
  } else {
    const loop = (t) => { draw(t); raf = requestAnimationFrame(loop); };
    raf = requestAnimationFrame(loop);
    document.addEventListener("visibilitychange", () => {
      cancelAnimationFrame(raf); // 二重ループ防止
      if (!document.hidden) raf = requestAnimationFrame(loop);
    });
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。