CANVAS繪圖踩坑記錄

最近作了移動端生成圖片而且上傳的需求,踩了很多坑,這裏記錄一下。因爲本次使用canvas主要功能集中在繪製網絡圖片以及生成/上傳圖片,所以本文多爲和圖片相關的記錄。css

繪圖部分

onload

最開始使用canvas直接加載網絡圖片的時候,忘記考慮圖片加載的問題了,所以直接上手寫image.src = xxx接着就是ctx.drawImage,最後發現網絡圖片根本沒有被繪製上去,這纔想起來圖片必需要先加載完以後才能使用ctx.drawImage去繪製,不然圖片沒有加載完,canvas直接繪製一張空的圖片。webpack

export class Canvas {
    // code here...
    // 加載圖片
    addImage(src: string) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                resolve(img);
            }
            img.src = src;
        });
    }
    // code here...
}
複製代碼

圖片跨域問題

修改完畢以後,原本覺得此次代碼能跑起來了,卻發現因爲訪問了CDN圖片地址,Image默認不支持訪問跨域資源,必需要手動指定crossOrigin屬性才能夠跨域訪問圖片。git

export class Canvas {
    // code here...
    // 加載圖片
    addImage(src: string) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
                resolve(img);
            }
            img.src = src;
        });
    }
    // code here...
}
複製代碼

渲染的層級關係

圖片加載出來以後,原本感受問題已經解決了,可是發現我原本應該渲染的三張圖片,最後只出來了一張背景圖,另外兩個圖都不見了,可是在代碼裏面打印日誌是能夠看到canvas確實執行了這兩張圖片的渲染邏輯。github

查了查資料發現canvas只能按照渲染的前後順序來展現繪圖的層級關係,沒法手動指定層級,所以咱們想要在背景圖上方繪製圖片的話,必需要等到背景圖繪製完畢以後才能繼續執行其餘的邏輯。web

export class Canvas {
    canvas: HTMLCanvasElement;
    ctx: CanvasRenderingContext2D;
    constructor() {
        // other code
        this.canvas = document.createElement('canvas');
        this.ctx = this.canvas.getContext('2d')!;
        // other code
    }
    // code here...
    addImage(src: string) {
        // ...
    }
    drawImage() {
        const bg = 'https://xxx';
        this.addImage(bg).then((img: ImageBitmap) => {
            this.ctx.drawImage(img, 0, 0, img.width, img.height);
            // 在這以後才能繼續繪製其餘圖片
            this.addImage(xxx);
        });
    }
}
複製代碼

繪製圓形圖片

全部繪製都結束以後,產品忽然過來跟我說,但願在最後面加上一行用戶信息的區域,包括用戶頭像和用戶名,原本覺得是很簡單的工做,按照上面的邏輯再繪製一張圖片和一段文字便可,後來發現用戶頭像的圖片都是方形的,可是產品但願要一張圓形頭像圖片,在css中只須要很簡單一行border-radius: 50%的東西讓我很頭疼。typescript

嘗試了各類辦法都失敗以後,上網查了查資料才發現原來context還有保存和還原方法,再配合clip裁切就能夠完成一個圓形圖片了!canvas

export class Canvas {
    constructor() {
        // ...
    }
    addImage() {
        // ...
    }
    drawImage() {
        // ...
    }
    drawUserInfo() {
        // other code
        const src = 'https://xxx';
         this.addImage(src).then((img: ImageBitmap) => {
            this.ctx.save();
            this.ctx.arc(x, y, r, 0, Math.PI * 2);
            this.ctx.clip();
            this.ctx.drawImage(img, x, y, img.width, img.height);
            this.ctx.restore();
        });
    }
}
複製代碼

這樣先保存當前畫布的狀態,而後經過arc在頭像圖片的位置繪製一個圓形,而後裁切掉多餘的部分,接着繪製頭像,最後再恢復畫布狀態便可。後端

優化部分

3x圖

以上步驟就進行完以後,我測試了一下繪製圖片而且上傳CDN的功能,一切正常!跨域

而後滿心歡喜的展現這張圖片的時候,發如今手機上展現出來的實在太模糊了,甚至連文字都看不清楚。數組

這時我才意識到咱們平時用的圖片都是3x或者2x圖,如今我按照UI稿的360px寬度繪製的這張圖片只是1x圖,在咱們高分辨率的手機上展現出來就會很模糊,所以爲了讓圖片不模糊,我也須要將圖片變爲3x圖。

所以將繪圖時候全部的寬高及其餘數組所有都×3.

this.width = 360 * 3;
this.height = 500 * 3;

this.ctx.drawImage(bg, 0, 0, this.width * 3, this.height * 3);
// 其餘改動同理
複製代碼

這樣改動以後,展現出來的圖片就很是清晰了!

緩存圖片致使繪製卡主

因爲須要加載多張圖片,所以我這裏須要監聽全部圖片都加載而且繪製成功以後,才能執行最終的canvas.toBlob()邏輯而且上傳圖片。

export class Canvas {
    constructor() {
        // ...
        this.loadedImageNumber = 0;
    }
    // code here
    imageOnLoaded() {
        this.loadedImageNumber++;
        if(this.laodedImageNumber >= 3) {
            // upload image
        }
    }
}
複製代碼

結果pm和qa同窗測試的時候,常常發現上傳過程卡住了,一直處於loading狀態,後來通過無數次嘗試和排查問題以後,發現由於前面有些圖片已經加載過一次了,這裏的圖片有可能從瀏覽器的緩存裏面獲取了,所以根本不會執行onload函數,這樣就致使this.loadedImageNumber一直達不成大於等於3的條件,因此就卡在loading狀態了。

解決辦法是完善一下addImage函數,在監聽onload的同時判斷一下img的狀態,若是是complete的話也執行一遍回調邏輯,順便也加了一下關於onerror的處理。

export class Canvas {
    // code here...
    // 加載圖片
    addImage(src: string) {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                resolve(img);
            }
            img.onerror = () => {
                // error callback
                reject();
            }
            img.src = src;
            if(img.complete) {
                resolve(img);
            }
        });
    }
    // code here...
}
複製代碼

這樣測試了一下全部圖片均可以正常被加載出來了~

polyfill

上面問題解決了以後,QA同窗有反饋有一些低端手機依然卡在loading狀態,我原本還覺得是圖片繪製依然有問題,而後我借過手機來調試了一下,發現並非卡在圖片繪製過程,而是canvas.toBlob()的時候報錯了,因而後面的邏輯都卡住了。

export class Canvas {
    // code here
    imageOnLoaded() {
        this.loadedImageNumber++;
        if(this.laodedImageNumber >= 3) {
            this.canvas.toBlob(blob => {
                // 真正的上傳函數
                this.uploadImage(blob);
            },
            'image/jpeg',
            1.0
            )
        }
    }
    // code here
}
複製代碼

再次上網查了查資料,發現須要打polyfill才行。

github地址:JavaScript-Canvas-to-Blob

網上不少使用介紹的文章,或者直接看github官網的readme也很容易看懂。

if(__BROWSER__) {
    // import canvas toblob polyfill
    require('blueimp-canvas-to-blob');
}
export class Canvas {
    // ...
}
複製代碼

__BROWSER__webpack.DefinePlugin定義的客戶端渲染環境。

這下感受應該萬事大吉了。

優化圖片大小

而後就又被QA同窗找了。。

QA同窗反饋說圖片上傳太慢了,弱網狀況下要loading好久纔會結束,或者甚至直接到後端接口返回超時了也尚未結束圖片上傳。

我抓包看了看圖片上傳的接口,發現確實有點慢,由於生成的圖片體積太大了,足足有2.6M多

問了一下同事,原來是最初圖片模糊的時候,我想要提升圖片質量,改爲3x圖的同時又在toBlob()的時候指定圖片質量是1.0,因此致使了圖片體積過大。

把這裏的圖片質量指定爲0.8左右以後體積一下就降下來了,而且圖片質量其實並無改變多少。

export class Canvas {
    // code here
    imageOnLoaded() {
        this.loadedImageNumber++;
        if(this.laodedImageNumber >= 3) {
            this.canvas.toBlob(blob => {
                // 真正的上傳函數
                this.uploadImage(blob);
            },
            'image/jpeg',
            0.8
            )
        }
    }
    // code here
}
複製代碼

到這裏這個功能總算是完成了 OwO

相關文章
相關標籤/搜索