写点什么

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

  • 2020-02-24
  • 本文字数:5487 字

    阅读完需:约 18 分钟

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


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

本文主要是解析了移动端常用手势的原理,及从前端的角度学习过程中所使用的数学知识。希望能对大家有一点点的启发作用。

一、引言

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


近期,公司的多个项目中都对手势有着较高的需求,已有的手势库无法完全 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); -webkit-transform: 成 matrix(1,0,0,1,30,30); -moz-transform: 成 matrix(1,0,0,1,30,30); -o-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); -webkit-transform: ,matrix(-1,0,0,1,0,0); -moz-transform: ,matrix(-1,0,0,1,0,0); -o-transform: ,matrix(-1,0,0,1,0,0);



MatrixTo


然而 matrix 虽然强大,但可读性却不好,而且我们的写入是通过 translate/rotate/scale 的属性,然而通过 getComputedStyle 读取到的 transform 却是 matrix:


matrix(1.41421, 1.41421, -1.41421, 1.41421, -50, -50); -webkit-transform: >matrix(1.41421, 1.41421, -1.41421, 1.41421, -50, -50); -moz-transform: >matrix(1.41421, 1.41421, -1.41421, 1.41421, -50, -50); -o-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', { 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); -webkit-transform: matrix(1.41421,1.41421,-1.41421,1.41421,-50,-50); -moz-transform: matrix(1.41421,1.41421,-1.41421,1.41421,-50,-50); -o-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); -webkit-transform: :translate(-50px,-50px) scale(2) rotate(45deg); -moz-transform: :translate(-50px,-50px) scale(2) rotate(45deg); -o-transform: :translate(-50px,-50px) scale(2) rotate(45deg);
复制代码

四、结语

至此,相信大家对手势的原理已经有基础的了解,基于这些原理,我们可以再封装出更多的手势,例如双击,长按,扫动,甚至更酷炫的三指、四指操作等,让应用拥有更多人性化的特质。


本文转载自美图技术公众号。


原文链接:https://mp.weixin.qq.com/s/BXzIKSExGqmqm8cDaETNuw


2020-02-24 19:31752

评论

发布
暂无评论
发现更多内容

采集新闻数据,助力产业研究/内容聚合分发/行业研究/舆情监控

八爪鱼采集器︱RPA机器人

爬虫 采集

GreatSQL 在SQL中使用 HINT 语法修改会话变量

GreatSQL

数据库

全局视角看技术-Java多线程演进史

京东科技开发者

第三届OpenHarmony技术大会应用生态实践分论坛成功举办

科技热闻

HPE Aruba Networking连续七年蝉联Gartner SD-WAN魔力象限领导者

科技热闻

淘宝商品评论API:获取商品升级迭代后的用户反馈

技术冰糖葫芦

API 接口 API 文档 API 测试 API 性能测试

阿里架构师:天天高并发,这个时代达不到百万以上的并发量都不叫高并发!!!

程序员高级码农

多线程 架构师 Java高并发 Java’ 高并发‘’

如何对 GitLab 老旧版本进行升级?

极狐GitLab

gitlab 安全漏洞

阿里Java面试手册-Java面试题总结(附答案)——互联网大厂都在问的Java面试题,而你从没看过!

程序员高级码农

Java 面试 架构师 Java’ Java 面试题 春招‘

Java程序员真的还有未来吗?如何备战2025春招Java面试?并狂拿大厂offer?(java高级岗)

程序员高级码农

Java 面试 架构师 Java’ 面试‘ Java 面试题

IPQ9574,IPQ5322,IPQ9570-WiFi 7 chip diversity: Performance, application and market differences analysis

wifi6-yiyi

5G router WiFi7

CAE和CAD的区别

智造软件

计算机 CAE cad 仿真技术 辅助设计

使用豆包MarsCode 来处理 Excel 的数据吧!

豆包MarsCode

人工智能 程序员 AI

Caffeine学习笔记

京东科技开发者

【质量视角】可观测性背景下的质量保障思路

京东科技开发者

为什么说“全面绩效”是企业管理的必选项?

ToB行业头条

人机识别到底难在哪?

芯盾时代

身份安全

RAG vs 长上下文 LLMs:谁主沉浮?

Baihai IDP

程序员 AI LLMs rag Baihai IDP

第三届OpenHarmony技术大会硬件生态分论坛圆满举办

科技热闻

全面洞察商业情报,助力企业破解增长难题

八爪鱼采集器︱RPA机器人

爬虫 采集

Excelize 开源基础库 2.9.0 版本正式发布

xuri

golang 开源 办公自动化 Excelize 办公软件

用户的声音| 出色的表格解析能力!TextIn文档解析助力金融信息化企业数据底座建设

合合技术团队

金融 #科技

基于Ascend C的Matmul算子性能优化最佳实践

华为云开发者联盟

人工智能 性能优化 算子 Ascend

在Abaqus中施加恒定载荷应选择静态还是动态分析步?

思茂信息

载荷 abaqus 有限元分析

QCA9880 vs QCA9882: Finding the Perfect Wi-Fi Solution for Your Business

wallyslilly

QCA9880 QCA9882

近期,除了“纯血鸿蒙公测”,校园开发者还有这件事要知道!

YG科技

分布式电商项目:天猫 Java 亿级高并发架构设计笔记

程序员高级码农

数据库 高并发 电商 分布式, 消息列队

采集医药行业数据,赋能企业创新与决策

八爪鱼采集器︱RPA机器人

爬虫 采集

开发小程序使用AI需要花多长时间?效果又是怎样?

Geek_2305a8

盘点15款国内外社交聆听工具

八爪鱼采集器︱RPA机器人

爬虫 采集

如何用支付宝实现靠脸吃饭

盐焗代码虾

支付宝 刷脸支付 一脸通行

HTML5中手势原理分析与数学知识的实践_行业深度_郭晓东_InfoQ精选文章