力と加速度——F=maをキャンバスで
前回は「速度があれば物体は動き続ける」という慣性を実装しました。しかし現実の世界では、物体は加速したり減速したりします。その原因が力(Force)です。
ニュートンの第二法則はたった1行の式で書けます。
F = m * a
力は質量と加速度の積に等しい。これをコードに落とし込むことで、重力・風・爆発・バネ——あらゆる力を扱えるようになります。
F = ma をコードに変換する
物理シミュレーションでは、この式を次のように変形して使います。
a = F / m (加速度は力を質量で割ったもの)
v += a * dt (速度は加速度を積分)
x += v * dt (位置は速度を積分)
コードに直すと:
// 毎フレームの更新
var ax = Fx / mass; // X方向の加速度
var ay = Fy / mass; // Y方向の加速度
vx += ax * dt; // 速度を更新
vy += ay * dt;
x += vx * dt; // 位置を更新
y += vy * dt;
この「加速度 → 速度 → 位置」という3段階の積分がすべての物理シミュレーションの骨格です。
重力加速度の実装
地球上の重力加速度は約 9.8 m/s² ですが、ピクセル単位のシミュレーションでは単位を合わせる必要があります。スクリーン上では 200〜400 px/s² 程度を使うことが多いです。
重力は常に下向き(Y方向の正)に働く一定の加速度です。
var GRAVITY = 300; // px/s² (9.8 m/s² に相当するスケール)
// 毎フレーム
vy += GRAVITY * dt; // Y速度が増え続ける(下に加速)
vx には何も足していないので、水平方向は等速のまま——これが放物運動です。
var x = W / 2, y = 30;
var vx = 0, vy = 0;
var GRAVITY = 350;
var r = 16;
function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;
vy += GRAVITY * dt;
y += vy * dt;
x += vx * dt;
if (y + r > H) {
y = H - r;
vy = -vy * 0.75;
}
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fillStyle = '#ff6b6b';
ctx.fill();
ctx.fillStyle = '#333';
ctx.fillRect(0, H - 4, W, 4);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 重力があると速度が毎フレーム増加し、ボールが徐々に加速して落ちていきます。床に当たると速度を反転させ(vy = -vy * 0.75)、少し弱めて跳ね返ります。
放物運動(斜め上に投げる)
初速度に水平成分と垂直成分を両方持たせると、放物線を描く投擲運動になります。軌跡を描いて可視化してみましょう。
var GRAVITY = 280;
var balls = [];
var timer = 0;
function spawnBall() {
var angle = -Math.PI / 4 - Math.random() * Math.PI / 4;
var speed = 300 + Math.random() * 150;
balls.push({
x: 30,
y: H - 20,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
hue: Math.random() * 360,
trail: []
});
}
spawnBall();
function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;
timer += dt;
if (timer > 1.5) {
timer = 0;
if (balls.length < 5) spawnBall();
}
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#1a2a1a';
ctx.fillRect(0, H - 20, W, 20);
balls = balls.filter(function(b) { return b.y < H + 50; });
balls.forEach(function(b) {
b.vy += GRAVITY * dt;
b.x += b.vx * dt;
b.y += b.vy * dt;
b.trail.push({x: b.x, y: b.y});
if (b.trail.length > 40) b.trail.shift();
for (var i = 1; i < b.trail.length; i++) {
ctx.beginPath();
ctx.moveTo(b.trail[i-1].x, b.trail[i-1].y);
ctx.lineTo(b.trail[i].x, b.trail[i].y);
ctx.strokeStyle = 'hsla(' + b.hue + ',80%,60%,' + (i / b.trail.length) + ')';
ctx.lineWidth = 2;
ctx.stroke();
}
ctx.beginPath();
ctx.arc(b.x, b.y, 8, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + b.hue + ',80%,65%)';
ctx.fill();
});
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 軌跡が美しい放物線を描きます。水平速度は変わらず、垂直速度だけが重力によって増加していくのが分かります。
複数の力の合成
1つの物体に複数の力が働くときは、X・Y方向それぞれで足し合わせます。
// 複数の力を合成する
var Fx = 0, Fy = 0;
// 重力
Fy += mass * GRAVITY;
// 風(右向き)
Fx += WIND_FORCE;
// 推力(上向き)
Fy -= THRUST;
// 合成した加速度を計算
var ax = Fx / mass;
var ay = Fy / mass;
vx += ax * dt;
vy += ay * dt;
このように「力を全部足してから加速度に変換する」のが正しい手順です。
空気抵抗の実装
空気抵抗は速度に比例した、速度と逆方向の力です。
F_drag = -k * v
k は抵抗係数、v は速度です。速いほど大きな抵抗がかかります。
var k = 0.5; // 空気抵抗係数
// 毎フレーム
var drag_x = -k * vx;
var drag_y = -k * vy;
// 重力と空気抵抗を合成
var ax = drag_x / mass;
var ay = (mass * GRAVITY + drag_y) / mass;
vx += ax * dt;
vy += ay * dt;
var GRAVITY = 300;
var balls = [
{x: W*0.3, y: 30, vx: 200, vy: -50, k: 0, hue: 0, label: '抵抗なし'},
{x: W*0.3, y: 30, vx: 200, vy: -50, k: 1.5, hue: 120, label: '抵抗あり(k=1.5)'},
];
function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#1a1a2a';
ctx.fillRect(0, H - 16, W, 16);
balls.forEach(function(b) {
if (b.y < H - 16) {
var dragX = -b.k * b.vx;
var dragY = -b.k * b.vy;
b.vx += (dragX) * dt;
b.vy += (GRAVITY + dragY) * dt;
b.x += b.vx * dt;
b.y += b.vy * dt;
}
ctx.beginPath();
ctx.arc(b.x, b.y, 12, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + b.hue + ',80%,60%)';
ctx.fill();
ctx.fillStyle = 'hsl(' + b.hue + ',80%,75%)';
ctx.font = '12px monospace';
ctx.fillText(b.label, 8, b.hue === 0 ? 20 : 40);
});
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 赤(抵抗なし)は水平方向に速度を保ったまま遠くまで飛びます。緑(抵抗あり)は速度に比例した抵抗で徐々に減速し、短い距離で落下します。
質量の違いによる運動の差
F = ma から a = F / m を考えると、同じ力でも質量が大きいほど加速しにくいことが分かります。
重力は F = m * g なので、加速度 a = F/m = m*g/m = g となり、質量によらず同じ加速度になります(これはガリレオが発見した「重力加速度は質量によらず同じ」という事実と一致しています)。
しかし、空気抵抗は速度に比例するため、同じ速度でも軽いものの方が大きな影響を受けます。
// 重力加速度:質量に関係なく同じ
ay_gravity = GRAVITY;
// 空気抵抗の加速度:質量が大きいほど影響が小さい
ay_drag = -k * vy / mass;
var GRAVITY = 300;
var K = 0.8;
var masses = [0.5, 1, 2, 4];
var balls = masses.map(function(m, i) {
return {
x: (i + 1) * W / (masses.length + 1),
y: 30,
vx: 0,
vy: 0,
mass: m,
hue: i * 90
};
});
function loop(ts) {
if (!loop.last) loop.last = ts;
var dt = Math.min((ts - loop.last) / 1000, 0.05);
loop.last = ts;
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#1a1a2a';
ctx.fillRect(0, H - 16, W, 16);
balls.forEach(function(b, i) {
if (b.y < H - 16 - b.mass * 8) {
var dragY = -K * b.vy / b.mass;
b.vy += (GRAVITY + dragY) * dt;
b.y += b.vy * dt;
} else {
b.vy = 0;
b.y = H - 16 - b.mass * 8;
}
var r = Math.max(6, b.mass * 8);
ctx.beginPath();
ctx.arc(b.x, b.y, r, 0, Math.PI * 2);
ctx.fillStyle = 'hsl(' + b.hue + ',80%,60%)';
ctx.fill();
ctx.fillStyle = '#aaa';
ctx.font = '11px monospace';
ctx.textAlign = 'center';
ctx.fillText('m=' + b.mass, b.x, H - 2);
ctx.textAlign = 'left';
});
requestAnimationFrame(loop);
}
requestAnimationFrame(loop); 軽いボールは空気抵抗の影響が大きく、ゆっくり落ちます。重いボールは抵抗の影響が小さく、速く落ちます。
まとめ
この回でやったこと:
F = ma→a = F/m→v += a*dt→x += v*dtという連鎖を実装した- 重力加速度(
vy += GRAVITY * dt)を実装し、放物運動を作った - 複数の力を合成する手順(X・Y方向で加算)を学んだ
- 空気抵抗
F = -k * vの実装と、抵抗なしとの比較を確認した - 質量によって空気抵抗の影響が変わることを確認した
次回はこの「更新則」の数値解法——オイラー法を詳しく掘り下げます。dtを大きくすると誤差が蓄積される仕組みを可視化し、より精度の高い中点法との比較を行います。