Vue + Canvas專案總結

雪碧亦作酒發表於2018-07-23

演示地址

演示地址
PC端的專案啦,需要在電腦上看哦,而且最好用Chrome開啟

引言

這是今年三月份幫學長做的一個專案,陪我度過了兩個月的春招生活,整個專案做下來也是學到了很多東西,下面就開始我的分享啦,包括一些知識點總結和遇到的坑,dalao莫笑哈。

專案概述

功能展示
主要功能如上圖,左邊是圖形工具欄,右邊是canvas,上面是清除、刪除、旋轉、切換格子背景、儲存並下載圖片的操作。

程式碼是基於vue-cli碼的,所以路由、vuex這些都不用講啦,我們把重點放在canvas上面吧。

知識點總結

拖拽

這裡的拖拽是指把左邊工具欄裡的圖形圖形拖拽到右邊畫布裡,三步完成:

  1. 被拖拽元素設定draggable="true"
  2. 被拖拽元素還有三個相應的事件dragstart drag dragend,分別對應拖拽開始、拖拽中和拖拽結束,如果你希望在這些過程加上特效,可以試試,但更多的還是用作響應資料,比如讓畫布知道具體是哪個元素被拖拽進來了;
  3. 被放置元素設定dragover drop兩個事件,分別表示被拖拽元素在該元素範圍內移動、被拖拽元素著陸,這裡注意dragover事件函式內需設定event.preventDefault()防止彈出新頁面,然後我們就可以愉快地在drop事件函式裡畫圖形到畫布上啦。

HEX => RGBA

由於設計圖上顏色都沒有透明度,所以我們需要手動加一個0.3的alpha,不然畫布上圖形相互層疊,會覆蓋掉層級低的圖形和背景圖。

function hex2rgba(hex) {
      // hex格式如#ffffff
      let colorArr = [];
      for(let i = 1; i<7; i += 2){
        colorArr.push(parseInt("0x" + hex.slice(i,i+2))); // 16進位制值轉10進位制
      }
      return `rgba(${colorArr.join(",")},0.3)`;
}
複製程式碼

另外如果有興趣瞭解RGBA轉RGB的小夥伴,可以看看這篇部落格RGBA轉換成RGB

canvas基本用法

下面就是關於canvas的內容了,如果對它的基礎用法還不太瞭解的小夥伴,可以看看JavaScript之Canvas畫布

save與restore

save可以儲存當前canvas的狀態,包括strokeStylefillStyle、變換矩陣、剪下區域等,restore可以恢復到canvas狀態棧中的上一個狀態,所以我們在這兩個函式中間做的canvas狀態改變相當於被隔離起來了,不會汙染外部的canvas操作

這樣看來,我們最好在每次畫圖前呼叫save,畫完後呼叫restore,從而保證每次繪製都有一個純粹的狀態。

這裡有一篇講得特別好的文章,如果嫌本直男沒講清楚的話,一定要看哦。Canvas學習:save()和restore()

drawImage

可能有些小夥伴會小看這個API,認為它只能繪製圖片,實際上它還能svg、canvas繪製到畫布上,我們先來看看如何繪製svg咯。

我們功能介面左側工具欄裡的圖示其實都是svg,我一開始是想把他們截圖下來切成一個個背景透明的png,然後畫到canvas上,後來發現放大看的話會比較模糊,畢竟是畫素圖嘛,所以新的需求來了。

我自己的程式碼不好貼出來,那就看看dalao的吧,將 DOM 物件繪製到 canvas 中,他這裡是將DOM塞到svg裡再往canvas上畫的,如果你只需要畫現成的svg,則可以不用foreignObject包裹。

另外,如果你的svg有.svg格式圖片,可以直接呼叫drawImage去繪製。

橢圓與貝賽爾曲線

canvas已經有畫橢圓的API了,但相容性還不夠好,在其他所有模擬繪製橢圓的方式裡,貝塞爾曲線可以說是最優雅的一種了,好吧,掃盲文 => 貝塞爾曲線原理(簡單闡述)

三維貝塞爾曲線需要一個起始點、兩個中間點、一個終止點確定,當然起始點一般預設當前點,所以bezierCurveTo的引數就是按順序的後三個點座標了;當這四個點恰好圍成一個矩形時,就有點橢圓的模樣啦。

 let a = this.width / 2;
 let b = this.height / 2;
 let ox = 0.5 * a,
     oy = 0.6 * b;
 this.ctx.beginPath();
 // 從橢圓縱軸下端開始逆時針方向繪製
 this.ctx.moveTo(0, b);
 // 把橢圓劃成四份分開來畫
 this.ctx.bezierCurveTo(ox, b, a, oy, a, 0);
 this.ctx.bezierCurveTo(a, -oy, ox, -b, 0, -b);
 this.ctx.bezierCurveTo(-ox, -b, -a, -oy, -a, 0);
 this.ctx.bezierCurveTo(-a, oy, -ox, b, 0, b);
 this.ctx.closePath();
 this.ctx.fill();
複製程式碼

這裡有一篇整理得比較完整的橢圓繪製方法的文章 可以參考 HTML5 Canvas中繪製橢圓的5種方法

線條

帶箭頭的實線

實線好畫,但是箭頭怎麼來做呢?Emmm,其實就是計算線段與畫布x軸的夾角,然後線上段終點畫偏移對應角度的三角形嘛

drawArrow(x1, y1, x2, y2) {
    // (x1, y1)是線段起點  (x2, y2)是線段終點
    // 反正切函式計算夾角
    let endRadians = Math.atan((y2 - y1) / (x2 - x1));
    // 三角形的底邊與線段垂直,所以還要再轉 π / 2
    endRadians += ((x2 >= x1) ? 90 : -90) * Math.PI / 180;
    this.ctx.save();
    this.ctx.beginPath();
    // 座標原點 => (x2, y2)
    this.ctx.translate(x2, y2);
    this.ctx.rotate(endRadians);
    this.ctx.moveTo(0, 0);
    this.ctx.lineTo(5, 15);
    this.ctx.lineTo(-5, 15);
    this.ctx.closePath();
    this.ctx.fill();
    this.ctx.restore();
}
複製程式碼

虛線

  • 比較傳統的一種做法是修改CanvasRenderingContext2D的原型,手動增加一個dashedLine的方法,原理大概是從起始點先畫一段實線,然後跳過一段,moveTo到下一個點繼續畫實線,這樣迴圈到終止點,就能得到虛線。具體實現見html5 實現畫虛線
  • 其實canvas已經支援畫虛線了,畫線前用setLineDash即可指定虛線的樣式,詳見Canvas學習:繪製虛線和圓點線
    但是這個方法用起來有些問題,角度不好或者間隔太小的時候,畫出來的虛線看起來就像是實線。

波浪線

一般常見的波浪線都是用正弦曲線來模擬的吧,y = A * sin(ω * x + φ),指定它的A和ω就可以確定波浪線的振幅和頻率(或者說每個波浪的高度和寬度)

let len = Math.sqrt(width * width + height * height);
this.ctx.save();
this.ctx.moveTo(this.start.x,this.start.y); // 起點
this.ctx.translate(this.start.x,this.start.y);
this.ctx.beginPath();
let x = 0;
let y = 0;
let amplitude = 5; // 振幅
let frequency = 5; // 頻率
while (x < len) {
    y = amplitude * Math.sin(x / frequency);
    this.ctx.lineTo(x, y);
    x = x + 1;
}
this.ctx.stroke();
this.ctx.restore();
複製程式碼

參考文章:Draw a Sine Wave in JavaScript

圖形棧

儲存

簡單來說,我們畫布上的圖形都是一個類的例項,儲存在一個陣列中,每次有更新時都會清除畫布,再全部重新繪製一遍(後面會將優化)。這個圖形例項需要儲存的屬性一般有起始和終點座標、顏色、偏移角度等,根據自己的需求設定,還至少需要一個方法去動態計算該圖形的有效範圍,以便滑鼠事件找到它。

刪除

選中某圖形例項後,從圖形棧陣列中刪除即可。

旋轉

由於我們每次畫圖形的時候,都會把座標原點暫時移到圖形的中心,所以只需要rotate一個角度再畫就可以實現旋轉啦

拖拽移動

Emmm,每個圖形不太一樣,有興趣的話看看專案原始碼唄

判斷一個點是否在某個四邊形內

  • 向量法
    詳見 判斷一個點是否在四邊形內部,但是這種方法有點侷限性,首先,圖形邊數必須事先確定,而且邊數多起來了程式碼會很長;其次,這種方法只是適用於凸多邊形,舉個凹多邊形的反例想想就能明白了。
  • 射線法
    詳見射線法理論,程式碼實現如下:
function inRange(x, y, points){
    // points表示多邊形的頂點集合
    let inside = false;
    for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
        let xi = points[i][0], yi = points[i][1];
        let xj = points[j][0], yj = points[j][1];
        let intersect = ((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
        if (intersect) inside = !inside;
    }
    return inside;
}
複製程式碼
  • 一個公式
    任意點(x,y),繞一個座標點(rx0,ry0)逆時針旋轉a角度後的新的座標設為(x0, y0),有公式:
    x0= (x - rx0)*cos(a) - (y - ry0)*sin(a) + rx0 ;
    y0= (x - rx0)*sin(a) + (y - ry0)*cos(a) + ry0 ;
    極座標的知識啦,不想推就直接套公式唄。

撤銷與回退

類似PS的功能嘛,我這個專案沒做,但是思路不難,用past、present、future三個陣列來儲存圖形棧,Emm好像講起來還是有點長,可以參考實現撤銷歷史的思路。

優先順序

圖形棧裡的例項被依次取出繪製,後畫上去的圖形會覆蓋掉之前的圖形,所以這裡涉及到一個優先順序,重要的東西放在後面畫

我們可以把儲存圖形的陣列再細分類,陣列的每個子元素都是一個Array,專門儲存某一種圖形,優先順序越高,對應的索引值越大,這樣我們就可以把重要的圖形全部放在後面畫了。

vuex中的狀態實現雙向繫結

一般我們用於雙向繫結的值都會放在vue例項的data中,因為它預設提供了gettersetter;但vuex的狀態一般都需要computed來讀取,但computed預設是沒有setter方法的,需要手動設定,程式碼如下:

computed:{
      text : {
        get(){
          return this.$store.state.text;
        },
        set(value){
          this.$store.commit('setText',value);
        }
      }
}
複製程式碼

遇到的坑

html2canvas的一個小bug

在實現儲存圖片功能的時候,我希望能擷取一段DOM的內容,而不僅僅是canvas的內容,所以找到了這個外掛html2canvas,它可以把dom轉換成canvas,然後我們就能canvas.toDataURL()把它轉換成圖片了。

轉換並儲存成圖片下載的程式碼如下:

downImg() {
        html2canvas( this.$refs.ground, {
          onrendered: function(canvas) {
            let url = canvas.toDataURL();
            let a = document.createElement('a');
            a.href = url;
            a.download = new Date() + ".png";
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
          }
        });
    }
複製程式碼

但是出現了一個bug,就是下載下來的圖片不清晰,左上角一大片空白。
於是我嘗試了網上的很多方法,都行不通,最後只能把專案從零開始慢慢加東西,最後發現是我畫虛線的時候改了CanvasRenderingContext2D的原型,我滴媽耶,做夢也沒想到會是這裡出問題,用外掛有風險啊。

上傳到gh-pages時的路徑錯誤

如果上傳到https://XXX.github.io/(GitHub的個人部落格)上,則跟上傳到伺服器上操作一致,但如果是傳到某個倉庫的gh-pages,那麼一堆問題都來了,解決步驟如下:

  1. .gitignore檔案裡的/dist刪掉,忽略了的話,還怎麼上傳打包檔案到master分支呢;
  2. /config/index.js裡build部分裡的assetsPublicPath由'/'改成'./',相當於說把伺服器根目錄改成了相對路徑,倉庫gh-pages的根目錄不是'/'而是'/倉庫名';
  3. 相對應的,如果使用了history模式,請改成hash模式,不然github可能會把前端路由識別成後端api;
  4. 還有一些static裡的圖片,使用了絕對路徑,可能上傳後顯示不出來;
  5. git subtree push --prefix dist origin gh-pages敲完命令,應該就可以看到上傳成功了。

優化

多層次畫布

上面提到,我們的畫布每次更新時,總是要全部清除,然後重新再畫一遍,對於那些背景圖片等不變的內容來說,是不是可以優化呢?Emmm,好尬的設問句。

我們用多個同樣大小層疊的canvas來完成,層級低的下層canvas用來畫背景圖片等靜態圖形,層級高的上層canvas用來畫動態變化的圖形,這樣就可以每次渲染都優化一點啦。

離屏渲染

當我們在畫布上拖拽圖形時,一般做法是隨著滑鼠移動mousemove,重新繪製所有圖形,但其實這個過程中,要繪製的可以分為兩部分,一個是被拖拽移動的圖形,另一個就是其他圖形;我們可以分別動態建立兩個canvas,把兩部分畫在兩個離屏畫布上,mousemove時只要呼叫兩次drawImage(離屏canvas)即可,這樣是不是效能又花了很多呢

程式碼地址

程式碼地址
雖然程式碼質量差,我自己都不忍直視,但還是放出來吧,萬一哪裡看不懂了還可以翻翻原始碼嘛

相關文章