マウス追従粒子トレイル

ポインタの軌跡に沿って虹色の粒子を放出し、加算合成で発光させるインタラクティブ演出。カーソル装飾やゲームUIに使えます。

#canvas#particles#interactive#animation

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:ペンライト演出(マウス追従トレイル)のライブ告知画面 -->
<section class="sk-live">
  <!-- 主役:カーソル軌跡に桜色の光が舞う -->
  <canvas class="sk-live__fx" id="skTrail"></canvas>

  <!-- 前景UI:ライブ告知 -->
  <div class="sk-live__inner">
    <span class="sk-live__pill">🌸 SPRING LIVE 2026</span>
    <h1 class="sk-live__title">桜花繚乱<br><small>SAKURA SPRING TOUR</small></h1>
    <p class="sk-live__lead">画面の上でカーソルを動かすと、ペンライトの光がきらめきます。</p>

    <div class="sk-live__dates">
      <div class="sk-date">
        <b>04.18</b><span>東京 / 桜ドーム</span>
      </div>
      <div class="sk-date">
        <b>04.25</b><span>大阪 / 花見アリーナ</span>
      </div>
      <div class="sk-date">
        <b>05.03</b><span>福岡 / うみほし館</span>
      </div>
    </div>
    <a class="sk-live__btn" href="#">チケット先行予約</a>
  </div>
</section>
CSS
/* Sakura:ペンライト光トレイルのライブ告知 */
:root {
  --pink: #ffd1e0;
  --pink-deep: #ff7fa8;
  --gray: #f4f1f3;
}

* { box-sizing: border-box; }

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

.sk-live {
  position: relative;
  height: 400px;
  overflow: hidden;
  background:
    radial-gradient(700px 360px at 80% 0%, #fff0f5, transparent),
    linear-gradient(165deg, #ffe3ee, #ffd1e0 60%, #f7c2d6);
}

/* 主役:発光トレイルのキャンバス(前景UIの背後) */
.sk-live__fx {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

.sk-live__inner {
  position: relative;
  z-index: 2;
  padding: 36px 30px;
  max-width: 470px;
  pointer-events: none; /* キャンバスのマウス追従を妨げない */
}
.sk-live__inner a,
.sk-live__inner .sk-date { pointer-events: auto; }

.sk-live__pill {
  display: inline-block;
  padding: 6px 14px;
  border-radius: 999px;
  background: rgba(255,255,255,0.75);
  color: var(--pink-deep);
  font-size: 11px;
  font-weight: 800;
  letter-spacing: 0.08em;
  box-shadow: 0 4px 14px rgba(255,127,168,0.25);
}
.sk-live__title {
  margin: 16px 0 12px;
  font-size: 34px;
  line-height: 1.3;
  font-weight: 900;
  color: #5a2b3d;
  text-shadow: 0 2px 10px rgba(255,255,255,0.6);
}
.sk-live__title small {
  display: block;
  font-size: 12px;
  letter-spacing: 0.3em;
  font-weight: 700;
  color: var(--pink-deep);
  margin-top: 6px;
}
.sk-live__lead {
  margin: 0 0 20px;
  font-size: 12.5px;
  line-height: 1.7;
  color: #7a5563;
}

.sk-live__dates { display: flex; gap: 10px; margin-bottom: 22px; flex-wrap: wrap; }
.sk-date {
  flex: 1;
  min-width: 110px;
  padding: 12px 12px;
  border-radius: 14px;
  background: rgba(255,255,255,0.82);
  box-shadow: 0 6px 18px rgba(196,120,150,0.18);
}
.sk-date b {
  display: block;
  font-size: 19px;
  color: #5a2b3d;
  letter-spacing: 0.02em;
}
.sk-date span { font-size: 11px; color: #93707e; }

.sk-live__btn {
  display: inline-block;
  padding: 12px 26px;
  border-radius: 999px;
  background: linear-gradient(135deg, var(--pink-deep), #ff9cbb);
  color: #fff;
  font-size: 13.5px;
  font-weight: 800;
  text-decoration: none;
  box-shadow: 0 10px 24px rgba(255,127,168,0.45);
  transition: transform 0.2s ease;
}
.sk-live__btn:hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .sk-live__btn { transition: none; }
}
JavaScript
// Sakura:カーソル追従の桜色トレイル(加算合成で発光)
(() => {
  const canvas = document.getElementById('skTrail');
  if (!canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

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

  // コンテナ全体にフィット
  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);
  }
  resize();

  // ポインタ位置に粒子を放出
  function spawn(x, y) {
    const n = 3;
    for (let i = 0; i < n; i++) {
      particles.push({
        x, y,
        vx: (Math.random() - 0.5) * 1.4,
        vy: (Math.random() - 0.5) * 1.4 - 0.4,
        life: 1,
        size: Math.random() * 3 + 1.5,
        hue: hue + (Math.random() * 40 - 20)
      });
    }
    hue = (hue + 1.5) % 360; // 桜〜ピンク〜紫を巡回
  }

  function step() {
    // 残像を残すため半透明で塗りつぶし
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = 'rgba(255,209,224,0.12)';
    ctx.fillRect(0, 0, w, h);

    // 加算合成で発光
    ctx.globalCompositeOperation = 'lighter';
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.02; // ゆるい重力
      p.life -= 0.022;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue},85%,72%,${p.life})`;
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';
    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();
    const x = e.clientX - r.left;
    const y = e.clientY - r.top;
    if (last.has) {
      const steps = Math.max(1, Math.floor(Math.hypot(x - last.x, y - last.y) / 6));
      for (let s = 0; s < steps; s++) {
        const t = s / steps;
        spawn(last.x + (x - last.x) * t, last.y + (y - last.y) * t);
      }
    }
    last = { x, y, has: true };
  });
  canvas.addEventListener('pointerleave', () => { last.has = false; });

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

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

コード

HTML
<!-- マウス追従粒子 -->
<div class="stage">
  <canvas id="trailCanvas"></canvas>
  <p class="hint">マウスを動かしてみて</p>
</div>
CSS
/* マウス追従粒子のステージ */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  background: radial-gradient(900px 400px at 50% 120%, #1b1035, #0b0716 70%);
  font-family: "Segoe UI", system-ui, sans-serif;
  cursor: crosshair;
}
#trailCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
/* 中央のヒント(粒子が無いときの案内) */
.hint {
  position: absolute;
  inset: 0;
  margin: auto;
  height: 1.4em;
  text-align: center;
  color: rgba(255, 255, 255, .35);
  font-size: 14px;
  letter-spacing: .15em;
  pointer-events: none;
  animation: fade 3s ease-in-out infinite;
}
@keyframes fade { 0%, 100% { opacity: .25 } 50% { opacity: .6 } }
@media (prefers-reduced-motion: reduce) {
  .hint { animation: none; }
}
JavaScript
// マウス追従粒子(虹色トレイル)デモ
(() => {
  const canvas = document.getElementById('trailCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');

  let w = 0, h = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const particles = [];
  let hue = 0;
  let last = { x: 0, y: 0, set: false };

  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);
  }
  resize();
  window.addEventListener('resize', resize);

  // 指定座標に粒子を生成
  function spawn(x, y) {
    hue = (hue + 6) % 360;
    const n = 3;
    for (let i = 0; i < n; i++) {
      particles.push({
        x, y,
        vx: (Math.random() - 0.5) * 2.4,
        vy: (Math.random() - 0.5) * 2.4 - 0.4,
        life: 1,
        size: Math.random() * 4 + 2,
        hue
      });
    }
  }

  function pointer(e) {
    const r = canvas.getBoundingClientRect();
    const x = e.clientX - r.left, y = e.clientY - r.top;
    if (last.set) {
      // 動きの間を補間して途切れない軌跡に
      const steps = Math.min(8, Math.floor(Math.hypot(x - last.x, y - last.y) / 6) + 1);
      for (let s = 0; s < steps; s++) {
        spawn(last.x + (x - last.x) * (s / steps), last.y + (y - last.y) * (s / steps));
      }
    }
    last = { x, y, set: true };
  }
  canvas.addEventListener('pointermove', pointer);
  canvas.addEventListener('pointerleave', () => { last.set = false; });

  function step() {
    // 半透明の黒で残像を残しつつ消す
    ctx.fillStyle = 'rgba(11,7,22,0.18)';
    ctx.fillRect(0, 0, w, h);
    ctx.globalCompositeOperation = 'lighter';

    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.04;        // 軽い重力
      p.vx *= 0.98;
      p.life -= 0.02;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue},90%,60%,${p.life})`;
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';
    requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
})();

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

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

# 追加してほしい効果
マウス追従粒子トレイル(Canvas エフェクト)
ポインタの軌跡に沿って虹色の粒子を放出し、加算合成で発光させるインタラクティブ演出。カーソル装飾やゲームUIに使えます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- マウス追従粒子 -->
<div class="stage">
  <canvas id="trailCanvas"></canvas>
  <p class="hint">マウスを動かしてみて</p>
</div>

【CSS】
/* マウス追従粒子のステージ */
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
.stage {
  position: relative;
  width: 100%;
  height: 360px;
  overflow: hidden;
  background: radial-gradient(900px 400px at 50% 120%, #1b1035, #0b0716 70%);
  font-family: "Segoe UI", system-ui, sans-serif;
  cursor: crosshair;
}
#trailCanvas {
  display: block;
  width: 100%;
  height: 100%;
}
/* 中央のヒント(粒子が無いときの案内) */
.hint {
  position: absolute;
  inset: 0;
  margin: auto;
  height: 1.4em;
  text-align: center;
  color: rgba(255, 255, 255, .35);
  font-size: 14px;
  letter-spacing: .15em;
  pointer-events: none;
  animation: fade 3s ease-in-out infinite;
}
@keyframes fade { 0%, 100% { opacity: .25 } 50% { opacity: .6 } }
@media (prefers-reduced-motion: reduce) {
  .hint { animation: none; }
}

【JavaScript】
// マウス追従粒子(虹色トレイル)デモ
(() => {
  const canvas = document.getElementById('trailCanvas');
  if (!canvas) return;
  const ctx = canvas.getContext('2d');

  let w = 0, h = 0;
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  const particles = [];
  let hue = 0;
  let last = { x: 0, y: 0, set: false };

  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);
  }
  resize();
  window.addEventListener('resize', resize);

  // 指定座標に粒子を生成
  function spawn(x, y) {
    hue = (hue + 6) % 360;
    const n = 3;
    for (let i = 0; i < n; i++) {
      particles.push({
        x, y,
        vx: (Math.random() - 0.5) * 2.4,
        vy: (Math.random() - 0.5) * 2.4 - 0.4,
        life: 1,
        size: Math.random() * 4 + 2,
        hue
      });
    }
  }

  function pointer(e) {
    const r = canvas.getBoundingClientRect();
    const x = e.clientX - r.left, y = e.clientY - r.top;
    if (last.set) {
      // 動きの間を補間して途切れない軌跡に
      const steps = Math.min(8, Math.floor(Math.hypot(x - last.x, y - last.y) / 6) + 1);
      for (let s = 0; s < steps; s++) {
        spawn(last.x + (x - last.x) * (s / steps), last.y + (y - last.y) * (s / steps));
      }
    }
    last = { x, y, set: true };
  }
  canvas.addEventListener('pointermove', pointer);
  canvas.addEventListener('pointerleave', () => { last.set = false; });

  function step() {
    // 半透明の黒で残像を残しつつ消す
    ctx.fillStyle = 'rgba(11,7,22,0.18)';
    ctx.fillRect(0, 0, w, h);
    ctx.globalCompositeOperation = 'lighter';

    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.04;        // 軽い重力
      p.vx *= 0.98;
      p.life -= 0.02;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      ctx.beginPath();
      ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
      ctx.fillStyle = `hsla(${p.hue},90%,60%,${p.life})`;
      ctx.fill();
    }
    ctx.globalCompositeOperation = 'source-over';
    requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
})();

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

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