专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
51好读  ›  专栏  ›  前端大全

HTML5 中手势原理分析与数学知识的实践

前端大全  · 公众号  · 前端  · 2017-08-14 22:04

正文

(点击 上方公众号 ,可快速关注)


作者:郭东东

https://segmentfault.com/a/1190000010511484

如有好文章投稿,请点击 → 这里了解详情


引言

在这触控屏的时代,人性化的手势操作已经深入了我们生活的每个部分。现代应用越来越重视与用户的交互及体验,手势是最直接且最为有效的交互方式,一个好的手势交互,能降低用户的使用成本和流程,大大提高了用户的体验。

近期,公司的多个项目中都对手势有着较高的需求,已有的手势库无法完全cover,因此便撸了一个轻量、便于使用的移动端手势库。这篇博文主要是解析了移动端常用手势的原理,及从前端的角度学习过程中所使用的数学知识。希望能对大家有一点点的启发作用,也期待大神们指出不足甚至错误,感恩。

主要讲解项目中经常使用到的五种手势:

  • 拖动: drag

  • 双指缩放: pinch

  • 双指旋转: rotate

  • 单指缩放: singlePinch

  • 单指旋转: singleRotate


Tips :
因为 tap 及 swipe 很多基础库中包含,为了轻便,因此并没有包含,但如果需要,可进行扩展;


实现原理

众所周知,所有的手势都是基于浏览器原生事件touchstart, touchmove, touchend, touchcancel进行的上层封装,因此封装的思路是通过一个个相互独立的事件回调仓库handleBus,然后在原生touch事件中符合条件的时机触发并传出计算后的参数值,完成手势的操作。实现原理较为简单清晰,先不急,我们先来理清一些使用到的数学概念并结合代码,将数学运用到实际问题中,数学部分可能会比较枯燥,但希望大家坚持读完,相信会收益良多。

基础数学知识函数

我们常见的坐标系属于线性空间,或称向量空间(Vector Space)。这个空间是一个由点(Point) 和 向量(Vector) 所组成集合;


点(Point)

可以理解为我们的坐标点,例如原点O(0,0),A(-1,2),通过原生事件对象的touches可以获取触摸点的坐标,参数index代表第几接触点;



向量(Vector)

是坐标系中一种 既有大小也有方向的线段,例如由原点O(0,0)指向点A(1,1)的箭头线段,称为向量a,则a=(1-0,1-0)=(1,1);


如下图所示,其中i与j向量称为该坐标系的单位向量,也称为基向量,我们常见的坐标系单位为1,即i=(1,0);j=(0,1);


获取向量的函数:

向量模

代表 向量的长度,记为|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基,e为x轴的偏移量,f为y轴的偏移量;因此上栗便很好理解,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个参数会同时受到 rotate scale 的影响,具有两个变量,因此需要通过前两个参数根据上面的转换方式列出两个不等式:

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' , {







请到「今天看啥」查看全文