HTML5中手勢原理分析與數學知識的實踐

HTML5中手勢原理分析與數學知識的實踐

引言

在這觸控屏的時代,人性化的手勢操做已經深刻了咱們生活的每一個部分。現代應用愈來愈重視與用戶的交互及體驗,手勢是最直接且最爲有效的交互方式,一個好的手勢交互,能下降用戶的使用成本和流程,大大提升了用戶的體驗。css

近期,公司的多個項目中都對手勢有着較高的需求,已有的手勢庫沒法徹底cover,所以便擼了一個輕量、便於使用的移動端手勢庫。這篇博文主要是解析了移動端經常使用手勢的原理,及從前端的角度學習過程當中所使用的數學知識。但願能對你們有一點點的啓發做用,也期待大神們指出不足甚至錯誤,感恩。html

主要講解項目中常用到的五種手勢:前端

  • 拖動: drag
  • 雙指縮放: pinch
  • 雙指旋轉: rotate
  • 單指縮放: singlePinch
  • 單指旋轉: singleRotate
Tips :
由於 tapswipe 不少基礎庫中包含,爲了輕便,所以並無包含,但若是須要,可進行擴展;

實現原理

衆所周知,全部的手勢都是基於瀏覽器原生事件touchstart, touchmove, touchend, touchcancel進行的上層封裝,所以封裝的思路是經過一個個相互獨立的事件回調倉庫handleBus,而後在原生touch事件中符合條件的時機觸發並傳出計算後的參數值,完成手勢的操做。實現原理較爲簡單清晰,先不急,咱們先來理清一些使用到的數學概念並結合代碼,將數學運用到實際問題中,數學部分可能會比較枯燥,但但願你們堅持讀完,相信會收益良多。css3

基礎數學知識函數

咱們常見的座標系屬於線性空間,或稱向量空間(Vector Space)。這個空間是一個由點(Point) 和 向量(Vector) 所組成集合;git

點(Point)

能夠理解爲咱們的座標點,例如原點O(0,0),A(-1,2),經過原生事件對象的touches能夠獲取觸摸點的座標,參數index表明第幾接觸點;github

圖片描述

向量(Vector)

是座標系中一種 既有大小也有方向的線段,例如由原點O(0,0)指向點A(1,1)的箭頭線段,稱爲向量a,則a=(1-0,1-0)=(1,1);web

以下圖所示,其中ij向量稱爲該座標系的單位向量,也稱爲基向量,咱們常見的座標系單位爲1,即i=(1,0);j=(0,1)canvas

圖片描述

獲取向量的函數:api

圖片描述

向量模

表明 向量的長度,記爲|a|,是一個標量,只有大小,沒有方向;瀏覽器

幾何意義表明的是以x,y爲直角邊的直角三角形的斜邊,經過勾股定理進行計算;

圖片描述

getLength函數:

圖片描述

向量的數量積

向量一樣也具備能夠運算的屬性,它能夠進行加、減、乘、數量積和向量積等運算,接下來就介紹下咱們使用到的數量積這個概念,也稱爲點積,被定義爲公式:

當a=(x1,y1),b=(x2,y2),則a·b=|a|·|b|·cosθ=x1·x2+y1·y2;

共線定理

共線,即兩個向量處於 平行 的狀態,當a=(x1,y1),b=(x2,y2),則存在惟一的一個實數λ,使得a=λb,代入座標點後,能夠獲得 x1·y2= y1·x2;

所以當x1·y2-x2·y1>0 時,既斜率 ka > kb ,因此此時b向量相對於a向量是屬於順時針旋轉,反之,則爲逆時針;

旋轉角度

經過數量積公式咱們能夠推到求出兩個向量的夾角:

cosθ=(x1·x2+y1·y2)/(|a|·|b|);

而後經過共線定理咱們能夠判斷出旋轉的方向,函數定義爲:

圖片描述

矩陣與變換

因爲空間最本質的特徵就是其能夠容納運動,所以在線性空間中,

咱們用向量來刻畫對象,而矩陣即是用來描述對象的運動;

而矩陣是如何描述運動的呢?

咱們知道,經過一個座標系基向量即可以肯定一個向量,例如 a=(-1,2),咱們一般約定的基向量是 i = (1,0) 與 j = (0,1); 所以:

a = -1i + 2j = -1 (1,0) + 2(0,1) = (-1+0,0+2) = (-1,2);

而矩陣變換的,其實即是經過矩陣轉換了基向量,從而完成了向量的變換;

例如上面的栗子,把a向量經過矩陣(1,2,3,0)進行變換,此時基向量i(1,0)變換成(1,-2)j(0,1)變換成(3,0),沿用上面的推導,則

a = -1i + 2j = -1(1,-2) + 2(3,0) = (5,2);

以下圖所示:
A圖表示變換以前的座標系,此時a=(-1,2),經過矩陣變換後,基向量i,j的變換引發了座標系的變換,變成了下圖B,所以a向量由(-1,2)變換成了(5,2)

其實向量與座標系的關聯不變( a = -1i+2j),是基向量引發座標系變化,而後座標系沿用關聯致使了向量的變化;

圖片描述

結合代碼

其實CSS的transform等變換即是經過矩陣進行的,咱們平時所寫的translate/rotate等語法相似於一種封裝好的語法糖,便於快捷使用,而在底層都會被轉換成矩陣的形式。例如transform:translate(-30px,-30px)編譯後會被轉換成transform : matrix(1,0,0,1,30,30);

一般在二維座標系中,只須要 2X2 的矩陣便足以描述全部的變換了, 但因爲CSS是處於3D環境中的,所以CSS中使用的是 3X3 的矩陣,表示爲:

圖片描述

其中第三行的0,0,1表明的就是z軸的默認參數。這個矩陣中,(a,b) 即爲座標軸的 i基,而(c,d)既爲j基,ex軸的偏移量,fy軸的偏移量;所以上慄便很好理解,translate並無致使i,j基改變,只是發生了偏移,所以translate(-30px,-30px) ==> matrix(1,0,0,1,30,30)~

全部的transform語句,都會發生對應的轉換,以下:

// 發生偏移,但基向量不變;
transform:translate(x,y) ==> transform:matrix(1,0,0,1,x,y)

// 基向量旋轉;
transform:rotate(θdeg)==> transform:matrix(cos(θ·π/180),sin(θ·π/180),-sin(θ·π/180),cos(θ·π/180),0,0)

// 基向量放大且方向不變;
transform:scale(s) ==> transform:matrix(s,0,0,s,0,0)

translate/rotate/scale等語法十分強大,讓咱們的代碼更爲可讀且方便書寫,可是matrix有着更強大的轉換特性,經過matrix,能夠發生任何方式的變換,例如咱們常見的鏡像對稱transform:matrix(-1,0,0,1,0,0);

圖片描述

MatrixTo

然而matrix雖然強大,但可讀性卻很差,並且咱們的寫入是經過translate/rotate/scale的屬性,然而經過getComputedStyle讀取到的 transform倒是matrix:

transform:matrix(1.41421, 1.41421, -1.41421, 1.41421, -50, -50);

請問這個元素髮生了怎麼樣的變化?。。這就一臉懵逼了。-_-|||

所以,咱們必需要有個方法,來將matrix翻譯成咱們更爲熟悉的translate/rotate/scale方式,在理解了其原理後,咱們即可以着手開始表演咯~

咱們知道,前4個參數會同時受到rotatescale的影響,具備兩個變量,所以須要經過前兩個參數根據上面的轉換方式列出兩個不等式:

cos(θ·π/180)*s=1.41421;

sin(θ·π/180)*s=1.41421;

將兩個不等式相除,便可以輕鬆求出θs了,perfect!!函數以下:

圖片描述

手勢原理

接下來咱們將上面的函數用到實際環境中,經過圖示的方式來模擬手勢的操做,簡要地講解手勢計算的原理。但願各位大神理解這些基礎的原理後,能創造出更多炫酷的手勢,像咱們在mac觸控板上使用的同樣。

下面圖例:

圓點: 表明手指的觸碰點;

兩個圓點之間的虛線段: 表明雙指操做時組成的向量;

a向量/A點:表明在 touchstart 時獲取的初始向量/初始點;

b向量/B點:表明在 touchmove 時獲取的實時向量/實時點;

座標軸底部的公式表明須要計算的值;

Drag(拖動事件)

圖片描述

上圖是模擬了拖動手勢,由A點移動到B點,咱們要計算的即是這個過程的偏移量;

所以咱們在touchstart中記錄初始點A的座標:

// 獲取初始點A;
let startPoint = getPoint(ev,0);

而後在touchmove事件中獲取當前點並實時的計算出△x△y

// 實時獲取初始點B;
let curPoint = getPoint(ev,0);

// 經過A、B兩點,實時的計算出位移增量,觸發 drag 事件並傳出參數;
_eventFire('drag', {
    delta: {
        deltaX: curPoint.x - startPoint.x,
        deltaY: curPoint.y - startPoint.y,
    },
    origin: ev,
});
Tips: fire函數即遍歷執行 drag事件對應的回調倉庫便可;

Pinch(雙指縮放)

圖片描述

上圖是雙指縮放的模擬圖,雙指由a向量放大到b向量,經過初始狀態時的a向量的模與touchmove中獲取的b向量的模進行計算,即可得出縮放值:

// touchstart中計算初始雙指的向量模;
let vector1 = getVector(secondPoint, startPoint);
let pinchStartLength = getLength(vector1);

// touchmove中計算實時的雙指向量模;
let vector2 = getVector(curSecPoint, curPoint);
let pinchLength = getLength(vector2);
this._eventFire('pinch', {
    delta: {
        scale: pinchLength / pinchStartLength,
    },
    origin: ev,
});

Rotate(雙指旋轉)

圖片描述

初始時雙指向量a,旋轉到b向量,θ即是咱們須要的值,所以只要經過咱們上面構建的getAngle函數,即可求出旋轉的角度:

// a向量;
let vector1 = getVector(secondPoint, startPoint);

// b向量;
let vector2 = getVector(curSecPoint, curPoint);

// 觸發事件;
this._eventFire('rotate', {
    delta: {
        rotate: getAngle(vector1, vector2),
    },
    origin: ev,
});

singlePinch(單指縮放)

圖片描述

與上面的手勢不一樣,單指縮放和單指旋轉都須要多個特有概念:

操做元素( operator):須要操做的元素。上面三個手勢其實並不關心操做元素,由於單純靠手勢自身,便能計算得出正確的參數值,而單指縮放和旋轉須要依賴於操做元素的基準點(操做元素的中心點)進行計算;

按鈕:由於單指的手勢與拖動(drag)手勢是相互衝突的,須要一種特殊的交互方式來進行區分,這裏是經過特定的區域來區分,相似於一個按鈕,當在按鈕上操做時,是單指縮放或者旋轉,而在按鈕區域外,則是常規的拖動,實踐證實,這是一個用戶很容易接受且體驗較好的操做方式;

圖中由a向量單指放大到b向量,對操做元(正方形)素進行了中心放大,此時縮放值即爲b向量的模 / a向量的模;

// 計算單指操做時的基準點,獲取operator的中心點;
let singleBasePoint = getBasePoint(operator);

// touchstart 中計算初始向量模;
let pinchV1 = getVector(startPoint,singleBasePoint);
singlePinchStartLength = getLength(pinchV1);

// touchmove 中計算實時向量模;
pinchV2 = getVector(curPoint, singleBasePoint);
singlePinchLength = getLength(pinchV2);

// 觸發事件;
this._eventFire('singlePinch', {
    delta: {
        scale: singlePinchLength / singlePinchStartLength,
    },
    origin: ev,
});

singleRotate(單指旋轉)

圖片描述

結合單指縮放和雙指旋轉,能夠很簡單的知道 θ即是咱們須要的旋轉角度;

// 獲取初始向量與實時向量
let rotateV1 = getVector(startPoint, singleBasePoint);
let rotateV2 = getVector(curPoint, singleBasePoint);

// 經過 getAngle 獲取旋轉角度並觸發事件;
this._eventFire('singleRotate', {
    delta: {
        rotate: getAngle(rotateV1, rotateV2),
    },
    origin: ev,
});

運動增量

因爲touchmove事件是個高頻率的實時觸發事件,一個拖動操做,其實觸發了N次的touchmove事件,所以計算出來的值只是一種增量,即表明的是一次 touchmove事件增長的值,只表明一段很小的值,並非最終的結果值,所以須要由mtouch.js外部維護一個位置數據,相似於:

//    真實位置數據;
let dragTrans = {x = 0,y = 0};

// 累加上 mtouch 所傳遞出的增量 deltaX 與 deltaY;
dragTrans.x += ev.delta.deltaX;
dragTrans.y += ev.delta.deltaY;

// 經過 transform 直接操做元素;
set($drag,dragTrans);

初始位置

維護外部的這個位置數據,若是初始值像上述那樣直接取0,則遇到使用css設置了transform屬性的元素便沒法正確識別了,會致使操做元素開始時瞬間跳回(0,0)的點,所以咱們須要初始去獲取一個元素真實的位置值,再進行維護與操做。此時,便須要用到上面咱們提到的getComputedStyle方法與matrixTo函數:

// 獲取css transform屬性,此時獲得的是一個矩陣數據;
// transform:matrix(1.41421,1.41421,-1.41421,1.41421,-50,-50);
let style = window.getComputedStyle(el,null);
let cssTrans = style.transform || style.webkitTransform;

// 按規則進行轉換,獲得:
let initTrans = _.matrixTo(cssTrans);

// {x:-50,y:-50,scale:2,rotate:45};
// 即該元素設置了:transform:translate(-50px,-50px) scale(2) rotate(45deg);

結語

至此,相信你們對手勢的原理已經有基礎的瞭解,基於這些原理,咱們能夠再封裝出更多的手勢,例如雙擊,長按,掃動,甚至更酷炫的三指、四指操做等,讓應用擁有更多人性化的特質。

基於以上原理,我封裝了幾個常見的工具:(求star -.-)

Tips: 由於只針對移動端,需在移動設備中打開 demo,或者pc端開啓mobile調試模式!
  1. mtouch.js : 移動端的手勢庫,封裝了上述的五種手勢,精簡的api設計,涵蓋了常見的手勢交互,基於此也能夠很方便的進行擴展。

demo
github

  1. touchkit.js : 基於mtouch所封裝的一層更貼近業務的工具包,可用於製做多種手勢操做業務,一鍵開啓,一站式服務。

demo
github

  1. mcanvas.js : 基於canvas 開放極簡的api實現圖片<段落文字> <混排文字> <裁剪> <平移> <旋轉> <縮放> <水印添加> 一鍵導出等。

demo
github

致謝

相關文章
相關標籤/搜索