JavaScript中的圖片處理與合成(二)

郭東東發表於2018-03-06

引言

本系列分成以下4個部分:

  • 基礎型別圖片處理技術之縮放、裁剪與旋轉(傳送門);
  • 基礎型別圖片處理技術之圖片合成(傳送門);
  • 基礎型別圖片處理技術之文字合成;
  • 演算法型別圖片處理技術(傳送門);

上篇文章,我們介紹了圖片的裁剪/旋轉與縮放,接下來本文主要介紹 圖片的合成 ,這是基礎類圖片處理中比較實用且複雜的一部分,可以算第一篇文章內容的實踐。

通過這些積累,我封裝了幾個專案中常用的功能:

圖片合成     圖片裁剪     人像摳除

圖片的合成

圖片的合成在實際專案中運用也是十分的廣泛,大家可以試試這個demo(僅支援移動端): ???

小狗貼紙

JavaScript中的圖片處理與合成(二)

圖片的合成原理其實類似於photoshop的理念,通過 圖層的疊加 ,最後合成並匯出,相比於裁剪和縮放,其實基本原理是一致的,但是它涉及了更多的計算和比較複雜的流程,我們先一起來梳理下合成的整個邏輯。

相信大家對 photoshop都是較為了解的,我們可以借鑑它的思維方式:

  • 新建 psd 檔案, 設定寬高;
  • 設定背景圖;
  • 從底部到頂部一層層新增所需要的圖層;
  • 最後直接將整個檔案匯出成一張圖片;

以需要合成下圖為?:

JavaScript中的圖片處理與合成(二)

1、首先我們需要建立一個與原圖一樣大小的畫布;

2、載入背景圖並 新增背景圖層 ,也就是這個美女啦~

3、載入貓耳朵圖並新增美女頭上的 貓耳朵圖層 ( 2/3順序不可逆,否則耳朵會被美女蓋在下面哦。因此圖片的載入控制十分重要 );

4、將整個畫布 匯出圖片

合成部分,主要以封裝的外掛為栗子哈。這樣能儘可能的完整,避免遺漏點。在開始之前,為了確保圖片非同步繪製的順序,我們需要先來構建一套佇列系統。

佇列系統;

圖片的載入時間是 非同步且未知 的,而圖片的合成需要嚴格保證繪製 順序 ,越後繪製的圖片會置於越頂層,因此我們需要一套嚴格機制來控制圖片的載入與繪製,否則我們將無法避免的寫出 回撥地獄 ,這裡我使用到了簡單的佇列系統;

佇列系統的原理其實也很簡單,主要是為了我們能確保圖層從底到頂一層一層的繪製。我設計的使用方式如下, 佇列方式主要來確保add函式的按順序繪製:

// 建立畫布;
let mc = new MCanvas();

// 新增圖層;
mc.add(image-1).add(image-2);

// 繪製並匯出圖片;
mc.draw();
複製程式碼

這樣我們就明白了,這個佇列系統需要下面幾個點:

  • queue佇列: 用於存放圖層繪製函式;

  • next函式: 用於表示當前圖層已繪製完畢,執行下一圖層的繪製;

  • add函式: 作為統一新增圖層的方法,將繪製邏輯存入函式棧quene,幷包裹next函式;

  • draw函式: 作為繪製啟動函式,表示所有圖層素材已經準備完畢,可以按順序開始繪製;

MCanvas.queue = [];

MCanvas.prototype.add = function(){
    this.queue.push(()=>{
	// 繪製邏輯,之後詳解;
	...
		
	// 執行下個圖層繪製;
	this.next();
    });
}

MCanvas.prototype.next = function(){
    if(this.queue.length > 0){
    	// 當佇列中還有繪製任務時,則推出並執行;
        this.queue.shift()();
    }else{
    	// 當繪製完成後,呼叫成功事件,並傳出結果圖;
        this.fn.success();
    }
};

MCanvas.prototype.draw = function(){
	// 匯出邏輯;
	...
	
	// 設定成功事件,用於匯出結果圖;	
	this.fn.success = () => {
	// 使用 setTimeout 能略微提升效能表現;
	// 且佇列函式中都為真正的非同步,因此此處不會影響邏輯;
        setTimeout(()=>{
            b64 = this.canvas.toDataURL(`image/jpeg}`, 0.9);
            
            ...
        },0);
   };
   
   // 啟動佇列執行;
	this.next();
}
複製程式碼

此時,queueaddnextdraw便組成了一整套佇列系統,可確保圖片的順序載入和繪製,準備好素材和佇列後,我們便可以開始真正的合成圖片咯~~

建立畫布

MCanvas.prototype._init = function(){
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
};
複製程式碼

繪製背景圖

設定畫布大小並繪製美女背景圖。

通過調整背景圖的dx,dy,dw,dh引數,可以繪製出多種模式,類似於css中的background-sizecontain/cover等效果。

這裡主要以上面使用到的場景為例子,既原圖模式。

// 原圖/效果圖尺寸保持一致;
MCanvas.prototype.background = function(image, bgOps){
// 推入佇列系統;
this.queue.push(() => {
    let { iw, ih } = this._getSize(img);
		
    // 圖片與canvas的長寬比;
    let iRatio = iw / ih;
		
    // 背景繪製引數;
    let dx,dy,dwidth,dheight;
		
    // 設定畫布與背景圖尺寸一致;
    this.canvas.width = iw;
    this.canvas.height = ih;
    dx = dy = 0;
    dwidth = this.canvas.width;
    dheight = this.canvas.height;
		
    // 繪製背景圖;
    this.ctx.drawImage(img,dx,dy,dwidth,dheight);
		
    this._next(); 
});
return this;
};
複製程式碼

繪製貓耳朵貼紙

相信大家都玩過貼紙,其最大的特點,就是貼紙與背景圖的匹配。也就是使用者可以修改貼紙的 大小,位置,旋轉角度,通過手勢操作將貓耳朵完美地貼在照片人物的頭上。因此也就是說add這個方法,需要設定縮放,旋轉與位置等引數。

這裡先模擬出一份使用引數, 實際真實情況會根據不同的背景圖,使用者會調整出不同的位置引數。

{
	// 圖片路徑;
    image:'./images/ear.png',
    options:{
    	// 貼紙寬度;
        width:482,
        pos:{
        	// 貼紙左上點座標;
            x:150,
            y:58,
            // 貼紙放大係數;
            scale:1,
            // 貼紙旋轉系數;
            rotate:35,
        },
    },
}
複製程式碼

add函式

接下里我們便來在add函式中解析下各個引數的使用姿勢:

繪製小畫布來處理旋轉:

// 建立小畫布;
let lcvs = document.createElement('canvas'),
	lctx = lcvs.getContext('2d');

// 貼紙圖原始大小;
let { iw, ih } = this._getSize(img);
// 繪製引數;
let ldx, ldy, ldw, ldh;

// 貼紙原始尺寸;
ldw = iw;
ldh = ih;

// 繪製起始點;
ldx = - Math.round(ldw / 2);
ldy = - Math.round(ldh / 2);

// 上篇文章我們說過旋轉裁剪的問題,這裡就需要用到;
// 需要擴大小畫布的容器,以避免旋轉造成的裁剪;最大值為放大5倍;
let _ratio = iw > ih ? iw / ih : ih / iw;
let lctxScale = _ratio * 1.4 > 5 ? 5 : _ratio * 1.4;

lcvs.width =  ldw * lctxScale;
lcvs.height = ldh * lctxScale;

// 調整繪製基點;
lctx.translate(lcvs.width/2,lcvs.height/2);

// 旋轉畫板;
lctx.rotate(ops.pos.rotate);

// 繪製貼紙; 
lctx.drawImage(img,ldx,ldy,ldw,ldh);
複製程式碼

此時我們會得到一個小畫布,中心繪製這貓耳朵貼紙:

JavaScript中的圖片處理與合成(二)

接下來我們便是將貼紙繪製到背景圖上,需要注意的點就是,放大會增加貼紙畫布的空白區域,需要考慮到這部分割槽域,才能計算出最後真實的dx,dy值:

// 繪製引數;
let cratio = iw / ih;
let cdx, cdy, cdw, cdh;

// ops.width 為最終畫到大畫布上時的寬度;
// 由於小畫布進行了放大,因此最終寬度也需要等倍放大;
// 並乘以配置中還需要縮放的係數;
cdw = ops.width * lctxScale * ops.pos.scale;
cdh = cdw / cratio * ops.pos.scale;

// 放大後增加的空白區域;
spaceX = (lctxScale - 1) * ops.width / 2;
spaceY = spaceX / cratio;

// 獲取素材的最終位置;
// 配置的位置 - 配置放大係數的影響 - 小畫布放大倍數的影響;
cdx = ops.pos.x + cdw * ( 1 - ops.pos.scale )/2 - spaceX;
cdy = ops.pos.y + cdh * ( 1 - ops.pos.scale )/2 - spaceY;

this.ctx.drawImage(lcvs,cdx,cdy,cdw,cdh);

lcvs = lctx = null;
複製程式碼

這樣便能得到合成後的結果圖了,紅色邊框代表小畫布,黑色邊框代表大畫布:

JavaScript中的圖片處理與合成(二)

MCanvas.prototype.add = function(img, options){
    this.queue.push(()=>{
        // 繪製貼紙小畫布;
        ...
		
        // 繪製貼紙到大畫布上;
        ...
		
        this._next();
    });
    return this;
}
複製程式碼

這樣我們便完成了一系列方法,構建了一套完整的合成流程。通過這套流程,我們便能新增任意的圖片圖層併合成圖片。

結語

本文主要講解了圖片合成上的方法原理和一些需要填的坑,這整套流程也是經過了很長一段時間的打磨,填了許多坑後總結出來的,算比較成熟的方案,已經work在多個線上專案中,期望能對大家有所幫助!?。 下篇文章,我們會繼續介紹下文字的合成和幾何圖片的合成,敬請期待~~??

相關文章