All articles
24 min read

Pathfinding Algorithm: How to Visualize A*, Dijkstra, and BFS With Code

pathfindinga star algorithmdijkstraBFScreative codingJavaScriptalgorithm visualizationcanvasgame development

Pathfinding is one of the most visually satisfying algorithms to watch. A grid of cells, a start point, a goal, and walls in between — then an algorithm spreads across the grid like water, probing corridors, backtracking from dead ends, until it finds the shortest path. The search itself creates beautiful patterns: BFS expands in perfect diamond waves, Dijkstra flows like liquid around weighted terrain, and A* cuts a focused beam straight toward the goal.

This article builds eight pathfinding visualizations from scratch using JavaScript and Canvas. Every example is a self-contained HTML file — no libraries, no dependencies. You will see exactly how each algorithm decides which cell to explore next, and why A* is the gold standard for game AI and robotics.

How Pathfinding Visualizations Work

Every example uses the same grid architecture: a 2D array of cells where each cell can be empty, a wall, or weighted terrain. The algorithm maintains an open set (cells to explore) and a closed set (cells already explored). Each step, it picks the best cell from the open set, explores its neighbors, and yields control so we can render a frame.

Color coding:

  • Dark gray — unexplored cell
  • Teal/cyan — in the open set (frontier)
  • Dark blue — in the closed set (explored)
  • Yellow — final shortest path
  • Black — wall
  • Green — start
  • Red — goal

1. Breadth-First Search — the diamond wave

BFS is the simplest pathfinding algorithm: explore all neighbors at depth 1, then depth 2, then depth 3, and so on. On an unweighted grid, this guarantees the shortest path because you always find the closest cells first. The exploration pattern forms a beautiful expanding diamond (Manhattan distance). Click to place walls, then watch the wave flow around them.

<!DOCTYPE html>
<html><head><title>BFS Pathfinding</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=50,ROWS=35,S=16;
c.width=COLS*S;c.height=ROWS*S;
const grid=Array.from({length:ROWS},()=>new Uint8Array(COLS));
const start=[1,1],goal=[COLS-2,ROWS-2];
// 0=empty,1=wall,2=open,3=closed,4=path
for(let i=0;inew Array(COLS).fill(null));
  grid[start[1]][start[0]]=2;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let step=0;
  while(q.length){
    const[cx,cy]=q.shift();
    if(cx===goal[0]&&cy===goal[1]){found=true;break}
    grid[cy][cx]=3;visited++;
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){grid[py][px]=4;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}grid[start[1]][start[0]]=4;pathLen++}
  done=true;draw();
}
bfs();
</script>
</body></html>

Notice how BFS expands uniformly in all directions. It does not know where the goal is — it simply tries every cell at distance 1, then distance 2, etc. This means it explores many cells that are moving away from the goal. The diamond shape comes from using 4-directional movement with Manhattan distance.

2. Dijkstra’s Algorithm — the flood fill

Dijkstra’s algorithm is BFS with priorities. Instead of a simple queue, it uses a priority queue sorted by the total cost from the start. On an unweighted grid, Dijkstra behaves identically to BFS. But when cells have different costs (like mud vs road), Dijkstra finds the truly cheapest path. This visualization adds random weighted cells — darker cells cost more to traverse.

<!DOCTYPE html>
<html><head><title>Dijkstra Pathfinding</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=50,ROWS=35,S=16;
c.width=COLS*S;c.height=ROWS*S;
const cost=Array.from({length:ROWS},()=>Array.from({length:COLS},()=>1));
const wall=Array.from({length:ROWS},()=>new Uint8Array(COLS));
const start=[1,1],goal=[COLS-2,ROWS-2];
for(let i=0;inew Uint8Array(COLS));// 0=none,1=open,2=closed,3=path
let visited=0,pathLen=0,totalCost=0,done=false,found=false;
function draw(){
  for(let y=0;y1&&state[y][cx]<2){x.fillStyle='rgba(255,255,255,0.15)';x.font='9px sans-serif';x.fillText(cost[y][cx],cx*S+4,y*S+11)}
  }
  x.fillStyle='#4f4';x.fillRect(start[0]*S,start[1]*S,S-1,S-1);
  x.fillStyle='#f44';x.fillRect(goal[0]*S,goal[1]*S,S-1,S-1);
  x.fillStyle='#fff';x.font='13px monospace';
  x.fillText('Dijkstra — Visited: '+visited+(found?' Path cost: '+totalCost:''),8,ROWS*S-6);
}
// Simple binary heap priority queue
class PQ{constructor(){this.d=[]}push(v){this.d.push(v);this._up(this.d.length-1)}pop(){const t=this.d[0],b=this.d.pop();if(this.d.length){this.d[0]=b;this._dn(0)}return t}get length(){return this.d.length}_up(i){while(i>0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(lnew Float64Array(COLS).fill(1e9));
  const prev=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  dist[start[1]][start[0]]=0;
  const pq=new PQ();pq.push([0,start[0],start[1]]);state[start[1]][start[0]]=1;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let step=0;
  while(pq.length){
    const[d,cx,cy]=pq.pop();
    if(state[cy][cx]===2)continue;
    state[cy][cx]=2;visited++;
    if(cx===goal[0]&&cy===goal[1]){found=true;totalCost=d;break}
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){state[py][px]=3;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}state[start[1]][start[0]]=3}
  done=true;draw();
}
dijkstra();
</script>
</body></html>

Dijkstra prefers cheap cells over expensive ones. You can see the frontier bulging around high-cost areas and flowing through low-cost corridors. This is the key insight: Dijkstra finds the minimum-cost path, not just the minimum-hop path. But like BFS, it still explores in all directions — it has no sense of where the goal is.

3. A* Search — the guided missile

A* adds a heuristic to Dijkstra: instead of sorting by distance-from-start alone, it sorts by f = g + h where g is the actual cost so far and h is an estimate of remaining cost. With Manhattan distance as the heuristic, A* cuts a focused beam toward the goal, exploring dramatically fewer cells than BFS or Dijkstra.

<!DOCTYPE html>
<html><head><title>A* Pathfinding</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=50,ROWS=35,S=16;
c.width=COLS*S;c.height=ROWS*S;
const wall=Array.from({length:ROWS},()=>new Uint8Array(COLS));
const start=[1,1],goal=[COLS-2,ROWS-2];
for(let i=0;inew Uint8Array(COLS));
let visited=0,pathLen=0,done=false,found=false;
function heuristic(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
function draw(){
  for(let y=0;y0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(lnew Float64Array(COLS).fill(1e9));
  const prev=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  g[start[1]][start[0]]=0;
  const pq=new PQ();pq.push([heuristic(start[0],start[1],goal[0],goal[1]),start[0],start[1]]);
  state[start[1]][start[0]]=1;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let step=0;
  while(pq.length){
    const[,cx,cy]=pq.pop();
    if(state[cy][cx]===2)continue;
    state[cy][cx]=2;visited++;
    if(cx===goal[0]&&cy===goal[1]){found=true;break}
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){state[py][px]=3;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}state[start[1]][start[0]]=3;pathLen++}
  done=true;draw();
}
astar();
</script>
</body></html>

Compare the number of visited cells to BFS — A* typically explores 50-70% fewer cells. The heuristic acts as a compass, pulling the search toward the goal. The closed set (dark blue) forms a narrow corridor instead of BFS’s wide diamond. This is why A* is the standard pathfinding algorithm in games: same optimal path, far less computation.

4. A* With Diagonal Movement — eight directions

Real game characters do not move only in four directions. This version allows 8-directional movement with diagonal cost of √2 ≈ 1.414. The heuristic switches to Octile distance: max(|dx|, |dy|) + (sqrt(2) - 1) * min(|dx|, |dy|). Paths become smoother and more natural, and the exploration pattern changes from diamond to octagon.

<!DOCTYPE html>
<html><head><title>A* Diagonal Movement</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=50,ROWS=35,S=16;
c.width=COLS*S;c.height=ROWS*S;
const wall=Array.from({length:ROWS},()=>new Uint8Array(COLS));
const start=[1,1],goal=[COLS-2,ROWS-2];
for(let i=0;inew Uint8Array(COLS));
let visited=0,pathLen=0,done=false,found=false;
const SQRT2=Math.SQRT2;
function heuristic(ax,ay,bx,by){const dx=Math.abs(ax-bx),dy=Math.abs(ay-by);return Math.max(dx,dy)+(SQRT2-1)*Math.min(dx,dy)}
function draw(){
  for(let y=0;y0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(lnew Float64Array(COLS).fill(1e9));
  const prev=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  g[start[1]][start[0]]=0;
  const pq=new PQ();pq.push([heuristic(start[0],start[1],goal[0],goal[1]),start[0],start[1]]);
  state[start[1]][start[0]]=1;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
  let step=0;
  while(pq.length){
    const[,cx,cy]=pq.pop();
    if(state[cy][cx]===2)continue;
    state[cy][cx]=2;visited++;
    if(cx===goal[0]&&cy===goal[1]){found=true;break}
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){state[py][px]=3;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}state[start[1]][start[0]]=3;pathLen++}
  done=true;draw();
}
astar();
</script>
</body></html>

The corner-cutting check (if(dx!==0&&dy!==0&&(wall[cy][cx+dx]||wall[cy+dy][cx]))continue) prevents diagonal movement through wall corners. Without it, the path could slip through diagonal gaps between walls. Notice how the path is now smoother — the algorithm prefers diagonals when they shorten the overall distance.

5. Maze Solving With A* — perfect labyrinth

This example generates a perfect maze using recursive backtracking, then solves it with A*. Perfect mazes have exactly one path between any two points, so the path found is automatically the only path. The maze generation itself is mesmerizing to watch — carving corridors through a grid of walls.

<!DOCTYPE html>
<html><head><title>Maze Solving A*</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const MR=25,MC=35,S=Math.min(20,Math.floor(800/MC));
const W=MC*2+1,H=MR*2+1;
c.width=W*S;c.height=H*S;
const grid=Array.from({length:H},()=>new Uint8Array(W).fill(1));
// Generate maze via recursive backtracker
function carve(cy,cx){
  grid[cy*2+1][cx*2+1]=0;
  const dirs=[[0,1],[0,-1],[1,0],[-1,0]].sort(()=>Math.random()-0.5);
  for(const[dy,dx]of dirs){
    const ny=cy+dy,nx=cx+dx;
    if(ny>=0&&ny=0&&nxnew Uint8Array(W));
let visited=0,pathLen=0,found=false;
function heuristic(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
function draw(){
  for(let y=0;y0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(lnew Float64Array(W).fill(1e9));
  const prev=Array.from({length:H},()=>new Array(W).fill(null));
  gCost[start[1]][start[0]]=0;
  const pq=new PQ();pq.push([heuristic(start[0],start[1],goal[0],goal[1]),start[0],start[1]]);
  state[start[1]][start[0]]=1;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let step=0;
  while(pq.length){
    const[,cx,cy]=pq.pop();
    if(state[cy][cx]===2)continue;
    state[cy][cx]=2;visited++;
    if(cx===goal[0]&&cy===goal[1]){found=true;break}
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){state[py][px]=3;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}state[start[1]][start[0]]=3;pathLen++}
  draw();
}
solve();
</script>
</body></html>

In a perfect maze, A*’s heuristic provides limited advantage over BFS because every path is forced through narrow corridors. But in mazes with open areas or multiple paths (imperfect mazes), A* shines by choosing corridors that head toward the goal. The yellow path is guaranteed optimal.

6. Weighted Terrain — rivers, mountains, and roads

This visualization creates a mini landscape with different terrain types: roads (cost 1), grass (cost 2), forest (cost 4), mountains (cost 8), and water (impassable). A* navigates through this terrain, preferring roads and avoiding expensive mountain crossings. The result looks like a realistic route planner.

<!DOCTYPE html>
<html><head><title>Weighted Terrain A*</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=60,ROWS=40,S=14;
c.width=COLS*S;c.height=ROWS*S;
// terrain: 0=water,1=road,2=grass,4=forest,8=mountain
const terrain=Array.from({length:ROWS},()=>new Uint8Array(COLS).fill(2));
// Generate terrain with noise-like approach
function noise(px,py){let v=0;for(let o=0;o<3;o++){const f=0.05*(1<0.6)terrain[y][cx]=8;
  else if(n>0.2)terrain[y][cx]=4;
  else if(n<-0.7)terrain[y][cx]=0;
  else terrain[y][cx]=2;
}
// Roads
for(let cx=0;cx>1][cx]!==0)terrain[ROWS>>1][cx]=1;
for(let y=0;y>1]!==0)terrain[y][COLS>>1]=1;
const tColors={0:'#1a3a5c',1:'#8a8a6a',2:'#3a6a3a',4:'#2a4a2a',8:'#5a5a5a'};
const start=[2,2],goal=[COLS-3,ROWS-3];
const state=Array.from({length:ROWS},()=>new Uint8Array(COLS));
let visited=0,pathLen=0,totalCost=0,found=false;
function heuristic(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
function draw(){
  for(let y=0;y0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(lnew Float64Array(COLS).fill(1e9));
  const prev=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  g[start[1]][start[0]]=0;
  const pq=new PQ();pq.push([heuristic(start[0],start[1],goal[0],goal[1]),start[0],start[1]]);
  state[start[1]][start[0]]=1;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let step=0;
  while(pq.length){
    const[,cx,cy]=pq.pop();
    if(state[cy][cx]===2)continue;
    state[cy][cx]=2;visited++;
    if(cx===goal[0]&&cy===goal[1]){found=true;totalCost=g[cy][cx];break}
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){state[py][px]=3;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}state[start[1]][start[0]]=3}
  draw();
}
solve();
</script>
</body></html>

Watch how the path detours around mountains and follows roads when possible. The algorithm makes trade-offs: a longer path along a road can be cheaper than a short path through mountains. This is the same principle behind car navigation — highways are faster than straight lines through neighborhoods.

7. Bidirectional A* — meet in the middle

Bidirectional search runs two A* instances simultaneously: one from start to goal, one from goal to start. When their frontiers meet, we have found the shortest path. This can explore up to 50% fewer cells because two small search circles cover less area than one large one. The two frontiers are shown in different colors.

<!DOCTYPE html>
<html><head><title>Bidirectional A*</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=50,ROWS=35,S=16;
c.width=COLS*S;c.height=ROWS*S;
const wall=Array.from({length:ROWS},()=>new Uint8Array(COLS));
const start=[1,1],goal=[COLS-2,ROWS-2];
for(let i=0;inew Uint8Array(COLS));
let visited=0,pathLen=0,found=false;
function h(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
function draw(){
  for(let y=0;y0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(lnew Float64Array(COLS).fill(1e9));
  const gB=Array.from({length:ROWS},()=>new Float64Array(COLS).fill(1e9));
  const prevF=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  const prevB=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  gF[start[1]][start[0]]=0;gB[goal[1]][goal[0]]=0;
  const pqF=new PQ(),pqB=new PQ();
  pqF.push([h(start[0],start[1],goal[0],goal[1]),start[0],start[1]]);
  pqB.push([h(goal[0],goal[1],start[0],start[1]),goal[0],goal[1]]);
  state[start[1]][start[0]]=1;state[goal[1]][goal[0]]=3;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let mu=1e9,meetX=-1,meetY=-1,step=0;
  function expand(pq,gArr,prevArr,otherG,openS,closedS,otherClosed,tx,ty){
    if(!pq.length)return false;
    const[,cx,cy]=pq.pop();
    if(state[cy][cx]===closedS)return false;
    state[cy][cx]=closedS;visited++;
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&ny=mu){found=true;break}
    }
    if(!pqF.length&&!pqB.length)break;
    if(++step%3===0){draw();await new Promise(r=>requestAnimationFrame(r))}
  }
  if(found&&meetX>=0){
    let[px,py]=[meetX,meetY];
    while(px!==start[0]||py!==start[1]){state[py][px]=5;pathLen++;const p=prevF[py][px];if(!p)break;px=p[0];py=p[1]}
    state[start[1]][start[0]]=5;pathLen++;
    [px,py]=[meetX,meetY];
    const p0=prevB[py][px];if(p0){px=p0[0];py=p0[1];
    while(px!==goal[0]||py!==goal[1]){state[py][px]=5;pathLen++;const p=prevB[py][px];if(!p)break;px=p[0];py=p[1]}}
    state[goal[1]][goal[0]]=5;pathLen++;
  }
  draw();
}
bidir();
</script>
</body></html>

The cyan frontier spreads from the start while the magenta frontier spreads from the goal. When they collide, the meeting point connects both partial paths into a complete shortest path. Notice how the total explored area (dark blue + dark purple) is smaller than what a single A* search would explore. This technique is used in real-world navigation systems like Google Maps.

8. Interactive Pathfinding Playground

The ultimate comparison: all algorithms side by side on the same grid. Click to toggle walls, drag start (green) and goal (red) to reposition them, and pick an algorithm from the buttons. Watch the explored area and path length change for the same obstacle configuration.

<!DOCTYPE html>
<html><head><title>Pathfinding Playground</title></head>
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:center;height:100vh;flex-direction:column;font-family:monospace">
<div id="btns" style="margin-bottom:8px"></div>
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
const COLS=50,ROWS=32,S=16;
c.width=COLS*S;c.height=ROWS*S;
const wall=Array.from({length:ROWS},()=>new Uint8Array(COLS));
let start=[1,1],goal=[COLS-2,ROWS-2];
const state=Array.from({length:ROWS},()=>new Uint8Array(COLS));
let visited=0,pathLen=0,found=false,running=false,algo='astar';
const algos=['bfs','dijkstra','astar','greedy'];
const algoNames={bfs:'BFS',dijkstra:'Dijkstra',astar:'A*',greedy:'Greedy'};
const btnDiv=document.getElementById('btns');
algos.forEach(a=>{const b=document.createElement('button');b.textContent=algoNames[a];b.style.cssText='margin:0 4px;padding:6px 14px;background:'+(a===algo?'#0aa':'#333')+';color:#fff;border:none;border-radius:4px;cursor:pointer;font-family:monospace';b.onclick=()=>{algo=a;document.querySelectorAll('#btns button').forEach((bb,i)=>bb.style.background=algos[i]===a?'#0aa':'#333');reset();run()};btnDiv.appendChild(b)});
function h(ax,ay,bx,by){return Math.abs(ax-bx)+Math.abs(ay-by)}
function draw(){
  for(let y=0;y0){const p=i-1>>1;if(this.d[p][0]<=this.d[i][0])break;[this.d[p],this.d[i]]=[this.d[i],this.d[p]];i=p}}_dn(i){const n=this.d.length;while(true){let s=i,l=2*i+1,r=2*i+2;if(l{if(running)return;const rect=c.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,gx=mx/S|0,gy=my/S|0;
  if(gx===start[0]&&gy===start[1]){dragging='start';return}
  if(gx===goal[0]&&gy===goal[1]){dragging='goal';return}
  mouseDown=true;drawing=wall[gy][gx]?0:1;wall[gy][gx]=drawing;draw()};
c.onmousemove=e=>{const rect=c.getBoundingClientRect(),mx=e.clientX-rect.left,my=e.clientY-rect.top,gx=Math.min(COLS-1,Math.max(0,mx/S|0)),gy=Math.min(ROWS-1,Math.max(0,my/S|0));
  if(dragging){if(dragging==='start')start=[gx,gy];else goal=[gx,gy];draw();return}
  if(!mouseDown||running)return;if((gx===start[0]&&gy===start[1])||(gx===goal[0]&&gy===goal[1]))return;wall[gy][gx]=drawing;draw()};
c.onmouseup=()=>{mouseDown=false;dragging=null};
async function run(){
  reset();running=true;
  const gCost=Array.from({length:ROWS},()=>new Float64Array(COLS).fill(1e9));
  const prev=Array.from({length:ROWS},()=>new Array(COLS).fill(null));
  gCost[start[1]][start[0]]=0;
  const pq=new PQ();
  const hVal=h(start[0],start[1],goal[0],goal[1]);
  if(algo==='bfs')pq.push([0,start[0],start[1]]);
  else pq.push([algo==='greedy'?hVal:hVal,start[0],start[1]]);
  state[start[1]][start[0]]=1;
  const dirs=[[1,0],[-1,0],[0,1],[0,-1]];
  let step=0,bfsOrder=0;
  while(pq.length){
    const[,cx,cy]=pq.pop();
    if(state[cy][cx]===2)continue;
    state[cy][cx]=2;visited++;
    if(cx===goal[0]&&cy===goal[1]){found=true;break}
    for(const[dx,dy]of dirs){
      const nx=cx+dx,ny=cy+dy;
      if(nx>=0&&nx=0&&nyrequestAnimationFrame(r))}
  }
  if(found){let[px,py]=goal;while(px!==start[0]||py!==start[1]){state[py][px]=3;pathLen++;const p=prev[py][px];px=p[0];py=p[1]}state[start[1]][start[0]]=3;pathLen++}
  running=false;draw();
}
draw();
run();
</script>
</body></html>

Try the same wall configuration with different algorithms and compare the visited count. Greedy best-first search is fast but does not guarantee optimal paths — it can find longer paths than A*. BFS and Dijkstra always find optimal paths but explore more cells. A* is the sweet spot: optimal paths with minimal exploration.

The Mathematics Behind A*

A* evaluates each node with f(n) = g(n) + h(n) where:

  • g(n) — the actual cost from start to node n (known exactly)
  • h(n) — the heuristic estimate from n to goal (estimated)
  • f(n) — the estimated total cost of the cheapest path through n

The heuristic must be admissible (never overestimate the true cost) for A* to guarantee optimality. Common heuristics:

  • Manhattan distance: |dx| + |dy| — perfect for 4-directional grids
  • Euclidean distance: √(dx² + dy²) — admissible but less tight for grids
  • Octile distance: max(|dx|, |dy|) + (√2 − 1) × min(|dx|, |dy|) — perfect for 8-directional grids
  • Zero heuristic: h(n) = 0 — reduces A* to Dijkstra’s algorithm

Complexity Comparison

AlgorithmOptimal?Time ComplexityBest For
BFSYes (unweighted)O(V + E)Unweighted grids, simple scenarios
DijkstraYesO((V + E) log V)Weighted graphs without heuristic
A*Yes (admissible h)O(b^d) worst, much better avgGeneral shortest path with known goal
Greedy BFSNoO(b^d)Speed over optimality
Bidirectional A*YesO(b^(d/2))Large open spaces

Pathfinding is where computer science becomes visual poetry. The expanding frontiers, the backtracking through dead ends, the moment the path lights up in yellow — these algorithms reveal the hidden structure of space and connectivity. Every game you have ever played, every GPS route you have followed, every robot that navigates a room uses some version of these algorithms.

Want to explore more algorithm-driven art? Visit Lumitree to discover unique generative micro-worlds, or browse the full collection of creative coding tutorials for more visual programming guides.

Related articles