+ {/* Speed Control */}
+
+
+
+
+ {[0.1, 0.5, 1, 2, 5, 10].map((preset) => (
+
+ ))}
+
+
+
onSpeedChange(parseFloat(e.target.value))}
+ className={`w-full h-3 rounded-lg appearance-none cursor-pointer ${
+ settings.theme === 'neon' ? 'bg-gray-800' : 'bg-gray-200'
+ }`}
+ style={{
+ background: `linear-gradient(to right, ${
+ settings.theme === 'neon'
+ ? '#a855f7'
+ : settings.theme === 'minimal'
+ ? '#374151'
+ : '#3b82f6'
+ } 0%, ${
+ settings.theme === 'neon'
+ ? '#a855f7'
+ : settings.theme === 'minimal'
+ ? '#374151'
+ : '#3b82f6'
+ } ${(speed / 10) * 100}%, ${
+ settings.theme === 'neon' ? '#1f2937' : '#e5e7eb'
+ } ${(speed / 10) * 100}%, ${
+ settings.theme === 'neon' ? '#1f2937' : '#e5e7eb'
+ } 100%)`
+ }}
+ />
-
onSpeedChange(1200 - parseInt(e.target.value))}
- className="w-full h-3 bg-gray-200 rounded-lg appearance-none cursor-pointer"
- style={{
- background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${speedPercentage}%, #e5e7eb ${speedPercentage}%, #e5e7eb 100%)`
- }}
- />
-
-
Slow
-
Fast
+
+ {/* Theme Selection */}
+
+
+
+ {['classic', 'neon', 'minimal'].map((theme) => (
+
+ ))}
+
diff --git a/frontend/src/components/Sorting/SortingCanvas.jsx b/frontend/src/components/Sorting/SortingCanvas.jsx
index 16436c7..5dd2a32 100644
--- a/frontend/src/components/Sorting/SortingCanvas.jsx
+++ b/frontend/src/components/Sorting/SortingCanvas.jsx
@@ -1,23 +1,68 @@
import React, { useEffect, useRef, forwardRef, useCallback } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
+const themeColors = {
+ classic: {
+ background: (isDark) => isDark ? '#1f2937' : '#f8fafc',
+ bar: (isDark) => isDark ? '#6b7280' : '#e5e7eb',
+ comparing: '#ef4444',
+ swapping: '#f59e0b',
+ sorted: '#10b981',
+ text: (isDark) => isDark ? '#ffffff' : '#000000',
+ glow: '#3b82f6'
+ },
+ neon: {
+ background: () => '#0f172a',
+ bar: () => '#312e81',
+ comparing: '#f0abfc',
+ swapping: '#f472b6',
+ sorted: '#22d3ee',
+ text: () => '#ffffff',
+ glow: '#a855f7'
+ },
+ minimal: {
+ background: (isDark) => isDark ? '#18181b' : '#fafafa',
+ bar: (isDark) => isDark ? '#3f3f46' : '#d4d4d8',
+ comparing: '#525252',
+ swapping: '#737373',
+ sorted: '#404040',
+ text: (isDark) => isDark ? '#ffffff' : '#000000',
+ glow: '#71717a'
+ }
+};
+
const SortingCanvas = forwardRef(({
- data,
- algorithm,
+ array,
+ comparing = [],
+ swapping = [],
+ sorted = [],
+ theme = 'classic',
compact = false,
- selectedAlgorithm,
- enhanced = false
}, ref) => {
const { isDark } = useTheme();
const canvasRef = useRef(null);
const animationFrameRef = useRef(null);
const lastRenderTime = useRef(0);
+ const getColor = useCallback((index) => {
+ const colors = themeColors[theme] || themeColors.classic;
+
+ if (sorted.includes(index)) {
+ return colors.sorted;
+ }
+ if (swapping.includes(index)) {
+ return colors.swapping;
+ }
+ if (comparing.includes(index)) {
+ return colors.comparing;
+ }
+ return colors.bar(isDark);
+ }, [theme, isDark, comparing, swapping, sorted]);
+
const drawCanvas = useCallback(() => {
const canvas = canvasRef.current;
- if (!canvas || !data || !data.array) return;
+ if (!canvas || !array) return;
- // Prevent excessive rendering
const now = Date.now();
if (now - lastRenderTime.current < 16) return; // Limit to ~60fps
lastRenderTime.current = now;
@@ -26,55 +71,52 @@ const SortingCanvas = forwardRef(({
const ctx = canvas.getContext('2d');
const { width, height } = canvas;
- // Clear canvas
- ctx.fillStyle = isDark ? '#1f2937' : '#f8fafc';
+ // Clear canvas with theme background
+ const colors = themeColors[theme] || themeColors.classic;
+ ctx.fillStyle = colors.background(isDark);
ctx.fillRect(0, 0, width, height);
- // Validate array data
- if (!Array.isArray(data.array) || data.array.length === 0) return;
+ if (!Array.isArray(array) || array.length === 0) return;
- // Draw bars
- const maxValue = Math.max(...data.array);
+ const maxValue = Math.max(...array);
if (maxValue <= 0) return;
- const barWidth = width / data.array.length;
+ const barWidth = width / array.length;
const maxBarHeight = height - 40;
- data.array.forEach((value, index) => {
+ // Draw bars with glow effect for neon theme
+ array.forEach((value, index) => {
if (typeof value !== 'number' || value < 0) return;
const barHeight = (value / maxValue) * maxBarHeight;
const x = index * barWidth;
const y = height - barHeight - 20;
- // Determine bar color
- let barColor = isDark ? '#6b7280' : '#e5e7eb';
-
- if (data.highlighted?.includes(index)) {
- barColor = '#f59e0b'; // Orange for highlighted
- } else if (data.comparing?.includes(index)) {
- barColor = '#ef4444'; // Red for comparing
+ if (theme === 'neon') {
+ ctx.shadowColor = colors.glow;
+ ctx.shadowBlur = 15;
+ } else {
+ ctx.shadowBlur = 0;
}
// Draw bar
- ctx.fillStyle = barColor;
+ ctx.fillStyle = getColor(index);
ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
- // Draw value label
- ctx.fillStyle = isDark ? '#ffffff' : '#000000';
+ // Draw value label with theme color
+ ctx.fillStyle = colors.text(isDark);
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(value.toString(), x + barWidth / 2, height - 5);
});
- // Update ref for canvas export
if (ref) {
ref.current = canvas;
}
} catch (error) {
console.error('Canvas drawing error:', error);
}
- }, [data, algorithm, isDark, enhanced, ref]);
+ }, [array, comparing, swapping, sorted, theme, isDark, getColor, ref]);
useEffect(() => {
// Cancel any pending animation frame
diff --git a/frontend/src/contexts/AlgorithmSettingsContext.js b/frontend/src/contexts/AlgorithmSettingsContext.js
new file mode 100644
index 0000000..757e73a
--- /dev/null
+++ b/frontend/src/contexts/AlgorithmSettingsContext.js
@@ -0,0 +1,71 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+
+const defaultSettings = {
+ speed: 1, // 0.1x to 10x
+ stepMode: false,
+ theme: 'classic', // 'classic' | 'neon' | 'minimal'
+ soundEffects: false,
+ showComplexity: true,
+ compareMode: false,
+};
+
+const AlgorithmSettingsContext = createContext(null);
+
+export const useAlgorithmSettings = () => {
+ const context = useContext(AlgorithmSettingsContext);
+ if (!context) {
+ throw new Error('useAlgorithmSettings must be used within an AlgorithmSettingsProvider');
+ }
+ return context;
+};
+
+export const AlgorithmSettingsProvider = ({ children }) => {
+ const [settings, setSettings] = useState(() => {
+ // Load settings from localStorage on initial render
+ const savedSettings = localStorage.getItem('algorithmSettings');
+ return savedSettings ? JSON.parse(savedSettings) : defaultSettings;
+ });
+
+ // Save settings to localStorage whenever they change
+ useEffect(() => {
+ localStorage.setItem('algorithmSettings', JSON.stringify(settings));
+ }, [settings]);
+
+ const updateSettings = (newSettings) => {
+ setSettings(prev => ({ ...prev, ...newSettings }));
+ };
+
+ const resetSettings = () => {
+ setSettings(defaultSettings);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const themes = {
+ classic: {
+ primary: 'from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700',
+ secondary: 'bg-gray-100 text-gray-700 hover:bg-gray-200',
+ bar: 'bg-blue-500',
+ text: 'text-gray-700',
+ background: 'bg-white',
+ },
+ neon: {
+ primary: 'from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600',
+ secondary: 'bg-gray-900 text-purple-400 hover:bg-gray-800',
+ bar: 'bg-purple-500',
+ text: 'text-purple-200',
+ background: 'bg-gray-900',
+ },
+ minimal: {
+ primary: 'from-gray-700 to-gray-800 hover:from-gray-800 hover:to-gray-900',
+ secondary: 'bg-gray-100 text-gray-600 hover:bg-gray-200',
+ bar: 'bg-gray-700',
+ text: 'text-gray-600',
+ background: 'bg-gray-50',
+ },
+};
diff --git a/frontend/src/pages/SortingVisualizer.jsx b/frontend/src/pages/SortingVisualizer.jsx
index 2d05a55..35d39b4 100644
--- a/frontend/src/pages/SortingVisualizer.jsx
+++ b/frontend/src/pages/SortingVisualizer.jsx
@@ -2,14 +2,16 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Zap, Settings, BookOpen, Download } from 'lucide-react';
import toast from 'react-hot-toast';
import { useTheme } from '../contexts/ThemeContext';
+import { AlgorithmSettingsProvider, useAlgorithmSettings } from '../contexts/AlgorithmSettingsContext';
import SortingCanvas from '../components/Sorting/SortingCanvas';
import ArrayInput from '../components/Sorting/ArrayInput';
import ControlPanel from '../components/Sorting/ControlPanel';
import ComplexityDisplay from '../components/Sorting/ComplexityDisplay';
import { sortingService } from '../services/api';
-const SortingVisualizer = () => {
+const SortingVisualizerContent = () => {
const { isDark, classes, getThemedGradient } = useTheme();
+ const { settings } = useAlgorithmSettings();
const [array, setArray] = useState([64, 34, 25, 12, 22, 11, 90]);
const [algorithm, setAlgorithm] = useState('bubble');
const [isPlaying, setIsPlaying] = useState(false);
@@ -26,6 +28,7 @@ const SortingVisualizer = () => {
const particleCanvasRef = useRef(null);
const timeoutCountRef = useRef(0);
const lastTimeoutCheck = useRef(Date.now());
+ const audioRef = useRef(new Audio('/sounds/swap.mp3'));
const algorithms = [
{
@@ -346,6 +349,31 @@ const SortingVisualizer = () => {
});
};
+ // Convert speed setting (0.1x - 10x) to delay in ms (1000ms - 100ms)
+ const getDelay = useCallback(() => {
+ return Math.round(1000 / settings.speed);
+ }, [settings.speed]);
+
+ // Handle sound effects
+ const playSwapSound = useCallback(() => {
+ if (settings.soundEffects && audioRef.current) {
+ audioRef.current.currentTime = 0;
+ audioRef.current.play().catch(() => {});
+ }
+ }, [settings.soundEffects]);
+
+ useEffect(() => {
+ // Update speed when settings change
+ setSpeed(getDelay());
+ }, [settings.speed, getDelay]);
+
+ // Play step sound when moving forward
+ useEffect(() => {
+ if (currentStep > 0 && settings.soundEffects) {
+ playSwapSound();
+ }
+ }, [currentStep, settings.soundEffects, playSwapSound]);
+
useEffect(() => {
if (isPlaying && currentStep < steps.length - 1) {
const timer = setTimeout(() => {
@@ -367,356 +395,82 @@ const SortingVisualizer = () => {
}, [isPlaying, currentStep, steps.length, speed]);
return (
-
- {/* Conditional Particle Background */}
- {shouldShowParticles && (
-
- )}
-
- {/* Timeout Warning */}
- {timeoutDetected && (
-
- ⚠️ Performance issue detected. Some visual effects disabled.
-
- )}
-
-
-
- {/* Left Panel - Enhanced Array & Controls */}
-
-
-
-
-
- Configuration & Controls
-
-
- {/* Add algorithm selector here */}
-
-
-
-
-
-
-
- {/* Enhanced Array Section */}
-
-
-
- Array Configuration
-
-
- {/* Array Size Slider */}
-
-
- setArraySize(parseInt(e.target.value))}
- className="w-full h-3 bg-gray-200 rounded-lg appearance-none cursor-pointer"
- style={{
- background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${((arraySize - 5) / 10) * 100}%, #e5e7eb ${((arraySize - 5) / 10) * 100}%, #e5e7eb 100%)`
- }}
- />
-
-
-
-
-
- {/* Enhanced Controls Section */}
-
-
-
- Playback Controls
-
-
-
-
- {/* Download Button */}
-
-
-
+
+
+ {/* Main content */}
+
+ {/* Algorithm selection and array input */}
+
- {/* Right Panel - Enhanced Visualization */}
-
-
- {/* Visualization Header */}
-
-
-
-
- {selectedAlgorithm?.name} Visualization
-
-
-
-
- Step {currentStep + 1} of {steps.length || 1}
-
-
-
- Ops: {currentStepData.operations_count || 0}
-
-
-
-
- {selectedAlgorithm?.complexity}
-
-
-
-
-
- {/* Enhanced Visualization Canvas */}
-
-
-
-
- {/* Enhanced Step Explanation with Typewriter Effect */}
-
-
-
-
-
- Step Explanation
-
-
-
-
-
-
-
-
- Current Operation:
-
-
- {currentStepData.operation}
-
-
-
-
-
- What's Happening:
-
-
- {typewriterText}
- |
-
-
-
- {selectedAlgorithm && (
-
-
- Algorithm Info:
-
-
- {selectedAlgorithm.explanation}
-
-
- )}
-
- {/* Detailed Log */}
- {showDetailedLog && (
-
-
- Step History:
-
-
- {steps.slice(Math.max(0, currentStep - 5), currentStep + 1).map((step, index) => (
-
- {step.operation}
-
- ))}
-
-
- )}
-
-
+ {/* Control panel with new settings */}
+
+ settings.updateSettings({ speed: newSpeed })}
+ speed={settings.speed}
+ currentStep={currentStep}
+ totalSteps={steps.length}
+ isLoading={isLoading}
+ onStepForward={stepForward}
+ onStepBackward={stepBackward}
+ onCompareMode={() => settings.updateSettings({ compareMode: !settings.compareMode })}
+ onThemeChange={(theme) => settings.updateSettings({ theme })}
+ />
-
- {/* Enhanced Bottom Panel - Analysis & Metrics */}
-
-
-
-
- Complexity Analysis
-
-
-
-
+
+ {showParticles && (
+
-
-
-
-
-
-
- Performance Metrics
-
-
-
-
-
-
- {array.length}
-
-
- Array Size
-
-
-
-
- {steps.length}
-
-
- Total Steps
-
-
-
-
- {currentStepData.operations_count || 0}
-
-
- Operations
-
-
-
-
- {steps.length > 0 ? Math.round((currentStep / (steps.length - 1)) * 100) : 0}%
-
-
- Progress
-
-
-
-
+ )}
-
- {/* Custom CSS for animations */}
-
);
};
+const SortingVisualizer = () => {
+ return (
+
+
+
+ );
+};
+
export default SortingVisualizer;
diff --git a/frontend/src/utils/soundEffects.js b/frontend/src/utils/soundEffects.js
new file mode 100644
index 0000000..5d28896
--- /dev/null
+++ b/frontend/src/utils/soundEffects.js
@@ -0,0 +1,89 @@
+const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+
+const soundEffects = {
+ swap: async () => {
+ try {
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.type = 'sine';
+ oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
+ oscillator.frequency.exponentialRampToValueAtTime(880, audioContext.currentTime + 0.1);
+
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.1);
+ } catch (error) {
+ console.error('Failed to play sound effect:', error);
+ }
+ },
+
+ compare: async () => {
+ try {
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.type = 'sine';
+ oscillator.frequency.setValueAtTime(330, audioContext.currentTime);
+
+ gainNode.gain.setValueAtTime(0.05, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.05);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.05);
+ } catch (error) {
+ console.error('Failed to play sound effect:', error);
+ }
+ },
+
+ complete: async () => {
+ try {
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.type = 'sine';
+ oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
+ oscillator.frequency.exponentialRampToValueAtTime(880, audioContext.currentTime + 0.15);
+
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.15);
+
+ oscillator.start(audioContext.currentTime);
+ oscillator.stop(audioContext.currentTime + 0.15);
+
+ // Add a higher pitch sound after a short delay
+ setTimeout(() => {
+ const oscillator2 = audioContext.createOscillator();
+ const gainNode2 = audioContext.createGain();
+
+ oscillator2.connect(gainNode2);
+ gainNode2.connect(audioContext.destination);
+
+ oscillator2.type = 'sine';
+ oscillator2.frequency.setValueAtTime(660, audioContext.currentTime);
+ oscillator2.frequency.exponentialRampToValueAtTime(1320, audioContext.currentTime + 0.15);
+
+ gainNode2.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode2.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.15);
+
+ oscillator2.start(audioContext.currentTime);
+ oscillator2.stop(audioContext.currentTime + 0.15);
+ }, 150);
+ } catch (error) {
+ console.error('Failed to play sound effect:', error);
+ }
+ }
+};
+
+export default soundEffects;