カーソルトレイル

canvasにマウスの軌跡を残光として描く虹色トレイル。ヒーローセクションやランディングページの遊び心ある背景演出に向いています。

#canvas#js#animation#particles

ライブデモ

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

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

HTML
<!-- Sakura: アイドルのライブ告知1画面。canvasの虹色トレイルを主役に -->
<div class="sk" data-trail-root>
  <canvas class="sk__canvas" data-canvas></canvas>

  <header class="sk__bar">
    <span class="sk__logo">🌸 Sakura</span>
    <nav class="sk__nav">
      <a href="#">LIVE</a>
      <a href="#">MUSIC</a>
      <a href="#">NEWS</a>
    </nav>
  </header>

  <section class="sk__hero">
    <p class="sk__tag">SPRING TOUR 2026</p>
    <h1 class="sk__title">桜咲くアリーナへ。</h1>
    <p class="sk__lead">5人がつむぐ春のステージ。<br>あなたの声で、夜空をピンクに染めよう。</p>
    <div class="sk__cta">
      <span class="sk__btn">チケット先行受付</span>
      <span class="sk__date">2026.04.05 / 横浜</span>
    </div>
    <p class="sk__hint">画面の上でカーソルを動かすと光の軌跡が舞います</p>
  </section>
</div>
CSS
/* Sakura アイドルテーマ: 桜ピンク/白/淡グレー */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
  color: #4a3a40;
  overflow: hidden;
}

.sk {
  position: relative;
  height: 400px;
  background:
    radial-gradient(circle at 78% 22%, #ffd1e0 0%, transparent 52%),
    radial-gradient(circle at 16% 86%, #ffe3ec 0%, transparent 50%),
    linear-gradient(160deg, #fff 0%, #fdf4f7 100%);
  overflow: hidden;
}

/* トレイル描画用canvas(背景の上、UIの下) */
.sk__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}

/* ヘッダー */
.sk__bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 22px;
  padding: 16px 28px;
  font-size: 14px;
}
.sk__logo {
  font-weight: 800;
  letter-spacing: .06em;
  font-size: 17px;
  color: #e06a92;
}
.sk__nav {
  display: flex;
  gap: 18px;
  margin-left: auto;
  font-weight: 700;
  letter-spacing: .08em;
  font-size: 12px;
}
.sk__nav a { color: #8a6f78; text-decoration: none; }

/* ヒーロー */
.sk__hero {
  position: relative;
  z-index: 2;
  height: calc(400px - 56px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: 12px;
  padding: 0 28px;
  pointer-events: none; /* トレイルを画面全体で楽しめるように */
}
.sk__tag {
  margin: 0;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .26em;
  color: #e06a92;
}
.sk__title {
  margin: 0;
  font-size: 36px;
  font-weight: 800;
  letter-spacing: .03em;
  color: #3a2b30;
  text-shadow: 0 2px 14px rgba(255,209,224,.8);
}
.sk__lead {
  margin: 0;
  font-size: 13px;
  line-height: 1.8;
  color: #7a626b;
}
.sk__cta {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  margin-top: 6px;
}
.sk__btn {
  padding: 11px 24px;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(135deg, #ff9bbb, #e06a92);
  border-radius: 999px;
  box-shadow: 0 12px 26px rgba(224,106,146,.4);
}
.sk__date {
  font-size: 12px;
  font-weight: 600;
  color: #8a6f78;
}
.sk__hint {
  margin: 4px 0 0;
  font-size: 11px;
  letter-spacing: .04em;
  color: #b39aa2;
}
JavaScript
// Sakura: canvasの虹色トレイル。待機中は仮想カーソルが自動で舞い、操作で本物に追従
(() => {
  const root = document.querySelector('[data-trail-root]');
  const canvas = document.querySelector('[data-canvas]');
  if (!root || !canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

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

  // 高解像度対応
  let dpr = Math.min(window.devicePixelRatio || 1, 2);
  const resize = () => {
    const r = root.getBoundingClientRect();
    canvas.width = Math.max(1, r.width * dpr);
    canvas.height = Math.max(1, r.height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  };
  resize();
  window.addEventListener('resize', resize);

  const trail = [];
  const MAX = reduce ? 14 : 28;
  let mx = -100, my = -100;
  let usePointer = false;   // 本物追従中か
  let lastMove = 0;
  const IDLE = 1500;        // 無操作で自動巡回へ(ms)

  root.addEventListener('pointermove', (e) => {
    const r = root.getBoundingClientRect();
    mx = e.clientX - r.left;
    my = e.clientY - r.top;
    usePointer = true;
    lastMove = performance.now();
  });
  root.addEventListener('pointerleave', () => { usePointer = false; });

  // 仮想カーソルの自動経路: ステージを横切るように舞う
  const autoPos = (t) => {
    const r = root.getBoundingClientRect();
    const cx = r.width / 2, cy = r.height * 0.52;
    return {
      x: cx + Math.sin(t * 0.0009) * r.width * 0.34,
      y: cy + Math.sin(t * 0.0015 + 1.2) * r.height * 0.22,
    };
  };

  // 桜ピンク寄りの虹色(ピンク〜紫〜橙)
  const hueAt = (s) => (320 + s * 120) % 360;

  const draw = (now) => {
    if (usePointer && now - lastMove > IDLE) usePointer = false;
    if (!usePointer) {
      const p = autoPos(now);
      mx = p.x; my = p.y;
    }

    const w = canvas.width / dpr;
    const h = canvas.height / dpr;
    ctx.clearRect(0, 0, w, h);

    trail.unshift({ x: mx, y: my });
    if (trail.length > MAX) trail.pop();

    // 発光する尾
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    for (let i = 0; i < trail.length - 1; i++) {
      const a = trail[i];
      const b = trail[i + 1];
      const s = i / MAX;
      const alpha = (1 - s) * 0.85;
      ctx.strokeStyle = `hsla(${hueAt(s)}, 90%, 72%, ${alpha})`;
      ctx.lineWidth = (1 - s) * 16 + 2;
      ctx.beginPath();
      ctx.moveTo(a.x, a.y);
      ctx.lineTo(b.x, b.y);
      ctx.stroke();
    }

    // 先頭の明るいコア
    if (trail.length) {
      const head = trail[0];
      ctx.fillStyle = 'rgba(255,255,255,0.95)';
      ctx.beginPath();
      ctx.arc(head.x, head.y, 5, 0, Math.PI * 2);
      ctx.fill();
    }

    requestAnimationFrame(draw);
  };
  requestAnimationFrame(draw);
})();

コード

HTML
<!-- カーソルトレイル:canvasに残光の軌跡を描く -->
<div class="stage" data-trail-root>
  <canvas class="trail-canvas" data-canvas></canvas>
  <div class="content">
    <h1 class="title">カーソルトレイル</h1>
    <p class="lead">マウスを動かすと、光の粒が尾を引いて流れます。</p>
  </div>
</div>
CSS
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }

.stage {
  position: relative;
  height: 360px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background:
    radial-gradient(800px 600px at 50% 50%, #1a1140 0%, transparent 70%),
    #07060f;
  color: #f3f0ff;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  cursor: none;
}

/* canvasは背面いっぱいに敷く */
.trail-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.content {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 24px;
  pointer-events: none; /* テキストはトレイルを邪魔しない */
}
.title {
  margin: 0 0 12px;
  font-size: clamp(32px, 6.5vw, 52px);
  font-weight: 800;
  letter-spacing: .03em;
  background: linear-gradient(90deg, #ff9ad4, #9ab8ff, #8affe6);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.lead { margin: 0; color: #c7c2e8; font-size: 14px; }
JavaScript
// カーソルトレイル:マウス位置にパーティクルを生成し、フェードしながら追従
(() => {
  const root = document.querySelector('[data-trail-root]');
  const canvas = document.querySelector('[data-canvas]');
  if (!root || !canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

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

  // 高解像度対応:実ピクセルに合わせてスケール
  let dpr = Math.min(window.devicePixelRatio || 1, 2);
  const resize = () => {
    const r = root.getBoundingClientRect();
    canvas.width = Math.max(1, r.width * dpr);
    canvas.height = Math.max(1, r.height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  };
  resize();
  window.addEventListener('resize', resize);

  // トレイル用の点列。新しい点ほど先頭に追加
  const trail = [];
  const MAX = reduce ? 12 : 26;
  let mx = -100, my = -100, hasMoved = false;

  root.addEventListener('pointermove', (e) => {
    const r = root.getBoundingClientRect();
    mx = e.clientX - r.left;
    my = e.clientY - r.top;
    hasMoved = true;
  });
  root.addEventListener('pointerleave', () => { hasMoved = false; });

  // 虹色のグラデを線分で描く
  const hueAt = (t) => (200 + t * 160) % 360;

  const draw = () => {
    const w = canvas.width / dpr;
    const h = canvas.height / dpr;
    ctx.clearRect(0, 0, w, h);

    if (hasMoved) {
      trail.unshift({ x: mx, y: my });
      if (trail.length > MAX) trail.pop();
    } else if (trail.length) {
      trail.pop(); // マウスが離れたら尾を縮める
    }

    // 線分を重ねて発光する尾を描画
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    for (let i = 0; i < trail.length - 1; i++) {
      const a = trail[i];
      const b = trail[i + 1];
      const t = i / MAX;
      const alpha = (1 - t) * 0.9;
      ctx.strokeStyle = `hsla(${hueAt(t)}, 95%, 65%, ${alpha})`;
      ctx.lineWidth = (1 - t) * 18 + 2;
      ctx.beginPath();
      ctx.moveTo(a.x, a.y);
      ctx.lineTo(b.x, b.y);
      ctx.stroke();
    }

    // 先頭に明るいコアの円
    if (trail.length) {
      const head = trail[0];
      ctx.fillStyle = 'rgba(255,255,255,0.95)';
      ctx.beginPath();
      ctx.arc(head.x, head.y, 6, 0, Math.PI * 2);
      ctx.fill();
    }

    requestAnimationFrame(draw);
  };
  requestAnimationFrame(draw);
})();

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

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

# 追加してほしい効果
カーソルトレイル(カスタムカーソル)
canvasにマウスの軌跡を残光として描く虹色トレイル。ヒーローセクションやランディングページの遊び心ある背景演出に向いています。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- カーソルトレイル:canvasに残光の軌跡を描く -->
<div class="stage" data-trail-root>
  <canvas class="trail-canvas" data-canvas></canvas>
  <div class="content">
    <h1 class="title">カーソルトレイル</h1>
    <p class="lead">マウスを動かすと、光の粒が尾を引いて流れます。</p>
  </div>
</div>

【CSS】
* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }

.stage {
  position: relative;
  height: 360px;
  overflow: hidden;
  display: grid;
  place-items: center;
  background:
    radial-gradient(800px 600px at 50% 50%, #1a1140 0%, transparent 70%),
    #07060f;
  color: #f3f0ff;
  font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
  cursor: none;
}

/* canvasは背面いっぱいに敷く */
.trail-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.content {
  position: relative;
  z-index: 1;
  text-align: center;
  padding: 24px;
  pointer-events: none; /* テキストはトレイルを邪魔しない */
}
.title {
  margin: 0 0 12px;
  font-size: clamp(32px, 6.5vw, 52px);
  font-weight: 800;
  letter-spacing: .03em;
  background: linear-gradient(90deg, #ff9ad4, #9ab8ff, #8affe6);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.lead { margin: 0; color: #c7c2e8; font-size: 14px; }

【JavaScript】
// カーソルトレイル:マウス位置にパーティクルを生成し、フェードしながら追従
(() => {
  const root = document.querySelector('[data-trail-root]');
  const canvas = document.querySelector('[data-canvas]');
  if (!root || !canvas) return; // null安全
  const ctx = canvas.getContext('2d');
  if (!ctx) return;

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

  // 高解像度対応:実ピクセルに合わせてスケール
  let dpr = Math.min(window.devicePixelRatio || 1, 2);
  const resize = () => {
    const r = root.getBoundingClientRect();
    canvas.width = Math.max(1, r.width * dpr);
    canvas.height = Math.max(1, r.height * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  };
  resize();
  window.addEventListener('resize', resize);

  // トレイル用の点列。新しい点ほど先頭に追加
  const trail = [];
  const MAX = reduce ? 12 : 26;
  let mx = -100, my = -100, hasMoved = false;

  root.addEventListener('pointermove', (e) => {
    const r = root.getBoundingClientRect();
    mx = e.clientX - r.left;
    my = e.clientY - r.top;
    hasMoved = true;
  });
  root.addEventListener('pointerleave', () => { hasMoved = false; });

  // 虹色のグラデを線分で描く
  const hueAt = (t) => (200 + t * 160) % 360;

  const draw = () => {
    const w = canvas.width / dpr;
    const h = canvas.height / dpr;
    ctx.clearRect(0, 0, w, h);

    if (hasMoved) {
      trail.unshift({ x: mx, y: my });
      if (trail.length > MAX) trail.pop();
    } else if (trail.length) {
      trail.pop(); // マウスが離れたら尾を縮める
    }

    // 線分を重ねて発光する尾を描画
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    for (let i = 0; i < trail.length - 1; i++) {
      const a = trail[i];
      const b = trail[i + 1];
      const t = i / MAX;
      const alpha = (1 - t) * 0.9;
      ctx.strokeStyle = `hsla(${hueAt(t)}, 95%, 65%, ${alpha})`;
      ctx.lineWidth = (1 - t) * 18 + 2;
      ctx.beginPath();
      ctx.moveTo(a.x, a.y);
      ctx.lineTo(b.x, b.y);
      ctx.stroke();
    }

    // 先頭に明るいコアの円
    if (trail.length) {
      const head = trail[0];
      ctx.fillStyle = 'rgba(255,255,255,0.95)';
      ctx.beginPath();
      ctx.arc(head.x, head.y, 6, 0, Math.PI * 2);
      ctx.fill();
    }

    requestAnimationFrame(draw);
  };
  requestAnimationFrame(draw);
})();

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

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