In the blink of an eye, and soon to the New Year’s Tanabata Festival, just met today’s company made a small Tanabata activities, with JS to write a bubble sorting algorithm, the way to write the animation sorting process.

In fact, there are many examples of this algorithm animation effect on the web, but today I am interested in it, so I took time to realize it myself.

The optimization of the code is not discussed here, it is entirely to achieve their own small satisfaction, interested children can do their own code optimization on the computer.

Here is the effect (part) :


Dynamic rendering moves elements

Without further ado, get straight to the code (big guy can do it better than I can) :

// Render the container<div id="container"></div>
Copy the code
// The array to sort
let arr = [27.37.2.50.10.5.41.12.41.6.4.24.47.31.12];
// The left margin unit of each DOM element
let posLeft = 57;

// Get the render container
const container = document.getElementById('container');

/ * * *@description Dynamic rendering moves elements *@param { Object } Elem render container *@param { Array } Arr data list *@return { Void }* /
const renderHTML = (elem, arr) = > {
    let html = ' ',
        className = ' ',
        totalWidth = 0;
    arr.forEach((item, index) = > {
        if (item * 3 < 18) className = 'out';
        else className = ' ';
        totalWidth = index;
        html += `<li class="item" data-index="${ index }" style="height: ${ item * 3 }px; left: ${ posLeft * index }px;" > <span class="${ className }">${ item }</span>
                </li>`;
    });
    elem.innerHTML = `<ul class="list" style="width: ${ posLeft * totalWidth + 48 }px;" >${ html }</ul>`;
}

renderHTML(container, arr);
Copy the code
html.body {
    margin: 0;
    padding: 0;
}
#container.h1 {
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}
h1 {
    height: auto;
    margin: 6% 0;
}
.list {
    position: relative;
    display: flex;
    align-items: flex-end;
    list-style: none;
    padding: 0;
    margin: 0;
    margin-top: 10%;
}
.item {
    width: 45px;
    margin-right: 12px;
    display: inline-flex;
    justify-content: center;
    align-items: center;
    background-color: rgb(173.216.230);
    font-size: 20px;
    color: # 333;
    pointer-events: none;
    transition: all .3s ease;
    position: absolute;
}
.item:last-child {
    margin-right: 0;
}
.item.left..item.right {
    background-color: rgb(0.128.0);
}
.item.left::before..item.right::before {
    content: "";
    position: absolute;
    left: -4px;
    top: -4px;
    width: calc(100% + 4px);
    height: calc(100% + 4px);
    border: 2px solid red;
}
.item.left::after..item.right::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: -20px;
    width: 0;
    height: 0;
    border: 10px solid transparent;
    border-bottom-color: red;
    transform: translateX(-50%);
}
.item.success {
    background-color: rgb(255.165.0);
}
.out {
    position: absolute;
    bottom: calc(100% + 2px);
    left: 50%;
    transform: translateX(-50%);
    color: # 333;
}
Copy the code

In the above code, className = ‘out’ is used to determine if the number is outside the element, and if it is, it is displayed outside the element, not inside it. Prevents the appearance of a cross between fonts and elements.

${item * 3}, multiplied by 3 only to make the element appear larger; otherwise, the height of the element would be almost invisible if the number to be sorted included <10.

${posLeft * index}, as the element renders, the left value will change as the index value increases.

  • 57 times 0 is 0
  • 57 times 1 is 57
  • 57 times 2 is 114
  • .

${posLeft * totalWidth + 48} set the width of the parent element according to the number of child elements.

Of course, not setting the width of the parent element does not affect execution, but to the extent that the list is not easily centered horizontally or vertically in the window.

Now that the list has been rendered, but no sorting algorithm has been performed, let’s add the sorting algorithm.





Bubble sort algorithm

const bubbleSort = arr= > {
    const len = arr.length;
    for (let i = 0; i < len; i ++) {
        for (let j = i + 1; j < len; j ++) {
            if(arr[i] > arr[j]) { [arr[i], arr[j]] = [arr[j], arr[i]]; }}}return arr;
}

// Array: [27, 37, 2, 50, 10, 5, 41, 12, 41, 6, 4, 24, 47, 31, 12]
bubbleSort(arr);
Copy the code

[arr[I], arr[j]] = [arr[j], ARr [I]] array deconstruction way, swap the position of two elements.

Perhaps this expression is difficult to understand, of course, the master drift over ha. In fact, I did not understand at the beginning, but by exploring and combining with the form of animation, I can understand the process of the algorithm more clearly.

The result is that they are lined up, but they can’t get them to move, want to move, and continue to do it with me step by step.





Let the elements move

To make an element move, we need two identifiers, one left and one right.

Left is called:.item.left Left boundary element. Right is called the:.item.right right boundary element. I use these two elements here just to make it easier to distinguish between them when performing the animation.

Represents: the left boundary element needs to be compared with the right boundary element every time, if there is less than the left element, then the switch, otherwise it does not move.

Like this:

It’s always moving the right edge, not the left edge, and the left edge here is just acting as a base point for comparison with other elements.

Give it a makeover:

const list = document.getElementsByClassName('list') [0];
// The extension string is needed to extend the list of elements into a real DOM array, otherwise the following filter syntax cannot be used
const items = [...document.getElementsByClassName('item')];

const setPos = (left, right) = > {
    // Get the left and right boundary elements
    let eleLeft = items.filter(item= > item.getAttribute('data-index') == left);
    let eleRight = items.filter(item= > item.getAttribute('data-index') == right);

    let leftInfo = {
        pos: eleLeft[0].offsetLeft,
        index: eleLeft[0].getAttribute('data-index')};let rightInfo = {
        pos: eleRight[0].offsetLeft,
        index: eleRight[0].getAttribute('data-index')};// Set the distance and highlighting of the left and right boundary elements
    // The class name must be swapped because it is to be swapped
    eleLeft[0].style.left = rightInfo.pos + 'px';
    eleLeft[0].className = 'item right';
    eleRight[0].style.left = leftInfo.pos + 'px';
    eleRight[0].className = 'item left';
}

// All elements smaller than the left index are highlighted to represent the sorted elements
const setSuccess = (arr, index) = > {
    for (let i = 0, len = arr.length; i < len; i ++) {
        if (i < index) arr[i].className = 'item success'; }}// Clear all highlighting if type is not passed, or if className contains right
const clearClass = type= > {
    for (let i = 0, len = items.length; i < len; i ++) {
        if(! type || items[i].className.includes(type)) { items[i].className ='item';
            break; }}}const bubbleSort = arr= > {
    const len = arr.length;
    for (let i = 0; i < len; i ++) {
        // Get the list element again
        const items = document.getElementsByClassName('item');
        // Clear styles
        clearClass();
        // Sets the highlighted elements to be sorted
        setSuccess(items, i);
        items[i].className = 'item left';
        if(! items[i +1]) {
            // If there are no more elements, stop sorting, finish, and highlight the last element
            setSuccess(items, i + 1);
            break;
        }
        // Compare all the elements in the right boundary with the left boundary element
        for (let j = i + 1; j < len; j ++) {
            // Empty only the highlighting containing the right boundary element
            clearClass('right');
            items[j].className = 'item right';
            // If the left margin is larger than the right margin
            if (arr[i] > arr[j]) {
                // Transpose the two elements
                setPos(i, j);
                // Swap the two values in the array[arr[i], arr[j]] = [arr[j], arr[i]]; }}}}Copy the code

Refresh the page at this point, ok, I am blindsided… All messed up.

Because I added 300 milliseconds to the element animation time in the CSS above: Transition all.3s ease.

But the loop is too fast to animate, and the element does the next render in the middle of the motion, resulting in this mindless situation…

To solve this problem, we need a way to slow the loop down, and we can do this, as slow as we want.

const sleep = time= > {
    return new Promise(resolve= > setTimeout(resolve, time));
}

const bubbleSort = async arr => {
    const len = arr.length;
    for (let i = 0; i < len; i ++) {
        // Get the list element again
        const items = document.getElementsByClassName('item');
        // Clear styles
        clearClass();
        // Sets the highlighted elements to be sorted
        setSuccess(items, i);
        items[i].className = 'item left';
        // Wait 600 milliseconds before performing subsequent operations
        await sleep(600);
        if(! items[i +1]) {
            // If there are no more elements, stop sorting, finish, and highlight the last element
            setSuccess(items, i + 1);
            break;
        }
        // Compare all the elements in the right boundary with the left boundary element
        for (let j = i + 1; j < len; j ++) {
            // Empty only the highlighting containing the right boundary element
            clearClass('right');
            items[j].className = 'item right';
            // Every 600 milliseconds
            await sleep(600);
            // If the left margin is larger than the right margin
            if (arr[i] > arr[j]) {
                // Transpose the two elements
                setPos(i, j);
                // Swap the two values in the array
                [arr[i], arr[j]] = [arr[j], arr[i]];
                // Make sure that the move animation is completed before the next round of comparison
                await sleep(800); }}}}Copy the code

We can use async and promise syntax to simulate an asynchronous operation. In the above example, the loop must wait until the sleep method returns a value before it can proceed with the subsequent loop. The time parameter passed in is milliseconds to generate a timer set within the time range until it waits for the return.

What’s going on here, although it’s doing animation in a methodical way, it’s obviously not right. It’s all messed up…

const setPos = (left, right) = > {
    // ...
    // A delay is needed to set the index value and the actual position of the transposed element after the animation effect is completed
    setTimeout(() = > {
        // Because the position of the element has changed, we need to retrieve the element list again
        const items = document.getElementsByClassName('item');
        // Set the index of the left boundary element to the index of the right boundary element
        eleLeft[0].setAttribute('data-index', rightInfo.index);
        // Inserts the left bounding element before the next sibling of the right bounding element
        list.insertBefore(eleLeft[0], items[right].nextElementSibling);
        // Set the index of the right boundary element to the index of the left boundary element
        eleRight[0].setAttribute('data-index', leftInfo.index);
        // Insert the right boundary element in front of the left
        list.insertBefore(eleRight[0], items[left]);
    }, 400);
}
Copy the code

That’s because after the motion element is animated, it’s just updated in view. It does change visually, but it changes the element’s left value, not the actual DOM list position.

So we need to retrieve the DOM list every time we swap the position of the element, because the position of the element has changed and the list needs to be updated.

Then set the index value of the swapped element to ensure that the new index and the swapped index are one-to-one.

List. InsertBefore (eleLeft[0], items[right].nextelementSibling); Insert the left boundary element in front of the sibling element below the right boundary.

if(! items[i +1]) {
    // If there are no more elements, stop sorting, finish, and highlight the last element
    setSuccess(items, i + 1);
    break;
}
Copy the code

So in the loop, I made a decision here to see if there are any other elements that follow.

If not, the swap element function will not be executed, otherwise the items[right]. NextElementSibling will report an error when the loop reaches the last element and there are no more elements to follow.

list.insertBefore(eleRight[0], items[left]); , inserts the right boundary element in front of the left.

If look again like this, there is no problem!

In fact, there is no perfect demo, if the pursuit of perfection, need to optimize a lot of places, such as: code reuse, code is not concise, whether the naming standard, compatibility is feasible and so on.

Interested partners can try to do their own optimization, I believe you must be better than me.





The last

Thank you for taking the time to read this article. I hope it will be helpful.

If you have any questions or suggestions, welcome to exchange more, we make progress together.

In the process of reading, if there is any incorrect place, I hope you can mention, I will try to correct and provide better quality articles.