パーティクル接続(星座)

近い粒子同士を線で結び星座のように見せる定番表現。マウス付近の粒子も結線します。ヒーロー背景やローディング演出に最適です。

#canvas#particles#animation#interactive

ライブデモ

使用例(お題: SaaS FlowDesk)

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

HTML
<!-- FlowDesk:星座パーティクル背景のSaaSヒーロー -->
<section class="fd-hero">
  <!-- 主役:接続パーティクル背景 -->
  <canvas class="fd-hero__bg" id="fdConstellation"></canvas>

  <!-- 前景UI:ナビ+ヒーローコピー+CTA -->
  <header class="fd-nav">
    <span class="fd-nav__logo"><i></i>FlowDesk</span>
    <nav class="fd-nav__menu">
      <a href="#">機能</a>
      <a href="#">料金</a>
      <a href="#">導入事例</a>
      <a class="fd-nav__cta" href="#">無料で始める</a>
    </nav>
  </header>

  <div class="fd-hero__inner">
    <span class="fd-hero__tag">TEAM WORKFLOW PLATFORM</span>
    <h1 class="fd-hero__title">チームの点と点を、<br>ひとつの流れに。</h1>
    <p class="fd-hero__lead">散らばったタスク・ファイル・会話をリアルタイムでつなぐ、次世代のコラボレーション基盤。</p>
    <div class="fd-hero__actions">
      <a class="fd-btn fd-btn--main" href="#">14日間 無料トライアル</a>
      <a class="fd-btn fd-btn--ghost" href="#">デモを見る</a>
    </div>
    <p class="fd-hero__note">クレジットカード不要・3,200社が導入</p>
  </div>
</section>
CSS
/* FlowDesk:星座背景のSaaSヒーロー */
:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  background: var(--navy);
  overflow: hidden;
}

.fd-hero {
  position: relative;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(900px 420px at 75% -10%, #1c2c52, transparent),
    linear-gradient(160deg, #0f1b34, #0a1226);
}

/* 主役:背景いっぱいの接続パーティクル */
.fd-hero__bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* ナビ */
.fd-nav {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
}
.fd-nav__logo {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 17px;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--white);
}
.fd-nav__logo i {
  width: 16px;
  height: 16px;
  border-radius: 5px;
  background: linear-gradient(135deg, var(--blue), #7aa0ff);
  box-shadow: 0 0 14px rgba(79,124,255,0.6);
}
.fd-nav__menu { display: flex; align-items: center; gap: 20px; }
.fd-nav__menu a {
  font-size: 13px;
  color: rgba(255,255,255,0.78);
  text-decoration: none;
  transition: color 0.2s ease;
}
.fd-nav__menu a:hover { color: var(--white); }
.fd-nav__cta {
  padding: 8px 16px;
  border-radius: 8px;
  background: var(--blue);
  color: var(--white) !important;
  font-weight: 700;
  box-shadow: 0 6px 18px rgba(79,124,255,0.4);
}

/* ヒーロー本文 */
.fd-hero__inner {
  position: relative;
  z-index: 2;
  padding: 34px 28px;
  max-width: 460px;
}
.fd-hero__tag {
  display: inline-block;
  font-size: 10px;
  letter-spacing: 0.28em;
  color: #8fb0ff;
  font-weight: 700;
}
.fd-hero__title {
  margin: 14px 0 14px;
  font-size: 30px;
  line-height: 1.35;
  font-weight: 800;
  color: var(--white);
}
.fd-hero__lead {
  margin: 0 0 22px;
  font-size: 13.5px;
  line-height: 1.8;
  color: rgba(255,255,255,0.78);
}
.fd-hero__actions { display: flex; gap: 12px; flex-wrap: wrap; }
.fd-btn {
  display: inline-block;
  padding: 11px 20px;
  border-radius: 9px;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  transition: transform 0.2s ease;
}
.fd-btn--main {
  background: var(--blue);
  color: var(--white);
  box-shadow: 0 10px 24px rgba(79,124,255,0.45);
}
.fd-btn--ghost {
  background: rgba(255,255,255,0.08);
  color: var(--white);
  border: 1px solid rgba(255,255,255,0.18);
}
.fd-btn:hover { transform: translateY(-2px); }
.fd-hero__note {
  margin: 16px 0 0;
  font-size: 11.5px;
  color: rgba(255,255,255,0.5);
}

@media (prefers-reduced-motion: reduce) {
  .fd-btn { transition: none; }
}
JavaScript
// FlowDesk:星座風パーティクル接続をヒーロー背景に(青系で配色)
(() => {
  const canvas = document.getElementById('fdConstellation');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  let w = 0, h = 0, particles = [], raf = 0, running = true;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const mouse = { x: -9999, y: -9999 };

  // コンテナ全体にフィット(高解像度対応)
  function resize() {
    const r = canvas.getBoundingClientRect();
    w = r.width; h = r.height;
    canvas.width = Math.max(1, w * dpr);
    canvas.height = Math.max(1, h * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // 画面サイズに応じた粒子生成
  function makeParticles() {
    const count = Math.max(36, Math.min(90, Math.floor((w * h) / 11000)));
    particles = Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      vx: (Math.random() - 0.5) * 0.35,
      vy: (Math.random() - 0.5) * 0.35,
      r: Math.random() * 1.5 + 0.6
    }));
  }

  resize();
  makeParticles();

  function step() {
    ctx.clearRect(0, 0, w, h);

    // 粒子の更新と描画(ノード)
    for (const p of particles) {
      p.x += p.vx; p.y += p.vy;
      if (p.x < 0 || p.x > w) p.vx *= -1;
      if (p.y < 0 || p.y > h) p.vy *= -1;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(150,180,255,0.85)';
      ctx.fill();
    }

    // 近い粒子同士を青い線で結ぶ
    for (let i = 0; i < particles.length; i++) {
      const a = particles[i];
      for (let j = i + 1; j < particles.length; j++) {
        const b = particles[j];
        const dist = Math.hypot(a.x - b.x, a.y - b.y);
        if (dist < 118) {
          ctx.strokeStyle = `rgba(79,124,255,${(1 - dist / 118) * 0.45})`;
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(a.x, a.y);
          ctx.lineTo(b.x, b.y);
          ctx.stroke();
        }
      }
      // マウス周辺も結線(インタラクティブ)
      const dm = Math.hypot(a.x - mouse.x, a.y - mouse.y);
      if (dm < 150) {
        ctx.strokeStyle = `rgba(122,160,255,${(1 - dm / 150) * 0.6})`;
        ctx.beginPath();
        ctx.moveTo(a.x, a.y);
        ctx.lineTo(mouse.x, mouse.y);
        ctx.stroke();
      }
    }
    raf = requestAnimationFrame(step);
  }

  // ループ制御(多重起動防止)
  function start() {
    if (running) return;
    running = true;
    raf = requestAnimationFrame(step);
  }
  function stop() {
    running = false;
    cancelAnimationFrame(raf);
  }

  // マウス座標(キャンバス基準)
  canvas.addEventListener('pointermove', (e) => {
    const r = canvas.getBoundingClientRect();
    mouse.x = e.clientX - r.left;
    mouse.y = e.clientY - r.top;
  });
  canvas.addEventListener('pointerleave', () => { mouse.x = mouse.y = -9999; });

  window.addEventListener('resize', () => { resize(); makeParticles(); });
  // タブ非表示で停止、復帰で再開
  document.addEventListener('visibilitychange', () => {
    document.hidden ? stop() : start();
  });

  running = false;
  start();
})();

コード

HTML
<!-- パーティクル接続(星座) -->
<div class="stage">
  <canvas id="constellationCanvas"></canvas>
  <div class="label">Constellation</div>
</div>
CSS
/* ステージ全体(背景は深い夜空のグラデ) */
:root {
  --bg-1: #0a0e27;
  --bg-2: #1a1f4a;
  --dot: 200, 220, 255;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: var(--bg-1);
}
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  background:
    radial-gradient(1200px 500px at 70% -10%, var(--bg-2), transparent),
    linear-gradient(160deg, var(--bg-1), #05071a);
}
#constellationCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
/* 右下の控えめなラベル */
.label {
  position: absolute;
  right: 16px;
  bottom: 12px;
  color: rgba(var(--dot), .55);
  font-size: 12px;
  letter-spacing: .35em;
  text-transform: uppercase;
  pointer-events: none;
}
JavaScript
// 星座風パーティクル接続デモ
(() => {
  const canvas = document.getElementById('constellationCanvas');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let w = 0, h = 0, dpr = Math.min(window.devicePixelRatio || 1, 2);
  const mouse = { x: -9999, y: -9999 };

  // キャンバスを親要素に合わせてリサイズ(高解像度対応)
  function resize() {
    const r = canvas.getBoundingClientRect();
    w = r.width; h = r.height;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // 画面サイズに応じた粒子数
  function makeParticles() {
    const count = Math.max(40, Math.min(110, Math.floor((w * h) / 9000)));
    return Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      vx: (Math.random() - 0.5) * 0.4,
      vy: (Math.random() - 0.5) * 0.4,
      r: Math.random() * 1.6 + 0.6
    }));
  }

  resize();
  let particles = makeParticles();

  function step() {
    ctx.clearRect(0, 0, w, h);

    // 粒子の更新と描画
    for (const p of particles) {
      if (!reduced) { p.x += p.vx; p.y += p.vy; }
      if (p.x < 0 || p.x > w) p.vx *= -1;
      if (p.y < 0 || p.y > h) p.vy *= -1;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(200,220,255,0.9)';
      ctx.fill();
    }

    // 近い粒子同士を線で結ぶ(距離で透明度変化)
    for (let i = 0; i < particles.length; i++) {
      for (let j = i + 1; j < particles.length; j++) {
        const a = particles[i], b = particles[j];
        const dx = a.x - b.x, dy = a.y - b.y;
        const dist = Math.hypot(dx, dy);
        if (dist < 120) {
          ctx.strokeStyle = `rgba(120,170,255,${(1 - dist / 120) * 0.5})`;
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(a.x, a.y);
          ctx.lineTo(b.x, b.y);
          ctx.stroke();
        }
      }
      // マウス周辺の粒子も結ぶ
      const dmx = particles[i].x - mouse.x, dmy = particles[i].y - mouse.y;
      const dm = Math.hypot(dmx, dmy);
      if (dm < 150) {
        ctx.strokeStyle = `rgba(255,200,140,${(1 - dm / 150) * 0.7})`;
        ctx.beginPath();
        ctx.moveTo(particles[i].x, particles[i].y);
        ctx.lineTo(mouse.x, mouse.y);
        ctx.stroke();
      }
    }
    requestAnimationFrame(step);
  }

  // マウス座標を取得(親基準)
  canvas.addEventListener('pointermove', (e) => {
    const r = canvas.getBoundingClientRect();
    mouse.x = e.clientX - r.left;
    mouse.y = e.clientY - r.top;
  });
  canvas.addEventListener('pointerleave', () => { mouse.x = mouse.y = -9999; });
  window.addEventListener('resize', () => { resize(); particles = makeParticles(); });

  requestAnimationFrame(step);
})();

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

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「パーティクル接続(星座)」の効果を追加してください。

# 追加してほしい効果
パーティクル接続(星座)(Canvas エフェクト)
近い粒子同士を線で結び星座のように見せる定番表現。マウス付近の粒子も結線します。ヒーロー背景やローディング演出に最適です。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- パーティクル接続(星座) -->
<div class="stage">
  <canvas id="constellationCanvas"></canvas>
  <div class="label">Constellation</div>
</div>

【CSS】
/* ステージ全体(背景は深い夜空のグラデ) */
:root {
  --bg-1: #0a0e27;
  --bg-2: #1a1f4a;
  --dot: 200, 220, 255;
}
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: var(--bg-1);
}
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  background:
    radial-gradient(1200px 500px at 70% -10%, var(--bg-2), transparent),
    linear-gradient(160deg, var(--bg-1), #05071a);
}
#constellationCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
/* 右下の控えめなラベル */
.label {
  position: absolute;
  right: 16px;
  bottom: 12px;
  color: rgba(var(--dot), .55);
  font-size: 12px;
  letter-spacing: .35em;
  text-transform: uppercase;
  pointer-events: none;
}

【JavaScript】
// 星座風パーティクル接続デモ
(() => {
  const canvas = document.getElementById('constellationCanvas');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let w = 0, h = 0, dpr = Math.min(window.devicePixelRatio || 1, 2);
  const mouse = { x: -9999, y: -9999 };

  // キャンバスを親要素に合わせてリサイズ(高解像度対応)
  function resize() {
    const r = canvas.getBoundingClientRect();
    w = r.width; h = r.height;
    canvas.width = w * dpr;
    canvas.height = h * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  // 画面サイズに応じた粒子数
  function makeParticles() {
    const count = Math.max(40, Math.min(110, Math.floor((w * h) / 9000)));
    return Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      vx: (Math.random() - 0.5) * 0.4,
      vy: (Math.random() - 0.5) * 0.4,
      r: Math.random() * 1.6 + 0.6
    }));
  }

  resize();
  let particles = makeParticles();

  function step() {
    ctx.clearRect(0, 0, w, h);

    // 粒子の更新と描画
    for (const p of particles) {
      if (!reduced) { p.x += p.vx; p.y += p.vy; }
      if (p.x < 0 || p.x > w) p.vx *= -1;
      if (p.y < 0 || p.y > h) p.vy *= -1;
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(200,220,255,0.9)';
      ctx.fill();
    }

    // 近い粒子同士を線で結ぶ(距離で透明度変化)
    for (let i = 0; i < particles.length; i++) {
      for (let j = i + 1; j < particles.length; j++) {
        const a = particles[i], b = particles[j];
        const dx = a.x - b.x, dy = a.y - b.y;
        const dist = Math.hypot(dx, dy);
        if (dist < 120) {
          ctx.strokeStyle = `rgba(120,170,255,${(1 - dist / 120) * 0.5})`;
          ctx.lineWidth = 1;
          ctx.beginPath();
          ctx.moveTo(a.x, a.y);
          ctx.lineTo(b.x, b.y);
          ctx.stroke();
        }
      }
      // マウス周辺の粒子も結ぶ
      const dmx = particles[i].x - mouse.x, dmy = particles[i].y - mouse.y;
      const dm = Math.hypot(dmx, dmy);
      if (dm < 150) {
        ctx.strokeStyle = `rgba(255,200,140,${(1 - dm / 150) * 0.7})`;
        ctx.beginPath();
        ctx.moveTo(particles[i].x, particles[i].y);
        ctx.lineTo(mouse.x, mouse.y);
        ctx.stroke();
      }
    }
    requestAnimationFrame(step);
  }

  // マウス座標を取得(親基準)
  canvas.addEventListener('pointermove', (e) => {
    const r = canvas.getBoundingClientRect();
    mouse.x = e.clientX - r.left;
    mouse.y = e.clientY - r.top;
  });
  canvas.addEventListener('pointerleave', () => { mouse.x = mouse.y = -9999; });
  window.addEventListener('resize', () => { resize(); particles = makeParticles(); });

  requestAnimationFrame(step);
})();

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

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