パーティクルフィールド

数千の点をTHREE.Pointsで一括描画し、マウスで視点が緩やかに動く粒子空間。背景演出やランディングの没入感づくりに向きます。

外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

#webgl#threejs#particles#interactive

ライブデモ

使用例(お題: カフェ MOON BREW)

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

HTML
<!-- MOON BREW:立ちのぼる香りの粒子をパーティクルで表現した店舗紹介ヒーロー -->
<section class="mb-aroma" aria-label="MOON BREW 店舗">
  <!-- 背景に漂う香りの粒子(マウスで緩やかに視点が動く) -->
  <canvas id="scene" class="mb-aroma__canvas" aria-hidden="true"></canvas>
  <div class="mb-aroma__fallback" id="mb-fallback" hidden></div>

  <header class="mb-bar">
    <span class="mb-logo">◐ MOON BREW</span>
    <nav class="mb-nav">
      <a href="#">焙煎所</a>
      <a href="#">アクセス</a>
    </nav>
  </header>

  <div class="mb-aroma__body">
    <span class="mb-kicker">ROASTERY &amp; CAFE</span>
    <h1 class="mb-title">香りに包まれる、<br>静かな朝の時間。</h1>
    <p class="mb-lead">焙煎機のすぐ隣で味わう一杯。<br>窓辺の席で、ゆっくりとどうぞ。</p>
    <div class="mb-hours">
      <span class="mb-hours__row"><b>平日</b> 8:00 – 20:00</span>
      <span class="mb-hours__row"><b>土日</b> 9:00 – 19:00</span>
    </div>
  </div>
</section>
CSS
/* MOON BREW:暗い焙煎所の中に香りの粒子が舞う */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Mincho ProN", "Yu Mincho", "Segoe UI", serif;
  background: var(--brown);
}

.mb-aroma {
  position: relative;
  width: 100%;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(130% 100% at 50% 0%, #3a281a 0%, #201610 55%, #140d07 100%);
  color: var(--cream);
}

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

/* フォールバック:粒が散ったような点描の背景 */
.mb-aroma__fallback {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(2px 2px at 20% 30%, rgba(201, 138, 59, 0.7), transparent),
    radial-gradient(2px 2px at 70% 60%, rgba(245, 237, 225, 0.5), transparent),
    radial-gradient(2px 2px at 45% 80%, rgba(201, 138, 59, 0.6), transparent),
    radial-gradient(2px 2px at 85% 25%, rgba(245, 237, 225, 0.4), transparent);
}

.mb-bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 26px;
}
.mb-logo { font-size: 17px; letter-spacing: 0.14em; font-weight: 700; }
.mb-nav { display: flex; gap: 18px; }
.mb-nav a {
  color: rgba(245, 237, 225, 0.82);
  text-decoration: none;
  font-size: 13px;
  letter-spacing: 0.06em;
  transition: color 0.2s ease;
}
.mb-nav a:hover { color: var(--amber); }

.mb-aroma__body {
  position: relative;
  z-index: 2;
  max-width: 460px;
  padding: 40px 26px;
  text-align: center;
  margin: 0 auto;
}
.mb-kicker {
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: 11px;
  letter-spacing: 0.3em;
  color: var(--amber);
  font-weight: 700;
}
.mb-title {
  margin: 14px 0 16px;
  font-size: 32px;
  line-height: 1.4;
  font-weight: 700;
  text-shadow: 0 2px 16px rgba(0, 0, 0, 0.5);
}
.mb-lead {
  margin: 0 0 24px;
  font-size: 14px;
  line-height: 1.9;
  color: rgba(245, 237, 225, 0.84);
}

.mb-hours {
  display: inline-flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px 26px;
  border: 1px solid rgba(201, 138, 59, 0.4);
  border-radius: 14px;
  background: rgba(20, 13, 7, 0.35);
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: 13px;
  letter-spacing: 0.05em;
}
.mb-hours__row b { color: var(--amber); font-weight: 700; margin-right: 10px; }
JavaScript
// MOON BREW ヒーロー:立ちのぼる香りの粒子。THREE.Pointsで一括描画+マウスで視点が緩やかに動く
(function () {
  "use strict";
  const canvas = document.getElementById("scene");
  const fallback = document.getElementById("mb-fallback");
  // Three.js未読込やcanvas不在なら安全にフォールバック表示
  if (!canvas || typeof THREE === "undefined") {
    if (fallback) fallback.hidden = false;
    return;
  }

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

  let renderer;
  try {
    renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  } catch (e) {
    if (fallback) fallback.hidden = false;
    return;
  }
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
  camera.position.set(0, 0, 9);

  // 香りの粒:琥珀とクリームを混ぜた点群
  const COUNT = 1400;
  const positions = new Float32Array(COUNT * 3);
  const colors = new Float32Array(COUNT * 3);
  const amber = new THREE.Color(0xc98a3b);
  const cream = new THREE.Color(0xf5ede1);
  for (let i = 0; i < COUNT; i++) {
    positions[i * 3] = (Math.random() - 0.5) * 16;
    positions[i * 3 + 1] = (Math.random() - 0.5) * 12;
    positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
    const c = Math.random() < 0.5 ? amber : cream;
    colors[i * 3] = c.r; colors[i * 3 + 1] = c.g; colors[i * 3 + 2] = c.b;
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

  const material = new THREE.PointsMaterial({
    size: 0.08,
    vertexColors: true,
    transparent: true,
    opacity: 0.85,
    depthWrite: false,
  });
  const points = new THREE.Points(geometry, material);
  scene.add(points);

  // マウスで視点をゆるやかに動かす
  let targetX = 0, targetY = 0;
  canvas.addEventListener("pointermove", (e) => {
    const r = canvas.getBoundingClientRect();
    targetX = ((e.clientX - r.left) / (r.width || 1) - 0.5) * 1.2;
    targetY = ((e.clientY - r.top) / (r.height || 1) - 0.5) * 0.8;
  });

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  let running = true;
  function animate() {
    if (!reduceMotion) {
      points.rotation.y += 0.0009;
      // 香りが立ちのぼるように上方向へゆっくり流す
      const pos = geometry.attributes.position;
      for (let i = 0; i < COUNT; i++) {
        let y = pos.getY(i) + 0.004;
        if (y > 6) y = -6; // 上に抜けたら下から再投入
        pos.setY(i, y);
      }
      pos.needsUpdate = true;
    }
    // カメラをマウス方向へイージング
    camera.position.x += (targetX - camera.position.x) * 0.04;
    camera.position.y += (targetY - camera.position.y) * 0.04;
    camera.lookAt(0, 0, 0);
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate();

  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      if (running) { cancelAnimationFrame(raf); running = false; }
    } else if (!running) {
      running = true;
      raf = requestAnimationFrame(animate);
    }
  });

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

コード

HTML
<!-- 奥行きのあるパーティクルフィールド。マウスで視点が緩やかに動く -->
<div class="stage">
  <canvas id="particles" aria-label="流れるパーティクルフィールド"></canvas>
  <div class="caption">
    <span class="badge">Points</span>
    <h2>Particle Field</h2>
    <p>数千の点を一括描画。背景演出やヒーローに最適</p>
  </div>
</div>
CSS
/* 配色変数 */
:root {
  --ink: #eaf6ff;
  --accent: #4fd6ff;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 100%;
  height: 360px;
  /* 深い夜空のグラデーション */
  background: linear-gradient(160deg, #03142b 0%, #050a1f 55%, #02060f 100%);
  cursor: crosshair;
}

#particles {
  display: block;
  width: 100%;
  height: 100%;
}

.caption {
  position: absolute;
  left: 28px;
  bottom: 24px;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
  pointer-events: none;
}

.badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(79, 214, 255, .18);
  border: 1px solid rgba(79, 214, 255, .45);
  color: var(--accent);
  margin-bottom: 10px;
}

.caption h2 {
  font-size: 22px;
  font-weight: 700;
}

.caption p {
  margin-top: 4px;
  font-size: 13px;
  opacity: .7;
}
JavaScript
// パーティクルフィールド:THREE.Pointsで数千点を一括描画
(function () {
  "use strict";
  const canvas = document.getElementById("particles");
  if (!canvas || typeof THREE === "undefined") return;

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

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x02060f, 0.055);
  const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
  camera.position.z = 16;

  // ランダムな立方空間に点を配置
  const COUNT = 2600;
  const positions = new Float32Array(COUNT * 3);
  const colors = new Float32Array(COUNT * 3);
  const palette = [new THREE.Color(0x4fd6ff), new THREE.Color(0x9d7bff), new THREE.Color(0xff77c8)];
  for (let i = 0; i < COUNT; i++) {
    const i3 = i * 3;
    positions[i3] = (Math.random() - 0.5) * 34;
    positions[i3 + 1] = (Math.random() - 0.5) * 24;
    positions[i3 + 2] = (Math.random() - 0.5) * 34;
    const c = palette[(Math.random() * palette.length) | 0];
    colors[i3] = c.r; colors[i3 + 1] = c.g; colors[i3 + 2] = c.b;
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

  const material = new THREE.PointsMaterial({
    size: 0.16,
    vertexColors: true,
    transparent: true,
    opacity: 0.9,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  const cloud = new THREE.Points(geometry, material);
  scene.add(cloud);

  // マウスで視点を緩やかに揺らす
  let targetX = 0, targetY = 0;
  canvas.addEventListener("pointermove", (e) => {
    const r = canvas.getBoundingClientRect();
    targetX = ((e.clientX - r.left) / r.width - 0.5) * 2;
    targetY = ((e.clientY - r.top) / r.height - 0.5) * 2;
  });

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  function animate() {
    if (!reduceMotion) {
      cloud.rotation.y += 0.0014;
      // 視点をマウス方向へイージング
      camera.position.x += (targetX * 4 - camera.position.x) * 0.04;
      camera.position.y += (-targetY * 3 - camera.position.y) * 0.04;
      camera.lookAt(scene.position);
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate();

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

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

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

# 追加してほしい効果
パーティクルフィールド(WebGL / Three.js)
数千の点をTHREE.Pointsで一括描画し、マウスで視点が緩やかに動く粒子空間。背景演出やランディングの没入感づくりに向きます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 奥行きのあるパーティクルフィールド。マウスで視点が緩やかに動く -->
<div class="stage">
  <canvas id="particles" aria-label="流れるパーティクルフィールド"></canvas>
  <div class="caption">
    <span class="badge">Points</span>
    <h2>Particle Field</h2>
    <p>数千の点を一括描画。背景演出やヒーローに最適</p>
  </div>
</div>

【CSS】
/* 配色変数 */
:root {
  --ink: #eaf6ff;
  --accent: #4fd6ff;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 100%;
  height: 360px;
  /* 深い夜空のグラデーション */
  background: linear-gradient(160deg, #03142b 0%, #050a1f 55%, #02060f 100%);
  cursor: crosshair;
}

#particles {
  display: block;
  width: 100%;
  height: 100%;
}

.caption {
  position: absolute;
  left: 28px;
  bottom: 24px;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
  pointer-events: none;
}

.badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(79, 214, 255, .18);
  border: 1px solid rgba(79, 214, 255, .45);
  color: var(--accent);
  margin-bottom: 10px;
}

.caption h2 {
  font-size: 22px;
  font-weight: 700;
}

.caption p {
  margin-top: 4px;
  font-size: 13px;
  opacity: .7;
}

【JavaScript】
// パーティクルフィールド:THREE.Pointsで数千点を一括描画
(function () {
  "use strict";
  const canvas = document.getElementById("particles");
  if (!canvas || typeof THREE === "undefined") return;

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

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x02060f, 0.055);
  const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
  camera.position.z = 16;

  // ランダムな立方空間に点を配置
  const COUNT = 2600;
  const positions = new Float32Array(COUNT * 3);
  const colors = new Float32Array(COUNT * 3);
  const palette = [new THREE.Color(0x4fd6ff), new THREE.Color(0x9d7bff), new THREE.Color(0xff77c8)];
  for (let i = 0; i < COUNT; i++) {
    const i3 = i * 3;
    positions[i3] = (Math.random() - 0.5) * 34;
    positions[i3 + 1] = (Math.random() - 0.5) * 24;
    positions[i3 + 2] = (Math.random() - 0.5) * 34;
    const c = palette[(Math.random() * palette.length) | 0];
    colors[i3] = c.r; colors[i3 + 1] = c.g; colors[i3 + 2] = c.b;
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
  geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

  const material = new THREE.PointsMaterial({
    size: 0.16,
    vertexColors: true,
    transparent: true,
    opacity: 0.9,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
  });
  const cloud = new THREE.Points(geometry, material);
  scene.add(cloud);

  // マウスで視点を緩やかに揺らす
  let targetX = 0, targetY = 0;
  canvas.addEventListener("pointermove", (e) => {
    const r = canvas.getBoundingClientRect();
    targetX = ((e.clientX - r.left) / r.width - 0.5) * 2;
    targetY = ((e.clientY - r.top) / r.height - 0.5) * 2;
  });

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  function animate() {
    if (!reduceMotion) {
      cloud.rotation.y += 0.0014;
      // 視点をマウス方向へイージング
      camera.position.x += (targetX * 4 - camera.position.x) * 0.04;
      camera.position.y += (-targetY * 3 - camera.position.y) * 0.04;
      camera.lookAt(scene.position);
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate();

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

# 外部ライブラリ
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

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