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:

$$ a = \operatorname{atan2}(m_y - t_y,\; m_x - t_x) $$

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:

$$ v_x = s\cos a, \qquad v_y = s\sin a $$

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

  1. Make the target drift in a circle (Unit 2 math) so hits require timing, not just aiming.
  2. Cap the fire rate: allow one bullet per 0.25 s using a cooldown timestamp.
  3. 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.