let array = [];
let arraySize = 20;
let selectedAlgorithm = 'bubble';
let sorting = false;
let comparisons = 0;
let swaps = 0;
let attempts = 0;
const asciiChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
// Audio context and sound system
let audioContext;
let isSoundEnabled = true;
let slideWhistleOscillator;
let slideWhistleGain;
let startTime;
let totalSortingTime = 10; // Will be dynamically adjusted
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
function startSlideWhistle() {
if (!isSoundEnabled || !audioContext) return;
// Create oscillator and gain for slide whistle
slideWhistleOscillator = audioContext.createOscillator();
slideWhistleGain = audioContext.createGain();
slideWhistleOscillator.type = 'sine';
// Start at low frequency
slideWhistleOscillator.frequency.setValueAtTime(200, audioContext.currentTime);
// Estimate sorting time based on array size and algorithm
totalSortingTime = estimateSortingTime();
// Schedule a smooth frequency ramp over the estimated time
slideWhistleOscillator.frequency.exponentialRampToValueAtTime(
800,
audioContext.currentTime + totalSortingTime
);
slideWhistleGain.gain.setValueAtTime(0.15, audioContext.currentTime);
slideWhistleOscillator.connect(slideWhistleGain);
slideWhistleGain.connect(audioContext.destination);
slideWhistleOscillator.start();
startTime = audioContext.currentTime;
}
function estimateSortingTime() {
// Estimate based on array size and algorithm complexity
const baseTime = array.length * getDelay() / 1000;
switch(selectedAlgorithm) {
case 'bubble':
case 'selection':
case 'insertion':
case 'cocktail':
return baseTime * array.length * 0.5;
case 'quick':
case 'merge':
case 'heap':
return baseTime * Math.log2(array.length) * 2;
case 'shell':
case 'comb':
return baseTime * array.length * 0.3;
case 'radix':
return baseTime * 3;
case 'gnome':
return baseTime * array.length * 0.7;
case 'pancake':
return baseTime * array.length * 0.8;
case 'bitonic':
return baseTime * Math.log2(array.length) * Math.log2(array.length);
case 'bogo':
return baseTime * 10; // Who knows!
default:
return baseTime * array.length * 0.5;
}
}
function updateSlideWhistle(progress) {
// With exponential ramping, we don't need to update manually
// The Web Audio API handles the smooth transition
}
function stopSlideWhistle() {
if (!slideWhistleOscillator) return;
// Cancel any scheduled frequency changes
slideWhistleOscillator.frequency.cancelScheduledValues(audioContext.currentTime);
// Set current frequency and fade out
const currentFreq = slideWhistleOscillator.frequency.value;
slideWhistleOscillator.frequency.setValueAtTime(currentFreq, audioContext.currentTime);
// Smooth fade out
slideWhistleGain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.2);
setTimeout(() => {
if (slideWhistleOscillator) {
slideWhistleOscillator.stop();
slideWhistleOscillator = null;
slideWhistleGain = null;
}
}, 200);
}
function playZoot() {
if (!isSoundEnabled || !audioContext) return;
// Create a fun "zoot" sound with multiple components
const oscillator1 = audioContext.createOscillator();
const oscillator2 = audioContext.createOscillator();
const oscillator3 = audioContext.createOscillator();
const gainNode = audioContext.createGain();
const filter = audioContext.createBiquadFilter();
// Three oscillators for a richer, more satisfying sound
oscillator1.type = 'sawtooth';
oscillator2.type = 'square';
oscillator3.type = 'triangle';
// Quick frequency sweep for "zoot" effect
const now = audioContext.currentTime;
// Main sweep
oscillator1.frequency.setValueAtTime(800, now);
oscillator1.frequency.exponentialRampToValueAtTime(1600, now + 0.05);
oscillator1.frequency.exponentialRampToValueAtTime(400, now + 0.15);
oscillator1.frequency.exponentialRampToValueAtTime(800, now + 0.25);
// Harmonic sweep
oscillator2.frequency.setValueAtTime(400, now);
oscillator2.frequency.exponentialRampToValueAtTime(800, now + 0.05);
oscillator2.frequency.exponentialRampToValueAtTime(200, now + 0.15);
oscillator2.frequency.exponentialRampToValueAtTime(400, now + 0.25);
// Sub bass sweep
oscillator3.frequency.setValueAtTime(100, now);
oscillator3.frequency.exponentialRampToValueAtTime(200, now + 0.1);
oscillator3.frequency.exponentialRampToValueAtTime(50, now + 0.25);
// Filter sweep for extra "zoot" character
filter.type = 'bandpass';
filter.frequency.setValueAtTime(400, now);
filter.frequency.exponentialRampToValueAtTime(2000, now + 0.1);
filter.frequency.exponentialRampToValueAtTime(800, now + 0.25);
filter.Q.value = 5;
// Envelope
gainNode.gain.setValueAtTime(0, now);
gainNode.gain.linearRampToValueAtTime(0.4, now + 0.02);
gainNode.gain.linearRampToValueAtTime(0.3, now + 0.1);
gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
// Connect
oscillator1.connect(filter);
oscillator2.connect(filter);
oscillator3.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);
// Play
oscillator1.start();
oscillator2.start();
oscillator3.start();
oscillator1.stop(now + 0.4);
oscillator2.stop(now + 0.4);
oscillator3.stop(now + 0.4);
}
// Add a function to calculate sorting progress
function getSortingProgress() {
let sortedCount = 0;
const rows = document.querySelectorAll('.bar-row');
rows.forEach(row => {
if (row.classList.contains('sorted')) {
sortedCount++;
}
});
return sortedCount / array.length;
}
// Toggle sound on/off
function toggleSound() {
isSoundEnabled = !isSoundEnabled;
const soundBtn = document.getElementById('soundBtn');
soundBtn.textContent = isSoundEnabled ? 'SOUND: ON' : 'SOUND: OFF';
// If sound is turned off while playing, stop the whistle
if (!isSoundEnabled && slideWhistleOscillator) {
stopSlideWhistle();
}
}
function generateArray() {
// Use smaller array for Bogo Sort
if (selectedAlgorithm === 'bogo') {
arraySize = 5; // Much smaller for Bogo!
document.getElementById('arraySize').textContent = arraySize;
} else if (arraySize === 5 && selectedAlgorithm !== 'bogo') {
arraySize = 20; // Reset to normal size
document.getElementById('arraySize').textContent = arraySize;
}
array = [];
for (let i = 0; i < arraySize; i++) {
array.push(Math.floor(Math.random() * 90) + 10);
}
renderArray();
resetStats();
}
function getAsciiBar(value, maxValue) {
const normalized = value / maxValue;
const maxBarLength = window.innerWidth < 768 ? 20 : 40;
const barLength = Math.floor(normalized * maxBarLength);
let bar = '';
for (let i = 0; i < barLength; i++) {
bar += asciiChars[Math.min(7, Math.floor(i / 5))];
}
return bar;
}
function renderArray() {
const container = document.getElementById('visualization');
const maxValue = Math.max(...array);
const isMobile = window.innerWidth < 768;
const borderLength = isMobile ? 30 : 50;
const padLength = isMobile ? 20 : 40;
let asciiDisplay = '';
// Top border
asciiDisplay += '┌' + '─'.repeat(borderLength) + '┐\n';
// Array visualization
array.forEach((value, index) => {
const bar = getAsciiBar(value, maxValue);
const paddedValue = String(value).padStart(3, '0');
const paddedIndex = String(index).padStart(2, '0');
if (isMobile) {
asciiDisplay += `│${paddedIndex}:[${paddedValue}]${bar.padEnd(padLength, ' ')}│\n`;
} else {
asciiDisplay += `│ ${paddedIndex}: [${paddedValue}] ${bar.padEnd(padLength, ' ')}│\n`;
}
});
// Bottom border
asciiDisplay += '└' + '─'.repeat(borderLength) + '┘';
container.innerHTML = asciiDisplay.split('\n').map((line, index) => {
if (index > 0 && index <= array.length) {
return `${line}`;
}
return `${line}`;
}).join('\n');
}
function resetStats() {
comparisons = 0;
swaps = 0;
attempts = 0;
updateStats();
}
function updateStats() {
document.getElementById('comparisons').textContent = String(comparisons).padStart(4, '0');
document.getElementById('swaps').textContent = String(swaps).padStart(4, '0');
document.getElementById('attempts').textContent = String(attempts).padStart(4, '0');
}
function selectAlgorithm(algo) {
selectedAlgorithm = algo;
document.querySelectorAll('.algorithm-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Show/hide attempts counter for Bogo Sort
const attemptsContainer = document.getElementById('attemptsContainer');
if (algo === 'bogo') {
attemptsContainer.style.display = 'block';
generateArray(); // Auto-generate smaller array for Bogo
} else {
attemptsContainer.style.display = 'none';
if (arraySize === 5) {
generateArray(); // Reset to normal size
}
}
}
async function startSort() {
if (sorting) return;
sorting = true;
document.getElementById('startBtn').disabled = true;
resetStats();
// Initialize audio on user interaction
initAudio();
// Start slide whistle
startSlideWhistle();
switch (selectedAlgorithm) {
case 'bubble':
await bubbleSort();
break;
case 'selection':
await selectionSort();
break;
case 'insertion':
await insertionSort();
break;
case 'quick':
await quickSort(0, array.length - 1);
break;
case 'merge':
await mergeSort(0, array.length - 1);
break;
case 'heap':
await heapSort();
break;
case 'shell':
await shellSort();
break;
case 'cocktail':
await cocktailSort();
break;
case 'radix':
await radixSort();
break;
case 'gnome':
await gnomeSort();
break;
case 'bogo':
await bogoSort();
break;
case 'comb':
await combSort();
break;
case 'pancake':
await pancakeSort();
break;
case 'bitonic':
await bitonicSort();
break;
}
// Stop slide whistle and play zoot
stopSlideWhistle();
playZoot();
markAllSorted();
sorting = false;
document.getElementById('startBtn').disabled = false;
}
function getDelay() {
return 101 - document.getElementById('speedSlider').value;
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function compare(i, j) {
comparisons++;
updateStats();
const rows = document.querySelectorAll('.bar-row');
if (rows[i]) rows[i].classList.add('comparing');
if (rows[j]) rows[j].classList.add('comparing');
// Subtle pitch update during comparisons for continuous glissando
const progress = getSortingProgress();
updateSlideWhistle(progress);
await sleep(getDelay());
if (rows[i]) rows[i].classList.remove('comparing');
if (rows[j]) rows[j].classList.remove('comparing');
}
async function swap(i, j) {
swaps++;
updateStats();
const temp = array[i];
array[i] = array[j];
array[j] = temp;
renderArray();
// Update slide whistle pitch based on progress
const progress = getSortingProgress();
updateSlideWhistle(progress);
await sleep(getDelay());
}
async function bubbleSort() {
for (let i = 0; i < array.length - 1; i++) {
for (let j = 0; j < array.length - i - 1; j++) {
await compare(j, j + 1);
if (array[j] > array[j + 1]) {
await swap(j, j + 1);
}
}
document.querySelectorAll('.bar-row')[array.length - i - 1].classList.add('sorted');
// Update slide whistle pitch
const progress = getSortingProgress();
updateSlideWhistle(progress);
}
document.querySelectorAll('.bar-row')[0].classList.add('sorted');
}
async function selectionSort() {
for (let i = 0; i < array.length - 1; i++) {
let minIdx = i;
for (let j = i + 1; j < array.length; j++) {
await compare(minIdx, j);
if (array[j] < array[minIdx]) {
minIdx = j;
}
}
if (minIdx !== i) {
await swap(i, minIdx);
}
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
// Update slide whistle pitch
const progress = getSortingProgress();
updateSlideWhistle(progress);
}
document.querySelectorAll('.bar-row')[array.length - 1].classList.add('sorted');
}
async function insertionSort() {
for (let i = 1; i < array.length; i++) {
let j = i - 1;
let key = array[i];
while (j >= 0 && array[j] > key) {
await compare(j, j + 1);
array[j + 1] = array[j];
renderArray();
j--;
await sleep(getDelay());
}
array[j + 1] = key;
renderArray();
document.querySelectorAll('.bar-row')[j + 1].classList.add('sorted');
}
markAllSorted();
}
async function quickSort(low, high) {
if (low < high) {
let pi = await partition(low, high);
await quickSort(low, pi - 1);
await quickSort(pi + 1, high);
}
}
async function partition(low, high) {
let pivot = array[high];
let i = low - 1;
for (let j = low; j < high; j++) {
await compare(j, high);
if (array[j] < pivot) {
i++;
await swap(i, j);
}
}
await swap(i + 1, high);
return i + 1;
}
async function mergeSort(left, right) {
if (left < right) {
let mid = Math.floor((left + right) / 2);
await mergeSort(left, mid);
await mergeSort(mid + 1, right);
await merge(left, mid, right);
}
}
async function merge(left, mid, right) {
let leftArr = array.slice(left, mid + 1);
let rightArr = array.slice(mid + 1, right + 1);
let i = 0, j = 0, k = left;
while (i < leftArr.length && j < rightArr.length) {
await compare(left + i, mid + 1 + j);
if (leftArr[i] <= rightArr[j]) {
array[k] = leftArr[i];
i++;
} else {
array[k] = rightArr[j];
j++;
}
k++;
renderArray();
await sleep(getDelay());
}
while (i < leftArr.length) {
array[k] = leftArr[i];
i++;
k++;
renderArray();
await sleep(getDelay());
}
while (j < rightArr.length) {
array[k] = rightArr[j];
j++;
k++;
renderArray();
await sleep(getDelay());
}
}
// Heap Sort
async function heapSort() {
// Build max heap
for (let i = Math.floor(array.length / 2) - 1; i >= 0; i--) {
await heapify(array.length, i);
}
// Extract elements from heap one by one
for (let i = array.length - 1; i > 0; i--) {
await swap(0, i);
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
await heapify(i, 0);
// Update slide whistle pitch
const progress = getSortingProgress();
updateSlideWhistle(progress);
}
document.querySelectorAll('.bar-row')[0].classList.add('sorted');
}
async function heapify(n, i) {
let largest = i;
let left = 2 * i + 1;
let right = 2 * i + 2;
if (left < n) {
await compare(left, largest);
if (array[left] > array[largest]) {
largest = left;
}
}
if (right < n) {
await compare(right, largest);
if (array[right] > array[largest]) {
largest = right;
}
}
if (largest !== i) {
await swap(i, largest);
await heapify(n, largest);
}
}
// Shell Sort
async function shellSort() {
let gap = Math.floor(array.length / 2);
while (gap > 0) {
for (let i = gap; i < array.length; i++) {
let temp = array[i];
let j = i;
while (j >= gap) {
await compare(j - gap, j);
if (array[j - gap] > temp) {
array[j] = array[j - gap];
j -= gap;
renderArray();
await sleep(getDelay());
} else {
break;
}
}
array[j] = temp;
renderArray();
await sleep(getDelay());
}
gap = Math.floor(gap / 2);
}
// Mark all as sorted
for (let i = 0; i < array.length; i++) {
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
}
}
// Cocktail Sort (Bidirectional Bubble Sort)
async function cocktailSort() {
let swapped = true;
let start = 0;
let end = array.length - 1;
while (swapped) {
swapped = false;
// Forward pass (like bubble sort)
for (let i = start; i < end; i++) {
await compare(i, i + 1);
if (array[i] > array[i + 1]) {
await swap(i, i + 1);
swapped = true;
}
}
if (!swapped) break;
document.querySelectorAll('.bar-row')[end].classList.add('sorted');
end--;
swapped = false;
// Backward pass
for (let i = end; i > start; i--) {
await compare(i, i - 1);
if (array[i] < array[i - 1]) {
await swap(i, i - 1);
swapped = true;
}
}
document.querySelectorAll('.bar-row')[start].classList.add('sorted');
start++;
}
// Mark remaining elements as sorted
for (let i = start; i <= end; i++) {
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
}
}
// Radix Sort (LSD - Least Significant Digit)
async function radixSort() {
// Find the maximum number to know number of digits
let max = Math.max(...array);
// Do counting sort for every digit
for (let exp = 1; Math.floor(max / exp) > 0; exp *= 10) {
await countingSortByDigit(exp);
}
// Mark all as sorted
for (let i = 0; i < array.length; i++) {
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
}
}
async function countingSortByDigit(exp) {
let output = new Array(array.length);
let count = new Array(10).fill(0);
// Store count of occurrences
for (let i = 0; i < array.length; i++) {
let digit = Math.floor(array[i] / exp) % 10;
count[digit]++;
}
// Change count[i] so it contains actual position
for (let i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// Build the output array
for (let i = array.length - 1; i >= 0; i--) {
let digit = Math.floor(array[i] / exp) % 10;
output[count[digit] - 1] = array[i];
count[digit]--;
// Visual comparison for the digit
if (i < array.length - 1) {
await compare(i, i + 1);
}
}
// Copy the output array to arr[]
for (let i = 0; i < array.length; i++) {
array[i] = output[i];
renderArray();
await sleep(getDelay());
}
}
// Gnome Sort (also called Stupid Sort)
async function gnomeSort() {
let index = 0;
while (index < array.length) {
if (index == 0) {
index++;
} else {
await compare(index, index - 1);
if (array[index] >= array[index - 1]) {
document.querySelectorAll('.bar-row')[index - 1].classList.add('sorted');
index++;
} else {
await swap(index, index - 1);
index--;
}
}
}
// Mark the last element as sorted
if (array.length > 0) {
document.querySelectorAll('.bar-row')[array.length - 1].classList.add('sorted');
}
}
// Bogo Sort (Warning: This is intentionally terrible!)
async function bogoSort() {
attempts = 0;
const maxAttempts = 10000; // Safety limit to prevent infinite loops
while (!isSorted() && attempts < maxAttempts) {
// Shuffle the array randomly
await shuffleArray();
attempts++;
updateStats();
}
if (attempts >= maxAttempts) {
alert("Bogo Sort gave up after " + maxAttempts + " attempts! Try a smaller array.");
}
// Mark all as sorted
for (let i = 0; i < array.length; i++) {
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
}
}
function isSorted() {
for (let i = 0; i < array.length - 1; i++) {
if (array[i] > array[i + 1]) {
return false;
}
}
return true;
}
async function shuffleArray() {
// Fisher-Yates shuffle with visualization
for (let i = array.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
await compare(i, j);
await swap(i, j);
}
}
function markAllSorted() {
document.querySelectorAll('.bar-row').forEach(row => {
row.classList.add('sorted');
});
}
// Comb Sort (improved Bubble Sort with gap)
async function combSort() {
let gap = array.length;
let shrink = 1.3;
let sorted = false;
while (gap > 1 || !sorted) {
// Update gap
gap = Math.floor(gap / shrink);
if (gap < 1) gap = 1;
sorted = true;
// Single pass with current gap
for (let i = 0; i + gap < array.length; i++) {
await compare(i, i + gap);
if (array[i] > array[i + gap]) {
await swap(i, i + gap);
sorted = false;
}
}
// Mark sorted elements when gap is 1 (final pass)
if (gap === 1 && sorted) {
for (let i = 0; i < array.length; i++) {
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
await sleep(getDelay() / 4);
}
}
}
}
// Pancake Sort (flip sort)
async function pancakeSort() {
for (let curr_size = array.length; curr_size > 1; curr_size--) {
// Find index of maximum element in unsorted portion
let maxIdx = 0;
for (let i = 1; i < curr_size; i++) {
await compare(i, maxIdx);
if (array[i] > array[maxIdx]) {
maxIdx = i;
}
}
// Move maximum element to end if it's not already there
if (maxIdx !== curr_size - 1) {
// Flip to move max to beginning
if (maxIdx !== 0) {
await flip(maxIdx + 1);
}
// Flip to move max to correct position
await flip(curr_size);
}
document.querySelectorAll('.bar-row')[curr_size - 1].classList.add('sorted');
}
document.querySelectorAll('.bar-row')[0].classList.add('sorted');
}
// Helper function for pancake sort - flips array from 0 to n-1
async function flip(n) {
let start = 0;
let end = n - 1;
while (start < end) {
await swap(start, end);
start++;
end--;
}
}
// Bitonic Sort (requires power of 2 array size)
async function bitonicSort() {
// Adjust array size to nearest power of 2 if needed
const originalLength = array.length;
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(array.length)));
// Pad with large values if necessary
while (array.length < nextPowerOf2) {
array.push(999);
}
renderArray();
// Start the bitonic sort
await bitonicSortRecursive(0, array.length, true);
// Remove padding if we added any
if (array.length > originalLength) {
array.length = originalLength;
renderArray();
}
// Mark all as sorted
for (let i = 0; i < originalLength; i++) {
document.querySelectorAll('.bar-row')[i].classList.add('sorted');
}
}
async function bitonicSortRecursive(lo, cnt, dir) {
if (cnt > 1) {
let k = Math.floor(cnt / 2);
// Sort in ascending order
await bitonicSortRecursive(lo, k, true);
// Sort in descending order
await bitonicSortRecursive(lo + k, k, false);
// Merge the whole sequence
await bitonicMerge(lo, cnt, dir);
}
}
async function bitonicMerge(lo, cnt, dir) {
if (cnt > 1) {
let k = Math.floor(cnt / 2);
for (let i = lo; i < lo + k; i++) {
await compare(i, i + k);
// Swap if needed based on direction
if ((array[i] > array[i + k]) === dir) {
await swap(i, i + k);
}
}
await bitonicMerge(lo, k, dir);
await bitonicMerge(lo + k, k, dir);
}
}
// Initialize
generateArray();
document.querySelector('.algorithm-btn').click();
// Re-render on window resize
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
renderArray();
}, 250);
});