引言
本系列分成以下4個部分:
上篇文章,我們介紹了圖片的裁剪/旋轉與縮放,接下來本文主要介紹 圖片的合成 ,這是基礎類圖片處理中比較實用且複雜的一部分,可以算第一篇文章內容的實踐。
通過這些積累,我封裝了幾個專案中常用的功能:
圖片合成 圖片裁剪 人像摳除
圖片的合成
圖片的合成在實際專案中運用也是十分的廣泛,大家可以試試這個demo
(僅支援移動端): ???
小狗貼紙
圖片的合成原理其實類似於photoshop
的理念,通過 圖層的疊加 ,最後合成並匯出,相比於裁剪和縮放,其實基本原理是一致的,但是它涉及了更多的計算和比較複雜的流程,我們先一起來梳理下合成的整個邏輯。
相信大家對 photoshop
都是較為了解的,我們可以借鑑它的思維方式:
- 新建
psd
檔案, 設定寬高; - 設定背景圖;
- 從底部到頂部一層層新增所需要的圖層;
- 最後直接將整個檔案匯出成一張圖片;
以需要合成下圖為?:
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();
}
複製程式碼
此時,queue
、add
、next
與draw
便組成了一整套佇列系統,可確保圖片的順序載入和繪製,準備好素材和佇列後,我們便可以開始真正的合成圖片咯~~
建立畫布
MCanvas.prototype._init = function(){
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
};
複製程式碼
繪製背景圖
設定畫布大小並繪製美女背景圖。
通過調整背景圖的dx,dy,dw,dh
引數,可以繪製出多種模式,類似於css
中的background-size
的contain
/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);
複製程式碼
此時我們會得到一個小畫布,中心繪製這貓耳朵貼紙:
接下來我們便是將貼紙繪製到背景圖上,需要注意的點就是,放大會增加貼紙畫布的空白區域,需要考慮到這部分割槽域,才能計算出最後真實的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;
複製程式碼
這樣便能得到合成後的結果圖了,紅色邊框代表小畫布,黑色邊框代表大畫布:
MCanvas.prototype.add = function(img, options){
this.queue.push(()=>{
// 繪製貼紙小畫布;
...
// 繪製貼紙到大畫布上;
...
this._next();
});
return this;
}
複製程式碼
這樣我們便完成了一系列方法,構建了一套完整的合成流程。通過這套流程,我們便能新增任意的圖片圖層併合成圖片。
結語
本文主要講解了圖片合成上的方法原理和一些需要填的坑,這整套流程也是經過了很長一段時間的打磨,填了許多坑後總結出來的,算比較成熟的方案,已經work在多個線上專案中,期望能對大家有所幫助!?。 下篇文章,我們會繼續介紹下文字的合成和幾何圖片的合成,敬請期待~~??