├── src ├── vite-env.d.ts ├── algorithms │ ├── tsconfig.json │ ├── types.ts │ ├── DFS.ts │ ├── BFS.ts │ ├── Dijkstra.ts │ └── AStar.ts ├── data-structures │ ├── tsconfig.json │ └── Node.ts ├── main.tsx ├── components │ ├── Node.css │ └── Node.tsx ├── utils │ └── dimension.ts ├── index.css ├── App.css └── App.tsx ├── public ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest └── images │ └── site-screenshot.png ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── index.html /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/site-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/images/site-screenshot.png -------------------------------------------------------------------------------- /public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonngdev/algorithms-visualizer/HEAD/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/algorithms/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "strict": true, 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/data-structures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "strict": true, 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /public/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/data-structures/Node.ts: -------------------------------------------------------------------------------- 1 | export interface NodeData { 2 | row: number; 3 | col: number; 4 | } 5 | 6 | export class Node { 7 | public isWall: boolean = false; 8 | public previousNode: Node | null = null; 9 | public neighbors: Node[] = []; 10 | 11 | constructor( 12 | public data: T 13 | ) {} 14 | 15 | isNeighbor(node: Node): boolean { 16 | return this.neighbors.includes(node); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Node.css: -------------------------------------------------------------------------------- 1 | .node { 2 | border: solid 1px black; 3 | width: var(--node-width); 4 | height: var(--node-width); 5 | } 6 | 7 | .node.start { 8 | background-color: #027DFC; 9 | } 10 | 11 | .node.end { 12 | background-color: #00E379; 13 | } 14 | 15 | .node.visited { 16 | background-color: #ffe88e; 17 | } 18 | 19 | .node.on-path { 20 | background-color: #f8bb37; 21 | } 22 | 23 | .node.wall { 24 | background-color: black; 25 | } 26 | 27 | .node.under-drag-start { 28 | background-color: #67b3ff; 29 | } 30 | 31 | .node.under-drag-end { 32 | background-color: #6ff5b6; 33 | } 34 | 35 | .node[draggable="true"] { 36 | cursor: grab; 37 | } 38 | 39 | .node[draggable="true"]:active { 40 | cursor: grabbing; 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-vite", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^6.2.1", 12 | "@fortawesome/free-brands-svg-icons": "^6.2.1", 13 | "@fortawesome/free-solid-svg-icons": "^6.2.1", 14 | "@fortawesome/react-fontawesome": "^0.2.0", 15 | "immer": "^9.0.16", 16 | "react": "^18.0.0", 17 | "react-device-detect": "^2.2.3", 18 | "react-dom": "^18.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.0.0", 22 | "@types/react-dom": "^18.0.0", 23 | "@vitejs/plugin-react": "^1.3.0", 24 | "typescript": "^4.6.3", 25 | "vite": "^2.9.9" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/dimension.ts: -------------------------------------------------------------------------------- 1 | interface GridDimension { 2 | rows: number; 3 | cols: number; 4 | } 5 | 6 | interface NodePosition { 7 | row: number; 8 | col: number; 9 | } 10 | 11 | interface DefaultPositions { 12 | start: NodePosition; 13 | end: NodePosition; 14 | } 15 | 16 | export function calculateGridDimension(): GridDimension { 17 | let rows: number; 18 | let cols: number; 19 | 20 | if (window.innerWidth < 962) { 21 | rows = 9; 22 | cols = 11; 23 | } else { 24 | rows = 15; 25 | cols = 30; 26 | } 27 | 28 | return { rows, cols }; 29 | } 30 | 31 | export function calculateDefaultNodePositions( 32 | gridDimension: GridDimension, 33 | ): DefaultPositions { 34 | const row = Math.floor(gridDimension.rows / 2); 35 | const startCol = Math.floor(gridDimension.cols / 3) - 1; 36 | const endCol = gridDimension.cols - 1 - startCol; 37 | 38 | return { 39 | start: { row, col: startCol }, 40 | end: { row, col: endCol }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/algorithms/types.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeData } from "../data-structures/Node"; 2 | 3 | export interface NodePosition { 4 | row: number; 5 | col: number; 6 | } 7 | 8 | export interface GridData { 9 | grid: Node[][], 10 | startNode: Node | null, 11 | endNode: Node | null, 12 | } 13 | 14 | export interface PathfindingResult { 15 | shortestPath: Node[]; 16 | visitedNodes: Node[]; 17 | } 18 | 19 | export type CreateGridDataFn = ( 20 | rows: number, 21 | cols: number, 22 | startNodePosition: NodePosition, 23 | endNodePosition: NodePosition, 24 | wallPositions: NodePosition[], 25 | ) => GridData; 26 | 27 | export type PerformAlgorithmFn = ( 28 | grid: Node[], 29 | startNode: Node, 30 | endNode: Node, 31 | ) => PathfindingResult 32 | 33 | export interface PathfindingAlgorithm { 34 | createGridData: CreateGridDataFn, 35 | performAlgorithm: PerformAlgorithmFn, 36 | } 37 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Righteous&display=swap'); 2 | 3 | :root { 4 | --color-accent-1: #027dfc; 5 | --color-accent-2: #00e379; 6 | --color-accent-3: #f43f5e; 7 | --color-foreground: #000000; 8 | --color-background: #edf2f7; 9 | --color-background-dark: #e0e5ea; 10 | 11 | --font-size-xxl: 30px; 12 | --font-size-xl: 20px; 13 | --font-size-lg: 18px; 14 | --font-size-md: 14px; 15 | --font-size-sm: 11px; 16 | 17 | --font-family-title: 'Righteous', serif; 18 | --font-family-body: 'Inter', sans-serif; 19 | 20 | --spacing-xl: 2rem; 21 | --spacing-lg: 1.5rem; 22 | --spacing-md: 1rem; 23 | --spacing-sm: 0.75rem; 24 | 25 | --border-radius-lg: 16px; 26 | --border-radius-md: 8px; 27 | --border-radius-sm: 6px; 28 | 29 | --box-shadow: 0px 25px 50px -12px rgba(0, 0, 0, 0.1); 30 | 31 | --gradient-accent: linear-gradient( 32 | to top right, 33 | var(--color-accent-1), 34 | var(--color-accent-2) 35 | ); 36 | 37 | --node-width: 28px; 38 | } 39 | 40 | * { 41 | box-sizing: border-box; 42 | } 43 | 44 | body { 45 | margin: 0; 46 | font-family: var(--font-family-body); 47 | font-size: var(--font-size-md); 48 | background-color: var(--color-background); 49 | -webkit-font-smoothing: antialiased; 50 | -moz-osx-font-smoothing: grayscale; 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

Algorithms Visualizer

6 |

7 | Visualization of common pathfinding algorithms 🧭 🗺️ 8 |

9 | 10 |

11 | Website 🔗 12 |

13 | 14 | ## Features 15 | 16 | - 🗺️ Visualize common pathfinding algorithms: DFS, BFS, Dijkstra, A*. 17 | - 🧭 Move start/end nodes freely to see optimal path changes. 18 | - 🧱 Allow walls creation. 19 | - 📞 Work on mobile, however some features are available on desktop only. 20 | 21 | ## Screenshots 22 | 23 |

Depth-first Search

24 |

25 | Visualization of Depth-first Search algorithm 28 |

29 | 30 |

Dijkstra's Algorithm

31 |

32 | Visualization of Dijkstra's algorithm 35 |

36 | 37 |

A* Algorithm

38 |

39 | Visualization of A-Star algorithm 42 |

43 | 44 | ## Clone this repo 45 | 46 | ```bash 47 | git clone git@github.com:sonngdev/algorithms-visualizer.git 48 | cd algorithms-visualizer 49 | npm install 50 | npm run dev 51 | ``` 52 | 53 | ## Author 54 | [Son Nguyen](https://github.com/sonngdev). 55 | Check out my [website](https://www.sonng.dev/). 56 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Algorithms Visualizer 30 | 31 | 32 |

33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/algorithms/DFS.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeData } from '../data-structures/Node'; 2 | import { CreateGridDataFn, PathfindingAlgorithm, PerformAlgorithmFn } from './types'; 3 | 4 | interface DFSNodeData extends NodeData { 5 | isVisited: boolean; 6 | } 7 | 8 | export const createGridData: CreateGridDataFn = ( 9 | rows, 10 | cols, 11 | startNodePosition, 12 | endNodePosition, 13 | wallPositions, 14 | ) => { 15 | let grid: Node[][] = []; 16 | let startNode: Node | null = null; 17 | let endNode: Node | null = null; 18 | 19 | for (let i = 0; i < rows; i++) { 20 | const row: Node[] = []; 21 | for (let j = 0; j < cols; j++) { 22 | const node = new Node({ 23 | row: i, 24 | col: j, 25 | isVisited: false, 26 | }); 27 | if (j > 0) { 28 | const leftNode = row[j - 1]; 29 | node.neighbors.push(leftNode); 30 | leftNode.neighbors.push(node); 31 | } 32 | if (i > 0) { 33 | const topNode = grid[i - 1][j]; 34 | node.neighbors.push(topNode); 35 | topNode.neighbors.push(node); 36 | } 37 | 38 | row.push(node); 39 | 40 | if (i === startNodePosition.row && j === startNodePosition.col) { 41 | startNode = node; 42 | } else if (i === endNodePosition.row && j === endNodePosition.col) { 43 | endNode = node; 44 | } 45 | } 46 | grid.push(row); 47 | } 48 | 49 | for (let wallPosition of wallPositions) { 50 | const { row, col } = wallPosition; 51 | grid[row][col].isWall = true; 52 | } 53 | 54 | return { grid, startNode, endNode }; 55 | }; 56 | 57 | export const performAlgorithm: PerformAlgorithmFn = ( 58 | grid, 59 | originNode, 60 | destinationNode, 61 | ) => { 62 | const shortestPath: Node[] = []; 63 | const visitedNodes: Node[] = []; 64 | 65 | for (let node of grid) { 66 | node.data.isVisited = false; 67 | node.previousNode = null; 68 | } 69 | 70 | const stack: Node[] = [originNode]; 71 | let nodeRunner: Node; 72 | 73 | while (stack.length > 0) { 74 | nodeRunner = stack.pop()!; 75 | 76 | // Still need to check here even though at the time of pushing 77 | // to the stack, we only push unvisited nodes, because by the 78 | // time we get back to it, it can be already visited from a 79 | // different node. 80 | if (nodeRunner.data.isVisited) { 81 | continue; 82 | } 83 | 84 | nodeRunner.data.isVisited = true; 85 | visitedNodes.push(nodeRunner); 86 | 87 | if (nodeRunner === destinationNode) { 88 | break; 89 | } 90 | 91 | const unvisitedNeighbors = nodeRunner.neighbors.filter( 92 | (node) => !node.data.isVisited && !node.isWall 93 | ); 94 | for (let neighbor of unvisitedNeighbors) { 95 | neighbor.previousNode = nodeRunner; 96 | } 97 | stack.push(...unvisitedNeighbors); 98 | } 99 | 100 | let shortestPathRunner = destinationNode; 101 | while (shortestPathRunner !== originNode) { 102 | shortestPath.unshift(shortestPathRunner); 103 | if (shortestPathRunner.previousNode) { 104 | shortestPathRunner = shortestPathRunner.previousNode; 105 | } else { 106 | // Can't backtrace to originNode, there is no shortest path 107 | break; 108 | } 109 | } 110 | 111 | // console.log(visitedNodes); 112 | return { shortestPath, visitedNodes }; 113 | }; 114 | 115 | const DFS: PathfindingAlgorithm = { 116 | createGridData, 117 | performAlgorithm, 118 | }; 119 | 120 | export default DFS; 121 | -------------------------------------------------------------------------------- /src/algorithms/BFS.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeData } from '../data-structures/Node'; 2 | import { CreateGridDataFn, PathfindingAlgorithm, PerformAlgorithmFn } from './types'; 3 | 4 | interface BFSNodeData extends NodeData { 5 | isVisited: boolean; 6 | } 7 | 8 | export const createGridData: CreateGridDataFn = ( 9 | rows, 10 | cols, 11 | startNodePosition, 12 | endNodePosition, 13 | wallPositions, 14 | ) => { 15 | let grid: Node[][] = []; 16 | let startNode: Node | null = null; 17 | let endNode: Node | null = null; 18 | 19 | for (let i = 0; i < rows; i++) { 20 | const row: Node[] = []; 21 | for (let j = 0; j < cols; j++) { 22 | const node = new Node({ 23 | row: i, 24 | col: j, 25 | isVisited: false, 26 | }); 27 | if (j > 0) { 28 | const leftNode = row[j - 1]; 29 | node.neighbors.push(leftNode); 30 | leftNode.neighbors.push(node); 31 | } 32 | if (i > 0) { 33 | const topNode = grid[i - 1][j]; 34 | node.neighbors.push(topNode); 35 | topNode.neighbors.push(node); 36 | } 37 | 38 | row.push(node); 39 | 40 | if (i === startNodePosition.row && j === startNodePosition.col) { 41 | startNode = node; 42 | } else if (i === endNodePosition.row && j === endNodePosition.col) { 43 | endNode = node; 44 | } 45 | } 46 | grid.push(row); 47 | } 48 | 49 | for (let wallPosition of wallPositions) { 50 | const { row, col } = wallPosition; 51 | grid[row][col].isWall = true; 52 | } 53 | 54 | return { grid, startNode, endNode }; 55 | }; 56 | 57 | export const performAlgorithm: PerformAlgorithmFn = ( 58 | grid, 59 | originNode, 60 | destinationNode, 61 | ) => { 62 | const shortestPath: Node[] = []; 63 | const visitedNodes: Node[] = []; 64 | 65 | for (let node of grid) { 66 | node.data.isVisited = false; 67 | node.previousNode = null; 68 | } 69 | 70 | const queue: Node[] = [originNode]; 71 | let nodeRunner: Node; 72 | 73 | while (queue.length > 0) { 74 | nodeRunner = queue.shift()!; 75 | 76 | // Still need to check here even though at the time of enqueuing 77 | // to the queue, we only enqueue unvisited nodes, because by the 78 | // time we get back to it, it can be already visited from a 79 | // different node. 80 | if (nodeRunner.data.isVisited) { 81 | continue; 82 | } 83 | 84 | nodeRunner.data.isVisited = true; 85 | visitedNodes.push(nodeRunner); 86 | 87 | if (nodeRunner === destinationNode) { 88 | break; 89 | } 90 | 91 | const unvisitedNeighbors = nodeRunner.neighbors.filter( 92 | (node) => !node.data.isVisited && !node.isWall 93 | ); 94 | for (let neighbor of unvisitedNeighbors) { 95 | neighbor.previousNode = nodeRunner; 96 | } 97 | queue.push(...unvisitedNeighbors); 98 | } 99 | 100 | let shortestPathRunner = destinationNode; 101 | while (shortestPathRunner !== originNode) { 102 | shortestPath.unshift(shortestPathRunner); 103 | if (shortestPathRunner.previousNode) { 104 | shortestPathRunner = shortestPathRunner.previousNode; 105 | } else { 106 | // Can't backtrace to originNode, there is no shortest path 107 | break; 108 | } 109 | } 110 | 111 | // console.log(visitedNodes); 112 | return { shortestPath, visitedNodes }; 113 | }; 114 | 115 | const BFS: PathfindingAlgorithm = { 116 | createGridData, 117 | performAlgorithm, 118 | }; 119 | 120 | export default BFS; 121 | -------------------------------------------------------------------------------- /src/components/Node.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, useEffect, useState } from 'react'; 2 | import { DragState } from '../App'; 3 | import './Node.css'; 4 | 5 | export enum NodeType { 6 | START, 7 | END, 8 | MIDDLE, 9 | } 10 | 11 | type NodeProps = { 12 | row: number; 13 | col: number; 14 | type: NodeType; 15 | isVisited: boolean; 16 | isOnPath: boolean; 17 | isWall: boolean; 18 | dragState: DragState; 19 | onDragStart: (nodeType: NodeType, row: number, col: number) => void; 20 | onDragEnter: (row: number, col: number) => void; 21 | onClick: (row: number, col: number) => void; 22 | onMouseDown: (row: number, col: number) => void; 23 | onMouseEnter: (row: number, col: number) => void; 24 | onMouseUp: () => void; 25 | }; 26 | 27 | export default function Node({ 28 | row, 29 | col, 30 | type, 31 | isOnPath, 32 | isVisited, 33 | isWall, 34 | dragState, 35 | onDragStart, 36 | onDragEnter, 37 | onClick, 38 | onMouseDown, 39 | onMouseEnter, 40 | onMouseUp, 41 | }: NodeProps) { 42 | const [isUnderDrag, setIsUnderDrag] = useState(false); 43 | 44 | let className = 'node'; 45 | if (type === NodeType.START) { 46 | className += ' start'; 47 | } else if (type === NodeType.END) { 48 | className += ' end'; 49 | } else if (isOnPath) { 50 | className += ' on-path'; 51 | } else if (isVisited) { 52 | className += ' visited'; 53 | } else if (isWall) { 54 | className += ' wall'; 55 | } else if (isUnderDrag) { 56 | if (dragState.nodeType === NodeType.START) { 57 | className += ' under-drag-start'; 58 | } else if (dragState.nodeType === NodeType.END) { 59 | className += ' under-drag-end'; 60 | } 61 | } 62 | 63 | //-------------Moving start/end nodes-------------// 64 | 65 | const handleDragStart = () => { 66 | onDragStart(type, row, col); 67 | }; 68 | 69 | const handleDragEnter = () => { 70 | if (!isWall) { 71 | onDragEnter(row, col); 72 | setIsUnderDrag(true); 73 | } 74 | }; 75 | 76 | const handleDragLeave = () => { 77 | setIsUnderDrag(false); 78 | }; 79 | 80 | //-------------Creating walls-------------// 81 | 82 | // Click a node to toggle a wall 83 | const handleClick = (event: MouseEvent) => { 84 | if (type === NodeType.MIDDLE && event.metaKey) { 85 | onClick(row, col); 86 | } 87 | }; 88 | 89 | // Mouse down and drag to create walls quickly 90 | const handleMouseDown = (event: MouseEvent) => { 91 | if (type === NodeType.MIDDLE && !event.metaKey) { 92 | onMouseDown(row, col); 93 | } 94 | }; 95 | 96 | const handleMouseEnter = (event: MouseEvent) => { 97 | if (type === NodeType.MIDDLE && !event.metaKey) { 98 | onMouseEnter(row, col); 99 | } 100 | }; 101 | 102 | const handleMouseUp = (event: MouseEvent) => { 103 | if (!event.metaKey) { 104 | onMouseUp(); 105 | } 106 | }; 107 | 108 | // Sometimes onDragLeave is not called, resulting in false nodes 109 | // being highlighted. 110 | useEffect(() => { 111 | if (!dragState.isActive) { 112 | setIsUnderDrag(false); 113 | } 114 | }, [dragState.isActive]); 115 | 116 | return ( 117 |
128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /src/algorithms/Dijkstra.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeData } from '../data-structures/Node'; 2 | import { CreateGridDataFn, PathfindingAlgorithm, PerformAlgorithmFn } from './types'; 3 | 4 | interface DijkstraNodeData extends NodeData { 5 | distance: number; 6 | isVisited: boolean; 7 | } 8 | 9 | export const createGridData: CreateGridDataFn = ( 10 | rows, 11 | cols, 12 | startNodePosition, 13 | endNodePosition, 14 | wallPositions, 15 | ) => { 16 | let grid: Node[][] = []; 17 | let startNode: Node | null = null; 18 | let endNode: Node | null = null; 19 | 20 | for (let i = 0; i < rows; i++) { 21 | const row: Node[] = []; 22 | for (let j = 0; j < cols; j++) { 23 | const node = new Node({ 24 | row: i, 25 | col: j, 26 | distance: Infinity, 27 | isVisited: false, 28 | }); 29 | if (j > 0) { 30 | const leftNode = row[j - 1]; 31 | node.neighbors.push(leftNode); 32 | leftNode.neighbors.push(node); 33 | } 34 | if (i > 0) { 35 | const topNode = grid[i - 1][j]; 36 | node.neighbors.push(topNode); 37 | topNode.neighbors.push(node); 38 | } 39 | 40 | row.push(node); 41 | 42 | if (i === startNodePosition.row && j === startNodePosition.col) { 43 | startNode = node; 44 | } else if (i === endNodePosition.row && j === endNodePosition.col) { 45 | endNode = node; 46 | } 47 | } 48 | grid.push(row); 49 | } 50 | 51 | for (let wallPosition of wallPositions) { 52 | const { row , col } = wallPosition; 53 | grid[row][col].isWall = true; 54 | } 55 | 56 | return { grid, startNode, endNode }; 57 | } 58 | 59 | export const performAlgorithm: PerformAlgorithmFn = ( 60 | grid, 61 | originNode, 62 | destinationNode, 63 | ) => { 64 | const shortestPath: Node[] = []; 65 | const visitedNodes: Node[] = []; 66 | 67 | for (let node of grid) { 68 | if (node === originNode) { 69 | node.data.distance = 0; 70 | } else if (node.isNeighbor(originNode)) { 71 | node.data.distance = 1; 72 | } else { 73 | node.data.distance = Infinity; 74 | } 75 | node.data.isVisited = false; 76 | node.previousNode = null; 77 | } 78 | 79 | const unvisitedNodes = grid.slice(); 80 | 81 | while (unvisitedNodes.length > 0) { 82 | unvisitedNodes.sort( 83 | (node1, node2) => node1.data.distance - node2.data.distance, 84 | ); 85 | const closestNode = unvisitedNodes.shift(); 86 | // If the closest node is infinitely far away, 87 | // it is no not connected to the current grid (graph) 88 | // and we should stop 89 | if (!closestNode || closestNode.data.distance === Infinity) { 90 | break; 91 | } 92 | if (closestNode.isWall) { 93 | continue; 94 | } 95 | 96 | closestNode.data.isVisited = true; 97 | visitedNodes.push(closestNode); 98 | if (closestNode === destinationNode) { 99 | break; 100 | } 101 | 102 | const unvisitedNeighbors = closestNode.neighbors.filter( 103 | (node) => !node.data.isVisited, 104 | ); 105 | for (let neighbor of unvisitedNeighbors) { 106 | neighbor.data.distance = closestNode.data.distance + 1; 107 | neighbor.previousNode = closestNode; 108 | } 109 | } 110 | 111 | let shortestPathRunner = destinationNode; 112 | while (shortestPathRunner !== originNode) { 113 | shortestPath.unshift(shortestPathRunner); 114 | if (shortestPathRunner.previousNode) { 115 | shortestPathRunner = shortestPathRunner.previousNode; 116 | } else { 117 | // Can't backtrace to originNode, there is no shortest path 118 | break; 119 | } 120 | } 121 | 122 | return { shortestPath, visitedNodes }; 123 | } 124 | 125 | const Dijkstra: PathfindingAlgorithm = { 126 | createGridData, 127 | performAlgorithm, 128 | }; 129 | 130 | export default Dijkstra; 131 | -------------------------------------------------------------------------------- /src/algorithms/AStar.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeData } from '../data-structures/Node'; 2 | import { CreateGridDataFn, PathfindingAlgorithm, PerformAlgorithmFn } from './types'; 3 | 4 | interface AStarNodeData extends NodeData { 5 | fScore: number; // Total cost 6 | gScore: number; // Cost to traverse from start node to this node 7 | hScore: number; // Heuristic (estimated cost from this node to end node) 8 | } 9 | 10 | export const createGridData: CreateGridDataFn = ( 11 | rows, 12 | cols, 13 | startNodePosition, 14 | endNodePosition, 15 | wallPositions, 16 | ) => { 17 | let grid: Node[][] = []; 18 | let startNode: Node | null = null; 19 | let endNode: Node | null = null; 20 | 21 | for (let i = 0; i < rows; i++) { 22 | const row: Node[] = []; 23 | for (let j = 0; j < cols; j++) { 24 | const node = new Node({ 25 | row: i, 26 | col: j, 27 | fScore: Infinity, 28 | gScore: Infinity, 29 | hScore: Infinity, 30 | }); 31 | if (j > 0) { 32 | const leftNode = row[j - 1]; 33 | node.neighbors.push(leftNode); 34 | leftNode.neighbors.push(node); 35 | } 36 | if (i > 0) { 37 | const topNode = grid[i - 1][j]; 38 | node.neighbors.push(topNode); 39 | topNode.neighbors.push(node); 40 | } 41 | 42 | row.push(node); 43 | 44 | if (i === startNodePosition.row && j === startNodePosition.col) { 45 | startNode = node; 46 | } else if (i === endNodePosition.row && j === endNodePosition.col) { 47 | endNode = node; 48 | } 49 | } 50 | grid.push(row); 51 | } 52 | 53 | for (let wallPosition of wallPositions) { 54 | const { row, col } = wallPosition; 55 | grid[row][col].isWall = true; 56 | } 57 | 58 | return { grid, startNode, endNode }; 59 | } 60 | 61 | export function calculateHeuristic( 62 | node: Node, 63 | endNode: Node, 64 | ): number { 65 | // For now, don't allow a node to move diagonally 66 | return ( 67 | Math.abs(node.data.row - endNode.data.row) + 68 | Math.abs(node.data.col - endNode.data.col) 69 | ); 70 | } 71 | 72 | /** 73 | * Pseudo code: https://en.wikipedia.org/wiki/A*_search_algorithm 74 | */ 75 | export const performAlgorithm: PerformAlgorithmFn = ( 76 | grid, 77 | startNode, 78 | endNode, 79 | ) => { 80 | const shortestPath: Node[] = []; 81 | const visitedNodes: Node[] = []; 82 | 83 | for (let node of grid) { 84 | node.data.gScore = node === startNode ? 0 : Infinity; 85 | node.data.hScore = calculateHeuristic(node, endNode); 86 | node.data.fScore = node.data.gScore + node.data.hScore; 87 | node.previousNode = null; 88 | } 89 | 90 | const openSet: Node[] = [startNode]; 91 | 92 | while (openSet.length > 0) { 93 | // debugger; 94 | openSet.sort((node1, node2) => node1.data.fScore - node2.data.fScore); 95 | const currentNode = openSet.shift(); 96 | if (!currentNode) { 97 | break; 98 | } 99 | if (currentNode.isWall) { 100 | continue; 101 | } 102 | 103 | visitedNodes.push(currentNode); 104 | if (currentNode === endNode) { 105 | break; 106 | } 107 | for (let neighbor of currentNode.neighbors) { 108 | let tentativeGScore = currentNode.data.gScore + 1; 109 | // Find a new best path to a neighbor, record it 110 | if (tentativeGScore < neighbor.data.gScore) { 111 | neighbor.previousNode = currentNode; 112 | neighbor.data.gScore = tentativeGScore; 113 | neighbor.data.fScore = tentativeGScore + neighbor.data.hScore; 114 | if (!openSet.includes(neighbor)) { 115 | openSet.push(neighbor); 116 | } 117 | } 118 | } 119 | } 120 | 121 | let shortestPathRunner = endNode; 122 | while (shortestPathRunner !== startNode) { 123 | shortestPath.unshift(shortestPathRunner); 124 | if (shortestPathRunner.previousNode) { 125 | shortestPathRunner = shortestPathRunner.previousNode; 126 | } else { 127 | // Can't backtrace to originNode, there is no shortest path 128 | break; 129 | } 130 | } 131 | 132 | return { shortestPath, visitedNodes }; 133 | } 134 | 135 | const AStar: PathfindingAlgorithm = { 136 | createGridData, 137 | performAlgorithm, 138 | }; 139 | 140 | export default AStar; 141 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | padding: var(--spacing-md); 6 | width: 100%; 7 | row-gap: var(--spacing-md); 8 | } 9 | 10 | .title { 11 | background: var(--gradient-accent); 12 | border-radius: var(--border-radius-lg); 13 | padding: var(--spacing-lg) var(--spacing-md); 14 | width: 100%; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | color: white; 19 | box-shadow: var(--box-shadow); 20 | } 21 | 22 | .title h1 { 23 | font-weight: normal; 24 | font-family: var(--font-family-title); 25 | font-size: var(--font-size-xl); 26 | margin: 0; 27 | } 28 | 29 | .title .desktop { 30 | display: none; 31 | } 32 | 33 | .title .mobile { 34 | margin-top: var(--spacing-sm); 35 | } 36 | 37 | .title .mobile .icon { 38 | margin-left: 0.5rem; 39 | } 40 | 41 | .title .mobile a, .title .mobile a:visited { 42 | color: white; 43 | } 44 | 45 | .main-grid, .actions, .tips { 46 | border-radius: var(--border-radius-lg); 47 | padding: var(--spacing-lg) var(--spacing-md); 48 | width: 100%; 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | background-color: white; 53 | box-shadow: var(--box-shadow); 54 | } 55 | 56 | .main-grid h2, .actions h2, .tips h2 { 57 | align-self: flex-start; 58 | margin: 0; 59 | font-size: var(--font-size-lg); 60 | font-weight: bold; 61 | background-image: var(--gradient-accent); 62 | color: transparent; 63 | background-clip: text; 64 | -webkit-background-clip: text; 65 | } 66 | 67 | .main-grid .legends { 68 | display: grid; 69 | grid-template-areas: 70 | "start unvisited shortest" 71 | "end visited wall"; 72 | width: 100%; 73 | row-gap: var(--spacing-sm); 74 | margin-top: var(--spacing-md); 75 | } 76 | 77 | .main-grid .legends .legend-group { 78 | display: flex; 79 | column-gap: 0.5rem; 80 | align-items: center; 81 | } 82 | 83 | .main-grid .legends .legend-group.start { 84 | grid-area: start; 85 | } 86 | 87 | .main-grid .legends .legend-group.end { 88 | grid-area: end; 89 | } 90 | 91 | .main-grid .legends .legend-group { 92 | grid-area: unvisited; 93 | } 94 | 95 | .main-grid .legends .legend-group.visited { 96 | grid-area: visited; 97 | } 98 | 99 | .main-grid .legends .legend-group.on-path { 100 | grid-area: shortest; 101 | } 102 | 103 | .main-grid .legends .legend-group.wall { 104 | grid-area: wall; 105 | } 106 | 107 | .main-grid .legends .legend-group .node { 108 | --node-width: 18px; 109 | } 110 | 111 | .main-grid .legends .legend-group .label { 112 | font-size: 0.75rem; 113 | } 114 | 115 | .main-grid .grid { 116 | --grid-gap: calc((100vw - 4 * var(--spacing-md) - 11 * var(--node-width)) / 10); 117 | grid-area: grid; 118 | justify-self: center; 119 | display: grid; 120 | row-gap: var(--grid-gap); 121 | column-gap: var(--grid-gap); 122 | width: fit-content; 123 | margin-top: var(--spacing-md); 124 | } 125 | 126 | .actions .algorithms { 127 | display: grid; 128 | grid-template-columns: 1fr 1fr; 129 | gap: var(--spacing-sm); 130 | width: 100%; 131 | margin-top: var(--spacing-md); 132 | } 133 | 134 | .actions .cleanup-buttons { 135 | display: grid; 136 | grid-template-columns: 1fr 1fr 1fr; 137 | column-gap: var(--spacing-sm); 138 | width: 100%; 139 | margin-top: var(--spacing-md); 140 | } 141 | 142 | .actions button { 143 | color: var(--color-foreground); 144 | background-color: var(--color-background); 145 | font-weight: bold; 146 | cursor: pointer; 147 | border: none; 148 | transition: background-color ease-in-out 0.1s; 149 | } 150 | 151 | .actions button:is(:hover, :focus-visible) { 152 | background-color: var(--color-background-dark); 153 | } 154 | 155 | .actions .algorithms button { 156 | padding: var(--spacing-md); 157 | border-radius: var(--border-radius-md); 158 | } 159 | 160 | .actions .cleanup-buttons button { 161 | padding: var(--spacing-sm); 162 | border-radius: var(--border-radius-sm); 163 | font-size: var(--font-size-sm); 164 | } 165 | 166 | .tips ul { 167 | margin: 0; 168 | margin-top: var(--spacing-md); 169 | padding-left: 1rem; 170 | align-self: flex-start; 171 | } 172 | 173 | .tips ul li:not(:last-child) { 174 | margin-bottom: var(--spacing-sm); 175 | } 176 | 177 | .tips ul li::marker { 178 | color: var(--color-accent-1); 179 | } 180 | 181 | code { 182 | background-color: var(--color-background); 183 | padding: 0.1rem 0.25rem; 184 | border-radius: 0.25rem; 185 | font-weight: bold; 186 | } 187 | 188 | footer { 189 | width: 100%; 190 | display: flex; 191 | flex-direction: column; 192 | align-items: center; 193 | margin-top: var(--spacing-xl); 194 | font-size: var(--font-size-sm); 195 | } 196 | 197 | footer .icon { 198 | margin-left: 0.2em; 199 | margin-right: 0.2em; 200 | color: var(--color-accent-3); 201 | } 202 | 203 | footer .contacts { 204 | margin: var(--spacing-md) 0; 205 | display: flex; 206 | gap: 0.75rem; 207 | align-items: center; 208 | } 209 | 210 | footer a, footer a:visited { 211 | color: var(--color-foreground); 212 | } 213 | 214 | @media screen and (min-width: 962px) { 215 | .App { 216 | padding: 2rem 4rem 1rem; 217 | display: grid; 218 | grid-template-areas: 219 | "title title" 220 | "grid controls" 221 | "grid tips" 222 | "footer footer"; 223 | grid-template-columns: auto 350px; 224 | grid-template-rows: auto auto 1fr auto; 225 | column-gap: var(--spacing-lg); 226 | } 227 | 228 | .title { 229 | background: transparent; 230 | border-radius: 0; 231 | padding: 0; 232 | width: 100%; 233 | position: relative; 234 | box-shadow: none; 235 | grid-area: title; 236 | margin-bottom: var(--spacing-md); 237 | } 238 | 239 | .title h1 { 240 | background-image: var(--gradient-accent); 241 | background-clip: text; 242 | -webkit-background-clip: text; 243 | color: transparent; 244 | font-size: var(--font-size-xxl) 245 | } 246 | 247 | .title .mobile { 248 | display: none; 249 | } 250 | 251 | .title .desktop { 252 | display: block; 253 | position: absolute; 254 | top: 50%; 255 | right: 0; 256 | transform: translateY(-50%); 257 | } 258 | 259 | .title .desktop a, .title .desktop a:visited { 260 | color: var(--color-foreground); 261 | } 262 | 263 | .main-grid { 264 | grid-area: grid; 265 | align-self: stretch; 266 | } 267 | 268 | .main-grid .legends { 269 | grid-template-areas: "start end unvisited visited shortest wall"; 270 | width: fit-content; 271 | column-gap: var(--spacing-md); 272 | } 273 | 274 | .main-grid .grid { 275 | --grid-gap: 2px; 276 | --node-width: 24px; 277 | } 278 | 279 | .actions { 280 | grid-area: controls; 281 | align-self: start; 282 | } 283 | 284 | .tips { 285 | grid-area: tips; 286 | align-self: stretch; 287 | } 288 | 289 | footer { 290 | grid-area: footer; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { DragEventHandler, useMemo, useRef, useState } from 'react'; 2 | import produce from 'immer'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faGithub, faLinkedinIn, faTwitter } from '@fortawesome/free-brands-svg-icons'; 5 | import { faHeart, faGlobe } from '@fortawesome/free-solid-svg-icons'; 6 | import { isMobile } from 'react-device-detect'; 7 | 8 | import Node, { NodeType } from './components/Node'; 9 | import { calculateDefaultNodePositions, calculateGridDimension } from './utils/dimension'; 10 | import { Node as NodeDS, NodeData as NodeDSData } from './data-structures/Node'; 11 | import { PathfindingAlgorithm } from './algorithms/types'; 12 | import DFS from './algorithms/DFS'; 13 | import BFS from './algorithms/BFS'; 14 | import Dijkstra from './algorithms/Dijkstra'; 15 | import AStar from './algorithms/AStar'; 16 | import './App.css'; 17 | 18 | type NodeState = { 19 | isVisited: boolean; 20 | isOnPath: boolean; 21 | isWall: boolean; 22 | }; 23 | 24 | type NodeData = { 25 | row: number; 26 | col: number; 27 | }; 28 | 29 | export type DragState = { 30 | isActive: boolean; 31 | nodeType: NodeType | null; 32 | row: number; 33 | col: number; 34 | }; 35 | 36 | const gridDimension = calculateGridDimension(); 37 | const NUM_ROWS = gridDimension.rows; 38 | const NUM_COLS = gridDimension.cols; 39 | const defaultPositions = calculateDefaultNodePositions(gridDimension); 40 | const DEFAULT_START_NODE_POS = defaultPositions.start; 41 | const DEFAULT_END_NODE_POS = defaultPositions.end; 42 | 43 | function App() { 44 | const [startNodePos, setStartNodePos] = useState(DEFAULT_START_NODE_POS); 45 | const [endNodePos, setEndNodePos] = useState(DEFAULT_END_NODE_POS); 46 | const initialNodeStates = useMemo(() => { 47 | const states: NodeState[][] = []; 48 | for (let i = 0; i < NUM_ROWS; i++) { 49 | const row: NodeState[] = []; 50 | for (let j = 0; j < NUM_COLS; j++) { 51 | row.push({ isVisited: false, isOnPath: false, isWall: false }); 52 | } 53 | states.push(row); 54 | } 55 | return states; 56 | }, [NUM_ROWS, NUM_COLS]); 57 | const [nodeStates, setNodeStates] = useState(initialNodeStates); 58 | const timeoutRef = useRef([]); 59 | const [dragState, setDragState] = useState({ 60 | isActive: false, 61 | nodeType: null, 62 | row: 0, 63 | col: 0, 64 | }); 65 | const [isCreatingWall, setIsCreatingWall] = useState(false); 66 | 67 | //-------------Helpers-------------// 68 | 69 | const clearVisualizedPath = () => { 70 | setNodeStates( 71 | produce((draft) => { 72 | for (let row of draft) { 73 | for (let nodeState of row) { 74 | nodeState.isVisited = false; 75 | nodeState.isOnPath = false; 76 | } 77 | } 78 | }), 79 | ); 80 | timeoutRef.current.forEach(clearTimeout); 81 | timeoutRef.current = []; 82 | }; 83 | 84 | const resetNodeStates = () => { 85 | setNodeStates(initialNodeStates); 86 | timeoutRef.current.forEach(clearTimeout); 87 | timeoutRef.current = []; 88 | }; 89 | 90 | const resetGrid = () => { 91 | setStartNodePos(DEFAULT_START_NODE_POS); 92 | setEndNodePos(DEFAULT_END_NODE_POS); 93 | setNodeStates(initialNodeStates); 94 | timeoutRef.current.forEach(clearTimeout); 95 | timeoutRef.current = []; 96 | }; 97 | 98 | //-------------Visualizing pathfinding algorithm-------------// 99 | 100 | const getWallPositions = () => { 101 | const wallPositions = []; 102 | for (let i = 0; i < nodeStates.length; i++) { 103 | for (let j = 0; j < nodeStates[i].length; j++) { 104 | if (nodeStates[i][j].isWall) { 105 | wallPositions.push({ row: i, col: j }); 106 | } 107 | } 108 | } 109 | return wallPositions; 110 | }; 111 | 112 | const animateAlgorithm = ( 113 | visitedNodes: NodeDS[], 114 | shortestPath: NodeDS[], 115 | ) => { 116 | for (let i = 0; i < visitedNodes.length; i++) { 117 | const timeout = setTimeout(() => { 118 | const { 119 | data: { row, col }, 120 | } = visitedNodes[i]; 121 | setNodeStates( 122 | produce((draft) => { 123 | draft[row][col].isVisited = true; 124 | }), 125 | ); 126 | }, i * 5); 127 | timeoutRef.current.push(timeout); 128 | } 129 | 130 | for (let i = 0; i < shortestPath.length; i++) { 131 | const timeout = setTimeout(() => { 132 | const { 133 | data: { row, col }, 134 | } = shortestPath[i]; 135 | setNodeStates( 136 | produce((draft) => { 137 | draft[row][col].isOnPath = true; 138 | }), 139 | ); 140 | }, (visitedNodes.length + i * 4) * 5); // Slow down shortest path animation by 4 times 141 | timeoutRef.current.push(timeout); 142 | } 143 | }; 144 | 145 | const visualizeAlgorithm = (algorithm: PathfindingAlgorithm) => { 146 | clearVisualizedPath(); 147 | 148 | const { grid, startNode, endNode } = algorithm.createGridData( 149 | NUM_ROWS, 150 | NUM_COLS, 151 | startNodePos, 152 | endNodePos, 153 | getWallPositions(), 154 | ); 155 | if (!startNode || !endNode) { 156 | return; 157 | } 158 | 159 | const { visitedNodes, shortestPath } = algorithm.performAlgorithm( 160 | grid.flat(), 161 | startNode, 162 | endNode, 163 | ); 164 | animateAlgorithm(visitedNodes, shortestPath); 165 | } 166 | 167 | //-------------Moving start/end nodes-------------// 168 | 169 | const handleDragStart = (nodeType: NodeType, row: number, col: number) => { 170 | clearVisualizedPath(); 171 | setDragState({ isActive: true, nodeType, row, col }); 172 | }; 173 | 174 | const handleDragEnter = (row: number, col: number) => { 175 | setDragState({ ...dragState, row, col }); 176 | }; 177 | 178 | const handleDragOver: DragEventHandler = (event) => { 179 | event.preventDefault(); 180 | event.dataTransfer.dropEffect = 'move'; 181 | }; 182 | 183 | const handleDrop: DragEventHandler = () => { 184 | const { nodeType, row, col } = dragState; 185 | if (nodeType === NodeType.START) { 186 | setStartNodePos({ row: row, col: col }); 187 | } else if (nodeType === NodeType.END) { 188 | setEndNodePos({ row: row, col: col }); 189 | } 190 | setDragState({ isActive: false, nodeType: null, row: 0, col: 0 }); 191 | }; 192 | 193 | //-------------Creating walls-------------// 194 | 195 | const handleNodeClick = (row: number, col: number) => { 196 | clearVisualizedPath(); 197 | setNodeStates( 198 | produce((draft) => { 199 | draft[row][col].isWall = !draft[row][col].isWall; 200 | }), 201 | ); 202 | }; 203 | 204 | const handleNodeMouseDown = (row: number, col: number) => { 205 | clearVisualizedPath(); 206 | setIsCreatingWall(true); 207 | setNodeStates( 208 | produce((draft) => { 209 | draft[row][col].isWall = true; 210 | }), 211 | ); 212 | }; 213 | 214 | const handleNodeMouseEnter = (row: number, col: number) => { 215 | if (isCreatingWall) { 216 | setNodeStates( 217 | produce((draft) => { 218 | draft[row][col].isWall = true; 219 | }), 220 | ); 221 | } 222 | }; 223 | 224 | const handleNodeMouseUp = () => { 225 | setIsCreatingWall(false); 226 | }; 227 | 228 | return ( 229 |
230 |
231 |

Pathfinding Algorithms Visualizer

232 |
233 | Check out on GitHub 234 | 235 |
236 |
237 | 238 | 239 | 240 |
241 |
242 | 243 |
244 |

Grid

245 |
246 |
247 |
248 | Start 249 |
250 |
251 |
252 | End 253 |
254 |
255 |
256 | Unvisited 257 |
258 |
259 |
260 | Visited 261 |
262 |
263 |
264 | Shortest 265 |
266 |
267 |
268 | Wall 269 |
270 |
271 | 272 |
281 | {nodeStates.map((row, rowIndex) => 282 | row.map((nodeState, colIndex) => { 283 | let type: NodeType; 284 | if ( 285 | rowIndex === startNodePos.row && 286 | colIndex === startNodePos.col 287 | ) { 288 | type = NodeType.START; 289 | } else if ( 290 | rowIndex === endNodePos.row && 291 | colIndex === endNodePos.col 292 | ) { 293 | type = NodeType.END; 294 | } else { 295 | type = NodeType.MIDDLE; 296 | } 297 | const { isVisited, isOnPath, isWall } = nodeState; 298 | 299 | return ( 300 | 316 | ); 317 | }), 318 | )} 319 |
320 |
321 | 322 |
323 |

Controls

324 | 325 |
326 | 333 | 340 | 347 | 354 |
355 | 356 |
357 | 364 | 371 | 374 |
375 |
376 | 377 |
378 |

Tips

379 | {isMobile ? ( 380 |
    381 |
  • 382 |
    383 | View this site on desktop to see extra features. 384 |
    385 |
  • 386 |
  • 387 |
    388 | Tap on a white node to toggle a wall. 389 |
    390 |
  • 391 |
392 | ) : ( 393 |
    394 |
  • 395 |
    396 | Try dragging the start/end nodes to new positions. 397 |
    398 |
  • 399 |
  • 400 |
    401 | Click on a white node and drag to create walls quickly. 402 |
    403 |
  • 404 |
  • 405 |
    406 | Hold ctrl/cmd and click on a white node to toggle a wall. 407 |
    408 |
  • 409 |
410 | )} 411 |
412 | 413 | 430 |
431 | ); 432 | } 433 | 434 | export default App; 435 | --------------------------------------------------------------------------------