Search code examples
javascriptanimationselection-sort

Using nested setTimeout to create an animated selection sort


I am working on a basic sorting visualizer with using only HTML, CSS, and JS, and I've run into a problem with the animation aspect. To initialize the array, I generate random numbers within some specified range and push them on to the array. Then based on the webpage dimensions, I create divs for each element and give each one height and width dimensions accordingly, and append each to my "bar-container" div currently in the dom.

function renderVisualizer() {
  var barContainer = document.getElementById("bar-container");

  //Empties bar-container div
  while (barContainer.hasChildNodes()) {
    barContainer.removeChild(barContainer.lastChild);
  }

  var heightMult = barContainer.offsetHeight / max_element;
  var temp = barContainer.offsetWidth / array.length;
  var barWidth = temp * 0.9;
  var margin = temp * 0.05;
  //Creating array element bars
  for (var i = 0; i < array.length; i++) {
    var arrayBar = document.createElement("div");
    arrayBar.className = "array-bar"
    if (barWidth > 30)
      arrayBar.textContent = array[i];
    //Style
    arrayBar.style.textAlign = "center";
    arrayBar.style.height = array[i] * heightMult + "px";
    arrayBar.style.width = barWidth;
    arrayBar.style.margin = margin;

    barContainer.appendChild(arrayBar);
  }
}

I wrote the following animated selection sort and it works well, but the only "animated" portion is in the outer for-loop, and I am not highlighting bars as I traverse through them.

function selectionSortAnimated() {
  var barContainer = document.getElementById("bar-container");
  var barArr = barContainer.childNodes;
  for (let i = 0; i < barArr.length - 1; i++) {
    let min_idx = i;
    let minNum = parseInt(barArr[i].textContent);
    for (let j = i + 1; j < barArr.length; j++) {
      let jNum = parseInt(barArr[j].textContent, 10);
      if (jNum < minNum) {
        min_idx = j;
        minNum = jNum;
      }
    }
    //setTimeout(() => {   
    barContainer.insertBefore(barArr[i], barArr[min_idx])
    barContainer.insertBefore(barArr[min_idx], barArr[i]);
    //}, i * 500);
  }
}

I am trying to use nested setTimeout calls to highlight each bar as I traverse through it, then swap the bars, but I'm running into an issue. I'm using idxContainer object to store my minimum index, but after each run of innerLoopHelper, it ends up being equal to i and thus there is no swap. I have been stuck here for a few hours and am utterly confused.

function selectionSortTest() {
  var barContainer = document.getElementById("bar-container");
  var barArr = barContainer.childNodes;
  outerLoopHelper(0, barArr, barContainer);
  console.log(array);
}

function outerLoopHelper(i, barArr, barContainer) {
  if (i < array.length - 1) {
    setTimeout(() => {
      var idxContainer = {
        idx: i
      };
      innerLoopHelper(i + 1, idxContainer, barArr);
      console.log(idxContainer);
      let minIdx = idxContainer.idx;
      let temp = array[minIdx];
      array[minIdx] = array[i];
      array[i] = temp;

      barContainer.insertBefore(barArr[i], barArr[minIdx])
      barContainer.insertBefore(barArr[minIdx], barArr[i]);
      //console.log("Swapping indices: " + i + " and " + minIdx);
      outerLoopHelper(++i, barArr, barContainer);
    }, 100);
  }
}

function innerLoopHelper(j, idxContainer, barArr) {
  if (j < array.length) {
    setTimeout(() => {
      if (j - 1 >= 0)
        barArr[j - 1].style.backgroundColor = "gray";
      barArr[j].style.backgroundColor = "red";
      if (array[j] < array[idxContainer.idx])
        idxContainer.idx = j;
      innerLoopHelper(++j, idxContainer, barArr);
    }, 100);
  }
}

I know this is a long post, but I just wanted to be as specific as possible. Thank you so much for reading, and any guidance will be appreciated!


Solution

  • Convert your sorting function to a generator function*, this way, you can yield it the time you update your rendering:

    const sorter = selectionSortAnimated();
    const array = Array.from( { length: 100 }, ()=> Math.round(Math.random()*50));
    const max_element = 50;
    renderVisualizer();
    anim();
    
    // The animation loop
    // simply calls itself until our generator function is done
    function anim() {
      if( !sorter.next().done ) {
        // schedules callback to before the next screen refresh
        // usually 60FPS, it may vary from one monitor to an other
        requestAnimationFrame( anim );
        // you could also very well use setTimeout( anim, t );
      }
    }
    // Converted to a generator function
    function* selectionSortAnimated() {
      const barContainer = document.getElementById("bar-container");
      const barArr = barContainer.children;
      for (let i = 0; i < barArr.length - 1; i++) {
        let min_idx = i;
        let minNum = parseInt(barArr[i].textContent);
        for (let j = i + 1; j < barArr.length; j++) {
          let jNum = parseInt(barArr[j].textContent, 10);
          if (jNum < minNum) {
            barArr[min_idx].classList.remove( 'selected' );
            min_idx = j;
            minNum = jNum;
            barArr[min_idx].classList.add( 'selected' );
          }
          // highlight
          barArr[j].classList.add( 'checking' );
          yield; // tell the outer world we are paused
          // once we start again
          barArr[j].classList.remove( 'checking' );
        }
        barArr[min_idx].classList.remove( 'selected' );
        barContainer.insertBefore(barArr[i], barArr[min_idx])
        barContainer.insertBefore(barArr[min_idx], barArr[i]);
        // pause here too?
        yield;
      }
    }
    // same as OP
    function renderVisualizer() {
      const barContainer = document.getElementById("bar-container");
    
      //Empties bar-container div
      while (barContainer.hasChildNodes()) {
        barContainer.removeChild(barContainer.lastChild);
      }
    
      var heightMult = barContainer.offsetHeight / max_element;
      var temp = barContainer.offsetWidth / array.length;
      var barWidth = temp * 0.9;
      var margin = temp * 0.05;
      //Creating array element bars
      for (var i = 0; i < array.length; i++) {
        var arrayBar = document.createElement("div");
        arrayBar.className = "array-bar"
        if (barWidth > 30)
          arrayBar.textContent = array[i];
        //Style
        arrayBar.style.textAlign = "center";
        arrayBar.style.height = array[i] * heightMult + "px";
        arrayBar.style.width = barWidth;
        arrayBar.style.margin = margin;
    
        barContainer.appendChild(arrayBar);
      }
    }
    #bar-container {
      height: 250px;
      white-space: nowrap;
      width: 3500px;
    }
    .array-bar {
      border: 1px solid;
      width: 30px;
      display: inline-block;
      background-color: #00000022;
    }
    .checking {
      background-color: green;
    }
    .selected, .checking.selected {
      background-color: red;
    }
    <div id="bar-container"></div>