Introduction

Every self-driving car, warehouse robot, and autonomous lawn mower shares a humble ancestor: a 1992 report from Carnegie Mellon titled "Implementation of the Pure Pursuit Path Tracking Algorithm." The idea inside is disarmingly simple.

Imagine you are driving and you fix your eyes on a point some distance ahead on the road. You don't plan the whole journey — you just steer toward that point, let the car move, pick a new point ahead, and repeat. That loop, executed dozens of times per second, is Pure Pursuit.

The algorithm was formalized by R. Craig Coulter for the NavLab autonomous vehicles project. It belongs to the family of geometric controllers: no sensor fusion, no predictive model, no optimization — just Euclidean geometry applied repeatedly. A circle through the vehicle's current position and the look-ahead point determines the exact steering angle needed.

The one free parameter you choose is the look-ahead distance LdL_d. Small LdL_d and the vehicle hugs tight corners but oscillates on straights. Large LdL_d and motion is smooth but corners are cut. That single knob is where all the engineering judgement lives.

Try It

The car below follows a closed oval track using Pure Pursuit. The orange dot is the look-ahead point — the spot on the path one look-ahead distance ahead of the car. The car always steers toward it.

<!-- Pure Pursuit demo — car follows an oval track by steering toward a look-ahead point -->
<div class="controls">
  <label for="ld-slider">Look-ahead: <span id="ld-val">60</span>px</label>
  <input id="ld-slider" type="range" min="20" max="140" value="60" step="5">
  <button id="btn-reset" type="button">Reset</button>
</div>
<canvas id="track" width="480" height="320"></canvas>
<div class="status" id="status">Running — drag the slider to change the look-ahead distance.</div>
/* Pure Pursuit demo styles — track canvas + controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls {
  display: flex; align-items: center; gap: .7rem; flex-wrap: wrap;
  margin-bottom: .6rem; font-size: .9rem;
}
label { font-weight: 600; white-space: nowrap; }
input[type=range] { width: 130px; accent-color: #1d3557; cursor: pointer; }
button {
  font: 600 13px system-ui; padding: .35rem .8rem;
  border: 1px solid #1d3557; background: #1d3557;
  color: #fff; border-radius: 6px; cursor: pointer;
}
canvas {
  display: block; border-radius: 10px;
  border: 1px solid #cdd9e3; background: #f5f8fb;
  max-width: 100%;
}
.status {
  margin-top: .5rem; font-size: .9rem; font-weight: 600; color: #1d3557;
  min-height: 1.3em;
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Canvas dimensions match the HTML attributes above */
const W = 480, H = 320;
/* Oval path centred on the canvas; RX/RY are the semi-axes in pixels */
const CX = W / 2, CY = H / 2, RX = 180, RY = 110;
const NPTS = 200;
/* Pre-compute NPTS evenly-spaced points around the ellipse */
function buildPath() {
  const pts = [];
  for (let i = 0; i < NPTS; i++) {
    const a = (2 * Math.PI * i) / NPTS;
    pts.push({ x: CX + RX * Math.cos(a), y: CY + RY * Math.sin(a) });
  }
  return pts;
}
const PATH = buildPath();

/* Car state — position, heading (radians), look-ahead distance, speed */
let car = { x: CX + RX, y: CY, heading: -Math.PI / 2 };
let Ld = 60;
const SPEED = 1.8;
const WHEELBASE = 28;
let animId = null;
let closestIdx = 0;

/* Return the path point closest to Ld ahead of the car position */
function findLookahead(cx, cy, heading) {
  let best = null, bestDist = Infinity;
  /* Walk forward from the nearest point until we bracket the look-ahead distance */
  for (let i = 0; i < PATH.length; i++) {
    const idx = (closestIdx + i) % PATH.length;
    const p = PATH[idx];
    const dx = p.x - cx, dy = p.y - cy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (Math.abs(dist - Ld) < bestDist) {
      bestDist = Math.abs(dist - Ld);
      best = { p, idx };
    }
    if (dist > Ld + 40) break;
  }
  return best ? best.p : PATH[(closestIdx + 1) % PATH.length];
}

/* Keep track of the nearest path index so look-ahead search stays local */
function updateClosest(cx, cy) {
  let minD = Infinity, idx = closestIdx;
  for (let i = -5; i < 15; i++) {
    const j = (closestIdx + NPTS + i) % NPTS;
    const p = PATH[j];
    const d = (p.x - cx) ** 2 + (p.y - cy) ** 2;
    if (d < minD) { minD = d; idx = j; }
  }
  closestIdx = idx;
}

/* One Pure Pursuit control step — compute steering, move car */
function step() {
  updateClosest(car.x, car.y);
  const lp = findLookahead(car.x, car.y, car.heading);

  /* Rotate the look-ahead offset into the car's local frame (x=forward, y=left) */
  const dx = lp.x - car.x, dy = lp.y - car.y;
  const localX =  dx * Math.cos(-car.heading) - dy * Math.sin(-car.heading);
  const localY =  dx * Math.sin(-car.heading) + dy * Math.cos(-car.heading);

  /* alpha = angle to look-ahead in local frame; kappa = 2*sin(alpha)/Ld */
  const alpha = Math.atan2(localY, localX);
  const kappa = 2 * Math.sin(alpha) / Ld;
  const delta = Math.atan(WHEELBASE * kappa);

  /* Bicycle-model integration — heading and position update */
  car.heading += (SPEED / WHEELBASE) * Math.tan(delta);
  car.x += SPEED * Math.cos(car.heading);
  car.y += SPEED * Math.sin(car.heading);
}

/* Render one animation frame — path, look-ahead indicator, car body */
function draw() {
  const canvas = document.getElementById('track');
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, W, H);

  /* Draw thick grey oval as the road surface */
  ctx.beginPath();
  PATH.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
  ctx.closePath();
  ctx.strokeStyle = '#b0bec5';
  ctx.lineWidth = 10;
  ctx.stroke();

  /* White dashed centre line over the road */
  ctx.beginPath();
  PATH.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
  ctx.closePath();
  ctx.strokeStyle = '#fff';
  ctx.setLineDash([6, 8]);
  ctx.lineWidth = 1.5;
  ctx.stroke();
  ctx.setLineDash([]);

  /* Dashed line from car to current look-ahead point */
  const lp = findLookahead(car.x, car.y, car.heading);
  ctx.beginPath();
  ctx.moveTo(car.x, car.y);
  ctx.lineTo(lp.x, lp.y);
  ctx.strokeStyle = 'rgba(230,57,70,.45)';
  ctx.lineWidth = 1.5;
  ctx.setLineDash([4, 5]);
  ctx.stroke();
  ctx.setLineDash([]);

  /* Orange dot marking the look-ahead point on the path */
  ctx.beginPath();
  ctx.arc(lp.x, lp.y, 5, 0, 2 * Math.PI);
  ctx.fillStyle = '#e63946';
  ctx.fill();

  /* Car body — dark blue rounded rectangle, rotated to current heading */
  ctx.save();
  ctx.translate(car.x, car.y);
  ctx.rotate(car.heading);
  ctx.fillStyle = '#1d3557';
  ctx.beginPath();
  ctx.roundRect(-10, -5, 20, 10, 3);
  ctx.fill();
  /* Small arrow on the front of the car to show heading direction */
  ctx.fillStyle = '#a8dadc';
  ctx.beginPath();
  ctx.moveTo(8, 0); ctx.lineTo(3, -4); ctx.lineTo(3, 4);
  ctx.closePath();
  ctx.fill();
  ctx.restore();
}

function loop() {
  step();
  draw();
  animId = requestAnimationFrame(loop);
}

/* Stop animation, reset car to start position, restart loop */
function reset() {
  if (animId) cancelAnimationFrame(animId);
  car = { x: CX + RX, y: CY, heading: -Math.PI / 2 };
  closestIdx = 0;
  draw();
  document.getElementById('status').textContent = 'Running — drag the slider to change the look-ahead distance.';
  animId = requestAnimationFrame(loop);
}

/* Update Ld from the range slider and display the new value */
document.getElementById('ld-slider').addEventListener('input', function () {
  Ld = +this.value;
  document.getElementById('ld-val').textContent = this.value;
  document.getElementById('status').textContent = i18nFmt('Look-ahead distance set to %ld% px. Watch the corner behaviour change.', { ld: this.value });
});
document.getElementById('btn-reset').addEventListener('click', reset);

reset();
Result
Console

Drag the Look-ahead slider and watch what changes. A short look-ahead makes the car react sharply to every bend; a long one smooths the ride but clips the corners. The algorithm computes a circular arc from the car's position through the look-ahead point, and the curvature of that arc becomes the steering command.

The Geometry

Pure Pursuit's steering law comes from a single geometric fact. Transform the coordinate system so the vehicle sits at the origin facing right. The look-ahead point (x,y)(x, y) lies on the path a distance LdL_d away. The unique circle passing through both the origin (vehicle) and (x,y)(x, y) that is tangent to the vehicle's heading has radius:

R=Ld22∣y∣R = \frac{L_d^2}{2 \lvert y \rvert}

Curvature is the reciprocal: κ=2∣y∣Ld2\kappa = \frac{2 \lvert y \rvert}{L_d^2}.

For a vehicle with wheelbase WW (distance between axles), the front-wheel steering angle δ\delta satisfies tan⁡δ=Wκ\tan \delta = W \kappa, giving:

δ=arctan⁡ ⁣(2W∣y∣Ld2)\delta = \arctan\!\left(\frac{2 W \lvert y \rvert}{L_d^2}\right)

where yy is the lateral offset of the look-ahead point in the vehicle's frame. Positive yy means steer left; negative means steer right. That is the entire algorithm — one formula, executed at every timestep.

Stability and convergence. Pure Pursuit is not derived from control theory; it is purely geometric. For a straight path it converges exponentially, but on tight curves with a large LdL_d it can develop a sustained chordal error (cutting the corner). The parameter LdL_d is often made speed-dependent — Ld=k⋅vL_d = k \cdot v for some constant kk — so the look-ahead scales with how fast the vehicle is moving, maintaining the same reaction time regardless of speed.

Where It Matters

Pure Pursuit's simplicity makes it surprisingly durable in real systems:

  • Autonomous cars (low-speed): parking lots, campus shuttles, and warehouse AGVs rely on Pure Pursuit because it is easy to tune and robust to small localization errors.
  • Agricultural robotics: tractors and harvesters follow GPS waypoints at low speed on predictable terrain — exactly the regime where Pure Pursuit shines.
  • Competitive robotics: Formula Student Driverless teams frequently use Pure Pursuit as their baseline path-following controller before adding more sophisticated planners.
  • Drone waypoint navigation: UAVs following a horizontal ground track apply the same circular-arc geometry in the horizontal plane.
  • Teaching control theory: Pure Pursuit is the canonical first example before introducing PID, LQR, and model-predictive control — its visual clarity makes the feedback loop tangible.

More sophisticated controllers (Stanley, MPC, LQR) outperform Pure Pursuit on high-speed or dynamically unstable vehicles, but for the vast majority of mobile robots that move slowly over known paths, Pure Pursuit remains the first tool engineers reach for.

Conclusion

Pure Pursuit is a reminder that the best algorithms are not always the most complex. A car steers toward a point ahead on the path, moves a little, picks a new point, and repeats — and from that loop emerges smooth, reliable path following across farms, warehouses, and campuses worldwide.

The single free parameter, look-ahead distance LdL_d, encodes the entire trade-off between agility and smoothness. Tune it well and the vehicle tracks any curve; tune it poorly and it oscillates or cuts corners. That simplicity is also a limitation: when vehicles move fast, when paths curve sharply, or when dynamics matter, engineers reach for richer controllers. But as a foundation — and as an introduction to the geometry of autonomous navigation — Pure Pursuit has no equal.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/pure-pursuit/Content licensed under CC BY-NC 4.0.