マウス追従粒子トレイル
ポインタの軌跡に沿って虹色の粒子を放出し、加算合成で発光させるインタラクティブ演出。カーソル装飾やゲームUIに使えます。
ライブデモ
使用例(お題: アイドルグループ 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で提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。