diff --git a/projects_registry.json b/projects_registry.json index cd0a3e3..7720c6d 100644 --- a/projects_registry.json +++ b/projects_registry.json @@ -651,5 +651,22 @@ "search" ], "path": "utilities/Pathfinding-Visualizer/Pathfinding-Visualizer.py" + }, + { + "name": "TSP Visualizer", + "emoji": "🗺️", + "category": "utilities", + "difficulty": "advanced", + "description": "Visualize the Traveling Salesperson Problem (TSP) using Nearest Neighbor and Brute Force algorithms.", + "keywords": [ + "tsp", + "visualizer", + "algorithm", + "salesperson", + "path", + "shortest", + "search" + ], + "path": "utilities/TSP-Visualizer/TSP-Visualizer.py" } ] \ No newline at end of file diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..a49fe03 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,3 @@ +def test_smoke(): + """A minimal smoke test to ensure the test suite runs.""" + assert True diff --git a/utilities/TSP-Visualizer/TSP-Visualizer.py b/utilities/TSP-Visualizer/TSP-Visualizer.py new file mode 100644 index 0000000..3ee4b69 --- /dev/null +++ b/utilities/TSP-Visualizer/TSP-Visualizer.py @@ -0,0 +1,169 @@ +import pygame +import math +import itertools +import random +import sys + +pygame.init() +WIDTH, HEIGHT = 800, 600 +WIN = pygame.display.set_mode((WIDTH, HEIGHT)) +pygame.display.set_caption("TSP Visualizer") + +# Colors +WHITE = (255, 255, 255) +BLACK = (10, 10, 10) +GRAY = (50, 50, 50) +RED = (255, 100, 100) +GREEN = (100, 255, 100) +BLUE = (100, 100, 255) +YELLOW = (255, 255, 100) + +font = pygame.font.SysFont("comicsansms", 20) +small_font = pygame.font.SysFont("comicsansms", 15) + +def get_distance(p1, p2): + return math.hypot(p1[0] - p2[0], p1[1] - p2[1]) + +def path_distance(nodes, order): + dist = 0 + for i in range(len(order) - 1): + dist += get_distance(nodes[order[i]], nodes[order[i+1]]) + if len(order) > 0: + dist += get_distance(nodes[order[-1]], nodes[order[0]]) + return dist + +def draw_text(win, text, x, y, color=WHITE): + text_surface = font.render(text, True, color) + win.blit(text_surface, (x, y)) + +def draw_nodes(win, nodes, order=[], current_edge=None, best_dist=0): + win.fill(BLACK) + + # Draw instructions + draw_text(win, "Click to add nodes | [R] Random | [C] Clear | [1] Nearest Neighbor | [2] Brute Force", 10, 10, GRAY) + draw_text(win, f"Nodes: {len(nodes)}", 10, 40, WHITE) + if best_dist > 0: + draw_text(win, f"Distance: {best_dist:.2f}", 10, 70, YELLOW) + + # Draw the path + if len(order) > 1: + for i in range(len(order) - 1): + pygame.draw.line(win, GREEN, nodes[order[i]], nodes[order[i+1]], 2) + pygame.draw.line(win, GREEN, nodes[order[-1]], nodes[order[0]], 2) + + if current_edge: + pygame.draw.line(win, RED, current_edge[0], current_edge[1], 2) + + # Draw nodes + for i, node in enumerate(nodes): + color = BLUE if i == 0 else WHITE + pygame.draw.circle(win, color, node, 6) + + pygame.display.update() + +def solve_nearest_neighbor(nodes): + if len(nodes) < 2: return + + unvisited = list(range(1, len(nodes))) + current = 0 + order = [0] + + while unvisited: + # visualization step + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + nearest = min(unvisited, key=lambda x: get_distance(nodes[current], nodes[x])) + + # Animate the connection being tested + draw_nodes(WIN, nodes, order, current_edge=(nodes[current], nodes[nearest]), best_dist=path_distance(nodes, order)) + pygame.time.delay(100) + + order.append(nearest) + unvisited.remove(nearest) + current = nearest + + draw_nodes(WIN, nodes, order, best_dist=path_distance(nodes, order)) + +def solve_brute_force(nodes): + if len(nodes) < 2: return + if len(nodes) > 10: + print("Too many nodes for brute force! (Max 10 recommended)") + return + + min_dist = float('inf') + best_order = [] + + # fix the start node to 0 to reduce permutations by N + nodes_idx = list(range(1, len(nodes))) + count = 0 + total = math.factorial(len(nodes_idx)) + + for perm in itertools.permutations(nodes_idx): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + + current_order = [0] + list(perm) + dist = path_distance(nodes, current_order) + if dist < min_dist: + min_dist = dist + best_order = current_order + + count += 1 + if count % max(1, total // 100) == 0: # animate periodically + draw_nodes(WIN, nodes, best_order, current_edge=(nodes[current_order[-1]], nodes[current_order[0]]), best_dist=min_dist) + + draw_nodes(WIN, nodes, best_order, best_dist=min_dist) + +def main(): + nodes = [] + order = [] + best_dist = 0 + run = True + + while run: + draw_nodes(WIN, nodes, order, best_dist=best_dist) + + for event in pygame.event.get(): + if event.type == pygame.QUIT: + run = False + + if event.type == pygame.MOUSEBUTTONDOWN: + x, y = pygame.mouse.get_pos() + nodes.append((x, y)) + order = [] + best_dist = 0 + + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_c: + nodes = [] + order = [] + best_dist = 0 + elif event.key == pygame.K_r: + nodes = [] + for _ in range(10): + x = random.randint(50, WIDTH - 50) + y = random.randint(100, HEIGHT - 50) + nodes.append((x, y)) + order = [] + best_dist = 0 + elif event.key == pygame.K_1: + if len(nodes) > 1: + solve_nearest_neighbor(nodes) + elif event.key == pygame.K_2: + if len(nodes) > 1: + if len(nodes) > 10: + draw_text(WIN, "Too many nodes for Brute Force! Use <= 10.", 10, 100, RED) + pygame.display.update() + pygame.time.delay(2000) + else: + solve_brute_force(nodes) + + pygame.quit() + +if __name__ == "__main__": + main() diff --git a/web-app/assets/banners/2048-game.webp b/web-app/assets/banners/2048-game.webp index 8be4c48..4b4dace 100644 Binary files a/web-app/assets/banners/2048-game.webp and b/web-app/assets/banners/2048-game.webp differ diff --git a/web-app/assets/banners/armstrong.webp b/web-app/assets/banners/armstrong.webp index e3d83bb..397e88a 100644 Binary files a/web-app/assets/banners/armstrong.webp and b/web-app/assets/banners/armstrong.webp differ diff --git a/web-app/assets/banners/binary-search.webp b/web-app/assets/banners/binary-search.webp index 3a059c7..b0ca47c 100644 Binary files a/web-app/assets/banners/binary-search.webp and b/web-app/assets/banners/binary-search.webp differ diff --git a/web-app/assets/banners/blackjack21.webp b/web-app/assets/banners/blackjack21.webp index c40893d..5e9fe57 100644 Binary files a/web-app/assets/banners/blackjack21.webp and b/web-app/assets/banners/blackjack21.webp differ diff --git a/web-app/assets/banners/bubble-sort.webp b/web-app/assets/banners/bubble-sort.webp index 429eb6b..15c5688 100644 Binary files a/web-app/assets/banners/bubble-sort.webp and b/web-app/assets/banners/bubble-sort.webp differ diff --git a/web-app/assets/banners/budget-tracker.webp b/web-app/assets/banners/budget-tracker.webp index 86134fd..97f5674 100644 Binary files a/web-app/assets/banners/budget-tracker.webp and b/web-app/assets/banners/budget-tracker.webp differ diff --git a/web-app/assets/banners/caesar-cipher.webp b/web-app/assets/banners/caesar-cipher.webp index 84b160e..aab8f6f 100644 Binary files a/web-app/assets/banners/caesar-cipher.webp and b/web-app/assets/banners/caesar-cipher.webp differ diff --git a/web-app/assets/banners/calculator.webp b/web-app/assets/banners/calculator.webp index adb8025..fccb873 100644 Binary files a/web-app/assets/banners/calculator.webp and b/web-app/assets/banners/calculator.webp differ diff --git a/web-app/assets/banners/chess.webp b/web-app/assets/banners/chess.webp index ab73fb7..b20230f 100644 Binary files a/web-app/assets/banners/chess.webp and b/web-app/assets/banners/chess.webp differ diff --git a/web-app/assets/banners/coin-flip.webp b/web-app/assets/banners/coin-flip.webp index 10a1528..4c43856 100644 Binary files a/web-app/assets/banners/coin-flip.webp and b/web-app/assets/banners/coin-flip.webp differ diff --git a/web-app/assets/banners/collatz.webp b/web-app/assets/banners/collatz.webp index e8467f0..796cdc6 100644 Binary files a/web-app/assets/banners/collatz.webp and b/web-app/assets/banners/collatz.webp differ diff --git a/web-app/assets/banners/color-palette.webp b/web-app/assets/banners/color-palette.webp index 520e1c7..cd3cc42 100644 Binary files a/web-app/assets/banners/color-palette.webp and b/web-app/assets/banners/color-palette.webp differ diff --git a/web-app/assets/banners/coordinate-polar-transform.webp b/web-app/assets/banners/coordinate-polar-transform.webp index 2e83964..1290f31 100644 Binary files a/web-app/assets/banners/coordinate-polar-transform.webp and b/web-app/assets/banners/coordinate-polar-transform.webp differ diff --git a/web-app/assets/banners/derivative-calculator.webp b/web-app/assets/banners/derivative-calculator.webp index 6c8b101..eaa7362 100644 Binary files a/web-app/assets/banners/derivative-calculator.webp and b/web-app/assets/banners/derivative-calculator.webp differ diff --git a/web-app/assets/banners/dice-rolling.webp b/web-app/assets/banners/dice-rolling.webp index bfaad04..6fc8ba3 100644 Binary files a/web-app/assets/banners/dice-rolling.webp and b/web-app/assets/banners/dice-rolling.webp differ diff --git a/web-app/assets/banners/dots-boxes.webp b/web-app/assets/banners/dots-boxes.webp index 6b1d1ed..4b77731 100644 Binary files a/web-app/assets/banners/dots-boxes.webp and b/web-app/assets/banners/dots-boxes.webp differ diff --git a/web-app/assets/banners/emoji-memory-game.webp b/web-app/assets/banners/emoji-memory-game.webp index 2c6a475..1b9e816 100644 Binary files a/web-app/assets/banners/emoji-memory-game.webp and b/web-app/assets/banners/emoji-memory-game.webp differ diff --git a/web-app/assets/banners/fibonacci.webp b/web-app/assets/banners/fibonacci.webp index fc75385..0be96a4 100644 Binary files a/web-app/assets/banners/fibonacci.webp and b/web-app/assets/banners/fibonacci.webp differ diff --git a/web-app/assets/banners/flames.webp b/web-app/assets/banners/flames.webp index 284bc34..a5aa44c 100644 Binary files a/web-app/assets/banners/flames.webp and b/web-app/assets/banners/flames.webp differ diff --git a/web-app/assets/banners/flappy-game.webp b/web-app/assets/banners/flappy-game.webp index f88150f..afca434 100644 Binary files a/web-app/assets/banners/flappy-game.webp and b/web-app/assets/banners/flappy-game.webp differ diff --git a/web-app/assets/banners/fourier-series.webp b/web-app/assets/banners/fourier-series.webp index 0d1435a..54d1336 100644 Binary files a/web-app/assets/banners/fourier-series.webp and b/web-app/assets/banners/fourier-series.webp differ diff --git a/web-app/assets/banners/hangman.webp b/web-app/assets/banners/hangman.webp index 2fe26e7..dfcf3b3 100644 Binary files a/web-app/assets/banners/hangman.webp and b/web-app/assets/banners/hangman.webp differ diff --git a/web-app/assets/banners/math-quiz.webp b/web-app/assets/banners/math-quiz.webp index ebc3fa6..321b10f 100644 Binary files a/web-app/assets/banners/math-quiz.webp and b/web-app/assets/banners/math-quiz.webp differ diff --git a/web-app/assets/banners/matrix-calculator.webp b/web-app/assets/banners/matrix-calculator.webp index 52bb894..7a6b7a0 100644 Binary files a/web-app/assets/banners/matrix-calculator.webp and b/web-app/assets/banners/matrix-calculator.webp differ diff --git a/web-app/assets/banners/morse-code.webp b/web-app/assets/banners/morse-code.webp index c59c4bc..a535603 100644 Binary files a/web-app/assets/banners/morse-code.webp and b/web-app/assets/banners/morse-code.webp differ diff --git a/web-app/assets/banners/number-converter.webp b/web-app/assets/banners/number-converter.webp index 470a004..dd1fbeb 100644 Binary files a/web-app/assets/banners/number-converter.webp and b/web-app/assets/banners/number-converter.webp differ diff --git a/web-app/assets/banners/number-guessing.webp b/web-app/assets/banners/number-guessing.webp index 9991da1..fb78857 100644 Binary files a/web-app/assets/banners/number-guessing.webp and b/web-app/assets/banners/number-guessing.webp differ diff --git a/web-app/assets/banners/number-sliding-puzzle.webp b/web-app/assets/banners/number-sliding-puzzle.webp index 9332e71..9557aa0 100644 Binary files a/web-app/assets/banners/number-sliding-puzzle.webp and b/web-app/assets/banners/number-sliding-puzzle.webp differ diff --git a/web-app/assets/banners/pascal-triangle.webp b/web-app/assets/banners/pascal-triangle.webp index 264c0ac..9e3ec08 100644 Binary files a/web-app/assets/banners/pascal-triangle.webp and b/web-app/assets/banners/pascal-triangle.webp differ diff --git a/web-app/assets/banners/password-forge.webp b/web-app/assets/banners/password-forge.webp index cf73523..0b8d967 100644 Binary files a/web-app/assets/banners/password-forge.webp and b/web-app/assets/banners/password-forge.webp differ diff --git a/web-app/assets/banners/pathfinding-visualizer.webp b/web-app/assets/banners/pathfinding-visualizer.webp index d2c3d31..d0297f1 100644 Binary files a/web-app/assets/banners/pathfinding-visualizer.webp and b/web-app/assets/banners/pathfinding-visualizer.webp differ diff --git a/web-app/assets/banners/prime-analyzer.webp b/web-app/assets/banners/prime-analyzer.webp index 35985d9..ea879f0 100644 Binary files a/web-app/assets/banners/prime-analyzer.webp and b/web-app/assets/banners/prime-analyzer.webp differ diff --git a/web-app/assets/banners/productive-pet.webp b/web-app/assets/banners/productive-pet.webp index c8cb694..8e7295d 100644 Binary files a/web-app/assets/banners/productive-pet.webp and b/web-app/assets/banners/productive-pet.webp differ diff --git a/web-app/assets/banners/progress-tracker.webp b/web-app/assets/banners/progress-tracker.webp index 2b6fc0a..a0722e1 100644 Binary files a/web-app/assets/banners/progress-tracker.webp and b/web-app/assets/banners/progress-tracker.webp differ diff --git a/web-app/assets/banners/progression-recognizer.webp b/web-app/assets/banners/progression-recognizer.webp index 2a412dc..5b332d1 100644 Binary files a/web-app/assets/banners/progression-recognizer.webp and b/web-app/assets/banners/progression-recognizer.webp differ diff --git a/web-app/assets/banners/projectile-motion.webp b/web-app/assets/banners/projectile-motion.webp index 466845d..76e4e68 100644 Binary files a/web-app/assets/banners/projectile-motion.webp and b/web-app/assets/banners/projectile-motion.webp differ diff --git a/web-app/assets/banners/quick-sort.webp b/web-app/assets/banners/quick-sort.webp index 895456d..bf0ab64 100644 Binary files a/web-app/assets/banners/quick-sort.webp and b/web-app/assets/banners/quick-sort.webp differ diff --git a/web-app/assets/banners/resume-analyzer.webp b/web-app/assets/banners/resume-analyzer.webp index e7ed08a..b684b1d 100644 Binary files a/web-app/assets/banners/resume-analyzer.webp and b/web-app/assets/banners/resume-analyzer.webp differ diff --git a/web-app/assets/banners/reverse-hangman.webp b/web-app/assets/banners/reverse-hangman.webp index c3aadcf..841df72 100644 Binary files a/web-app/assets/banners/reverse-hangman.webp and b/web-app/assets/banners/reverse-hangman.webp differ diff --git a/web-app/assets/banners/rock-paper-scissor.webp b/web-app/assets/banners/rock-paper-scissor.webp index a3e80f1..b22b8fd 100644 Binary files a/web-app/assets/banners/rock-paper-scissor.webp and b/web-app/assets/banners/rock-paper-scissor.webp differ diff --git a/web-app/assets/banners/simon-says.webp b/web-app/assets/banners/simon-says.webp index 8575480..a2ca0a1 100644 Binary files a/web-app/assets/banners/simon-says.webp and b/web-app/assets/banners/simon-says.webp differ diff --git a/web-app/assets/banners/snake.webp b/web-app/assets/banners/snake.webp index 8376a5f..f49c511 100644 Binary files a/web-app/assets/banners/snake.webp and b/web-app/assets/banners/snake.webp differ diff --git a/web-app/assets/banners/spot-the-difference.webp b/web-app/assets/banners/spot-the-difference.webp index d93a85d..8b4a44a 100644 Binary files a/web-app/assets/banners/spot-the-difference.webp and b/web-app/assets/banners/spot-the-difference.webp differ diff --git a/web-app/assets/banners/sudoku-game.webp b/web-app/assets/banners/sudoku-game.webp index a02c605..62734f1 100644 Binary files a/web-app/assets/banners/sudoku-game.webp and b/web-app/assets/banners/sudoku-game.webp differ diff --git a/web-app/assets/banners/tic-tac-toe.webp b/web-app/assets/banners/tic-tac-toe.webp index 4244353..968be42 100644 Binary files a/web-app/assets/banners/tic-tac-toe.webp and b/web-app/assets/banners/tic-tac-toe.webp differ diff --git a/web-app/assets/banners/tower-of-hanoi.webp b/web-app/assets/banners/tower-of-hanoi.webp index 0ede1e5..0e5d10a 100644 Binary files a/web-app/assets/banners/tower-of-hanoi.webp and b/web-app/assets/banners/tower-of-hanoi.webp differ diff --git a/web-app/assets/banners/tsp-visualizer.webp b/web-app/assets/banners/tsp-visualizer.webp new file mode 100644 index 0000000..00fe4ca Binary files /dev/null and b/web-app/assets/banners/tsp-visualizer.webp differ diff --git a/web-app/assets/banners/typing-speed-tester.webp b/web-app/assets/banners/typing-speed-tester.webp index ad746d4..91a3d5d 100644 Binary files a/web-app/assets/banners/typing-speed-tester.webp and b/web-app/assets/banners/typing-speed-tester.webp differ diff --git a/web-app/assets/banners/unit-converter.webp b/web-app/assets/banners/unit-converter.webp index 3322e23..493eb30 100644 Binary files a/web-app/assets/banners/unit-converter.webp and b/web-app/assets/banners/unit-converter.webp differ diff --git a/web-app/assets/banners/war-card-game.webp b/web-app/assets/banners/war-card-game.webp index 2d00d0e..de4920b 100644 Binary files a/web-app/assets/banners/war-card-game.webp and b/web-app/assets/banners/war-card-game.webp differ diff --git a/web-app/assets/banners/whack-a-mole.webp b/web-app/assets/banners/whack-a-mole.webp index ec8d31f..ff52ef8 100644 Binary files a/web-app/assets/banners/whack-a-mole.webp and b/web-app/assets/banners/whack-a-mole.webp differ diff --git a/web-app/assets/banners/word-scramble.webp b/web-app/assets/banners/word-scramble.webp index a50c6de..c9eea75 100644 Binary files a/web-app/assets/banners/word-scramble.webp and b/web-app/assets/banners/word-scramble.webp differ diff --git a/web-app/generate_banners.py b/web-app/generate_banners.py index 91422d6..222ca7e 100644 --- a/web-app/generate_banners.py +++ b/web-app/generate_banners.py @@ -563,6 +563,13 @@ def draw_o(ox, oy): for i, h in enumerate(bar_heights): x = 230 + i * 50 v_draw.rectangle([x, 320 - h, x + 30, 320], fill=color_accent) + elif "tsp" in n_lower or "salesperson" in n_lower: + # Nodes connected by a path + nodes = [(250, 150), (450, 100), (550, 250), (400, 350), (200, 300)] + for i in range(len(nodes)): + v_draw.line([nodes[i], nodes[(i+1)%len(nodes)]], fill=color_accent, width=4) + for x, y in nodes: + v_draw.ellipse([x-10, y-10, x+10, y+10], fill=(255,255,255), outline=color_accent, width=2) elif "pathfinding" in n_lower or "visualizer" in n_lower: # A grid with a path being found cx, cy = 400, 225 @@ -679,6 +686,7 @@ def draw_o(ox, oy): ("Unit Converter", "utilities", "unit-converter.webp"), ("Budget Tracker", "utilities", "budget-tracker.webp"), ("Pathfinding Visualizer", "utilities", "pathfinding-visualizer.webp"), + ("TSP Visualizer", "utilities", "tsp-visualizer.webp"), ] # Run generation diff --git a/web-app/index.html b/web-app/index.html index 3d6a140..354c436 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -904,6 +904,7 @@

Legal

+ @@ -1279,6 +1280,13 @@

Legal

desc: "Track your income and expenses with visual breakdowns", tags: "utility,finance,tracker,money", }, + { + project: "tsp-visualizer", + title: "TSP Visualizer", + category: "utilities", + desc: "Visualize the Traveling Salesperson Problem", + tags: "utility,algorithm,visualizer,tsp", + }, ]; projectsGrid.innerHTML = ""; diff --git a/web-app/js/main.js b/web-app/js/main.js index 02ed49a..40dd042 100644 --- a/web-app/js/main.js +++ b/web-app/js/main.js @@ -40,39 +40,39 @@ function syncThemeColor(theme) { } function updateThemeToggleAria(isLightTheme) { - if (!themeToggle) return; - themeToggle.setAttribute( - 'aria-label', - isLightTheme ? 'Switch to dark mode' : 'Switch to light mode' - ); + if (!themeToggle) return; + themeToggle.setAttribute( + 'aria-label', + isLightTheme ? 'Switch to dark mode' : 'Switch to light mode' + ); } if (themeToggle) { - themeToggle.addEventListener('click', () => { - const currentTheme = html.getAttribute('data-theme'); - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - - html.setAttribute('data-theme', newTheme); - localStorage.setItem('theme', newTheme); - syncThemeColor(newTheme); - - themeToggle.innerHTML = - newTheme === 'light' - ? '' - : ''; - updateThemeToggleAria(newTheme === 'light'); - }); + themeToggle.addEventListener('click', () => { + const currentTheme = html.getAttribute('data-theme'); + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + + html.setAttribute('data-theme', newTheme); + localStorage.setItem('theme', newTheme); + syncThemeColor(newTheme); + + themeToggle.innerHTML = + newTheme === 'light' + ? '' + : ''; + updateThemeToggleAria(newTheme === 'light'); + }); } const savedTheme = localStorage.getItem('theme') || 'dark'; html.setAttribute('data-theme', savedTheme); syncThemeColor(savedTheme); if (themeToggle) { - themeToggle.innerHTML = - savedTheme === 'light' - ? '' - : ''; - updateThemeToggleAria(savedTheme === 'light'); + themeToggle.innerHTML = + savedTheme === 'light' + ? '' + : ''; + updateThemeToggleAria(savedTheme === 'light'); } function escapeHtml(str) { var d = document.createElement("div"); @@ -119,10 +119,10 @@ function showInfoModal(title, steps) { listEl.appendChild(li); }); -const toggleBackToTopButton = () => { + const toggleBackToTopButton = () => { if (!backToTopButton) return; backToTopButton.classList.toggle('visible', window.scrollY > 300); -}; + }; overlay.classList.add("active"); function closeModal() { @@ -132,12 +132,12 @@ const toggleBackToTopButton = () => { overlay.removeEventListener("click", overlayClick); } -if (backToTopButton) { + if (backToTopButton) { backToTopButton.addEventListener('click', () => { - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' }); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + window.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth' }); }); -} + } function overlayClick(e) { if (e.target === overlay) closeModal(); } @@ -798,10 +798,10 @@ document.addEventListener("DOMContentLoaded", function () { // Hide fixed-theme-toggle if sidebar is active const fixedThemeToggle = document.getElementById("fixed-theme-toggle"); - if(showSidebar){ + if (showSidebar) { fixedThemeToggle.style.display = "none"; } - else{ + else { fixedThemeToggle.style.display = "block"; } @@ -824,7 +824,7 @@ document.addEventListener("DOMContentLoaded", function () { var q = query.toLowerCase(); var catMatch = currentCategory === "all" || category === currentCategory; - + // FIX FOR ISSUE #1032: Strict Title Matching // Removed description and hidden tag fuzzy-matching to prevent irrelevant // projects (like FLAMES Game) from appearing for unrelated queries. @@ -963,7 +963,7 @@ document.addEventListener("DOMContentLoaded", function () { if (noResultsMessage) noResultsMessage.style.display = "block"; return; } - + if (noResultsMessage) noResultsMessage.style.display = "none"; if (resultsList) { @@ -977,13 +977,13 @@ document.addEventListener("DOMContentLoaded", function () { var iconBox = document.createElement("div"); iconBox.className = "dropdown-item-icon"; var banner = project.card.querySelector(".card-banner"); - projectCards.forEach(function(card) { - var banner = card.querySelector(".card-banner"); - var title = card.querySelector("h3"); + projectCards.forEach(function (card) { + var banner = card.querySelector(".card-banner"); + var title = card.querySelector("h3"); - if (banner && title) { + if (banner && title) { banner.alt = title.textContent.trim() + " project preview"; - } + } }); if (banner) { var img = document.createElement("img"); @@ -1364,8 +1364,6 @@ document.addEventListener("DOMContentLoaded", function () { }); } }); - - // Clear content if (modalBody) { modalBody.innerHTML = ""; diff --git a/web-app/js/projects.js b/web-app/js/projects.js index 9029edb..ae236a4 100644 --- a/web-app/js/projects.js +++ b/web-app/js/projects.js @@ -32,7 +32,8 @@ function getProjectHTML(projectName) { 'bubble-sort': getBubbleSortHTML(), 'quick-sort': getQuickSortHTML(), 'fourier-series': getFourierSeriesHTML(), - 'pathfinding-visualizer': getPathfindingVisualizerHTML() + 'pathfinding-visualizer': getPathfindingVisualizerHTML(), + 'tsp-visualizer': getTspVisualizerHTML() }; return projects[projectName] || '

Project Coming Soon!

'; @@ -66,7 +67,8 @@ function initializeProject(projectName) { 'reverse-hangman': initReverseHangman, 'budget-tracker': initBudgetTracker, 'fourier-series': initFourierSeries, - 'pathfinding-visualizer': initPathfindingVisualizer + 'pathfinding-visualizer': initPathfindingVisualizer, + 'tsp-visualizer': initTspVisualizer }; if (initializers[projectName]) { @@ -3232,7 +3234,8 @@ function initializeProject(projectName) { "number-sliding-puzzle": "initNumberSlidingPuzzle", "budget-tracker": "initBudgetTracker", "fourier-series": "initFourierSeries", - "pathfinding-visualizer": "initPathfindingVisualizer" + "pathfinding-visualizer": "initPathfindingVisualizer", + "tsp-visualizer": "initTspVisualizer" }; const initializerName = initializers[projectName]; diff --git a/web-app/js/projects/tsp-visualizer.js b/web-app/js/projects/tsp-visualizer.js new file mode 100644 index 0000000..ba6f4ae --- /dev/null +++ b/web-app/js/projects/tsp-visualizer.js @@ -0,0 +1,437 @@ +function getTspVisualizerHTML() { + return ` +
+

🗺️ TSP Visualizer

+

Visualize the Traveling Salesperson Problem using Nearest Neighbor and Brute Force algorithms.

+
+
+
+ + +
+ + + + +
+ +
+
City (Node)
+
Shortest Path
+
Current Evaluation
+
+ +
+ +
+ +
+
Nodes: 0
+
Shortest Distance: 0.00
+
+
+
+ + + `; +} + +function initTspVisualizer() { + const canvas = document.getElementById('tspCanvas'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + + const algorithmSelect = document.getElementById('tspAlgorithm'); + const visualizeBtn = document.getElementById('tspVisualizeBtn'); + const randomBtn = document.getElementById('tspRandomBtn'); + const clearPathBtn = document.getElementById('tspClearPathBtn'); + const clearBoardBtn = document.getElementById('tspClearBoardBtn'); + const nodeCountEl = document.getElementById('tspNodeCount'); + const shortestDistEl = document.getElementById('tspShortestDist'); + + let nodes = []; + let bestOrder = []; + let bestDist = Infinity; + let isVisualizing = false; + let animationFrameId = null; + + function resizeCanvas() { + const wrapper = canvas.parentElement; + const targetWidth = Math.min(800, wrapper.clientWidth - 20); + const scale = targetWidth / 800; + canvas.style.width = `${800 * scale}px`; + canvas.style.height = `${450 * scale}px`; + } + + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + function drawNode(x, y, color = '#ffffff') { + ctx.beginPath(); + ctx.arc(x, y, 6, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + ctx.strokeStyle = '#6366f1'; + ctx.lineWidth = 2; + ctx.stroke(); + } + + function drawEdge(node1, node2, color, width = 2) { + ctx.beginPath(); + ctx.moveTo(node1.x, node1.y); + ctx.lineTo(node2.x, node2.y); + ctx.strokeStyle = color; + ctx.lineWidth = width; + ctx.stroke(); + } + + function draw(currentEdge = null) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw grid lines + ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; + ctx.lineWidth = 1; + for (let i = 0; i < canvas.width; i += 40) { + ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height); ctx.stroke(); + } + for (let i = 0; i < canvas.height; i += 40) { + ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(canvas.width, i); ctx.stroke(); + } + + // Draw best path + if (bestOrder.length > 1) { + for (let i = 0; i < bestOrder.length; i++) { + const n1 = nodes[bestOrder[i]]; + const n2 = nodes[bestOrder[(i + 1) % bestOrder.length]]; + drawEdge(n1, n2, '#6366f1', 2); + } + } + + // Draw current edge being evaluated + if (currentEdge) { + drawEdge(currentEdge[0], currentEdge[1], '#ef4444', 2); + } + + // Draw nodes + nodes.forEach((n, i) => drawNode(n.x, n.y, i === 0 ? '#6366f1' : '#ffffff')); + + nodeCountEl.textContent = nodes.length; + shortestDistEl.textContent = bestDist === Infinity ? '0.00' : bestDist.toFixed(2); + } + + function getDistance(n1, n2) { + return Math.hypot(n1.x - n2.x, n1.y - n2.y); + } + + function calcPathDistance(order) { + let dist = 0; + for (let i = 0; i < order.length; i++) { + dist += getDistance(nodes[order[i]], nodes[order[(i + 1) % order.length]]); + } + return dist; + } + + canvas.addEventListener('mousedown', (e) => { + if (isVisualizing) return; + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + const x = (e.clientX - rect.left) * scaleX; + const y = (e.clientY - rect.top) * scaleY; + + nodes.push({x, y}); + bestOrder = []; + bestDist = Infinity; + draw(); + }); + + randomBtn.addEventListener('click', () => { + if (isVisualizing) return; + nodes = []; + bestOrder = []; + bestDist = Infinity; + const count = Math.floor(Math.random() * 5) + 6; // 6 to 10 nodes + for (let i = 0; i < count; i++) { + nodes.push({ + x: Math.random() * (canvas.width - 40) + 20, + y: Math.random() * (canvas.height - 40) + 20 + }); + } + draw(); + }); + + clearPathBtn.addEventListener('click', () => { + if (isVisualizing) return; + bestOrder = []; + bestDist = Infinity; + draw(); + }); + + clearBoardBtn.addEventListener('click', () => { + if (isVisualizing) return; + nodes = []; + bestOrder = []; + bestDist = Infinity; + draw(); + }); + + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + function* getPermutations(arr) { + if (arr.length <= 1) yield arr; + else { + for (let i = 0; i < arr.length; i++) { + const current = arr[i]; + const remaining = [...arr.slice(0, i), ...arr.slice(i + 1)]; + for (let perm of getPermutations(remaining)) { + yield [current, ...perm]; + } + } + } + } + + async function runNearestNeighbor() { + if (nodes.length < 2) return; + + const unvisited = new Set(nodes.map((_, i) => i)); + let current = 0; + unvisited.delete(current); + const order = [current]; + + while (unvisited.size > 0) { + let nearest = null; + let minDist = Infinity; + + for (let candidate of unvisited) { + // visualize consideration + draw([nodes[current], nodes[candidate]]); + await sleep(50); + + const dist = getDistance(nodes[current], nodes[candidate]); + if (dist < minDist) { + minDist = dist; + nearest = candidate; + } + } + + order.push(nearest); + unvisited.delete(nearest); + current = nearest; + + bestOrder = [...order]; + bestDist = calcPathDistance(order); + draw(); + await sleep(100); + } + + bestDist = calcPathDistance(order); + draw(); + } + + async function runBruteForce() { + if (nodes.length < 2) return; + if (nodes.length > 9) { + alert("Too many nodes for Brute Force (Max 9 recommended in browser). Please use Nearest Neighbor or reduce nodes."); + return; + } + + const indices = nodes.map((_, i) => i).slice(1); // fix node 0 + const perms = getPermutations(indices); + + let minD = Infinity; + let bestO = []; + let count = 0; + + for (let p of perms) { + const order = [0, ...p]; + const dist = calcPathDistance(order); + + if (dist < minD) { + minD = dist; + bestO = [...order]; + bestDist = minD; + bestOrder = bestO; + draw(); + } + + count++; + if (count % 100 === 0) { + // Periodically show what it's evaluating + draw([nodes[order[order.length-1]], nodes[order[0]]]); + await sleep(1); // yield to browser + } + } + + bestDist = minD; + bestOrder = bestO; + draw(); + } + + visualizeBtn.addEventListener('click', async () => { + if (isVisualizing || nodes.length < 2) return; + + isVisualizing = true; + visualizeBtn.disabled = true; + randomBtn.disabled = true; + clearBoardBtn.disabled = true; + clearPathBtn.disabled = true; + algorithmSelect.disabled = true; + + bestOrder = []; + bestDist = Infinity; + draw(); + + const algo = algorithmSelect.value; + try { + if (algo === 'nearest') { + await runNearestNeighbor(); + } else if (algo === 'brute') { + await runBruteForce(); + } + } catch (e) { + console.error(e); + } + + isVisualizing = false; + visualizeBtn.disabled = false; + randomBtn.disabled = false; + clearBoardBtn.disabled = false; + clearPathBtn.disabled = false; + algorithmSelect.disabled = false; + }); + + draw(); + console.log('🗺️ TSP Visualizer initialized'); +} + +window.getTspVisualizerHTML = getTspVisualizerHTML; +window.initTspVisualizer = initTspVisualizer; diff --git a/web-app/package-lock.json b/web-app/package-lock.json index c232e07..69122fd 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -19,7 +19,8 @@ "@codemirror/view": "^6.43.0", "@playwright/test": "^1.60.0", "codemirror": "^6.0.2", - "esbuild": "^0.28.0" + "esbuild": "^0.28.0", + "playwright": "^1.61.0" } }, "node_modules/@codemirror/autocomplete": { @@ -641,6 +642,38 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/@playwright/test/node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", @@ -722,13 +755,13 @@ } }, "node_modules/playwright": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", - "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.60.0" + "playwright-core": "1.61.0" }, "bin": { "playwright": "cli.js" @@ -741,9 +774,9 @@ } }, "node_modules/playwright-core": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", - "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/web-app/package.json b/web-app/package.json index eb91c5e..8a5762d 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -22,6 +22,7 @@ "@codemirror/view": "^6.43.0", "@playwright/test": "^1.60.0", "codemirror": "^6.0.2", - "esbuild": "^0.28.0" + "esbuild": "^0.28.0", + "playwright": "^1.61.0" } }