面試官:canvas如何實現多張圖片編輯的圖片編輯器

前言

圖片編輯器雖然是圖片塗鴉工具,可是在當前疫情的大背景下,該工具還能夠成爲老師在線修改學生提交的家庭做業的工具使用的,它能夠極大地減輕了老師的批改做業的工做負擔,相信這軟件是有市場的。javascript

本次不具體介紹canvas的各類操做畫布的api,並且相信你們在網上能搜到大量的關於canvas塗鴉的開源代碼,即便是這樣,我也相信真正把canvas塗鴉用於公司實際的項目的很少,改裝成圖片編輯器的會更少,而用於多張圖片編輯而後多張統一保存、上傳的更是寥寥無幾。下面,我將在我曾經寫過的一個canvas塗鴉的基礎上,將多張圖片編輯器的開發和思考過程記錄下來。html

圖片編輯器產品需求

先說需求,因爲涉及到實際公司的項目開發,知足需求的圖片編輯器可能只是編輯一張單獨的圖片,多是給你一個圖片列表也就是多張圖片的編輯,在用戶保存以前,用戶能夠來回切換如今編輯哪張圖片,並且要記住每張照片的編輯操做,都要容許能夠撤銷,最後統一點擊保存按鈕,沒有編輯過的圖片將被丟棄,已經編輯過的帶着塗鴉的圖片上傳給服務器。java

準備工做,必須瞭解的相關知識點

html2canvas的使用

html2canvas這個插件實際是將網頁上的普通html元素轉化成canvas。那麼爲何要將html元素轉化成canvas呢?那是由於canva具備許多html元素不具有的特色,例如能夠在canvas上畫圖,畫線條等等操做,並且canvas直接提供api能夠將畫布上展現的內容導成圖片。是否是有點相似於截圖,html2canvas就是利用這一點。 我最開始使用html2canvas時在公司h5項目有一個手寫板的項目。須要在是手寫板中籤字,而後將簽字以後的圖片導出而後上傳。後來又有一個在web端的圖片編輯器的項目,又用到了html2canvas這個插件,下面來介紹下該插件web

介紹

html2canvas的缺點:npm

html2canvas轉成的canvas,在最後生成的視覺效果上並非100%還原原來的元素,也就是說會有部分像素點會丟失。canvas

html2canvas的安裝api

npm install html2canvas
複製代碼

html2canvas的引入跨域

import html2canvas from 'html2canvas'
複製代碼

html2canvas的使用數組

若是一個帶圖片(不管是img標籤仍是背景圖)的html元素在轉成canvas以後,該生成的canvas中img或者背景圖缺失,變成了白色的底,而且控制檯都告訴你圖片跨域了,解決方案以下:瀏覽器

如下第1條必須知足,而後第二、3是根據是html元素是img標籤仍是背景圖,來添加不一樣的配置項。

1.圖片服務器配置Access-Control-Allow-Origin 或使用代理

2.html2canvas的配置項中配置useCORS:true

3.img標籤增長 crossOrigin='anonymous'

html轉canvas,而且解決圖片跨域問題

html2canvas(dom, {
  useCORS: true, // 背景圖片跨域
  scale: window.devicePixelRatio,//像素比
  width: dom.offsetWidth
  height: dom.offsetHeight
}).then(canvas => {
  //獲得base64
  let base64 = canvas.toDataURL() 
})
複製代碼

由於html2canvas將普通的html轉成了canvas,而canvas自帶將自身轉成base64的方法,而base64格式文件能夠直接做爲img標籤的src,展現在頁面上

<img :src='base64' />
複製代碼

此時若是但願將生成的圖片上傳,base64是沒法直接上傳的,須要將base64轉成blob格式。因此:咱們將獲得的base64數據傳進base64ToBlob函數,最後函數return的結果就是blob數據。請注意:該方法只是單純的將base64轉成blob的通用方法

base64轉blob

function base64ToBlob(base64){
    var arr = base64.split(','), mime = arr[0].match(/:(.*?);/)[1],
        bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
      while (n--) {
        u8arr[n] = bstr.charCodeAt(n);
      }
      return (new Blob([u8arr], { type: mime }))
}
複製代碼

其實canvas自帶將canas轉化成blob對象的方法

canvas轉blob

canvas.toBlob(file=>{
    //此時file就是blob數據格式。
    //成功上傳成blob以後須要爲該blob數據添加name屬性,不然後面上傳到服務器會報錯:size not set
    file.name = 'cover.png'
})
複製代碼

可是在實際項目開發中遇到部分低版本的手機瀏覽器中的canvas元素不支持toBlob方法,報錯toBlob is not a function。因此此時咱們須要對當前不支持canvas.toBlo方法的瀏覽器作polyfill

DOM canvas元素暴露了HTMLCanvasElement接口,該接口提供了用來操做一個canvas元素佈局和呈現的屬性和方法HTMLCanvasElement接口繼承了element接口的屬性和方法。(來自MDN)

toBlob方法兼容性寫法

if (!HTMLCanvasElement.prototype.toBlob) {
    Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
      value: function (callback, type, quality) {
        var binStr = atob(this.toDataURL(type, quality).split(',')[1])
        var len = binStr.length
        var arr = new Uint8Array(len)

        for (var i = 0; i < len; i++) {
          arr[i] = binStr.charCodeAt(i)
        }

        callback(new Blob([arr], { type: type || 'image/png' }))
     }
    })
}
複製代碼

以上代碼判斷當前瀏覽器HTMLCanvasElement.prototype內是否含有toBlob方法,若是沒有該方法,則爲HTMLCanvasElement.prototype添加toBlob方法。

到目前爲止,不管是base64直接轉成blob,仍是canvas直接轉成blob,最後目的都是爲了將blob數據上傳到服務器。請注意:當前頁面的blob數據是存儲在緩衝區的,而不是像電腦本地文件存儲在硬盤中上傳那樣,因此須要添加 isLocalFile:false屬性,fileData屬性接收一個數組,若是是一張圖片,則是個長度爲1的,元素爲blob格式的數組。上傳文件到服務器在這裏不作介紹。

提交blob到服務器

// 轉完圖片數據,提交服務器
    async upload (blobArr) {
      const oss = await this.$uploadOSS.init({
        fileData: blobArr,
        isLocalFile: false,
        onProgress: percent => {},
        onSuccess: (res) => {
          console.log(res)
        }
      })
      if (oss) {
        const data = await oss.start()
        if (!data.code) {
          this.$message.error(data.message)
        }
      }
    },
複製代碼

以上是對html2canvas好和canvas的常見的屬性的介紹,同時也是開發圖片編輯器的前提。下面會一步步詳細介紹下在已有的canvas塗鴉板的基礎上的開發多張圖片編輯器的思路和過程。

基於canvas的圖片編輯器的怎麼作?

一、既然要編輯圖片,天然要提供給咱們的圖片的地址,多是本地相對路徑,也多是遠程地址,也多是一張或者是多張圖片。因此進行操做的數據結構是數組,若是隻有一張圖那就是長度爲1的,元素爲圖片地址的數組,另外須要解決圖片跨域來避免生成的圖片只有塗鴉部分,且背景白色、頭像或者圖片丟失的狀況,上面已經提供了詳細的解決方案。

二、圖片編輯器確定是個通用組件,因此根據父組件傳進來的圖片src的一個長度爲length的數組,來填充一個相同長度的二維數組,二維數組中的元素記錄的是每一張圖片被編輯的歷史記錄的集合,也就是每一次操做canvas的時候生成的base64數據,在開始編輯以前初始化的時候該圖片默認填充空數組[],在這裏介紹一個填充數組很好用的方法fill,值得注意:fill的內容若是是引用類型,會致使fill的每一項都是同一個引用,致使在操做其中一個元素時,其他全部的元素都一塊兒改變。

//result:[[],[],...]
this.canvasArrBase = Array(length).fill(0).map(item => {
  return []
})
複製代碼

二、點擊不一樣的圖片,將該圖片的src傳進編輯器組件,並將該src做爲canvas背景圖,此時視覺上看到了一圖,在dom中實際上是canvas的一張背景圖爲src。

三、開始在頁面塗鴉畫布,爲了不沒法在surface等觸控屏設備上使用畫布塗鴉或者圖片編輯器,請使用touch事件,而不是mouse事件。

// 開始繪製
  canvas.addEventListener('touchstart', e=> {
    ...
  },false)
  
  // 繪製中
  canvas.addEventListener('touchmove', e=>{
   ...
  },false)
  
  // 結束繪製
  canvas.addEventListener('touchend', e=>{
    ...
  },false)
複製代碼

四、在進入圖片編輯器以前將點擊的圖片的索引和圖片所在的圖片數組,一塊兒做爲props傳入圖片編輯器,記住當前在編輯第幾張圖片currentIndex,才能將塗鴉或者撤銷操做都記錄在這張圖片之上。當用戶在畫布上每操做一次,也就是開始touchstart,而後touchmove,最後touchend的時候,將當前的canvas畫布的內容(僅僅把canvas塗鴉的內容)使用canvas.toDateURL()生成base64,該base64不會帶有以前設置的背景圖,而只是canvas塗鴉內容,這是canvas.toDataURL的特性決定的。(感謝底下評論網友@機智的楊浩提供的思路)

並將其push進咱們上面介紹的this.canvasArrBase[currenIndex]裏,這時currenIndex所在數組的圖片多了一張歷史記錄,也就是剛剛塗鴉的畫面被咱們保存下來了。這也就是canvas圖片編輯器,撤銷的基礎,有了它咱們才知道撤銷以後展現什麼。

//新的土塗鴉操做生成的快照存儲在當前索引的數組內
 this.canvasArrBase[this.currentIdex].push(canvas.toDataURL())
複製代碼

五、切換圖片也就意味着改變了canvas的背景圖,改變了currentIndex。在點下鼠標以後,背景圖片改變以前,如何將當前帶有背景圖和全部塗鴉的視圖轉化成base64完成保存?

解答:須要將該canvas被一個div包裹,此時的div是包含這個canvas的背景和全部塗鴉內容的一個聚合,而後使用上面提到的html2canvas把整個div轉成canvas元素,並將其toDataURL,存儲在該圖片的數組內部。

而後在新的currentIndex上對另一張圖片進行編輯,其實也是一樣的步驟,無非就是往當前編輯的圖片的push畫布剛剛展現的全部內容。

六、重點來了,撤銷如何作呢?如何撤銷,如何在畫布上一步步撤銷而且看到上一次,上上次的繪畫記錄呢?

隨着每張圖片的編輯,每張圖片產生一些歷史記錄,咱們就是利用這些歷史記錄的來進行回滾撤銷操做的。當咱們在當前已經編輯過的圖片中點一次撤銷的時候,咱們須要將上次canvas中展現的畫布內容畫到當前的畫布中。在撤銷的時候能夠直接將咱們存儲的數據源直接刪掉,因此只須要利用 canvas.drawImage() 方法,將須要展現的那一條記錄畫到canvas畫布中便可。

drawImage方法有不少參數,用法點擊查看詳情

let newBaseArr = this.canvasArrBase[this.currentIdex]
let { offsetWidth: canvasWidth, offsetHeight: canvasHeight } = canvas

//生成一張新的圖片
let image = new Image()

//圖片的src賦值成當前須要展現的base64的值(也就是讓用戶看到撤銷後的畫面)
image.src = this.canvasArrBase[this.currentIdex][newBaseArr.length-1]
//給img的src賦值後,使用onload監聽加載圖片狀況,加載完成後再繪製,不加onload就很可能繪製失敗。
image.onload = function () {
    canvas.drawImage(image, 0, 0, canvasWidth, canvasHeight)
}
複製代碼

七、關於性能優化:因爲你根本不知道用戶會拿多麼大的圖片來進行編輯,因此若是有9張圖片,每張圖片都編輯10次,那麼存在內存中的base64數據量就會很是龐大,在進行頁面切換或者圖片塗鴉歷史撤銷的時候就可能形成頁面的卡頓,爲了不這種狀況,在切換圖片以前,我只會存儲當前圖片最後一張圖片的base64。

八、提交,保存以前判斷this.canvasArrBase中每一項的長度,大於0,則是有編輯過,不然被過濾掉。而後利用前面介紹上傳方法上傳到服務器,就OK啦!

總結

整個多圖片編輯器的核心就是,初始展現編輯器的時候,不要直接將要展現的圖片drawImage到canvas,而是應該使用背景圖。只有撤銷的時候才須要drawImage,其他要麼是第一次進,就是源圖做爲canvas背景圖,要麼就是已經編輯完成後的圖,再切換回來的時候再做爲背景圖。只是在每一次塗鴉完成以後push到該圖片歷史記錄的是當前canvas除了背景圖的全部內容。

相關文章
相關標籤/搜索