用canvas可视化冒泡排序算法

666 阅读2分钟

效果演示

bubble.gif

制作思路

用一个Item类来代表每一个数组成员,调用window.requestAnimationFrame 来绘制每一帧

制作Item类

对每一个数组成员我们需要知道他的数值、宽高、颜色以及canvas上的位置,并且还要设置好每帧移动的像素数,用vx、vy来代表速度向量。 drawItem函数是其绘制函数。

// Item.js
class Item {
   constructor(value, width, heigth){
       this.value = value;
       this.width = width;
       this.heigth = heigth;
       // 左下角点
       this.x = 0;
       this.y = 0;
       this.vx = 5;
       this.vy = 0;
       this.color = 'gray';
   }
   drawItem (ctx) {
       ctx.save();
       //坐标轴移动到左下角
       ctx.translate(this.x, this.y);
       // 绘制矩形
       ctx.fillStyle = this.color;
       ctx.beginPath();        
       ctx.moveTo(0, 0);
       ctx.lineTo(this.width, 0);
       ctx.lineTo(this.width, -this.heigth);
       ctx.lineTo(0,-this.heigth);
       ctx.lineTo(0, 0);
       ctx.closePath();
       ctx.stroke();
       ctx.fill();
       
       //绘制数字
       ctx.fillStyle = "black";
       ctx.font = `${this.width-20}px Arial`;
       ctx.fillText(this.value, 0, 0);
       
       ctx.restore();

   }
}

由于耗时主要耗费在两物体移动上(另一个是比较大小上色过程),若需要设置倍速演示,只需要简单地在之后程序中将vx值乘以对应倍率。

处理数组

let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
//随机生成一个arr数组
let arr = new Array(Math.ceil(Math.random()*15+1)).fill(0);
arr = arr.map(() => Math.ceil(Math.random()*29+1));
//arrHeigth数组依据arr成员值存放对应高度(设置最高高度为400px)

//utils.js里设置一个返回高度的函数arrHeigth
let arrHeigth = utils.arrHeigth(arr, 400);
let arrWidth = 50;
//Arr存放对应的Item成员
let Arr = [];
let numArr = arr.length;
// utils.js
let utils = {};
// 通过数值按比例确定数组成员高度
utils.arrHeigth = function (_array, maxheigth) {
    let max = Math.max(..._array);
    let pro = maxheigth / max;
    let _arrHeigth = _array.map((item, index) => {
        return item * pro;
    });
    return _arrHeigth;  
}

Arr存放Item,之后只需循环Arr来绘制每一个成员

for (let item, i = 0; i < numArr; i++) {
     item = new Item(arr[i], arrWidth, arrHeigth[i]);
     item.x = Math.ceil((canvas.width - 60*numArr) / 2) + 60*i;
     item.y = 400;
     Arr.push(item);
}

比较(上色)

由于比较大小(同时变绿色、黄色)在一帧里就可以完成,但移动需要很多帧,如果不处理的话会导致瞬间就完成上色与褪色,不符合观感,如下图。

20211020_180204 00_00_01-00_00_04.gif 所以定义一个变量来控制保持状态的时间,这里定义了frame。 在比较大小的bubbleCompare函数中有一个数组compareArr, 其第一个值代表是否完成比较(如果你想让比较时间也就是绿色状态保持30帧的时间,就用 frame%30),如果没到时间就不进行之后的操作,继续下一帧 ; 其第二个值代表是否需要交换位置,为后面是否需要绘制交换过程提供判断。

let frame = 0;
let compareArr = [0, 0];

function bubbleCompare(array, j){
    frame++;
    compareArr = [0, 0];
    array[j].color = 'green';
    array[j+1].color = 'yellow';
    if (frame%30 == 0){
    frame = 0;
    compareArr[0] = 1;
    }
    if (array[j].value > array[j+1].value){
    compareArr[1] = 1;
    }
}

绘制移动

移动需要很多帧的时间,为判断是否交换完成,让drawBubble函数返回一个布尔值,供后续判断是否执行操作(及将Arr数组中两个Item交换位置,以及调节对应参数)

function drawBubble(array, j){
    array[j].x += array[j].vx;
    array[j+1].x -= array[j+1].vx;
    if (array[j].x == (Math.ceil((canvas.width - 60*numArr) / 2) + 60*(j+1)))
    {
    return true;
    } else {
    return false;
    }
}

现在可以开始绘制每一帧了

整体结构:

(function drawFrame() {
        let raf = window.requestAnimationFrame(drawFrame, canvas);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        // ......
        // 进行比较
        // 移动物体(改变Item.x)
        // 参数调整
        // 判断是否完成
        // ...
        // 遍历Arr 绘制每一个Item :
        Arr.forEach((item) => {
        item.drawItem(ctx);});
    }());

完整代码:

    let _i = 0,
        _j = 0,
        //compare_bool判断是否需要执行bubbleCompare比较函数
        compare_bool = 1;
    
    (function drawFrame() {
        let raf = window.requestAnimationFrame(drawFrame, canvas);
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        if (compare_bool){
            // 开始比较
            bubbleCompare(Arr, _j);
        }
        
        if (compareArr[0] == 1){
            
            compare_bool = 0;
            
            if(compareArr[1] == 1){
                // 开始移动(设置Item.x)
                if(drawBubble(Arr, _j)){
                    // 到达位置,移动完成
                    let t = Arr[_j];
                    Arr[_j] = Arr[_j+1];
                    Arr[_j+1] = t;
                    Arr[_j].color = 'gray';
                    Arr[_j+1].color = 'gray';
                    _j++;
                    compare_bool = 1;
                }
            }
            if (compareArr[1] == 0){
                Arr[_j].color = 'gray';
                Arr[_j+1].color = 'gray';
                _j++;
                compare_bool = 1;
            }
        }
        // 调整参数
        if (_j == numArr-_i-1) {
            Arr[_j].color = 'red';
            _j = 0;
            _i++;
            // 判断是否完成所有排序
            if (_i == numArr-1) {
                Arr[0].color = 'red';
                Arr[0].drawItem(ctx);
                window.cancelAnimationFrame(raf);
        }
    }
        // 绘制
        Arr.forEach((item) => {
        item.drawItem(ctx);});
    }());