JavaScript · 22242 bytes Raw Blame History
1 let array = [];
2 let arraySize = 20;
3 let selectedAlgorithm = 'bubble';
4 let sorting = false;
5 let comparisons = 0;
6 let swaps = 0;
7 let attempts = 0;
8
9 const asciiChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
10
11 // Audio context and sound system
12 let audioContext;
13 let isSoundEnabled = true;
14 let slideWhistleOscillator;
15 let slideWhistleGain;
16 let startTime;
17 let totalSortingTime = 10; // Will be dynamically adjusted
18
19 function initAudio() {
20 if (!audioContext) {
21 audioContext = new (window.AudioContext || window.webkitAudioContext)();
22 }
23 }
24
25 function startSlideWhistle() {
26 if (!isSoundEnabled || !audioContext) return;
27
28 // Create oscillator and gain for slide whistle
29 slideWhistleOscillator = audioContext.createOscillator();
30 slideWhistleGain = audioContext.createGain();
31
32 slideWhistleOscillator.type = 'sine';
33
34 // Start at low frequency
35 slideWhistleOscillator.frequency.setValueAtTime(200, audioContext.currentTime);
36
37 // Estimate sorting time based on array size and algorithm
38 totalSortingTime = estimateSortingTime();
39
40 // Schedule a smooth frequency ramp over the estimated time
41 slideWhistleOscillator.frequency.exponentialRampToValueAtTime(
42 800,
43 audioContext.currentTime + totalSortingTime
44 );
45
46 slideWhistleGain.gain.setValueAtTime(0.15, audioContext.currentTime);
47
48 slideWhistleOscillator.connect(slideWhistleGain);
49 slideWhistleGain.connect(audioContext.destination);
50
51 slideWhistleOscillator.start();
52 startTime = audioContext.currentTime;
53 }
54
55 function estimateSortingTime() {
56 // Estimate based on array size and algorithm complexity
57 const baseTime = array.length * getDelay() / 1000;
58
59 switch(selectedAlgorithm) {
60 case 'bubble':
61 case 'selection':
62 case 'insertion':
63 case 'cocktail':
64 return baseTime * array.length * 0.5;
65 case 'quick':
66 case 'merge':
67 case 'heap':
68 return baseTime * Math.log2(array.length) * 2;
69 case 'shell':
70 return baseTime * array.length * 0.3;
71 case 'radix':
72 return baseTime * 3;
73 case 'gnome':
74 return baseTime * array.length * 0.7;
75 case 'bogo':
76 return baseTime * 10; // Who knows!
77 default:
78 return baseTime * array.length * 0.5;
79 }
80 }
81
82 function updateSlideWhistle(progress) {
83 // With exponential ramping, we don't need to update manually
84 // The Web Audio API handles the smooth transition
85 }
86
87 function stopSlideWhistle() {
88 if (!slideWhistleOscillator) return;
89
90 // Cancel any scheduled frequency changes
91 slideWhistleOscillator.frequency.cancelScheduledValues(audioContext.currentTime);
92
93 // Set current frequency and fade out
94 const currentFreq = slideWhistleOscillator.frequency.value;
95 slideWhistleOscillator.frequency.setValueAtTime(currentFreq, audioContext.currentTime);
96
97 // Smooth fade out
98 slideWhistleGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.2);
99
100 setTimeout(() => {
101 if (slideWhistleOscillator) {
102 slideWhistleOscillator.stop();
103 slideWhistleOscillator = null;
104 slideWhistleGain = null;
105 }
106 }, 200);
107 }
108
109 function playZoot() {
110 if (!isSoundEnabled || !audioContext) return;
111
112 // Create a fun "zoot" sound with multiple components
113 const oscillator1 = audioContext.createOscillator();
114 const oscillator2 = audioContext.createOscillator();
115 const oscillator3 = audioContext.createOscillator();
116 const gainNode = audioContext.createGain();
117 const filter = audioContext.createBiquadFilter();
118
119 // Three oscillators for a richer, more satisfying sound
120 oscillator1.type = 'sawtooth';
121 oscillator2.type = 'square';
122 oscillator3.type = 'triangle';
123
124 // Quick frequency sweep for "zoot" effect
125 const now = audioContext.currentTime;
126
127 // Main sweep
128 oscillator1.frequency.setValueAtTime(800, now);
129 oscillator1.frequency.exponentialRampToValueAtTime(1600, now + 0.05);
130 oscillator1.frequency.exponentialRampToValueAtTime(400, now + 0.15);
131 oscillator1.frequency.exponentialRampToValueAtTime(800, now + 0.25);
132
133 // Harmonic sweep
134 oscillator2.frequency.setValueAtTime(400, now);
135 oscillator2.frequency.exponentialRampToValueAtTime(800, now + 0.05);
136 oscillator2.frequency.exponentialRampToValueAtTime(200, now + 0.15);
137 oscillator2.frequency.exponentialRampToValueAtTime(400, now + 0.25);
138
139 // Sub bass sweep
140 oscillator3.frequency.setValueAtTime(100, now);
141 oscillator3.frequency.exponentialRampToValueAtTime(200, now + 0.1);
142 oscillator3.frequency.exponentialRampToValueAtTime(50, now + 0.25);
143
144 // Filter sweep for extra "zoot" character
145 filter.type = 'bandpass';
146 filter.frequency.setValueAtTime(400, now);
147 filter.frequency.exponentialRampToValueAtTime(2000, now + 0.1);
148 filter.frequency.exponentialRampToValueAtTime(800, now + 0.25);
149 filter.Q.value = 5;
150
151 // Envelope
152 gainNode.gain.setValueAtTime(0, now);
153 gainNode.gain.linearRampToValueAtTime(0.4, now + 0.02);
154 gainNode.gain.linearRampToValueAtTime(0.3, now + 0.1);
155 gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
156
157 // Connect
158 oscillator1.connect(filter);
159 oscillator2.connect(filter);
160 oscillator3.connect(filter);
161 filter.connect(gainNode);
162 gainNode.connect(audioContext.destination);
163
164 // Play
165 oscillator1.start();
166 oscillator2.start();
167 oscillator3.start();
168 oscillator1.stop(now + 0.4);
169 oscillator2.stop(now + 0.4);
170 oscillator3.stop(now + 0.4);
171 }
172
173 // Add a function to calculate sorting progress
174 function getSortingProgress() {
175 let sortedCount = 0;
176 const rows = document.querySelectorAll('.bar-row');
177 rows.forEach(row => {
178 if (row.classList.contains('sorted')) {
179 sortedCount++;
180 }
181 });
182 return sortedCount / array.length;
183 }
184
185 // Toggle sound on/off
186 function toggleSound() {
187 isSoundEnabled = !isSoundEnabled;
188 const soundBtn = document.getElementById('soundBtn');
189 soundBtn.textContent = isSoundEnabled ? 'SOUND: ON' : 'SOUND: OFF';
190
191 // If sound is turned off while playing, stop the whistle
192 if (!isSoundEnabled && slideWhistleOscillator) {
193 stopSlideWhistle();
194 }
195 }
196
197 function generateArray() {
198 // Use smaller array for Bogo Sort
199 if (selectedAlgorithm === 'bogo') {
200 arraySize = 5; // Much smaller for Bogo!
201 document.getElementById('arraySize').textContent = arraySize;
202 } else if (arraySize === 5 && selectedAlgorithm !== 'bogo') {
203 arraySize = 20; // Reset to normal size
204 document.getElementById('arraySize').textContent = arraySize;
205 }
206
207 array = [];
208 for (let i = 0; i < arraySize; i++) {
209 array.push(Math.floor(Math.random() * 90) + 10);
210 }
211 renderArray();
212 resetStats();
213 }
214
215 function getAsciiBar(value, maxValue) {
216 const normalized = value / maxValue;
217 const maxBarLength = window.innerWidth < 768 ? 20 : 40;
218 const barLength = Math.floor(normalized * maxBarLength);
219 let bar = '';
220
221 for (let i = 0; i < barLength; i++) {
222 bar += asciiChars[Math.min(7, Math.floor(i / 5))];
223 }
224
225 return bar;
226 }
227
228 function renderArray() {
229 const container = document.getElementById('visualization');
230 const maxValue = Math.max(...array);
231 const isMobile = window.innerWidth < 768;
232 const borderLength = isMobile ? 30 : 50;
233 const padLength = isMobile ? 20 : 40;
234 let asciiDisplay = '';
235
236 // Top border
237 asciiDisplay += '┌' + '─'.repeat(borderLength) + '┐\n';
238
239 // Array visualization
240 array.forEach((value, index) => {
241 const bar = getAsciiBar(value, maxValue);
242 const paddedValue = String(value).padStart(3, '0');
243 const paddedIndex = String(index).padStart(2, '0');
244 if (isMobile) {
245 asciiDisplay += `│${paddedIndex}:[${paddedValue}]${bar.padEnd(padLength, ' ')}│\n`;
246 } else {
247 asciiDisplay += `│ ${paddedIndex}: [${paddedValue}] ${bar.padEnd(padLength, ' ')}│\n`;
248 }
249 });
250
251 // Bottom border
252 asciiDisplay += '└' + '─'.repeat(borderLength) + '┘';
253
254 container.innerHTML = asciiDisplay.split('\n').map((line, index) => {
255 if (index > 0 && index <= array.length) {
256 return `<span class="bar-row" data-index="${index-1}">${line}</span>`;
257 }
258 return `<span>${line}</span>`;
259 }).join('\n');
260 }
261
262 function resetStats() {
263 comparisons = 0;
264 swaps = 0;
265 attempts = 0;
266 updateStats();
267 }
268
269 function updateStats() {
270 document.getElementById('comparisons').textContent = String(comparisons).padStart(4, '0');
271 document.getElementById('swaps').textContent = String(swaps).padStart(4, '0');
272 document.getElementById('attempts').textContent = String(attempts).padStart(4, '0');
273 }
274
275 function selectAlgorithm(algo) {
276 selectedAlgorithm = algo;
277 document.querySelectorAll('.algorithm-btn').forEach(btn => {
278 btn.classList.remove('active');
279 });
280 event.target.classList.add('active');
281
282 // Show/hide attempts counter for Bogo Sort
283 const attemptsContainer = document.getElementById('attemptsContainer');
284 if (algo === 'bogo') {
285 attemptsContainer.style.display = 'block';
286 generateArray(); // Auto-generate smaller array for Bogo
287 } else {
288 attemptsContainer.style.display = 'none';
289 if (arraySize === 5) {
290 generateArray(); // Reset to normal size
291 }
292 }
293 }
294
295 async function startSort() {
296 if (sorting) return;
297
298 sorting = true;
299 document.getElementById('startBtn').disabled = true;
300 resetStats();
301
302 // Initialize audio on user interaction
303 initAudio();
304
305 // Start slide whistle
306 startSlideWhistle();
307
308 switch (selectedAlgorithm) {
309 case 'bubble':
310 await bubbleSort();
311 break;
312 case 'selection':
313 await selectionSort();
314 break;
315 case 'insertion':
316 await insertionSort();
317 break;
318 case 'quick':
319 await quickSort(0, array.length - 1);
320 break;
321 case 'merge':
322 await mergeSort(0, array.length - 1);
323 break;
324 case 'heap':
325 await heapSort();
326 break;
327 case 'shell':
328 await shellSort();
329 break;
330 case 'cocktail':
331 await cocktailSort();
332 break;
333 case 'radix':
334 await radixSort();
335 break;
336 case 'gnome':
337 await gnomeSort();
338 break;
339 case 'bogo':
340 await bogoSort();
341 break;
342 }
343
344 // Stop slide whistle and play zoot
345 stopSlideWhistle();
346 playZoot();
347
348 markAllSorted();
349 sorting = false;
350 document.getElementById('startBtn').disabled = false;
351 }
352
353 function getDelay() {
354 return 101 - document.getElementById('speedSlider').value;
355 }
356
357 async function sleep(ms) {
358 return new Promise(resolve => setTimeout(resolve, ms));
359 }
360
361 async function compare(i, j) {
362 comparisons++;
363 updateStats();
364 const rows = document.querySelectorAll('.bar-row');
365 if (rows[i]) rows[i].classList.add('comparing');
366 if (rows[j]) rows[j].classList.add('comparing');
367
368 // Subtle pitch update during comparisons for continuous glissando
369 const progress = getSortingProgress();
370 updateSlideWhistle(progress);
371
372 await sleep(getDelay());
373 if (rows[i]) rows[i].classList.remove('comparing');
374 if (rows[j]) rows[j].classList.remove('comparing');
375 }
376
377 async function swap(i, j) {
378 swaps++;
379 updateStats();
380
381 const temp = array[i];
382 array[i] = array[j];
383 array[j] = temp;
384
385 renderArray();
386
387 // Update slide whistle pitch based on progress
388 const progress = getSortingProgress();
389 updateSlideWhistle(progress);
390
391 await sleep(getDelay());
392 }
393
394 async function bubbleSort() {
395 for (let i = 0; i < array.length - 1; i++) {
396 for (let j = 0; j < array.length - i - 1; j++) {
397 await compare(j, j + 1);
398 if (array[j] > array[j + 1]) {
399 await swap(j, j + 1);
400 }
401 }
402 document.querySelectorAll('.bar-row')[array.length - i - 1].classList.add('sorted');
403
404 // Update slide whistle pitch
405 const progress = getSortingProgress();
406 updateSlideWhistle(progress);
407 }
408 document.querySelectorAll('.bar-row')[0].classList.add('sorted');
409 }
410
411 async function selectionSort() {
412 for (let i = 0; i < array.length - 1; i++) {
413 let minIdx = i;
414 for (let j = i + 1; j < array.length; j++) {
415 await compare(minIdx, j);
416 if (array[j] < array[minIdx]) {
417 minIdx = j;
418 }
419 }
420 if (minIdx !== i) {
421 await swap(i, minIdx);
422 }
423 document.querySelectorAll('.bar-row')[i].classList.add('sorted');
424
425 // Update slide whistle pitch
426 const progress = getSortingProgress();
427 updateSlideWhistle(progress);
428 }
429 document.querySelectorAll('.bar-row')[array.length - 1].classList.add('sorted');
430 }
431
432 async function insertionSort() {
433 for (let i = 1; i < array.length; i++) {
434 let j = i - 1;
435 let key = array[i];
436
437 while (j >= 0 && array[j] > key) {
438 await compare(j, j + 1);
439 array[j + 1] = array[j];
440 renderArray();
441 j--;
442 await sleep(getDelay());
443 }
444
445 array[j + 1] = key;
446 renderArray();
447 document.querySelectorAll('.bar-row')[j + 1].classList.add('sorted');
448 }
449 markAllSorted();
450 }
451
452 async function quickSort(low, high) {
453 if (low < high) {
454 let pi = await partition(low, high);
455 await quickSort(low, pi - 1);
456 await quickSort(pi + 1, high);
457 }
458 }
459
460 async function partition(low, high) {
461 let pivot = array[high];
462 let i = low - 1;
463
464 for (let j = low; j < high; j++) {
465 await compare(j, high);
466 if (array[j] < pivot) {
467 i++;
468 await swap(i, j);
469 }
470 }
471
472 await swap(i + 1, high);
473 return i + 1;
474 }
475
476 async function mergeSort(left, right) {
477 if (left < right) {
478 let mid = Math.floor((left + right) / 2);
479 await mergeSort(left, mid);
480 await mergeSort(mid + 1, right);
481 await merge(left, mid, right);
482 }
483 }
484
485 async function merge(left, mid, right) {
486 let leftArr = array.slice(left, mid + 1);
487 let rightArr = array.slice(mid + 1, right + 1);
488 let i = 0, j = 0, k = left;
489
490 while (i < leftArr.length && j < rightArr.length) {
491 await compare(left + i, mid + 1 + j);
492 if (leftArr[i] <= rightArr[j]) {
493 array[k] = leftArr[i];
494 i++;
495 } else {
496 array[k] = rightArr[j];
497 j++;
498 }
499 k++;
500 renderArray();
501 await sleep(getDelay());
502 }
503
504 while (i < leftArr.length) {
505 array[k] = leftArr[i];
506 i++;
507 k++;
508 renderArray();
509 await sleep(getDelay());
510 }
511
512 while (j < rightArr.length) {
513 array[k] = rightArr[j];
514 j++;
515 k++;
516 renderArray();
517 await sleep(getDelay());
518 }
519 }
520
521 // Heap Sort
522 async function heapSort() {
523 // Build max heap
524 for (let i = Math.floor(array.length / 2) - 1; i >= 0; i--) {
525 await heapify(array.length, i);
526 }
527
528 // Extract elements from heap one by one
529 for (let i = array.length - 1; i > 0; i--) {
530 await swap(0, i);
531 document.querySelectorAll('.bar-row')[i].classList.add('sorted');
532 await heapify(i, 0);
533
534 // Update slide whistle pitch
535 const progress = getSortingProgress();
536 updateSlideWhistle(progress);
537 }
538 document.querySelectorAll('.bar-row')[0].classList.add('sorted');
539 }
540
541 async function heapify(n, i) {
542 let largest = i;
543 let left = 2 * i + 1;
544 let right = 2 * i + 2;
545
546 if (left < n) {
547 await compare(left, largest);
548 if (array[left] > array[largest]) {
549 largest = left;
550 }
551 }
552
553 if (right < n) {
554 await compare(right, largest);
555 if (array[right] > array[largest]) {
556 largest = right;
557 }
558 }
559
560 if (largest !== i) {
561 await swap(i, largest);
562 await heapify(n, largest);
563 }
564 }
565
566 // Shell Sort
567 async function shellSort() {
568 let gap = Math.floor(array.length / 2);
569
570 while (gap > 0) {
571 for (let i = gap; i < array.length; i++) {
572 let temp = array[i];
573 let j = i;
574
575 while (j >= gap) {
576 await compare(j - gap, j);
577 if (array[j - gap] > temp) {
578 array[j] = array[j - gap];
579 j -= gap;
580 renderArray();
581 await sleep(getDelay());
582 } else {
583 break;
584 }
585 }
586
587 array[j] = temp;
588 renderArray();
589 await sleep(getDelay());
590 }
591
592 gap = Math.floor(gap / 2);
593 }
594
595 // Mark all as sorted
596 for (let i = 0; i < array.length; i++) {
597 document.querySelectorAll('.bar-row')[i].classList.add('sorted');
598 }
599 }
600
601 // Cocktail Sort (Bidirectional Bubble Sort)
602 async function cocktailSort() {
603 let swapped = true;
604 let start = 0;
605 let end = array.length - 1;
606
607 while (swapped) {
608 swapped = false;
609
610 // Forward pass (like bubble sort)
611 for (let i = start; i < end; i++) {
612 await compare(i, i + 1);
613 if (array[i] > array[i + 1]) {
614 await swap(i, i + 1);
615 swapped = true;
616 }
617 }
618
619 if (!swapped) break;
620
621 document.querySelectorAll('.bar-row')[end].classList.add('sorted');
622 end--;
623 swapped = false;
624
625 // Backward pass
626 for (let i = end; i > start; i--) {
627 await compare(i, i - 1);
628 if (array[i] < array[i - 1]) {
629 await swap(i, i - 1);
630 swapped = true;
631 }
632 }
633
634 document.querySelectorAll('.bar-row')[start].classList.add('sorted');
635 start++;
636 }
637
638 // Mark remaining elements as sorted
639 for (let i = start; i <= end; i++) {
640 document.querySelectorAll('.bar-row')[i].classList.add('sorted');
641 }
642 }
643
644 // Radix Sort (LSD - Least Significant Digit)
645 async function radixSort() {
646 // Find the maximum number to know number of digits
647 let max = Math.max(...array);
648
649 // Do counting sort for every digit
650 for (let exp = 1; Math.floor(max / exp) > 0; exp *= 10) {
651 await countingSortByDigit(exp);
652 }
653
654 // Mark all as sorted
655 for (let i = 0; i < array.length; i++) {
656 document.querySelectorAll('.bar-row')[i].classList.add('sorted');
657 }
658 }
659
660 async function countingSortByDigit(exp) {
661 let output = new Array(array.length);
662 let count = new Array(10).fill(0);
663
664 // Store count of occurrences
665 for (let i = 0; i < array.length; i++) {
666 let digit = Math.floor(array[i] / exp) % 10;
667 count[digit]++;
668 }
669
670 // Change count[i] so it contains actual position
671 for (let i = 1; i < 10; i++) {
672 count[i] += count[i - 1];
673 }
674
675 // Build the output array
676 for (let i = array.length - 1; i >= 0; i--) {
677 let digit = Math.floor(array[i] / exp) % 10;
678 output[count[digit] - 1] = array[i];
679 count[digit]--;
680
681 // Visual comparison for the digit
682 if (i < array.length - 1) {
683 await compare(i, i + 1);
684 }
685 }
686
687 // Copy the output array to arr[]
688 for (let i = 0; i < array.length; i++) {
689 array[i] = output[i];
690 renderArray();
691 await sleep(getDelay());
692 }
693 }
694
695 // Gnome Sort (also called Stupid Sort)
696 async function gnomeSort() {
697 let index = 0;
698
699 while (index < array.length) {
700 if (index == 0) {
701 index++;
702 } else {
703 await compare(index, index - 1);
704 if (array[index] >= array[index - 1]) {
705 document.querySelectorAll('.bar-row')[index - 1].classList.add('sorted');
706 index++;
707 } else {
708 await swap(index, index - 1);
709 index--;
710 }
711 }
712 }
713
714 // Mark the last element as sorted
715 if (array.length > 0) {
716 document.querySelectorAll('.bar-row')[array.length - 1].classList.add('sorted');
717 }
718 }
719
720 // Bogo Sort (Warning: This is intentionally terrible!)
721 async function bogoSort() {
722 attempts = 0;
723 const maxAttempts = 10000; // Safety limit to prevent infinite loops
724
725 while (!isSorted() && attempts < maxAttempts) {
726 // Shuffle the array randomly
727 await shuffleArray();
728 attempts++;
729 updateStats();
730 }
731
732 if (attempts >= maxAttempts) {
733 alert("Bogo Sort gave up after " + maxAttempts + " attempts! Try a smaller array.");
734 }
735
736 // Mark all as sorted
737 for (let i = 0; i < array.length; i++) {
738 document.querySelectorAll('.bar-row')[i].classList.add('sorted');
739 }
740 }
741
742 function isSorted() {
743 for (let i = 0; i < array.length - 1; i++) {
744 if (array[i] > array[i + 1]) {
745 return false;
746 }
747 }
748 return true;
749 }
750
751 async function shuffleArray() {
752 // Fisher-Yates shuffle with visualization
753 for (let i = array.length - 1; i > 0; i--) {
754 let j = Math.floor(Math.random() * (i + 1));
755 await compare(i, j);
756 await swap(i, j);
757 }
758 }
759
760 function markAllSorted() {
761 document.querySelectorAll('.bar-row').forEach(row => {
762 row.classList.add('sorted');
763 });
764 }
765
766 // Initialize
767 generateArray();
768 document.querySelector('.algorithm-btn').click();
769
770 // Re-render on window resize
771 let resizeTimeout;
772 window.addEventListener('resize', () => {
773 clearTimeout(resizeTimeout);
774 resizeTimeout = setTimeout(() => {
775 renderArray();
776 }, 250);
777 });