#07 ビジュアルコーディング パーティクル
軌跡とトレイル——動きの痕跡を描く
パーティクルはその瞬間の「点」ですが、軌跡(トレイル)を残すことで「動き」そのものを視覚化できます。流れ星・レーザービーム・光る蛇——どれも軌跡の表現です。
今回は残像・履歴配列・グラデーションという3つのアプローチを実装し、それぞれの特徴を理解します。
アプローチ1: 単純残像
最もシンプルな方法は、背景を半透明で上書きし続けることです。前回までのデモでも使っていた手法です。
// 完全消去(残像なし)
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
// 残像あり(約10フレームで消える)
ctx.fillStyle = 'rgba(13, 17, 23, 0.1)';
ctx.fillRect(0, 0, W, H);
アルファ値の意味:
0.05→ 約20フレーム残る(長い尾)0.1→ 約10フレーム残る(中程度の尾)0.3→ 約3フレーム残る(短い尾)1.0→ 残像なし
残像つきの軌道アニメーション——背景を薄く上書き
var t = 0;
var particles = [];
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.06)';
ctx.fillRect(0, 0, W, H);
t += 0.025;
var cx = W / 2 + Math.cos(t) * W * 0.28;
var cy = H / 2 + Math.sin(t * 1.7) * H * 0.3;
for (var i = 0; i < 3; i++) {
var angle = Math.random() * Math.PI * 2;
particles.push({
x: cx, y: cy,
vx: Math.cos(angle) * (Math.random() * 1.5 + 0.3),
vy: Math.sin(angle) * (Math.random() * 1.5 + 0.3),
life: 40, maxLife: 40,
hue: (t * 40) % 360
});
}
particles.forEach(function(p) {
p.x += p.vx; p.y += p.vy; p.life--;
var ratio = p.life / p.maxLife;
ctx.fillStyle = 'hsl(' + p.hue + ', 90%, 70%)';
ctx.globalAlpha = ratio;
ctx.beginPath();
ctx.arc(p.x, p.y, 2.5 * ratio, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
particles = particles.filter(function(p) { return p.life > 0; });
requestAnimationFrame(loop);
}
loop(); アプローチ2: 位置履歴配列
残像は「過去の描画内容が薄く残る」という副作用を利用しています。より明示的に軌跡を制御したい場合は、位置の履歴を配列に保存する方法を使います。
var history = [];
// 毎フレーム、現在位置を追加
history.push({ x: p.x, y: p.y });
// 一定数を超えたら古いものを削除
if (history.length > 20) {
history.shift(); // 先頭の要素を取り除く
}
保存した履歴を順番に描画します。古い点ほど小さく・透明にすることで、尾のような表現になります。
history.forEach(function(pos, i) {
var ratio = i / history.length; // 古いほど0、新しいほど1
ctx.globalAlpha = ratio;
ctx.fillStyle = 'cyan';
ctx.beginPath();
ctx.arc(pos.x, pos.y, 3 * ratio, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
20フレームの位置を繋いで軌跡を描く蛇
var snakes = [];
for (var s = 0; s < 3; s++) {
snakes.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 4,
vy: (Math.random() - 0.5) * 4,
history: [],
hue: s * 120
});
}
function loop() {
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
snakes.forEach(function(snake) {
snake.vx += (Math.random() - 0.5) * 0.4;
snake.vy += (Math.random() - 0.5) * 0.4;
var spd = Math.sqrt(snake.vx * snake.vx + snake.vy * snake.vy);
if (spd > 4) { snake.vx = snake.vx / spd * 4; snake.vy = snake.vy / spd * 4; }
snake.x += snake.vx; snake.y += snake.vy;
if (snake.x < 0 || snake.x > W) snake.vx *= -1;
if (snake.y < 0 || snake.y > H) snake.vy *= -1;
snake.x = Math.max(0, Math.min(W, snake.x));
snake.y = Math.max(0, Math.min(H, snake.y));
snake.history.push({ x: snake.x, y: snake.y });
if (snake.history.length > 30) snake.history.shift();
snake.history.forEach(function(pos, i) {
var ratio = (i + 1) / snake.history.length;
ctx.globalAlpha = ratio;
ctx.fillStyle = 'hsl(' + snake.hue + ', 90%, ' + (50 + ratio * 30) + '%)';
ctx.beginPath();
ctx.arc(pos.x, pos.y, 5 * ratio, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
});
});
requestAnimationFrame(loop);
}
loop(); アプローチ3: グラデーショントレイル
履歴配列の点を ctx.lineTo で繋げてラインとして描くと、より滑らかな軌跡になります。さらに lineWidth を変化させることでグラデーショントレイルになります。
for (var i = 1; i < history.length; i++) {
var ratio = i / history.length;
ctx.globalAlpha = ratio;
ctx.strokeStyle = 'hsl(180, 80%, 70%)';
ctx.lineWidth = 4 * ratio;
ctx.beginPath();
ctx.moveTo(history[i - 1].x, history[i - 1].y);
ctx.lineTo(history[i].x, history[i].y);
ctx.stroke();
ctx.globalAlpha = 1;
}
1区間ずつ描画して globalAlpha と lineWidth を変化させています。
宇宙船のような光るグラデーショントレイル
var ships = [];
for (var i = 0; i < 5; i++) {
var angle = Math.random() * Math.PI * 2;
ships.push({
x: W / 2, y: H / 2,
vx: Math.cos(angle) * 3,
vy: Math.sin(angle) * 3,
history: [],
hue: i * 72
});
}
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.4)';
ctx.fillRect(0, 0, W, H);
ships.forEach(function(ship) {
ship.vx += (Math.random() - 0.5) * 0.5;
ship.vy += (Math.random() - 0.5) * 0.5;
var spd = Math.sqrt(ship.vx * ship.vx + ship.vy * ship.vy);
if (spd > 4) { ship.vx = ship.vx / spd * 4; ship.vy = ship.vy / spd * 4; }
ship.x += ship.vx; ship.y += ship.vy;
if (ship.x < 0) ship.x = W;
if (ship.x > W) ship.x = 0;
if (ship.y < 0) ship.y = H;
if (ship.y > H) ship.y = 0;
ship.history.push({ x: ship.x, y: ship.y });
if (ship.history.length > 40) ship.history.shift();
for (var i = 1; i < ship.history.length; i++) {
var ratio = i / ship.history.length;
ctx.globalAlpha = ratio * 0.9;
ctx.strokeStyle = 'hsl(' + ship.hue + ', 90%, ' + (60 + ratio * 20) + '%)';
ctx.lineWidth = 3 * ratio + 0.5;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(ship.history[i - 1].x, ship.history[i - 1].y);
ctx.lineTo(ship.history[i].x, ship.history[i].y);
ctx.stroke();
ctx.globalAlpha = 1;
}
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(ship.x, ship.y, 3, 0, Math.PI * 2);
ctx.fill();
});
requestAnimationFrame(loop);
}
loop(); マウスに追従する光のトレイル
マウスの動きを追って輝く軌跡を作ります。マウスの動きを履歴として記録し、それをグラデーションラインで描画します。
マウスに追従する光のトレイル
var history = [];
var t = 0;
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.25)';
ctx.fillRect(0, 0, W, H);
t += 0.03;
if (history.length === 0 || Math.hypot(mx - history[history.length-1].x, my - history[history.length-1].y) > 2) {
history.push({ x: mx, y: my, hue: (t * 60) % 360 });
}
if (history.length > 60) history.shift();
for (var i = 1; i < history.length; i++) {
var ratio = i / history.length;
ctx.globalAlpha = ratio * 0.85;
ctx.strokeStyle = 'hsl(' + history[i].hue + ', 90%, ' + (55 + ratio * 25) + '%)';
ctx.lineWidth = 6 * ratio;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.beginPath();
ctx.moveTo(history[i - 1].x, history[i - 1].y);
ctx.lineTo(history[i].x, history[i].y);
ctx.stroke();
ctx.globalAlpha = 1;
}
if (history.length > 0) {
var last = history[history.length - 1];
ctx.globalAlpha = 0.9;
ctx.fillStyle = 'white';
ctx.beginPath();
ctx.arc(last.x, last.y, 4, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
requestAnimationFrame(loop);
}
loop(); マウスをキャンバス上で動かすと、虹色の光の軌跡が描かれます。
3つのアプローチの比較
| 手法 | メリット | デメリット |
|---|---|---|
| 残像(背景半透明上書き) | 実装が最も簡単・軽量 | 全パーティクルの残像が一律に消える |
| 位置履歴配列 | 軌跡の長さを個別に制御できる | メモリ使用量が増える |
| ラインで繋ぐ | 最も滑らかな軌跡 | 実装が少し複雑 |
単純な炎やパーティクルには残像が向いており、「特定のオブジェクトに長い尾を付けたい」場合は履歴配列を使います。
まとめ
この回でやったこと:
- 背景を
rgbaで薄く上書きする残像エフェクトを使った history.push/shiftで位置履歴を一定数に保ちながら管理した- 古い点ほど
globalAlphaとradiusを小さくして蛇状の尾を表現した ctx.lineToで連続した線を描き、グラデーショントレイルを実装した
次回は煙エフェクトを実装します。大きく広がる半透明の円とグラデーションを使って、柔らかい煙の表現を作ります。