Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions projects_registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
3 changes: 3 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def test_smoke():
"""A minimal smoke test to ensure the test suite runs."""
assert True
169 changes: 169 additions & 0 deletions utilities/TSP-Visualizer/TSP-Visualizer.py
Original file line number Diff line number Diff line change
@@ -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()
Binary file modified web-app/assets/banners/2048-game.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/armstrong.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/binary-search.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/blackjack21.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/bubble-sort.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/budget-tracker.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/caesar-cipher.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/calculator.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/chess.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/coin-flip.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/collatz.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/color-palette.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/coordinate-polar-transform.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/derivative-calculator.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/dice-rolling.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/dots-boxes.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/emoji-memory-game.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/fibonacci.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/flames.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/flappy-game.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/fourier-series.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/hangman.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/math-quiz.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/matrix-calculator.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/morse-code.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web-app/assets/banners/number-converter.webp
Binary file modified web-app/assets/banners/number-guessing.webp
Binary file modified web-app/assets/banners/number-sliding-puzzle.webp
Binary file modified web-app/assets/banners/pascal-triangle.webp
Binary file modified web-app/assets/banners/password-forge.webp
Binary file modified web-app/assets/banners/pathfinding-visualizer.webp
Binary file modified web-app/assets/banners/prime-analyzer.webp
Binary file modified web-app/assets/banners/productive-pet.webp
Binary file modified web-app/assets/banners/progress-tracker.webp
Binary file modified web-app/assets/banners/progression-recognizer.webp
Binary file modified web-app/assets/banners/projectile-motion.webp
Binary file modified web-app/assets/banners/quick-sort.webp
Binary file modified web-app/assets/banners/resume-analyzer.webp
Binary file modified web-app/assets/banners/reverse-hangman.webp
Binary file modified web-app/assets/banners/rock-paper-scissor.webp
Binary file modified web-app/assets/banners/simon-says.webp
Binary file modified web-app/assets/banners/snake.webp
Binary file modified web-app/assets/banners/spot-the-difference.webp
Binary file modified web-app/assets/banners/sudoku-game.webp
Binary file modified web-app/assets/banners/tic-tac-toe.webp
Binary file modified web-app/assets/banners/tower-of-hanoi.webp
Binary file added web-app/assets/banners/tsp-visualizer.webp
Binary file modified web-app/assets/banners/typing-speed-tester.webp
Binary file modified web-app/assets/banners/unit-converter.webp
Binary file modified web-app/assets/banners/war-card-game.webp
Binary file modified web-app/assets/banners/whack-a-mole.webp
Binary file modified web-app/assets/banners/word-scramble.webp
8 changes: 8 additions & 0 deletions web-app/generate_banners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions web-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ <h3>Legal</h3>
<script defer src="js/projects/unit-converter.js"></script>
<script defer src="js/projects/fourier-series.js"></script>
<script defer src="js/projects/pathfinding-visualizer.js?v=2"></script>
<script defer src="js/projects/tsp-visualizer.js"></script>
<script defer src="js/projects.js?v=2"></script>
<script defer src="js/cm-editor.js"></script>
<script defer src="js/playground.js"></script>
Expand Down Expand Up @@ -1279,6 +1280,13 @@ <h3>Legal</h3>
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 = "";
Expand Down
80 changes: 39 additions & 41 deletions web-app/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
? '<i class="fas fa-sun" aria-hidden="true"></i>'
: '<i class="fas fa-moon" aria-hidden="true"></i>';
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'
? '<i class="fas fa-sun" aria-hidden="true"></i>'
: '<i class="fas fa-moon" aria-hidden="true"></i>';
updateThemeToggleAria(newTheme === 'light');
});
}

const savedTheme = localStorage.getItem('theme') || 'dark';
html.setAttribute('data-theme', savedTheme);
syncThemeColor(savedTheme);
if (themeToggle) {
themeToggle.innerHTML =
savedTheme === 'light'
? '<i class="fas fa-sun" aria-hidden="true"></i>'
: '<i class="fas fa-moon" aria-hidden="true"></i>';
updateThemeToggleAria(savedTheme === 'light');
themeToggle.innerHTML =
savedTheme === 'light'
? '<i class="fas fa-sun" aria-hidden="true"></i>'
: '<i class="fas fa-moon" aria-hidden="true"></i>';
updateThemeToggleAria(savedTheme === 'light');
}
function escapeHtml(str) {
var d = document.createElement("div");
Expand Down Expand Up @@ -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() {
Expand All @@ -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();
}
Expand Down Expand Up @@ -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";
}

Expand All @@ -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.
Expand Down Expand Up @@ -963,7 +963,7 @@ document.addEventListener("DOMContentLoaded", function () {
if (noResultsMessage) noResultsMessage.style.display = "block";
return;
}

if (noResultsMessage) noResultsMessage.style.display = "none";

if (resultsList) {
Expand All @@ -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");
Expand Down Expand Up @@ -1364,8 +1364,6 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
});


// Clear content
if (modalBody) {
modalBody.innerHTML = "";
Expand Down
Loading
Loading