Tutorial
⏱️ ~55 minutes 🎓 Intermediate 🛠️ JavaScript · Canvas 2D · Algorithms

Build a Pathfinding Visualizer

From scratch in vanilla JavaScript: a canvas grid where you can draw walls, pick start/end points, and watch Dijkstra or A* explore the maze cell by cell. Along the way you'll implement a binary min-heap priority queue, admissible heuristics, and smooth step-by-step animation.

Prerequisites

Grid Data Structure and Rendering

The grid is a flat Uint8Array for cache efficiency. Each cell is: 0 = open, 1 = wall, 2 = start, 3 = end, 4 = visited, 5 = path:

const COLS = 40, ROWS = 30;
const CELL = 20; // pixels per cell
const canvas = document.getElementById('grid');
const ctx = canvas.getContext('2d');
canvas.width  = COLS * CELL;
canvas.height = ROWS * CELL;

const grid = new Uint8Array(COLS * ROWS); // flat array

const idx = (c, r) => r * COLS + c;

const COLORS = {
  0: '#1a1a2e', // open
  1: '#334155', // wall
  2: '#22c55e', // start
  3: '#ef4444', // end
  4: '#1e40af', // visited
  5: '#facc15', // path
};

function render() {
  for (let r = 0; r < ROWS; r++) {
    for (let c = 0; c < COLS; c++) {
      ctx.fillStyle = COLORS[grid[idx(c, r)]];
      ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 2, CELL - 2);
    }
  }
}

Binary Min-Heap Priority Queue

Both Dijkstra and A* need to efficiently pop the minimum-cost node. A binary min-heap gives O(log N) push/pop:

class MinHeap {
  constructor() { this.data = []; }
  push(item) {
    this.data.push(item);
    this._bubbleUp(this.data.length - 1);
  }
  pop() {
    const top = this.data[0];
    const last = this.data.pop();
    if (this.data.length) { this.data[0] = last; this._sinkDown(0); }
    return top;
  }
  get size() { return this.data.length; }
  _bubbleUp(i) {
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (this.data[parent].f <= this.data[i].f) break;
      [this.data[parent], this.data[i]] = [this.data[i], this.data[parent]];
      i = parent;
    }
  }
  _sinkDown(i) {
    const n = this.data.length;
    while (true) {
      let smallest = i;
      const l = 2*i+1, r = 2*i+2;
      if (l < n && this.data[l].f < this.data[smallest].f) smallest = l;
      if (r < n && this.data[r].f < this.data[smallest].f) smallest = r;
      if (smallest === i) break;
      [this.data[smallest], this.data[i]] = [this.data[i], this.data[smallest]];
      i = smallest;
    }
  }
}

Dijkstra's Algorithm

Dijkstra guarantees the shortest path on uniform-cost grids. It explores in all directions simultaneously, like water flooding outward:

function dijkstra(startC, startR, endC, endR) {
  const dist = new Float32Array(COLS * ROWS).fill(Infinity);
  const prev = new Int32Array(COLS * ROWS).fill(-1);
  const heap = new MinHeap();

  const start = idx(startC, startR);
  dist[start] = 0;
  heap.push({ f: 0, i: start });

  const steps = []; // for animation

  while (heap.size) {
    const { i } = heap.pop();
    if (i === idx(endC, endR)) break;

    const c = i % COLS, r = Math.floor(i / COLS);
    steps.push({ type: 'visit', i });

    // 4-directional neighbours
    for (const [dc, dr] of [[1,0],[-1,0],[0,1],[0,-1]]) {
      const nc = c + dc, nr = r + dr;
      if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) continue;
      const ni = idx(nc, nr);
      if (grid[ni] === 1) continue; // wall
      const newDist = dist[i] + 1;
      if (newDist < dist[ni]) {
        dist[ni] = newDist;
        prev[ni] = i;
        heap.push({ f: newDist, i: ni });
      }
    }
  }

  // Reconstruct path
  const path = [];
  let cur = idx(endC, endR);
  while (cur !== -1) { path.push(cur); cur = prev[cur]; }
  return { steps, path: path.reverse() };
}

A* with Manhattan Heuristic

A* adds a heuristic to guide expansion towards the goal. On a grid without diagonals, Manhattan distance is admissible (never overestimates) and efficient:

Dijkstra

  • f = g (cost from start)
  • Explores all directions equally
  • Guaranteed shortest path
  • Slower on large open grids

A*

  • f = g + h (cost + heuristic)
  • Biased towards the goal
  • Optimal if h is admissible
  • Much faster in practice
// Manhattan distance heuristic
function heuristic(c, r, endC, endR) {
  return Math.abs(c - endC) + Math.abs(r - endR);
}

function aStar(startC, startR, endC, endR) {
  const g = new Float32Array(COLS * ROWS).fill(Infinity);
  const prev = new Int32Array(COLS * ROWS).fill(-1);
  const heap = new MinHeap();
  const steps = [];

  const start = idx(startC, startR);
  g[start] = 0;
  heap.push({ f: heuristic(startC, startR, endC, endR), i: start });

  while (heap.size) {
    const { i } = heap.pop();
    if (i === idx(endC, endR)) break;

    const c = i % COLS, r = Math.floor(i / COLS);
    steps.push({ type: 'visit', i });

    for (const [dc, dr] of [[1,0],[-1,0],[0,1],[0,-1]]) {
      const nc = c + dc, nr = r + dr;
      if (nc < 0 || nc >= COLS || nr < 0 || nr >= ROWS) continue;
      const ni = idx(nc, nr);
      if (grid[ni] === 1) continue;
      const ng = g[i] + 1;
      if (ng < g[ni]) {
        g[ni] = ng;
        prev[ni] = i;
        const h = heuristic(nc, nr, endC, endR);
        heap.push({ f: ng + h, i: ni });
      }
    }
  }

  const path = [];
  let cur = idx(endC, endR);
  while (cur !== -1) { path.push(cur); cur = prev[cur]; }
  return { steps, path: path.reverse() };
}

Step-by-Step Animation

function animate(steps, path, speed = 15) {
  let stepIndex = 0;

  function frame() {
    // Process `speed` steps per frame
    for (let s = 0; s < speed && stepIndex < steps.length; s++, stepIndex++) {
      const { i } = steps[stepIndex];
      if (grid[i] !== 2 && grid[i] !== 3) grid[i] = 4; // mark visited
    }

    render();

    if (stepIndex < steps.length) {
      requestAnimationFrame(frame);
    } else {
      // Draw path when done
      for (const i of path) {
        if (grid[i] !== 2 && grid[i] !== 3) grid[i] = 5;
      }
      render();
    }
  }
  requestAnimationFrame(frame);
}

Mouse Interaction — Draw Walls

let painting = false;
let paintType = 1; // 1 = wall, 0 = erase

canvas.addEventListener('mousedown', e => {
  painting = true;
  paintType = e.button === 2 ? 0 : 1; // right-click erases
  paint(e);
});
canvas.addEventListener('mousemove', e => { if (painting) paint(e); });
canvas.addEventListener('mouseup',   () => painting = false);
canvas.addEventListener('contextmenu', e => e.preventDefault());

function paint(e) {
  const rect = canvas.getBoundingClientRect();
  const c = Math.floor((e.clientX - rect.left) / CELL);
  const r = Math.floor((e.clientY - rect.top)  / CELL);
  if (c < 0 || c >= COLS || r < 0 || r >= ROWS) return;
  const i = idx(c, r);
  if (grid[i] === 2 || grid[i] === 3) return; // don't overwrite start/end
  grid[i] = paintType;
  render();
}

Comparing Algorithms Visually

To run both and compare visited counts, run each on a copy of the grid:

function runAndCompare() {
  const startC = 1, startR = 1, endC = COLS-2, endR = ROWS-2;

  const resD = dijkstra(startC, startR, endC, endR);
  const resA = aStar(startC, startR, endC, endR);

  console.log(`Dijkstra visited: ${resD.steps.length}, path: ${resD.path.length}`);
  console.log(`A* visited:       ${resA.steps.length}, path: ${resA.path.length}`);

  // On an open grid A* typically visits 60-80% fewer nodes than Dijkstra
}

A* with Manhattan heuristic typically explores 40-80% fewer nodes than Dijkstra on open grids. They find identically-optimal paths. In mazes the difference shrinks because forced exploration is needed in both cases.

Continue Learning

Frequently Asked Questions

What will I learn in this tutorial?

Build an interactive A* and Dijkstra pathfinding visualizer in vanilla JavaScript: grid construction, priority queue, heuristics, and step-by-step animation.

What topics are covered in this tutorial?

This tutorial covers: Grid data structure and rendering, Binary min-heap priority queue, Dijkstra's algorithm, A* with Manhattan heuristic, Step-by-step animation, Mouse interaction — draw walls, Comparing algorithms visually.

What tools and technologies does this tutorial use?

This tutorial uses JavaScript, Canvas 2D, Algorithms.

How long does this tutorial take?

This tutorial takes approximately 55 minutes to complete.

What prerequisites do I need before starting?

Before starting, you should have: JavaScript arrays, objects, classes; Canvas 2D API basics ( fillRect , clearRect ); Basic understanding of graphs (nodes + edges).

🛠

Experiment in Playground

Implement your own pathfinding variant — write and run code directly in the browser.

Open Playground → View Simulation ↗