前端開發——讓算法"動"起來

正文

固然在咱們不清楚具體操做細節前咱們能夠先假設一下,咱們可以用什麼來實現。按照之前看過的排序動畫我將其分爲javascript

1.Js操做Dom,再搭配簡單的css
2.Canvas動畫

以後在查資料的時候發現還有人用d3這個庫來完成。css

做爲一個有(被)夢(坑)想(多)的前端,一開始就得考慮到如何實現套入多個算法,若是實現單步運行(可能的話還得有往回走的功能),如何實現動畫速度的控制等等。html

固然幻想那麼多一會兒實現是不現實的,咱們得先找一個簡單的例子來看看再一步步深刻。前端

先來看下效果圖java

imgn

以後咱們分析源碼:node

css:

#field {width:500px;height:510px;background:black;position:relative}
.bar {position:absolute;bottom:0;background:orange;border:1px solid brown;width:24px}

html:

<div id="field">
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
  <div class="bar"></div>
</div>

javascript:

!function(d){

  var bars = [].slice.call(d.querySelectorAll('.bar'));
  var arr = [8, 10, 3, 5, 6, 9, 2, 4, 7, 1];
  var state = [];

  var draw = function(){
    var bar, s;
    s = state.shift() || [];
    for(bar in bars){
      bars[bar].style.height = 25 * s[bar] + 'px';
      bars[bar].style.left = 25 * bar  + 'px';
    }
  }

  var sort = function(arr){
    for(var i = 0; i < arr.length; i++){
      for(var j = 0; j < arr.length - i - 1; j++){
        if(arr[j] > arr[j+1]){
          arr[j]   = arr[j] + arr[j+1];
          arr[j+1] = arr[j] - arr[j+1];
          arr[j]   = arr[j] - arr[j+1];
                      state.push(JSON.parse(JSON.stringify(arr)));
                      
                      
        }
      }
    }
  }
  
  sort(arr);
  setInterval(draw, 500);

}(document)

整個流程其實很清晰,可是其中部分代碼讓我疑惑了一段時間,通過google和問羣裏的朋友,終於解惑。咱們先來理清這段代碼的思路git

首先這個動畫是將大小寬度都定死了。以及排序數組的數量須要和html結構裏bar的數量一致。

1.html的bar是長方體,它的寬是24px而後有個1px的border,所以在代碼中動態改變left的時候須要設定爲25.

2.js代碼中用一個匿名當即函數包裹代碼。
  bars = [].slice.call(d.querySelectorAll('.bar'));
這段將獲取的nodelist轉爲成一個對象數組,這樣方便對其中每一個bar進行單獨修改樣式

3.設定一個state空數組來保存每個狀態,記住這纔是動畫的關鍵。

4.state.shift()臨時像將數組模擬成隊列,draw函數根據其第一個出列的內容來從新排列列表,在

setInterval(draw, 400)的配合下,就能造成一個動畫排序。

5 sort函數和咱們以前介紹的冒泡排序是同樣的,只不過這裏有一句

state.push(JSON.parse(JSON.stringify(arr)));

這句是核心,一看是乍看是否是很奇怪,爲何要JSON.stringify而後再JSON.parse。這裏須要你們認真思考一下。

想一想在哪裏看過它?
深拷貝?
對,就是深拷貝。對於深拷貝不理解的我這裏給出它的含義:

深拷貝是複製變量值,對於非基本類型的變量,則遞歸至基本類型變量後,再複製。

我這兩天也整理過深拷貝,可是我仍是一會兒沒理解爲何這裏要這麼寫。

一開始我想誤差了,我一開始認爲arr做爲一個數字數組,對它進行深拷貝和用一箇中間變量進行操做不是同樣麼,因而我加了這麼幾行代碼

var temp = arr

state.push(temp);

而後 動畫消失了,頁面變成最後排好序的樣子。

這時候羣裏有人提醒了我一句淺複製會修改原數組。我這才根據state.push反應過來。

在sort內部,每個push都是爲了保存交換後排序數組的狀態,若是我用temp來代替它,那麼state裏面將全放着相同的最後排完序的狀態。而JSON.parse(JSON.stringify(arr))對arr進行深複製,不會改動arr原數組,所以它就相似快照同樣把每次排序的狀態給push到state.而後配合setInterval一張一張的放映造成動畫。

一個簡單的排序動畫其實裏面也包含了很多有價值的內容。github

回過頭這麼一看,這不是很容易套公(算)式(法)麼web

把咱們以前學的插入排序拿來改改:算法

function exchange(array, i, j) {
      var t = array[i];
      array[i] = array[j];
      array[j] = t;
    }




  var sort2 = function(numbers){
     for (var i = 0; i < numbers.length; i++) {
        /*
         * 當已排序部分的當前元素大於value,
         * 就將當前元素向後移一位,再將前一位與value比較
         */
     for (var j = i; j > 0 && numbers[j] < numbers[j - 1]; j--) {
          // If the array is already sorted, we never enter this inner loop!
          exchange(numbers, j, j - 1);
          state.push(JSON.parse(JSON.stringify(numbers)));
          console.log("此時數組:" + numbers)
        }
     }

   }

分分鐘改變更畫效果。

imgn

這樣咱們就完成目標中的一小步了。

而後以前考慮的單步運行和動畫速度控制咱們均可以改變相應參數來完成。

不過在繼續深刻以前咱們先來看看如何用canvans來實現。

先來看效果

imgn

代碼以下:

html:

<div id="restart">從新生成數據並排序</div>
<canvas id="canvas"><canvas>

css:

body {
  background-color: black;
  text-align: center;
}
#restart {
  color: white;
  font-family: monospace;
}    

javascript:

!function(){

  var canvas = document.getElementById('canvas'); 
  var data = [];
  canvas.width = window.innerWidth-30;
  canvas.height = window.innerHeight-35;
  CreateData(IntRandom(300, 100));
  Render();


  function Restart() {
    data = [];
    CreateData(IntRandom(300, 100));
  }

  function CreateData(val) {
    for(var i = 0; i <= val; i++) 
      data[i] = IntRandom(500, 10);
   
  }

  function BubbleSort() {
    var temp;
    for(var i = 0; i <= data.length-1; i++) {
      if(data[i] > data[i+1]) {
        temp = data[i];
        data[i] = data[i+1];
        data[i+1] = temp;
      }
    }
  }

  function Draw() {
    var posX = 0,
        posY = canvas.height;

    for(var i = 0; i <= data.length-1; i++) {
      c.fillStyle = RandomColor(i);
      c.fillRect(posX, canvas.height, 5, -data[i]);
      
      posY--;
      posX+=6;
    }
  }

  document.onclick = function() {
    Clear();
    Restart();
  };

  function Render() {
      requestAnimationFrame(Render);
      Clear();
      BubbleSort();
      Draw();
  }

  function Clear() {
    c.fillStyle = "black";
    c.fillRect(0, 0, canvas.width, canvas.height);
  }

  function RandomColor(i) {
    var n = Math.random() * 360;
    return "hsl("+parseInt(i)+", 100%, 50%)"
  }

function IntRandom(max, min) {
  return Math.floor(Math.random() * max + min);
}    

}()

這份代碼實際上是有缺陷的,不過不要緊,在下面的分析中咱們能夠邊看邊改

1.首先是常規canvas操做,若是對canvas不上很熟悉的同窗建議把高程相關部分刷一遍。
具體操做就是,獲取整個瀏覽器屏幕長款,減去一部分做爲畫布的長寬,這是爲了讓生成的序列不會跟瀏覽器邊緣貼合。
CreateData()這個方法就是隨機生成一堆隨機高度的長方形。

BubbleSort()就是常見的冒泡排序,可是在這裏咱們看到這只是一個冒泡算法,並無像以前作一系列快照。(這裏就涉及到了咱們提到的缺陷)

draw()方法 從屏幕左側座標爲0的點開始,這個posY其實毫無用處,由於咱們的高度是根據以前隨機生成一堆隨機高度的長方形數組data來生成的。posX自加是爲了保持間隔。

RandomColor() 這個方法根據高度來改變顏色。

以後是本代碼的核心render()
以前咱們看到操做dom的版本,動畫效果是經過setInterval(draw, 500)來實現的,那麼這裏的動畫效果哪裏來?
咱們能夠看到這裏用到了HTML5的API:

requestAnimationFrame

它的優點在於保證跟瀏覽器的繪製走,若是瀏覽設備繪製間隔是16.7ms,那就這個間隔繪製;若是瀏覽設備繪製間隔是10ms, 就10ms繪製。不會存在過分繪製的問題,動畫不會掉幀。

這個從咱們的效果圖裏也能看出動畫的確很流暢。然而,這段代碼是有問題的。問題在於它沒有設置停止的標識,也就是它會不停刷新瀏覽器,時間久了將會卡住。並且細心的會發現以前咱們看到的冒泡排序它只有一層循環。render()方法靠着循環調用硬生生把這些無序數組給堆成有序了。。。

並且太流暢的動畫一鼓作氣,讓咱們沒法仔細觀察排序的通過,所以咱們須要對其進行修改。

這裏咱們停下來思考一下,該如何改?

想一想以前js操做dom版是怎麼作的?咱們一樣能夠套進去。
只須要把排序算法換成以前同樣的。而後把render改爲以下:

var fps = 1; //每秒幾幀

    var lastExecution = new Date().getTime();

    function Render() {
        if (state.length > 0) { //動畫播放,沒播完繼續

            var now = new Date().getTime();

            if ((now - lastExecution) > (1000 / fps)) {
                Clear();
                Draw();
                lastExecution = new Date().getTime();
            }

            requestAnimationFrame(Render);

        }
    }

不過須要注意的是當你想重置requestAnimationFrame的時候,須要一開始就註明var stopId = requestAnimationFrame(Render);

而後配合cancelAnimationFrame(stopId)便可暫停繼續。

最終效果以下:

imgn

有了以上基礎你徹底能夠本身開始構建一個屬於本身的排序動畫。這篇就到這裏。

結尾

全部代碼和別的補充已經放在github
並且會不斷更新。有興趣的能夠去看看並動手敲一遍。

資料

visualgo.net的排序動畫

David Galles教授Canvas+JS實現排序動畫

相關文章
相關標籤/搜索