Vue + Canvas項目總結

演示地址

演示地址
PC端的項目啦,須要在電腦上看哦,並且最好用Chrome打開html

引言

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

項目概述

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

代碼是基於vue-cli碼的,因此路由、vuex這些都不用講啦,咱們把重點放在canvas上面吧。vue

知識點總結

拖拽

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

  1. 被拖拽元素設置draggable="true"
  2. 被拖拽元素還有三個相應的事件dragstart drag dragend,分別對應拖拽開始、拖拽中和拖拽結束,若是你但願在這些過程加上特效,能夠試試,但更多的仍是用做響應數據,好比讓畫布知道具體是哪一個元素被拖拽進來了;
  3. 被放置元素設置dragover drop兩個事件,分別表示被拖拽元素在該元素範圍內移動、被拖拽元素着陸,這裏注意dragover事件函數內需設置event.preventDefault()防止彈出新頁面,而後咱們就能夠愉快地在drop事件函數裏畫圖形到畫布上啦。

HEX => RGBA

因爲設計圖上顏色都沒有透明度,因此咱們須要手動加一個0.3的alpha,否則畫布上圖形相互層疊,會覆蓋掉層級低的圖形和背景圖。git

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轉換成RGBgithub

canvas基本用法

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

save與restore

save能夠保存當前canvas的狀態,包括strokeStylefillStyle、變換矩陣、剪切區域等,restore能夠恢復到canvas狀態棧中的上一個狀態,因此咱們在這兩個函數中間作的canvas狀態改變至關於被隔離起來了,不會污染外部的canvas操做vue-cli

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

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

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)便可,這樣是否是性能又花了不少呢

代碼地址

代碼地址 雖然代碼質量差,我本身都不忍直視,但仍是放出來吧,萬一哪裏看不懂了還能夠翻翻源碼嘛

相關文章
相關標籤/搜索