#06 ビジュアルコーディング パーティクル
爆発エフェクト——インパクトのある瞬間を演出する
爆発は「瞬間」の表現です。ゲームのヒットエフェクト、UI のインタラクション強調、映像のオープニングなど、あらゆる場面で「インパクトの瞬間」を強調するために使われます。
本物らしい爆発は複数の要素で構成されています。今回はそれぞれを個別に実装し、最終的に統合します。
爆発の構成要素
迫力のある爆発は次の4つの要素が重なって成立します。
| 要素 | 特徴 | 持続時間 |
|---|---|---|
| 閃光 | 大きな白/黄の半透明円、一瞬で消える | 極短(5〜10フレーム) |
| 衝撃波 | 円が急速に広がりながら透明になる | 短(10〜20フレーム) |
| 破片 | 小さな断片がランダム方向に飛び散る | 中(30〜60フレーム) |
| 煙 | 大きく広がる半透明の円、ゆっくり消える | 長(60〜120フレーム) |
これらを同時に発生させ、それぞれ異なる速度で消えることで、瞬間→残響というリズムが生まれます。
閃光——一瞬の白い輝き
閃光は大きな半透明円で、非常に短時間で消えます。
function Flash(x, y) {
this.x = x; this.y = y;
this.life = 8; this.maxLife = 8;
}
Flash.prototype.draw = function() {
var ratio = this.life / this.maxLife;
ctx.globalAlpha = ratio * 0.8;
ctx.fillStyle = 'rgba(255, 220, 100, 1)';
ctx.beginPath();
ctx.arc(this.x, this.y, 60 * (1 - ratio * 0.5), 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
};
生まれた直後が最も大きく明るく、急速に消えることで「閃光」らしさが出ます。
衝撃波——拡大する輪
衝撃波は塗りつぶし円ではなく「輪(stroke)」を使います。半径が大きくなるにつれて透明になります。
function ShockWave(x, y) {
this.x = x; this.y = y;
this.radius = 5;
this.life = 20; this.maxLife = 20;
}
ShockWave.prototype.draw = function() {
var ratio = this.life / this.maxLife;
this.radius += 8; // 毎フレーム拡大
ctx.globalAlpha = ratio * 0.6;
ctx.strokeStyle = 'rgba(255, 200, 80, 1)';
ctx.lineWidth = 3 * ratio;
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
};
クリックで爆発——閃光・衝撃波デモ
var flashes = [], waves = [];
function explodeAt(x, y) {
flashes.push({ x: x, y: y, life: 10, maxLife: 10 });
for (var i = 0; i < 3; i++) {
waves.push({ x: x, y: y, r: 5 + i * 15, life: 20 - i * 3, maxLife: 20 - i * 3 });
}
}
(function() {
var el = document.querySelector('canvas');
if (el) el.addEventListener('click', function() { explodeAt(mx, my); });
})();
explodeAt(W / 2, H / 2);
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.3)';
ctx.fillRect(0, 0, W, H);
flashes.forEach(function(f) {
var ratio = f.life / f.maxLife;
ctx.globalAlpha = ratio * 0.9;
ctx.fillStyle = 'rgba(255,220,100,1)';
ctx.beginPath();
ctx.arc(f.x, f.y, 80 * (1.2 - ratio * 0.4), 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
f.life--;
});
waves.forEach(function(w) {
var ratio = w.life / w.maxLife;
w.r += 7;
ctx.globalAlpha = ratio * 0.7;
ctx.strokeStyle = 'rgba(255,180,60,1)';
ctx.lineWidth = 4 * ratio;
ctx.beginPath();
ctx.arc(w.x, w.y, w.r, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
w.life--;
});
flashes = flashes.filter(function(f) { return f.life > 0; });
waves = waves.filter(function(w) { return w.life > 0; });
requestAnimationFrame(loop);
}
loop(); 破片パーティクル——回転する矩形片
破片は円ではなく矩形(四角形)で表現します。矩形を回転させると「破片らしさ」が増します。
ctx.save() / ctx.translate() / ctx.rotate() / ctx.restore() を使って、破片ごとに異なる回転を適用します。
ctx.save();
ctx.translate(p.x, p.y); // 破片の位置に移動
ctx.rotate(p.angle); // 破片の角度に回転
ctx.fillRect(-p.size/2, -p.size/2, p.size, p.size); // 中心を原点として描画
ctx.restore(); // 元の変換行列に戻す
translate でキャンバスの原点を破片の中心に移動してから rotate することで、破片が自分の中心を軸に回転します。
破片が飛び散る爆発——ctx.rotate で矩形片を回転
var particles = [], flashes = [], waves = [];
function explodeAt(x, y) {
flashes.push({ x: x, y: y, life: 10, maxLife: 10 });
waves.push({ x: x, y: y, r: 10, life: 18, maxLife: 18 });
var n = 30 + Math.floor(Math.random() * 20);
for (var i = 0; i < n; i++) {
var angle = Math.random() * Math.PI * 2;
var speed = Math.random() * 6 + 2;
particles.push({
x: x, y: y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
angle: Math.random() * Math.PI * 2,
spin: (Math.random() - 0.5) * 0.3,
size: Math.random() * 8 + 3,
life: 50 + Math.floor(Math.random() * 30),
maxLife: 80,
hue: 30 + Math.random() * 40
});
}
}
(function() {
var el = document.querySelector('canvas');
if (el) el.addEventListener('click', function() { explodeAt(mx, my); });
})();
explodeAt(W / 2, H / 2);
function loop() {
ctx.fillStyle = 'rgba(13,17,23,0.25)';
ctx.fillRect(0, 0, W, H);
flashes.forEach(function(f) {
var ratio = f.life / f.maxLife;
ctx.globalAlpha = ratio * 0.85;
ctx.fillStyle = 'rgba(255,220,80,1)';
ctx.beginPath();
ctx.arc(f.x, f.y, 70, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
f.life--;
});
waves.forEach(function(w) {
var ratio = w.life / w.maxLife;
w.r += 9;
ctx.globalAlpha = ratio * 0.6;
ctx.strokeStyle = 'rgba(255,160,40,1)';
ctx.lineWidth = 3 * ratio;
ctx.beginPath();
ctx.arc(w.x, w.y, w.r, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
w.life--;
});
particles.forEach(function(p) {
p.vy += 0.18;
p.vx *= 0.97; p.vy *= 0.97;
p.x += p.vx; p.y += p.vy;
p.angle += p.spin;
p.life--;
var ratio = p.life / p.maxLife;
ctx.globalAlpha = ratio;
ctx.fillStyle = 'hsl(' + p.hue + ', 90%, ' + (40 + ratio * 40) + '%)';
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.angle);
ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size * 0.5);
ctx.restore();
ctx.globalAlpha = 1;
});
flashes = flashes.filter(function(f) { return f.life > 0; });
waves = waves.filter(function(w) { return w.life > 0; });
particles = particles.filter(function(p) { return p.life > 0; });
requestAnimationFrame(loop);
}
loop(); 後処理——煙パーティクルが上昇する
爆発後に煙パーティクルを追加すると、残響感が生まれます。煙は大きく・ゆっくり・上に流れます。
for (var i = 0; i < 8; i++) {
var delay = i * 5; // 時間差で出現
smokeParticles.push({
x: x + (Math.random() - 0.5) * 40,
y: y,
vx: (Math.random() - 0.5) * 0.8,
vy: -(Math.random() * 1.5 + 0.5),
radius: Math.random() * 10 + 5,
life: 80 + delay,
maxLife: 80 + delay,
delay: delay
});
}
シェイクエフェクト
爆発の瞬間にキャンバスを揺らすことで、物理的なインパクトを感じさせます。
var shakeTime = 0;
var shakeAmount = 0;
function triggerShake(amount) {
shakeAmount = amount;
shakeTime = 15;
}
function loop() {
if (shakeTime > 0) {
var dx = (Math.random() - 0.5) * shakeAmount;
var dy = (Math.random() - 0.5) * shakeAmount;
ctx.save();
ctx.translate(dx, dy);
shakeTime--;
shakeAmount *= 0.9;
}
// ... 描画処理 ...
if (shakeTime > 0) ctx.restore();
}
ctx.translate(dx, dy) でキャンバス全体をランダムに微小移動させることで揺れを表現します。
まとめ
この回でやったこと:
- 爆発を「閃光・衝撃波・破片・煙」の4要素に分解した
- 閃光を半透明の大きな円で表現した
- 衝撃波を
ctx.strokeと拡大する半径で表現した - 破片を
ctx.save/translate/rotate/restoreで回転する矩形として描いた spinプロパティで毎フレーム回転角度を更新した
次回は軌跡とトレイルを実装します。パーティクルが通った道を光の線として残すエフェクトです。