簽名板介紹
在最近的一個項目中,最后的一個功能是實現(xiàn)一個簽名板供客戶簽名使用。這需要用到canvas來實現(xiàn)。
我將這個功能分成實現(xiàn)和優(yōu)化兩個階段。首先來看實現(xiàn)
實現(xiàn)
對于一個canvas簽名板,首先我們要完成最最最基本的功能,簽名!
步驟為下:
1、提供畫布:
通過canvas的API,很容易定義出一塊畫布。
2、定義畫筆
實際情況上是沒有畫筆這個東西的,所以需要自己來假定一個畫筆。
畫筆具有三個狀態(tài),準(zhǔn)備畫,畫,畫完了。用一個flag和三個方法來表示這一過程:
mousePress:false // 這個flag表示是否開始畫了,準(zhǔn)備畫的時候這個標(biāo)志位置為true,畫完置為false。
beginDraw (e) { ... } // 對應(yīng)準(zhǔn)備畫的方法
drawing (e) {...} // 對應(yīng)畫的方法
endDraw (e) {...} // 對應(yīng)畫完的方法 。。。 這三個方法的實現(xiàn)下面會具體講,這里假定已經(jīng)實現(xiàn)了。
3、實現(xiàn)簽名
現(xiàn)在距離實現(xiàn)簽名就差一步,那就是記錄下鼠標(biāo)每次經(jīng)過的位置并用ctx.stroke()方法將它畫出來即可。
這里需要知道:畫過的路徑不會變,將要畫的路徑不知道,正在畫的地方能獲取。所以定義一個叫做last的對象來記錄上一次鼠標(biāo)畫過的點的位置并把它畫到現(xiàn)在的位置,以此類推,就可以畫出一條鼠標(biāo)經(jīng)過的線路了?,F(xiàn)在來具體實現(xiàn)畫畫這一過程:
a、beginDraw
beginDraw(e) {
mousePress = true;
}, // 沒什么好說的
b、endDraw
endDraw (e) {
e.preventDefault();
mousePress = false;
last = null;
} // 也沒什么好說的,畫完了標(biāo)志位和記錄對象都該還原了。
c、drawing
drawing (e) {
e.preventDefault();
if (!this.mousePress) {
return; // 這個雖然不會觸發(fā),但是好像需要這個邏輯也就寫上去了
}
var xy = this.getCoordinate(e); // 這個方法是獲取當(dāng)前的鼠標(biāo)的位置并將它賦值給一個叫做xy的對象
if (this.last != null) {
this.context.beginPath();
this.context.moveTo(this.last.x, this.last.y);
this.context.lineTo(xy.x, xy.y);
this.context.stroke();
}
// 開始移動,將坐標(biāo)賦值給last。那么下次再移動就會通過上面的操作從上一個xy移動到當(dāng)前的xy處
this.last = xy;
}
d、getCoordinate 獲取當(dāng)前鼠標(biāo)位置
getCoordinate(e) {
var x, y;
x = e.offsetX + e.target.offsetLeft;
y = e.offsetY + e.target.offsetTo; // 獲取當(dāng)前x,y坐標(biāo)
return {
x,y
} //es6語法多簡潔
},
4、事件綁定
接下來只需要在canvas上綁定事件去對應(yīng)準(zhǔn)備畫,畫和結(jié)束畫三個方法就可以了。
canvas.onmousedown = beginDraw; // 鼠標(biāo)按下事件
canvas.onmouseup = endDraw; // 鼠標(biāo)松開的事件
canvas.onmousemove = drawing; // 鼠標(biāo)移動事件
好了,如果你稍微了解一些canvas的基礎(chǔ),補(bǔ)全這些代碼,現(xiàn)在你的簽名板就實現(xiàn)了。
優(yōu)化
簽名板,在電腦上簽名,用鼠標(biāo)簽名,怎么說都顯得怪異。
優(yōu)化1、移動端的簽名:
首先,把移動端的事件綁定到對應(yīng)的三個方法:
canvas.addEventListener('touchstart',beginDraw,false)
canvas.addEventListener('touchmove',drawing,false)
canvas.addEventListener('touchend',endDraw,false)
然后,在移動端試了試,不行。那是因為在getCoordinate 方法中獲取的當(dāng)前位置有問題,在移動端,要用另一種方法來獲取。為了區(qū)分移動端和PC,需要用一個控制語句來區(qū)分它們。如下
getCoordinate(e) {
var x, y;
if (this.isTouch(e)) { // 判斷當(dāng)前事件是移動端事件還是PC端事件
x = e.touches[0].pageX;
y = e.touches[0].pageY;
}
else {
x = e.offsetX + e.target.offsetLeft;
y = e.offsetY + e.target.offsetTop;
}
return {
x,y
}
},
isTouch(e) { // 判斷當(dāng)前事件是移動端事件還是PC端事件
var type = e.type;
if (type.indexOf('touch') >= 0) {
return true;
} else {
return false;
}
},
OK,現(xiàn)在簽名板以及可以兼容PC端和移動端兩個部分了。
優(yōu)化2、個性化簽名
當(dāng)然,這個名還是要自己簽的,不過可以自己設(shè)定簽名的線條的粗細(xì),顏色等。這些樣式設(shè)定放在beginDraw這個方法處即可。具體實現(xiàn)就不說了。
優(yōu)化3、用Vue來實現(xiàn)。
用Vue的話,更方便地控制這個項目中需要用的變量等。具體改變就是把上述需要定義的變量放在vue的data中,方法房子啊methods中。綁定事件在canvas處,如圖:

這樣也是非常的方便。
優(yōu)化4、移動端橫豎屏畫布大小調(diào)整
在PC端的話,設(shè)定固定大小的畫布即可。但對于移動端來說,畫布的寬度需要撐滿屏幕才勉強(qiáng)可以做到簽名。這時候,需要實現(xiàn)將手機(jī)變成橫屏?xí)r,監(jiān)聽手機(jī)的變化,改變畫布的大小。這里,初始化畫布寬度為手機(jī)屏幕的寬度,高度為width*380/750。
這時候,需要用到window.onorientationchange來監(jiān)聽橫豎屏的轉(zhuǎn)變和window.orientation來獲取橫豎屏轉(zhuǎn)變的參數(shù),如圖所示:


0表示正常,90和-90表示橫屏,180就是把手機(jī)倒過來。然后把orientationChange方法下面開始方案。
方案1、獲取手機(jī)屏幕大小,直接進(jìn)行改變(寬變高,高變寬)。
這是我最初的實現(xiàn)方案,這種直接改變畫布寬和高是銷毀了原來的畫布重新生成了一張畫布。確實達(dá)到了橫屏簽名的效果,但這卻存在很大缺陷,那就是橫豎屏轉(zhuǎn)變重置畫布會消除畫布原有的內(nèi)容。在實際場景中,有些客戶如果想要橫屏簽字,豎屏再查看一下自己寫的什么樣子,就不滿足于客戶需求了。
方案2、由于橫豎屏轉(zhuǎn)變,畫布必然會被重置,所以就不需要考慮如何讓原有的簽名內(nèi)容不變保留過來了。解決方案就是在每次橫豎屏轉(zhuǎn)變的時候,獲取原有簽名板上內(nèi)容數(shù)據(jù),在橫豎屏轉(zhuǎn)變之后,將數(shù)據(jù)放入到新生成的畫布中。這里,我將畫布重置和畫布內(nèi)容導(dǎo)入封裝到了一個叫做resize的方法中,代碼如下:
/**
* 重置畫布操作
* @param {*} w 畫布寬
* @param {*} h 畫布高
* @param {*} flag 橫豎屏標(biāo)志
*/
resize(w, h, flag) {
var that = this;
var nc = document.createElement("canvas"); // nc用來保留原有畫布數(shù)據(jù)
nc.width = that.canvas.width;
nc.height = that.canvas.height;
nc.getContext("2d").drawImage(that.canvas, 0, 0); // 將原有畫布數(shù)據(jù)放到同樣大小的nc畫布中,就像是copy了一份
this.canvas.width = w;
this.canvas.height = h; //畫布大小重置
// 橫轉(zhuǎn)豎,原有數(shù)據(jù)放入到新的畫布中。等比放置簽名數(shù)據(jù)。
if (flag) {
// crossWidth是我設(shè)置的變量,表示橫屏情況下,屏幕的寬度,verticalWidth表示豎屏情況下屏幕的寬度。
this.context.drawImage(nc, 0, 0, this.crossWidth, this.crossHeight, 0, 0, w, h);
}
// 豎轉(zhuǎn)橫
else {
this.context.drawImage(nc, 0, 0, this.verticalWidth, this.verticalHeight, 0, 0, w, h);
}
},
優(yōu)化5、高清屏適配。
通常對于canvas來說,我們會在html中設(shè)置
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
這樣一串東西來規(guī)定這個頁面不能縮放,縮放比為1,這樣我們在開發(fā)canvas的時候就不會因為不同手機(jī)的dpr不同導(dǎo)致畫布呈現(xiàn)的有大有小,很不合理。當(dāng)然,設(shè)置這樣的一個約定不但不會影響簽名板的視覺呈現(xiàn)(畢竟名字線條單一沒那么復(fù)雜),也會很方便整個流程的開發(fā)。可是還是想讓IOS的高清屏可以顯示的更加清晰。
方案1、
由于項目采用的是手淘的flexible布局,這里,我們把上面的那個meta標(biāo)簽注釋掉,如果你畫布的寬度是這樣定義的:
this.verticalWidth = screen.width。// 因為我會將這個verticalWidth賦值給canvas.width,見resize()方法。
如果你的是安卓機(jī),那么畫布沒有變化,因為flexible默認(rèn)設(shè)置的安卓dpr = 1,也就是initial-scale=1/dpr=1,和上面meta標(biāo)簽的配置是一樣的。所以不會有變化。
如果你的是IPhone,那么你會發(fā)現(xiàn)你的畫布大小變小了。這是因為initial-scale不再是1了,而是1/dpr。這時候,我們只需要在設(shè)置屏幕寬度的時候再乘上當(dāng)前iPhone手機(jī)的dpr即可還原畫布大小。
this.verticalWidth = screen.width*dpr // dpr需要自己獲取
下面曬圖,采用高清的IPhone的3像素的簽名和不采用高清適配的簽名對比


好吧,在簽名這個功能處高清是真的沒什么優(yōu)勢。。。。但是可能在其他的canvas功能中,這種高清適配會讓canvas內(nèi)容更加清晰。
對于安卓來說,暫時的想法就是將dpr為整數(shù)的安卓機(jī)在flexible中設(shè)置為其本身的dpr(dpr>=3 設(shè)置為3),這樣就也可以讓一大部分dpr比較正常的安卓機(jī)也可以像IPhone一樣使用這種高清。
優(yōu)化6、兼容性問題
在網(wǎng)上拜讀了一位大佬對于orientation事件兼容性處理的方法后 文章,在簽名板上也加入了orientation兼容性處理。
由于部分低端手機(jī)不支持orientation事件,所以要讓他們也可以橫屏簽名,那么就要做一些改動。
解決方案:
保留原有方法不變,加上判斷語句,如下代碼:
var isOrientation = ('orientation' in window && 'onorientationchange' in window);
if (isOrientation) {
// 注冊橫豎屏事件,方案1;
window.onorientationchange = this.orientationChange;
this.orientationChange();
}
對于不支持orientation事件的機(jī)型,做resize事件處理(為了防止與我自己寫的resize方法沖突,所以我把自己在前面寫的resize方法修改成了resizeCanvas):
else {
// 使用 resize 來做監(jiān)聽機(jī)制。方案2:
window.addEventListener('resize',function(e){
var orientation=(window.innerWidth > window.innerHeight)? "landscape":"portrait"; // 判斷橫豎屏
if(orientation === 'portrait'){
// 豎屏 do something ……
that.screenCtrl = true;
setTimeout(function(){
that.height = that.getWarnHeight();
},500)
that.resizeCanvas(that.verticalWidth, that.verticalHeight, true);
return;
}
else {
// 橫屏 do something else ……
that.screenCtrl = false;
setTimeout(function(){
that.height = that.getBtnAreaHeight();
that.crossHeight = window.innerHeight - that.height;
that.resizeCanvas(that.crossWidth, that.crossHeight, false);
},500)
that.resizeCanvas(that.crossWidth, that.crossHeight, false);
}
},false)
}
在加上上面的兼容處理后,基本所有的機(jī)型都可以使用了。
這一塊會不定時更新如何優(yōu)化,我還在學(xué)習(xí)怎樣使canvas內(nèi)容更加清晰。
額外補(bǔ)充
從上圖可以看出,canvas畫布左上角相對于頁面而言不一定是(0,0)的位置。那么如果在drawing方法中不做些改變的話,在簽名的時候筆跡和畫出來的線條位置會不一樣,這時候就需要計算出canvas左上角相對于頁面左上角直接的差值,并改變drawing方法內(nèi)的代碼。補(bǔ)充代碼,獲取提示區(qū)域高度:
getWarnHeight() {
var warn = document.getElementById('warn');
var height = warn.offsetHeight;
warn = null;
return height;
},
總結(jié)
簽名板 JS代碼地址