Animation added

This commit is contained in:
2025-11-20 23:16:04 -05:00
parent 6d75c8e94e
commit 9197e464a5
8 changed files with 526 additions and 15 deletions

138
ANIMATION_FEATURE.md Normal file
View File

@@ -0,0 +1,138 @@
# Animation Feature - Technical Documentation
## Overview
The maze solving animation feature provides real-time visualization of pathfinding algorithms (DFS and BFS) with interactive controls.
## Features Implemented
### 1. Animated Solving Process
When a user clicks either "SOLVE (DFS)" or "SOLVE (BFS)" button:
- The algorithm executes on the server and returns the complete solution
- The visualization then **animates** the solving process step-by-step:
1. **Phase 1**: Shows cells being visited in order (gray cells)
2. **Phase 2**: Highlights the final solution path (green cells)
### 2. Animation Controls
A control panel appears during animation with:
#### **PAUSE/RESUME Button**
- Click to pause the animation at any point
- Button text changes to "RESUME" when paused
- Click again to continue from where it was paused
#### **STOP Button**
- Immediately stops the animation
- Shows the complete final result (all visited cells + solution path)
- Hides the animation controls
- Re-enables solve buttons
#### **SPEED Slider**
- Range: 1 (SLOW) to 100 (FAST)
- Dynamically adjusts animation speed
- Changes take effect immediately, even during animation
- SLOW = 200ms per step
- FAST = 10ms per step
### 3. Neo-Brutalism Styling
The animation controls follow the same design language:
- **White background** with **6px black border**
- **Pink drop shadow** (6px offset)
- **Yellow PAUSE/STOP buttons** with hard shadows
- **Pink slider thumb** with black border
- **Bold uppercase typography**
## Technical Implementation
### Animation State Management
```javascript
animationState = {
isAnimating: boolean, // Whether animation is running
isPaused: boolean, // Whether animation is paused
currentStep: number, // Current animation step
visitedCells: array, // All cells visited during solving
solutionPath: array, // Final solution path
animationId: timeout, // setTimeout ID for cancellation
speed: number // Delay between steps in ms
}
```
### Animation Flow
1. User clicks solve button
2. API call fetches solution data
3. `startSolvingAnimation()` initializes state and shows controls
4. `animateStep()` recursively renders each step:
- First loop: Animate visited cells
- Second loop: Animate solution path (slower)
5. `finishAnimation()` completes and cleans up
### Button States
- **During Animation**: Solve buttons disabled, controls visible
- **When Paused**: Animation frozen, can resume or stop
- **After Stop/Finish**: Controls hidden, solve buttons re-enabled
## User Experience
### Visual Feedback
- **Gray cells**: Cells being explored by the algorithm
- **Green cells**: Final solution path
- **Yellow cell (S)**: Start position
- **Pink cell (E)**: End position
### Animation Speed
- **Default**: Medium speed (50/100 = 110ms per step)
- **Visited cells**: Uses slider speed
- **Solution path**: 2x slower for clarity
### Control Visibility
- Controls **appear** only when animation starts
- Controls **hide** when animation completes or is stopped
- Prevents UI clutter when not needed
## Code Changes
### Files Modified
1. **web/static/js/app.js**
- Added animation state management
- Implemented `startSolvingAnimation()`
- Implemented `animateStep()` with recursive timeout
- Added `togglePause()`, `stopAnimation()`, `finishAnimation()`
- Added `updateSpeed()` for slider
2. **web/templates/index.html**
- Added animation controls section
- Added pause/stop buttons
- Added speed slider with labels
3. **web/static/css/styles.css**
- Styled `.animation-controls` with Neo-Brutalism theme
- Styled control buttons
- Styled speed slider (custom thumb styling)
- Added speed labels
4. **README.md**
- Documented new animation feature
- Added usage instructions for controls
## Benefits
### Educational Value
- Users can **see** how DFS vs BFS explores the maze differently
- DFS tends to go deep before backtracking
- BFS explores level-by-level (visible in animation)
### Engagement
- Makes the solving process **interactive** and **fun**
- Users can pause to study specific steps
- Speed control allows customization for different maze sizes
### Debugging/Analysis
- Developers can observe algorithm behavior
- Useful for understanding performance characteristics
- Can see exactly which cells each algorithm visits
## Future Enhancements (Not Implemented)
- Step-by-step forward/backward controls
- Export animation as GIF/video
- Comparison mode (run DFS and BFS side-by-side)
- Generation algorithm animation
- Custom color themes for visited/solution cells

46
CHANGELOG.md Normal file
View File

@@ -0,0 +1,46 @@
# Changelog
## [1.1.0] - 2025-01-20
### Added
- **Animated Solving Visualization**
- Real-time step-by-step animation of DFS and BFS algorithms
- Two-phase animation: visited cells exploration, then solution path highlighting
- Animation controls panel with Neo-Brutalism styling
- Pause/Resume button to control animation flow
- Stop button to immediately halt and show final result
- Speed slider (SLOW to FAST) for dynamic speed adjustment
- Automatic control visibility management
- **Footer**
- Copyleft notice with reversed copyright symbol (🄯)
- "Made with ❤️ and Python in New Jersey" message
- Black background with white text and neon pink accent
- Cyan drop shadow with negative offset (Neo-Brutalism style)
### Removed
- **Solver Dropdown** - Redundant UI element removed for cleaner interface
- Users now interact directly with "SOLVE (DFS)" and "SOLVE (BFS)" buttons
### Changed
- Solve buttons now trigger animated solving instead of instant results
- Solve buttons disabled during active animation to prevent conflicts
- Results display updated to show during animation
### Technical Details
- Added animation state management in `app.js`
- Implemented recursive timeout-based animation loop
- Added event listeners for pause, stop, and speed controls
- Enhanced Neo-Brutalism CSS with footer and animation control styles
## [1.0.0] - 2025-01-20
### Initial Release
- 8 maze generation algorithms
- 2 solving algorithms (DFS, BFS)
- Neo-Brutalism web interface
- Image export functionality
- Save/load maze files
- Analysis and benchmarking tools
- Docker containerization
- Comprehensive test suite (>90% coverage)

View File

@@ -29,6 +29,7 @@ Built with **Neo-Brutalism** design aesthetics featuring:
- Hard drop shadows - Hard drop shadows
- Chunky typography (Space Grotesk font) - Chunky typography (Space Grotesk font)
- Asymmetric layouts - Asymmetric layouts
- **Animated solving** with pause/stop controls and adjustable speed
### 📊 Analysis Tools ### 📊 Analysis Tools
- Dead end counting and percentage - Dead end counting and percentage
@@ -103,11 +104,17 @@ The web interface provides 9 main operations:
3. **Download Image** - Save maze as PNG file 3. **Download Image** - Save maze as PNG file
4. **Save to File** - Persist maze as JSON 4. **Save to File** - Persist maze as JSON
5. **Load from File** - Restore saved maze 5. **Load from File** - Restore saved maze
6. **Solve (DFS)** - Find path using Depth-First Search 6. **Solve (DFS)** - Find path using Depth-First Search with animated visualization
7. **Solve (BFS)** - Find shortest path using Breadth-First Search 7. **Solve (BFS)** - Find shortest path using Breadth-First Search with animated visualization
8. **Analyze** - Compute maze statistics and metrics 8. **Analyze** - Compute maze statistics and metrics
9. **Benchmark** - Compare algorithm performance 9. **Benchmark** - Compare algorithm performance
#### Animation Controls
When solving a maze, you can:
- **Pause/Resume** - Pause the animation and resume where you left off
- **Stop** - Stop the animation and show the final result
- **Adjust Speed** - Use the slider to control animation speed (slow to fast)
### API Endpoints ### API Endpoints
#### Generate Maze #### Generate Maze

26
run.bat Normal file
View File

@@ -0,0 +1,26 @@
@echo off
echo ========================================
echo MAZE GENERATOR - Starting Server
echo ========================================
echo.
REM Check if virtual environment exists
if not exist "venv" (
echo Creating virtual environment...
python -m venv venv
)
REM Activate virtual environment
call venv\Scripts\activate.bat
REM Install requirements if needed
echo Installing/updating dependencies...
pip install -q -r requirements.txt
REM Start the Flask application
echo.
echo Starting Flask server on http://localhost:5000
echo.
echo Press Ctrl+C to stop the server
echo.
python api\app.py

27
run.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
echo "========================================"
echo " MAZE GENERATOR - Starting Server"
echo "========================================"
echo ""
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo "Creating virtual environment..."
python3 -m venv venv
fi
# Activate virtual environment
source venv/bin/activate
# Install requirements if needed
echo "Installing/updating dependencies..."
pip install -q -r requirements.txt
# Start the Flask application
echo ""
echo "Starting Flask server on http://localhost:5000"
echo ""
echo "Press Ctrl+C to stop the server"
echo ""
python api/app.py

View File

@@ -356,6 +356,75 @@ body {
background-color: var(--neon-cyan); background-color: var(--neon-cyan);
} }
/* Animation Controls */
.animation-controls {
background-color: var(--white);
border: var(--border-thick) solid var(--black);
padding: var(--spacing-md);
margin-bottom: var(--spacing-md);
box-shadow: 6px 6px 0 var(--neon-pink);
}
.control-title {
font-size: 1.2rem;
font-weight: 700;
text-transform: uppercase;
margin-bottom: var(--spacing-md);
letter-spacing: 2px;
}
.control-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.btn-control {
background-color: var(--neon-yellow);
box-shadow: 4px 4px 0 var(--black);
padding: 12px;
}
.speed-slider {
width: 100%;
height: 8px;
background: var(--white);
border: var(--border-medium) solid var(--black);
outline: none;
-webkit-appearance: none;
margin-bottom: 8px;
}
.speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
background: var(--neon-pink);
border: var(--border-medium) solid var(--black);
cursor: pointer;
box-shadow: 2px 2px 0 var(--black);
}
.speed-slider::-moz-range-thumb {
width: 24px;
height: 24px;
background: var(--neon-pink);
border: var(--border-medium) solid var(--black);
cursor: pointer;
box-shadow: 2px 2px 0 var(--black);
border-radius: 0;
}
.speed-labels {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 1px;
}
/* Loading Spinner */ /* Loading Spinner */
.loading { .loading {
display: inline-block; display: inline-block;
@@ -370,3 +439,41 @@ body {
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
/* Footer */
.footer {
margin-top: var(--spacing-lg);
background-color: var(--black);
color: var(--white);
border: var(--border-thick) solid var(--black);
padding: var(--spacing-lg);
box-shadow: calc(var(--shadow-offset) * -1) calc(var(--shadow-offset) * -1) 0 var(--neon-cyan);
}
.footer-content {
text-align: center;
}
.copyleft {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: var(--spacing-sm);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.copyleft-symbol {
font-size: 1.5rem;
display: inline-block;
transform: scaleX(-1);
}
.footer-message {
font-size: 1.1rem;
font-weight: 700;
letter-spacing: 1px;
color: var(--neon-pink);
}

View File

@@ -4,6 +4,15 @@ const API_BASE = '/api';
let currentMazeId = null; let currentMazeId = null;
let currentMazeData = null; let currentMazeData = null;
let animationState = {
isAnimating: false,
isPaused: false,
currentStep: 0,
visitedCells: [],
solutionPath: [],
animationId: null,
speed: 50 // milliseconds per step
};
// DOM Elements // DOM Elements
const generateBtn = document.getElementById('generateBtn'); const generateBtn = document.getElementById('generateBtn');
@@ -15,15 +24,18 @@ const solveDfsBtn = document.getElementById('solveDfsBtn');
const solveBfsBtn = document.getElementById('solveBfsBtn'); const solveBfsBtn = document.getElementById('solveBfsBtn');
const analyzeBtn = document.getElementById('analyzeBtn'); const analyzeBtn = document.getElementById('analyzeBtn');
const benchmarkBtn = document.getElementById('benchmarkBtn'); const benchmarkBtn = document.getElementById('benchmarkBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const speedSlider = document.getElementById('speedSlider');
const algorithmSelect = document.getElementById('algorithm'); const algorithmSelect = document.getElementById('algorithm');
const rowsInput = document.getElementById('rows'); const rowsInput = document.getElementById('rows');
const colsInput = document.getElementById('cols'); const colsInput = document.getElementById('cols');
const seedInput = document.getElementById('seed'); const seedInput = document.getElementById('seed');
const solverSelect = document.getElementById('solver');
const resultsContent = document.getElementById('resultsContent'); const resultsContent = document.getElementById('resultsContent');
const mazeInfo = document.getElementById('mazeInfo'); const mazeInfo = document.getElementById('mazeInfo');
const animationControls = document.getElementById('animationControls');
// Disable buttons initially // Disable buttons initially
function disableActionButtons() { function disableActionButtons() {
@@ -56,6 +68,9 @@ solveDfsBtn.addEventListener('click', () => solveMaze('dfs'));
solveBfsBtn.addEventListener('click', () => solveMaze('bfs')); solveBfsBtn.addEventListener('click', () => solveMaze('bfs'));
analyzeBtn.addEventListener('click', analyzeMaze); analyzeBtn.addEventListener('click', analyzeMaze);
benchmarkBtn.addEventListener('click', runBenchmark); benchmarkBtn.addEventListener('click', runBenchmark);
pauseBtn.addEventListener('click', togglePause);
stopBtn.addEventListener('click', stopAnimation);
speedSlider.addEventListener('input', updateSpeed);
// Generate Maze // Generate Maze
async function generateMaze() { async function generateMaze() {
@@ -209,13 +224,16 @@ async function loadMazeFile(filename) {
} }
} }
// Solve Maze // Solve Maze with Animation
async function solveMaze(algorithm) { async function solveMaze(algorithm) {
if (currentMazeId === null) { if (currentMazeId === null) {
showError('No maze to solve'); showError('No maze to solve');
return; return;
} }
// Stop any ongoing animation
stopAnimation();
const btn = algorithm === 'dfs' ? solveDfsBtn : solveBfsBtn; const btn = algorithm === 'dfs' ? solveDfsBtn : solveBfsBtn;
setLoading(btn, true); setLoading(btn, true);
@@ -227,21 +245,141 @@ async function solveMaze(algorithm) {
}); });
const data = await response.json(); const data = await response.json();
setLoading(btn, false);
if (data.success) { if (data.success) {
renderMaze(currentMazeData, data.visited, data.path); // Start animated solving
startSolvingAnimation(data);
displaySolutionInfo(data); displaySolutionInfo(data);
showSuccess(`Maze solved using ${data.algorithm}!`);
} else { } else {
showError('Failed to solve maze'); showError('Failed to solve maze');
} }
} catch (error) { } catch (error) {
showError('Network error: ' + error.message); showError('Network error: ' + error.message);
} finally {
setLoading(btn, false); setLoading(btn, false);
} }
} }
// Animation Control Functions
function startSolvingAnimation(solutionData) {
animationState.isAnimating = true;
animationState.isPaused = false;
animationState.currentStep = 0;
animationState.visitedCells = solutionData.visited;
animationState.solutionPath = solutionData.path;
// Show animation controls
animationControls.style.display = 'block';
pauseBtn.textContent = 'PAUSE';
// Disable solve buttons during animation
solveDfsBtn.disabled = true;
solveBfsBtn.disabled = true;
// Start animation
animateStep();
}
function animateStep() {
if (!animationState.isAnimating || animationState.isPaused) {
return;
}
const { currentStep, visitedCells, solutionPath } = animationState;
if (currentStep < visitedCells.length) {
// Show visited cells up to current step
const visibleVisited = visitedCells.slice(0, currentStep + 1);
// Don't show solution path yet, only when all cells are visited
renderMaze(currentMazeData, visibleVisited, null);
animationState.currentStep++;
// Schedule next step
animationState.animationId = setTimeout(animateStep, animationState.speed);
} else if (currentStep < visitedCells.length + solutionPath.length) {
// Now animate the solution path
const pathStep = currentStep - visitedCells.length;
const visiblePath = solutionPath.slice(0, pathStep + 1);
renderMaze(currentMazeData, visitedCells, visiblePath);
animationState.currentStep++;
// Schedule next step (slower for solution path)
animationState.animationId = setTimeout(animateStep, animationState.speed * 2);
} else {
// Animation complete
finishAnimation();
}
}
function togglePause() {
if (!animationState.isAnimating) return;
animationState.isPaused = !animationState.isPaused;
if (animationState.isPaused) {
pauseBtn.textContent = 'RESUME';
if (animationState.animationId) {
clearTimeout(animationState.animationId);
}
} else {
pauseBtn.textContent = 'PAUSE';
animateStep();
}
}
function stopAnimation() {
if (!animationState.isAnimating) return;
animationState.isAnimating = false;
animationState.isPaused = false;
if (animationState.animationId) {
clearTimeout(animationState.animationId);
animationState.animationId = null;
}
// Hide animation controls
animationControls.style.display = 'none';
// Re-enable solve buttons
solveDfsBtn.disabled = false;
solveBfsBtn.disabled = false;
// Show final result if we have solution data
if (animationState.visitedCells.length > 0 && animationState.solutionPath.length > 0) {
renderMaze(currentMazeData, animationState.visitedCells, animationState.solutionPath);
} else {
renderMaze(currentMazeData);
}
}
function finishAnimation() {
animationState.isAnimating = false;
// Hide animation controls
animationControls.style.display = 'none';
// Re-enable solve buttons
solveDfsBtn.disabled = false;
solveBfsBtn.disabled = false;
// Show final result
renderMaze(currentMazeData, animationState.visitedCells, animationState.solutionPath);
showSuccess('Solving animation complete!');
}
function updateSpeed(event) {
// Speed slider: 1 (slow) to 100 (fast)
// Convert to delay: 200ms (slow) to 10ms (fast)
const sliderValue = parseInt(event.target.value);
animationState.speed = 210 - (sliderValue * 2);
}
// Analyze Maze // Analyze Maze
async function analyzeMaze() { async function analyzeMaze() {
if (currentMazeId === null) { if (currentMazeId === null) {

View File

@@ -57,14 +57,6 @@
</button> </button>
<!-- Solving Controls --> <!-- Solving Controls -->
<div class="control-group">
<label class="label">SOLVER</label>
<select id="solver" class="select-input">
<option value="dfs">Depth-First Search</option>
<option value="bfs">Breadth-First Search</option>
</select>
</div>
<button id="solveDfsBtn" class="btn btn-secondary"> <button id="solveDfsBtn" class="btn btn-secondary">
6. SOLVE (DFS) 6. SOLVE (DFS)
</button> </button>
@@ -72,6 +64,23 @@
7. SOLVE (BFS) 7. SOLVE (BFS)
</button> </button>
<!-- Animation Controls -->
<div id="animationControls" class="animation-controls" style="display: none;">
<h3 class="control-title">ANIMATION</h3>
<div class="control-buttons">
<button id="pauseBtn" class="btn btn-control">PAUSE</button>
<button id="stopBtn" class="btn btn-control">STOP</button>
</div>
<div class="control-group">
<label class="label">SPEED</label>
<input type="range" id="speedSlider" class="speed-slider" min="1" max="100" value="50">
<div class="speed-labels">
<span>SLOW</span>
<span>FAST</span>
</div>
</div>
</div>
<!-- Action Buttons --> <!-- Action Buttons -->
<button id="visualizeBtn" class="btn btn-accent"> <button id="visualizeBtn" class="btn btn-accent">
2. VISUALIZE 2. VISUALIZE
@@ -112,6 +121,19 @@
</div> </div>
</div> </div>
<!-- Footer -->
<footer class="footer">
<div class="footer-content">
<div class="copyleft">
<span class="copyleft-symbol">🄯</span>
<span>COPYLEFT 2025 - ALL WRONGS RESERVED</span>
</div>
<div class="footer-message">
Made with ❤️ and Python in New Jersey
</div>
</div>
</footer>
<!-- Load Modal --> <!-- Load Modal -->
<div id="loadModal" class="modal"> <div id="loadModal" class="modal">
<div class="modal-content"> <div class="modal-content">