フローフィールド(流れ場)

三角関数で作った疑似ノイズの流れ場に沿って大量の粒子が漂い、繊細な流線アートを描く生成的ビジュアル。アンビエントな背景に最適です。

#canvas#particles#generative#animation

ライブデモ

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

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

HTML
<!-- MOON BREW:ブランドストーリー(湯気のような流れ場の背景) -->
<section class="mb-story">
  <!-- 主役:アンビエントなフローフィールド -->
  <canvas class="mb-story__fx" id="mbFlow"></canvas>

  <!-- 前景UI:コンセプト文 -->
  <div class="mb-story__inner">
    <span class="mb-story__tag">OUR STORY</span>
    <h1 class="mb-story__title">一杯ごとに、<br>立ちのぼる物語。</h1>
    <p class="mb-story__lead">産地の畑から焙煎の香りまで。湯気のようにゆらめく時間を、あなたのテーブルへ。</p>
    <div class="mb-story__meta">
      <span># 自家焙煎</span>
      <span># 直輸入豆</span>
      <span># 月夜のカフェ</span>
    </div>
    <a class="mb-story__btn" href="#">こだわりを読む</a>
  </div>
</section>
CSS
/* MOON BREW:流れ場のアンビエント背景+ブランドストーリー */
:root {
  --cream: #f5ede1;
  --brown: #2b1d12;
  --amber: #c98a3b;
}

* { box-sizing: border-box; }

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

.mb-story {
  position: relative;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(700px 360px at 75% 0%, #3a2718, transparent),
    linear-gradient(160deg, #2b1d12, #1c130b);
}

/* 主役:流線アートの背景 */
.mb-story__fx {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

.mb-story__inner {
  position: relative;
  z-index: 2;
  padding: 40px 32px;
  max-width: 420px;
  color: var(--cream);
  pointer-events: none;
}
.mb-story__inner a { pointer-events: auto; }

.mb-story__tag {
  display: inline-block;
  font-size: 10px;
  letter-spacing: 0.32em;
  color: var(--amber);
  font-weight: 700;
}
.mb-story__title {
  margin: 13px 0 14px;
  font-size: 31px;
  line-height: 1.42;
  font-weight: 700;
  font-family: "Hiragino Mincho ProN", "Yu Mincho", serif;
}
.mb-story__lead {
  margin: 0 0 20px;
  font-size: 13px;
  line-height: 1.9;
  color: rgba(245,237,225,0.82);
  max-width: 340px;
}
.mb-story__meta { display: flex; gap: 14px; margin-bottom: 24px; flex-wrap: wrap; }
.mb-story__meta span {
  font-size: 11.5px;
  color: rgba(245,237,225,0.7);
}
.mb-story__btn {
  display: inline-block;
  padding: 11px 24px;
  border-radius: 999px;
  background: var(--amber);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  text-decoration: none;
  box-shadow: 0 10px 24px rgba(201,138,59,0.4);
  transition: transform 0.2s ease;
}
.mb-story__btn:hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .mb-story__btn { transition: none; }
}
JavaScript
// MOON BREW:三角関数の疑似ノイズで作る流れ場(湯気のような流線)
(() => {
  const canvas = document.getElementById('mbFlow');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

  let w = 0, h = 0, raf = 0, running = true, t = 0, agents = [];
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const scale = 0.006; // 流れ場の細かさ

  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);
    // 背景を一度暗く塗っておく
    ctx.fillStyle = '#1c130b';
    ctx.fillRect(0, 0, w, h);
  }

  function makeAgents() {
    const count = Math.max(120, Math.min(320, Math.floor((w * h) / 2600)));
    agents = Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      hue: 28 + Math.random() * 18 // 琥珀〜クリーム系
    }));
  }

  // 座標から流れの角度を求める(疑似ノイズ)
  function angleAt(x, y) {
    return (
      Math.sin(x * scale + t * 0.002) +
      Math.cos(y * scale - t * 0.0015) +
      Math.sin((x + y) * scale * 0.6)
    ) * Math.PI;
  }

  resize();
  makeAgents();

  function step() {
    // ごく薄く塗り重ねて余韻を残す(流線が積層)
    ctx.fillStyle = 'rgba(28,19,11,0.04)';
    ctx.fillRect(0, 0, w, h);

    for (const a of agents) {
      const ang = angleAt(a.x, a.y);
      const nx = a.x + Math.cos(ang) * 1.1;
      const ny = a.y + Math.sin(ang) * 1.1;

      ctx.strokeStyle = `hsla(${a.hue},55%,62%,0.18)`;
      ctx.lineWidth = 1;
      ctx.beginPath();
      ctx.moveTo(a.x, a.y);
      ctx.lineTo(nx, ny);
      ctx.stroke();

      a.x = nx;
      a.y = ny;
      // 画面外や寿命でランダム再配置
      if (a.x < 0 || a.x > w || a.y < 0 || a.y > h || Math.random() < 0.005) {
        a.x = Math.random() * w;
        a.y = Math.random() * h;
      }
    }
    t += 1;
    raf = requestAnimationFrame(step);
  }

  function start() {
    if (running) return;
    running = true;
    raf = requestAnimationFrame(step);
  }
  function stop() {
    running = false;
    cancelAnimationFrame(raf);
  }

  window.addEventListener('resize', () => { resize(); makeAgents(); });
  document.addEventListener('visibilitychange', () => {
    document.hidden ? stop() : start();
  });

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

コード

HTML
<!-- フローフィールド(流れ場に沿って漂う粒子) -->
<div class="stage">
  <canvas id="flowCanvas"></canvas>
  <div class="label">Flow Field</div>
</div>
CSS
/* フローフィールド */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #07070d;
}
#flowCanvas { display: block; width: 100%; height: 100%; }
.label {
  position: absolute;
  left: 16px; bottom: 12px;
  color: rgba(255, 255, 255, .4);
  font-size: 12px;
  letter-spacing: .3em;
  text-transform: uppercase;
  pointer-events: none;
}
JavaScript
// フローフィールドデモ(疑似ノイズの流れ場に沿って粒子が流れる)
(() => {
  const canvas = document.getElementById('flowCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let w = 0, h = 0, t = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  let particles = [];

  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);
    ctx.fillStyle = '#07070d';
    ctx.fillRect(0, 0, w, h); // 初期化
  }

  function makeParticles() {
    const count = Math.max(120, Math.min(320, Math.floor((w * h) / 3000)));
    particles = Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      hue: 180 + Math.random() * 120
    }));
  }

  // 三角関数を組み合わせた滑らかな疑似ノイズ角度
  function angleAt(x, y) {
    const s = 0.004;
    return (
      Math.sin(x * s + t) +
      Math.cos(y * s - t * 0.8) +
      Math.sin((x + y) * s * 0.6 + t * 0.5)
    ) * Math.PI;
  }

  resize();
  makeParticles();
  window.addEventListener('resize', () => { resize(); makeParticles(); });

  function step() {
    // ごく薄く暗幕をかけて軌跡をゆっくり消す
    ctx.fillStyle = 'rgba(7,7,13,0.04)';
    ctx.fillRect(0, 0, w, h);

    for (const p of particles) {
      const a = angleAt(p.x, p.y);
      p.x += Math.cos(a) * 1.4;
      p.y += Math.sin(a) * 1.4;

      ctx.beginPath();
      ctx.arc(p.x, p.y, 1, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue},85%,62%,0.7)`;
      ctx.fill();

      // 画面外に出たら再配置
      if (p.x < 0 || p.x > w || p.y < 0 || p.y > h) {
        p.x = Math.random() * w;
        p.y = Math.random() * h;
      }
    }
    if (!reduced) t += 0.002;
    requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
})();

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

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

# 追加してほしい効果
フローフィールド(流れ場)(Canvas エフェクト)
三角関数で作った疑似ノイズの流れ場に沿って大量の粒子が漂い、繊細な流線アートを描く生成的ビジュアル。アンビエントな背景に最適です。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- フローフィールド(流れ場に沿って漂う粒子) -->
<div class="stage">
  <canvas id="flowCanvas"></canvas>
  <div class="label">Flow Field</div>
</div>

【CSS】
/* フローフィールド */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #07070d;
}
#flowCanvas { display: block; width: 100%; height: 100%; }
.label {
  position: absolute;
  left: 16px; bottom: 12px;
  color: rgba(255, 255, 255, .4);
  font-size: 12px;
  letter-spacing: .3em;
  text-transform: uppercase;
  pointer-events: none;
}

【JavaScript】
// フローフィールドデモ(疑似ノイズの流れ場に沿って粒子が流れる)
(() => {
  const canvas = document.getElementById('flowCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');
  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  let w = 0, h = 0, t = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  let particles = [];

  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);
    ctx.fillStyle = '#07070d';
    ctx.fillRect(0, 0, w, h); // 初期化
  }

  function makeParticles() {
    const count = Math.max(120, Math.min(320, Math.floor((w * h) / 3000)));
    particles = Array.from({ length: count }, () => ({
      x: Math.random() * w,
      y: Math.random() * h,
      hue: 180 + Math.random() * 120
    }));
  }

  // 三角関数を組み合わせた滑らかな疑似ノイズ角度
  function angleAt(x, y) {
    const s = 0.004;
    return (
      Math.sin(x * s + t) +
      Math.cos(y * s - t * 0.8) +
      Math.sin((x + y) * s * 0.6 + t * 0.5)
    ) * Math.PI;
  }

  resize();
  makeParticles();
  window.addEventListener('resize', () => { resize(); makeParticles(); });

  function step() {
    // ごく薄く暗幕をかけて軌跡をゆっくり消す
    ctx.fillStyle = 'rgba(7,7,13,0.04)';
    ctx.fillRect(0, 0, w, h);

    for (const p of particles) {
      const a = angleAt(p.x, p.y);
      p.x += Math.cos(a) * 1.4;
      p.y += Math.sin(a) * 1.4;

      ctx.beginPath();
      ctx.arc(p.x, p.y, 1, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue},85%,62%,0.7)`;
      ctx.fill();

      // 画面外に出たら再配置
      if (p.x < 0 || p.x > w || p.y < 0 || p.y > h) {
        p.x = Math.random() * w;
        p.y = Math.random() * h;
      }
    }
    if (!reduced) t += 0.002;
    requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
})();

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

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