Building a Pathfinding Visualizer
Building a Pathfinding Visualizer: A Journey Into "Why Am I Making This?"
A step-by-step guide to creating an interactive pathfinding algorithm visualizer with vanilla JavaScript, CSS animations, and way too much free time.
The Origin Story (AKA My Descent Into Madness)
It all started innocently. I was scrolling through GitHub at 2 AM (because that's when all bad decisions are made), saw a pathfinding visualizer someone built, and thought: "How hard could that be?"
Spoiler alert: It wasn't that hard, but I definitely made it harder than necessary. Multiple times.
The result? A beautiful, interactive grid where you can watch computers stumble around trying to find shortcuts like they're using Google Maps in 1997. But it looks cool, so it counts.
In this post, I'll walk you through building the exact same app. No fluff. Well, okay, there's a lot of fluff, but also actual code.
Part 1: The Foundations (Setting Up Your Project)
Step 1: Create Your HTML Masterpiece
First, we need an HTML file. Nothing fancy—just a container where our DOM nodes will live and multiply like rabbits.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pathfinding Visualizer</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- Navigation Bar -->
<nav class="navbar">
<div class="nav-brand">Pathfinding Visualizer</div>
<div class="nav-controls">
<!-- Algorithm selector -->
<div class="control-group">
<label for="algorithm-select">Algorithm:</label>
<select id="algorithm-select">
<option value="dijkstra">Dijkstra's Algorithm</option>
<option value="astar">A* Search</option>
<option value="bfs">Breadth-First Search</option>
<option value="dfs">Depth-First Search</option>
</select>
</div>
<!-- Control buttons -->
<button id="visualize-btn" class="btn btn-primary">Visualize!</button>
<button id="pause-btn" class="btn btn-warning" disabled>Pause</button>
<button id="reset-btn" class="btn btn-danger" disabled>Reset</button>
<button id="clear-path-btn" class="btn">Clear Path</button>
<button id="clear-board-btn" class="btn">Clear Board</button>
<!-- Speed selector -->
<div class="control-group">
<label for="speed-select">Speed:</label>
<select id="speed-select">
<option value="fast">Fast</option>
<option value="medium">Medium</option>
<option value="slow">Slow</option>
</select>
</div>
</div>
</nav>
<!-- Legend -->
<div class="legend">
<div class="legend-item">
<div class="node node-start"></div>Start Node
</div>
<div class="legend-item">
<div class="node node-target"></div>Target Node
</div>
<div class="legend-item">
<div class="node node-wall"></div>Wall
</div>
<div class="legend-item">
<div class="node node-visited"></div>Visited
</div>
<div class="legend-item">
<div class="node node-path"></div>Shortest Path
</div>
</div>
<!-- The grid where all the magic happens -->
<div id="grid-container"></div>
<!-- Import our main JavaScript file (ES6 modules) -->
<script type="module" src="js/main.js"></script>
</body>
</html>Key takeaways:
- The grid will be generated dynamically via JavaScript (we're not masochists)
- The navbar gives us all our controls
- ES6 modules are imported via
type="module"(fancy!)
Part 2: The CSS (Making It Look Less Like 1997)
Now let's make this thing actually look like we spent time on it. CSS animations are the secret sauce here.
Core Styling
:root {
--primary-color: #34495e;
--secondary-color: #2c3e50;
--accent-color: #3498db;
--start-node-color: #2ecc71; /* Green */
--target-node-color: #e74c3c; /* Red */
--wall-node-color: #0c3547; /* Dark Blue */
--path-node-color: #f1c40f; /* Yellow */
--visited-node-color: #00bcd4; /* Light Blue */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
}
/* Make the navbar look professional */
.navbar {
background-color: var(--primary-color);
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.nav-brand {
font-size: 1.5rem;
font-weight: bold;
}
.nav-controls {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
/* Grid styling - this is where the nodes live */
.grid {
display: grid;
grid-template-columns: repeat(50, 1fr); /* 50 columns */
gap: 1px;
padding: 1rem;
background-color: #ecf0f1;
flex: 1;
overflow: auto;
}
.grid-row {
display: contents; /* This is *chef's kiss* for CSS Grid */
}
/* Individual nodes - the stars of the show */
.node {
width: 100%;
aspect-ratio: 1;
background-color: white;
border: 1px solid #ddd;
cursor: pointer;
transition: background-color 0.2s;
}
.node-start {
background-color: var(--start-node-color);
cursor: grab;
}
.node-target {
background-color: var(--target-node-color);
cursor: grab;
}
.node-wall {
background-color: var(--wall-node-color);
}
.node-visited {
animation: visitedAnimation 0.3s ease-out;
background-color: var(--visited-node-color);
}
.node-path {
animation: pathAnimation 0.3s ease-out;
background-color: var(--path-node-color);
}
/* THE MAGIC: CSS Animations */
@keyframes visitedAnimation {
0% {
transform: scale(0.3);
background-color: rgba(0, 0, 66, 0.75);
}
50% {
background-color: rgba(17, 104, 217, 0.75);
}
100% {
transform: scale(1);
background-color: rgba(0, 190, 218, 0.75);
}
}
@keyframes pathAnimation {
0% {
transform: scale(0.6);
background-color: rgba(241, 196, 15, 0.6);
}
50% {
background-color: rgba(241, 196, 15, 0.8);
}
100% {
transform: scale(1);
background-color: var(--path-node-color);
}
}
/* Buttons */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
background-color: #bdc3c7;
}
.btn:hover:not(:disabled) {
opacity: 0.9;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--accent-color);
color: white;
}
.btn-warning {
background-color: #f39c12;
color: white;
}
.btn-danger {
background-color: #c0392b;
color: white;
}
/* Legend */
.legend {
background-color: #ecf0f1;
padding: 0.5rem 1rem;
display: flex;
gap: 2rem;
flex-wrap: wrap;
border-top: 1px solid #ddd;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}Pro tip: That display: contents on .grid-row? That's the secret to making CSS Grid work nicely without extra wrapper divs. CSS is weird, but sometimes it's a good weird.
Part 3: The Node Class (The Foundation Of Everything)
Every square on the grid is a Node. A Node is basically a fancy JavaScript object that knows about itself.
// Node.js
export class Node {
constructor(row, col, type) {
this.row = row;
this.col = col;
this.type = type; // 'empty', 'start', 'target', 'wall'
// Pathfinding properties
this.distance = Infinity;
this.totalDistance = Infinity; // For A* (f = g + h)
this.heuristicDistance = Infinity; // For A* (h)
this.isVisited = false;
this.previousNode = null; // Used to reconstruct the path
this.element = null; // Reference to the DOM element
}
// Link the Node to its DOM representation
setElement(element) {
this.element = element;
this.updateClass();
}
// Change what type of node this is
setType(type) {
this.type = type;
this.updateClass();
}
// Update the CSS classes based on the node's current state
updateClass() {
if (!this.element) return;
// Clear old classes
this.element.className = 'node';
// Add appropriate class
if (this.type === 'start') {
this.element.classList.add('node-start');
} else if (this.type === 'target') {
this.element.classList.add('node-target');
} else if (this.type === 'wall') {
this.element.classList.add('node-wall');
}
}
// Mark this node as visited (triggers animation)
setVisited(visited) {
this.isVisited = visited;
if (this.element) {
if (visited && this.type !== 'start' && this.type !== 'target') {
this.element.classList.add('node-visited');
} else if (!visited) {
this.element.classList.remove('node-visited');
this.element.classList.remove('node-path');
}
}
}
// Mark this node as part of the shortest path
setPath(isPath) {
if (this.element && this.type !== 'start' && this.type !== 'target') {
if (isPath) {
this.element.classList.add('node-path');
} else {
this.element.classList.remove('node-path');
}
}
}
// Reset the node to initial state
reset() {
this.distance = Infinity;
this.totalDistance = Infinity;
this.heuristicDistance = Infinity;
this.isVisited = false;
this.previousNode = null;
this.setVisited(false);
this.setPath(false);
}
}What's happening here:
- The Node stores its position (
row,col) - It tracks algorithm-specific data (distance, visited, previous node)
- It bridges the gap between the data model and the DOM (via
element) - It updates CSS classes to trigger animations
Part 4: The Grid Class (Creating 1,050 Divs and Not Regretting It)
Now we need to create the actual grid. This is where we generate all those nodes we've been talking about.
// Grid.js
import { Node } from './Node.js';
export class Grid {
constructor(rows, cols) {
this.rows = rows;
this.cols = cols;
this.nodes = [];
// Starting positions (roughly centered)
this.startNodePosition = { row: Math.floor(rows / 2), col: Math.floor(cols / 4) };
this.targetNodePosition = { row: Math.floor(rows / 2), col: Math.floor(cols * 3 / 4) };
// Mouse tracking for drawing walls
this.isMousePressed = false;
this.isDraggingStart = false;
this.isDraggingTarget = false;
}
initialize() {
this.nodes = [];
const gridContainer = document.getElementById('grid-container');
gridContainer.innerHTML = ''; // Clear any existing grid
const table = document.createElement('div');
table.className = 'grid';
// Create the grid!
for (let row = 0; row < this.rows; row++) {
const rowDiv = document.createElement('div');
rowDiv.className = 'grid-row';
const currentRow = [];
for (let col = 0; col < this.cols; col++) {
let type = 'empty';
// Determine if this is the start or target node
if (row === this.startNodePosition.row && col === this.startNodePosition.col) {
type = 'start';
} else if (row === this.targetNodePosition.row && col === this.targetNodePosition.col) {
type = 'target';
}
// Create the Node object
const node = new Node(row, col, type);
// Create the DOM element
const nodeElement = document.createElement('div');
nodeElement.className = 'node';
nodeElement.dataset.row = row;
nodeElement.dataset.col = col;
// Attach event listeners
nodeElement.addEventListener('mousedown', (e) => this.handleMouseDown(e, row, col));
nodeElement.addEventListener('mouseenter', (e) => this.handleMouseEnter(e, row, col));
nodeElement.addEventListener('mouseup', () => this.handleMouseUp());
// Connect Node to its DOM element
node.setElement(nodeElement);
currentRow.push(node);
rowDiv.appendChild(nodeElement);
}
this.nodes.push(currentRow);
table.appendChild(rowDiv);
}
gridContainer.appendChild(table);
// Global mouseup to catch releases outside the grid
document.addEventListener('mouseup', () => this.handleMouseUp());
}
// When the mouse goes down
handleMouseDown(e, row, col) {
e.preventDefault();
this.isMousePressed = true;
const node = this.nodes[row][col];
// Check if we're clicking on the start or target node
if (node.type === 'start') {
this.isDraggingStart = true;
} else if (node.type === 'target') {
this.isDraggingTarget = true;
} else {
// Otherwise, toggle wall
this.toggleWall(node);
}
}
// When the mouse moves while pressed
handleMouseEnter(e, row, col) {
if (!this.isMousePressed) return;
const node = this.nodes[row][col];
if (this.isDraggingStart) {
this.moveStartNode(row, col);
} else if (this.isDraggingTarget) {
this.moveTargetNode(row, col);
} else {
this.toggleWall(node);
}
}
// When the mouse is released
handleMouseUp() {
this.isMousePressed = false;
this.isDraggingStart = false;
this.isDraggingTarget = false;
}
// Toggle wall on/off (don't overwrite start/target nodes)
toggleWall(node) {
if (node.type === 'start' || node.type === 'target') return;
if (node.type === 'wall') {
node.setType('empty');
} else {
node.setType('wall');
}
}
// Move the start node to a new position
moveStartNode(row, col) {
const oldStart = this.nodes[this.startNodePosition.row][this.startNodePosition.col];
oldStart.setType('empty');
this.startNodePosition = { row, col };
const newStart = this.nodes[row][col];
newStart.setType('start');
}
// Move the target node to a new position
moveTargetNode(row, col) {
const oldTarget = this.nodes[this.targetNodePosition.row][this.targetNodePosition.col];
oldTarget.setType('empty');
this.targetNodePosition = { row, col };
const newTarget = this.nodes[row][col];
newTarget.setType('target');
}
// Get the start node
getStartNode() {
return this.nodes[this.startNodePosition.row][this.startNodePosition.col];
}
// Get the target node
getTargetNode() {
return this.nodes[this.targetNodePosition.row][this.targetNodePosition.col];
}
// Clear all walls and path visualizations
clearBoard() {
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
const node = this.nodes[row][col];
if (node.type === 'wall') {
node.setType('empty');
}
node.reset();
}
}
}
// Clear only the path visualization (keep walls)
resetPath() {
for (let row = 0; row < this.rows; row++) {
for (let col = 0; col < this.cols; col++) {
const node = this.nodes[row][col];
node.reset();
}
}
}
}What's cool here:
- We're creating a 2D array of nodes
- Mouse events let us draw walls and drag the start/target nodes
- The grid manages its own state (walls, positions, etc.)
Part 5: The Algorithms (The Brains of the Operation)
Now for the fun part. Let's implement the actual pathfinding algorithms!
Dijkstra's Algorithm
Dijkstra is like water spreading from a source. It explores equally in all directions until it finds the target. It always finds the shortest path.
// algorithms/dijkstra.js
import { getNeighbors, getAllNodes } from './helpers.js';
export function dijkstra(grid, startNode, finishNode) {
const visitedNodesInOrder = [];
startNode.distance = 0;
const unvisitedNodes = getAllNodes(grid);
while (unvisitedNodes.length) {
// Find the unvisited node with the smallest distance
sortNodesByDistance(unvisitedNodes);
const closestNode = unvisitedNodes.shift();
// Skip walls
if (closestNode.type === 'wall') continue;
// If we hit infinity, we're trapped (surrounded by walls)
if (closestNode.distance === Infinity) return visitedNodesInOrder;
closestNode.isVisited = true;
visitedNodesInOrder.push(closestNode);
// Early exit if we found the target
if (closestNode === finishNode) return visitedNodesInOrder;
// Update all unvisited neighbors
updateUnvisitedNeighbors(closestNode, grid);
}
return visitedNodesInOrder;
}
// Sort nodes by their distance (simple but inefficient - MinHeap would be better)
function sortNodesByDistance(unvisitedNodes) {
unvisitedNodes.sort((nodeA, nodeB) => nodeA.distance - nodeB.distance);
}
// Update the distance and previous node for all neighbors
function updateUnvisitedNeighbors(node, grid) {
const unvisitedNeighbors = getNeighbors(node, grid)
.filter(neighbor => !neighbor.isVisited);
for (const neighbor of unvisitedNeighbors) {
neighbor.distance = node.distance + 1;
neighbor.previousNode = node;
}
}A* Search
A* is Dijkstra on steroids. It uses a heuristic (educated guess) to prioritize nodes that are closer to the target. It finds the shortest path faster.
// algorithms/astar.js
import { getNeighbors, getAllNodes } from './helpers.js';
export function astar(grid, startNode, finishNode) {
const visitedNodesInOrder = [];
startNode.distance = 0;
startNode.totalDistance = 0;
startNode.heuristicDistance = 0;
const openSet = [startNode]; // Nodes to be evaluated
while (openSet.length > 0) {
// Get the node with the lowest total cost (f = g + h)
sortNodesByTotalDistance(openSet);
const closestNode = openSet.shift();
if (closestNode.type === 'wall') continue;
closestNode.isVisited = true;
visitedNodesInOrder.push(closestNode);
if (closestNode === finishNode) return visitedNodesInOrder;
// Check all neighbors
const neighbors = getNeighbors(closestNode, grid);
for (const neighbor of neighbors) {
if (neighbor.isVisited || neighbor.type === 'wall') continue;
// Calculate the tentative distance
const tentativeGScore = closestNode.distance + 1;
// If we found a better path to this neighbor
if (tentativeGScore < neighbor.distance) {
neighbor.previousNode = closestNode;
neighbor.distance = tentativeGScore; // g score
neighbor.heuristicDistance = manhattanDistance(neighbor, finishNode); // h score
neighbor.totalDistance = neighbor.distance + neighbor.heuristicDistance; // f = g + h
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
}
}
}
}
return visitedNodesInOrder;
}
function sortNodesByTotalDistance(nodes) {
nodes.sort((nodeA, nodeB) => nodeA.totalDistance - nodeB.totalDistance);
}
// The heuristic: Manhattan distance (how many moves if you could go straight)
function manhattanDistance(nodeA, nodeB) {
return Math.abs(nodeA.row - nodeB.row) + Math.abs(nodeA.col - nodeB.col);
}Breadth-First Search
BFS explores layer by layer. Simple, and optimal for unweighted graphs.
// algorithms/bfs.js
import { getNeighbors } from './helpers.js';
export function bfs(grid, startNode, finishNode) {
const visitedNodesInOrder = [];
const queue = [startNode];
startNode.isVisited = true;
startNode.distance = 0;
while (queue.length) {
const currentNode = queue.shift(); // Remove from front of queue
visitedNodesInOrder.push(currentNode);
if (currentNode === finishNode) return visitedNodesInOrder;
// Add all unvisited, non-wall neighbors to the queue
const neighbors = getNeighbors(currentNode, grid);
for (const neighbor of neighbors) {
if (!neighbor.isVisited && neighbor.type !== 'wall') {
neighbor.isVisited = true;
neighbor.distance = currentNode.distance + 1;
neighbor.previousNode = currentNode;
queue.push(neighbor);
}
}
}
return visitedNodesInOrder;
}Helper Functions
These utility functions are used by all algorithms:
// algorithms/helpers.js
// Get all nodes in the grid as a flat array
export function getAllNodes(grid) {
const nodes = [];
for (const row of grid) {
for (const node of row) {
nodes.push(node);
}
}
return nodes;
}
// Get the neighbors of a node (up, down, left, right)
export function getNeighbors(node, grid) {
const neighbors = [];
const { row, col } = node;
const rows = grid.length;
const cols = grid[0].length;
// Up
if (row > 0) neighbors.push(grid[row - 1][col]);
// Down
if (row < rows - 1) neighbors.push(grid[row + 1][col]);
// Left
if (col > 0) neighbors.push(grid[row][col - 1]);
// Right
if (col < cols - 1) neighbors.push(grid[row][col + 1]);
return neighbors;
}
// Reconstruct the shortest path by following previousNode pointers
export function getNodesInShortestPathOrder(finishNode) {
const nodesInShortestPathOrder = [];
let currentNode = finishNode;
while (currentNode !== null) {
nodesInShortestPathOrder.unshift(currentNode); // Add to front
currentNode = currentNode.previousNode;
}
return nodesInShortestPathOrder;
}Part 6: The Visualizer (Making It All Dance)
This is where the magic happens. We run the algorithms instantly, then replay them with animations.
// Visualizer.js
import { dijkstra } from './algorithms/dijkstra.js';
import { astar } from './algorithms/astar.js';
import { bfs } from './algorithms/bfs.js';
import { dfs } from './algorithms/dfs.js';
import { getNodesInShortestPathOrder } from './algorithms/helpers.js';
export class Visualizer {
constructor(grid) {
this.grid = grid;
this.isRunning = false;
this.isPaused = false;
// Animation timing
this.speed = 10; // milliseconds per frame
this.animationSpeedMap = {
'fast': 10,
'medium': 30,
'slow': 100
};
// State tracking
this.currentAlgorithm = null;
this.animationStep = 0;
this.visitedNodes = [];
this.shortestPathNodes = [];
this.totalSteps = 0;
// Animation frame management
this.animationFrameId = null;
this.lastFrameTime = 0;
}
// Set the speed from the dropdown
setSpeed(speedName) {
this.speed = this.animationSpeedMap[speedName] || 10;
}
// Start the visualization!
startVisualization(algorithmName) {
if (this.isRunning) return;
this.resetInternalState();
this.grid.resetPath();
this.currentAlgorithm = algorithmName;
this.isRunning = true;
const startNode = this.grid.getStartNode();
const finishNode = this.grid.getTargetNode();
let visitedNodesInOrder = [];
// Run the algorithm (instantly)
switch (algorithmName) {
case 'dijkstra':
visitedNodesInOrder = dijkstra(this.grid.nodes, startNode, finishNode);
break;
case 'astar':
visitedNodesInOrder = astar(this.grid.nodes, startNode, finishNode);
break;
case 'bfs':
visitedNodesInOrder = bfs(this.grid.nodes, startNode, finishNode);
break;
case 'dfs':
visitedNodesInOrder = dfs(this.grid.nodes, startNode, finishNode);
break;
default:
return;
}
// Save the results
this.visitedNodes = visitedNodesInOrder;
this.shortestPathNodes = getNodesInShortestPathOrder(finishNode);
this.totalSteps = this.visitedNodes.length + this.shortestPathNodes.length;
// Start the animation loop
this.animate();
}
// The animation loop using requestAnimationFrame
animate(timestamp = 0) {
if (!this.isRunning) return;
// Pause handling
if (this.isPaused) {
this.animationFrameId = requestAnimationFrame((t) => this.animate(t));
return;
}
// Timing: only update every `this.speed` milliseconds
if (timestamp - this.lastFrameTime < this.speed) {
this.animationFrameId = requestAnimationFrame((t) => this.animate(t));
return;
}
this.lastFrameTime = timestamp;
// Update one node per animation frame
if (this.animationStep < this.visitedNodes.length) {
const node = this.visitedNodes[this.animationStep];
node.setVisited(true);
this.animationStep++;
} else if (this.animationStep < this.visitedNodes.length + this.shortestPathNodes.length) {
// Animate the path
const pathIndex = this.animationStep - this.visitedNodes.length;
const node = this.shortestPathNodes[pathIndex];
node.setPath(true);
this.animationStep++;
} else {
// Animation complete!
this.isRunning = false;
document.dispatchEvent(new Event('visualization-finished'));
return;
}
// Continue the loop
this.animationFrameId = requestAnimationFrame((t) => this.animate(t));
}
// Pause/resume the animation
togglePause() {
if (!this.isRunning) return;
this.isPaused = !this.isPaused;
}
// Reset everything
reset() {
this.isRunning = false;
this.isPaused = false;
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
}
this.grid.resetPath();
this.resetInternalState();
}
resetInternalState() {
this.animationStep = 0;
this.visitedNodes = [];
this.shortestPathNodes = [];
this.lastFrameTime = 0;
}
}Key insight: We use requestAnimationFrame instead of setTimeout for smooth animations. Much better! (I learned this the hard way after the first version completely tanked performance.)
Part 7: Wiring It All Together (main.js)
Finally, let's connect all the pieces:
// main.js
import { Grid } from './Grid.js';
import { Visualizer } from './Visualizer.js';
const GRID_ROWS = 21;
const GRID_COLS = 50;
document.addEventListener('DOMContentLoaded', () => {
// Initialize
const grid = new Grid(GRID_ROWS, GRID_COLS);
grid.initialize();
const visualizer = new Visualizer(grid);
// Get UI elements
const visualizeBtn = document.getElementById('visualize-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
const clearPathBtn = document.getElementById('clear-path-btn');
const clearBoardBtn = document.getElementById('clear-board-btn');
const algorithmSelect = document.getElementById('algorithm-select');
const speedSelect = document.getElementById('speed-select');
// When "Visualize!" is clicked
visualizeBtn.addEventListener('click', () => {
const algorithm = algorithmSelect.value;
visualizer.startVisualization(algorithm);
// Disable controls while running
visualizeBtn.disabled = true;
algorithmSelect.disabled = true;
clearPathBtn.disabled = true;
clearBoardBtn.disabled = true;
pauseBtn.disabled = false;
resetBtn.disabled = false;
});
// Pause/Resume
pauseBtn.addEventListener('click', () => {
visualizer.togglePause();
pauseBtn.textContent = visualizer.isPaused ? 'Resume' : 'Pause';
});
// Reset
resetBtn.addEventListener('click', () => {
visualizer.reset();
resetUI();
});
// Clear path visualization only
clearPathBtn.addEventListener('click', () => {
grid.resetPath();
});
// Clear everything (walls + path)
clearBoardBtn.addEventListener('click', () => {
grid.clearBoard();
if (visualizer.isRunning) {
visualizer.reset();
resetUI();
}
});
// Speed selection
speedSelect.addEventListener('change', (e) => {
visualizer.setSpeed(e.target.value);
});
// When animation finishes, re-enable controls
document.addEventListener('visualization-finished', () => {
resetUI();
});
function resetUI() {
visualizeBtn.disabled = false;
algorithmSelect.disabled = false;
clearPathBtn.disabled = false;
clearBoardBtn.disabled = false;
pauseBtn.disabled = true;
pauseBtn.textContent = 'Pause';
resetBtn.disabled = true;
}
});Part 8: Lessons Learned (AKA My Mistakes)
The Good Decisions
- Using
requestAnimationFrame- Way better thansetTimeoutfor animations - Separating concerns - Grid logic, Node state, Visualization, Algorithms are all separate
- CSS animations - The magic that makes this visually appealing
- 2D array for nodes - Makes neighbor lookup O(1)
- First version used
setTimeout- Caused lag and weird timing issues - Didn't track animation state - Led to bugs when pausing/resuming
- Mixed DOM updates with algorithm logic - Made algorithms slower than necessary
- Using
Array.includes()in A*'s openSet - O(n) lookup every time. Should use a Set or MinHeap - Use a MinHeap for Dijkstra and A* (current sorting is O(n log n) each iteration)
- Add diagonal movement (just a few extra getNeighbors lines)
- Implement weighted edges for more realistic pathfinding
- Add a "compare algorithms" mode to run them side-by-side
- Save all the files in the right structure
- Open
index.htmlin your browser - Draw some walls by clicking and dragging
- Select an algorithm
- Click "Visualize!"
- Watch the computer embarrass itself trying to find the path
The Mistakes
What I'd Do Differently
Running It
Final Thoughts
This project is a perfect example of how to take a complex concept (graph algorithms) and make it interactive and understandable through visualization.
Plus, when you show this to friends, it looks way more impressive than it actually is. "Yeah, I built a pathfinding algorithm visualizer" sounds way cooler than it deserves to.
You now have a working pathfinding visualizer! You can tweak colors, add more algorithms, change the grid size, or just enjoy watching little blue squares explore a grid looking for a red square.
Time well spent? You be the judge. ✨
Questions? Feel free to fork it, break it, improve it, and send a pull request!