~/blog/making_of_pathfinder
cd ..
Ξ~/blog/making_of_pathfindercat making_of_pathfinder.md

Building a Pathfinding Visualizer

Published:2025-10-30
Read time:15 min read

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

  1. Using requestAnimationFrame - Way better than setTimeout for animations
  2. Separating concerns - Grid logic, Node state, Visualization, Algorithms are all separate
  3. CSS animations - The magic that makes this visually appealing
  4. 2D array for nodes - Makes neighbor lookup O(1)
  5. The Mistakes

  6. First version used setTimeout - Caused lag and weird timing issues
  7. Didn't track animation state - Led to bugs when pausing/resuming
  8. Mixed DOM updates with algorithm logic - Made algorithms slower than necessary
  9. Using Array.includes() in A*'s openSet - O(n) lookup every time. Should use a Set or MinHeap
  10. What I'd Do Differently

  11. Use a MinHeap for Dijkstra and A* (current sorting is O(n log n) each iteration)
  12. Add diagonal movement (just a few extra getNeighbors lines)
  13. Implement weighted edges for more realistic pathfinding
  14. Add a "compare algorithms" mode to run them side-by-side

  15. Running It

  16. Save all the files in the right structure
  17. Open index.html in your browser
  18. Draw some walls by clicking and dragging
  19. Select an algorithm
  20. Click "Visualize!"
  21. Watch the computer embarrass itself trying to find the path

  22. 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!

#JavaScript#Algorithms#Visualization