Project: Turret Aiming
The problem
A turret sits at fixed \((t_x, t_y)\). It should rotate to face the mouse, and clicked shots should fly straight along the barrel. This is the core loop of tower defense, twin-stick shooters, and aiming UI.
Aim with atan2
Given mouse \((m_x, m_y)\), the barrel angle is:
On a y-down canvas this angle is clockwise-positive — which exactly matches ctx.rotate(), so drawing the barrel is one call. (This is why Inverse Trig Functions insisted on atan2 over atan: full-circle aiming needs all four quadrants.)
Bullets inherit the angle as velocity:
and a hit on a target of radius \(r\) at distance \(d\) is simply \(d < r\) — computed with Math.hypot.
Full build
var tx = W / 2, ty = H / 2; // turret
var mouse = { x: tx + 100, y: ty };
var bullets = [];
var target = { x: 120, y: 90, r: 22, hits: 0 };
canvas.addEventListener('mousemove', function (e) {
var rect = canvas.getBoundingClientRect();
var sx = canvas.width / rect.width; // canvas pixels per CSS pixel
mouse.x = (e.clientX - rect.left) * sx / (window.devicePixelRatio || 1);
mouse.y = (e.clientY - rect.top) * sx / (window.devicePixelRatio || 1);
});
canvas.addEventListener('click', function () {
var a = Math.atan2(mouse.y - ty, mouse.x - tx);
bullets.push({ x: tx, y: ty, vx: 420 * Math.cos(a), vy: 420 * Math.sin(a) });
});
var last = null;
loop(function (t) {
if (last === null) { last = t; }
var dt = Math.min(t - last, 0.05);
last = t;
clear();
// target
ctx.fillStyle = '#f472b6';
ctx.beginPath(); ctx.arc(target.x, target.y, target.r, 0, Math.PI * 2); ctx.fill();
// turret + barrel aimed at mouse
var a = Math.atan2(mouse.y - ty, mouse.x - tx);
ctx.save();
ctx.translate(tx, ty);
ctx.rotate(a);
ctx.fillStyle = '#e2e8f0';
ctx.fillRect(-14, -10, 28, 20);
ctx.fillStyle = '#38bdf8';
ctx.fillRect(0, -4, 34, 8);
ctx.restore();
// bullets
ctx.fillStyle = '#fbbf24';
bullets.forEach(function (b) { b.x += b.vx * dt; b.y += b.vy * dt; });
bullets = bullets.filter(function (b) {
var d = Math.hypot(b.x - target.x, b.y - target.y);
if (d < target.r) { target.hits++; print('hit! total: ' + target.hits); return false; }
return b.x > -20 && b.x < W + 20 && b.y > -20 && b.y < H + 20;
});
bullets.forEach(function (b) { ctx.beginPath(); ctx.arc(b.x, b.y, 5, 0, Math.PI * 2); ctx.fill(); });
});The mouse mapping divides out the playground’s device-pixel scaling so aim stays accurate on retina screens. In your own canvas code, use the same pattern whenever the backing store differs from CSS size.
Try it
- Make the target drift in a circle (Unit 2 math) so hits require timing, not just aiming.
- Cap the fire rate: allow one bullet per 0.25 s using a cooldown timestamp.
- Stretch goal: the target moves at constant velocity — aiming at it now misses. Intercepting it means solving for the meeting point (a quadratic in time). Try it!
Next: Project: Sound & Beats — hear what sine waves do.