必威-必威-欢迎您

必威,必威官网企业自成立以来,以策略先行,经营致胜,管理为本的商,业推广理念,一步一个脚印发展成为同类企业中经营范围最广,在行业内颇具影响力的企业。

这篇博文主要是解析了移动端常用手势的原理,

2019-12-08 20:38 来源:未知

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

2017/08/08 · HTML5 · 1 评论 · 手势

原文出处: 郭东东   

引言

最近在重构之前上架的一款画板应用,期间用到了一些UIView的transform相关的特性。借此机会也系统整理了一下transform相关的知识。在进入正题之前需要补充一点线性代数(数学专业应该叫高等代数)相关的知识。

一、CGAffineTransform介绍

一些感想

我最早系统地学习线性代数是在大二时候,当时特意选修了学校物理系开设的4学分的线代,大概也就是比我们自己专业的线代多了一章向量空间的内容,其实最后上完发现,整个课程内容还是偏向于计算,对线性代数的几何直觉少有提起,对线性代数的实际运用更是鲜有涉及。同济的那本薄薄的如同九阴真经一般的教材,把线性代数讲的云里雾里,当时一个人在自习教室度过多少不眠之夜,一点一点去思考其概念定理背后的实际意义,多半也是边猜边想,苦不堪言。直到多年以后,有幸在网上听到了MIT的Strang老师开设的线代公开课,才对一些基础概念渐渐明朗,虽然至今又过去了很多年,但是对一些本质的理解,依然清晰。
不过,仔细想想,国内的教材写的云里雾里,才促使了我自发的思考,如果一切得来太容易,也许就不会那么刻骨铭心。我很早之前就想过这个问题,国内的教科书作者简直就是在下一盘大棋,自己出版的书写的高深莫测,翻译国外的书又翻译的含糊曲折,那么留给学生的只有两条路,要么去看原版的英语书,要么就是自己一点点看云雾缭绕的国产书,边猜边想边证明,不管走哪条路,都能走向成功。
最近,在youtube上看到了3Blue1Brown的Essence of linear algebra这门课,有种如获至宝的感觉,整个课程的时间并不长,但是对线性代数的讲解却十分到位,有种浓缩版的Gilbert Strang线代课程的感觉。希望通过这个课程,重温一下Linear Algebra。

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

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

所谓齐次坐标系就是将一个原本是n维的向量用一个n+1维向量来表示。对于一个向量v以及基oabc,可以找到一组坐标使得v=v1a+v2b+v3c。而对于一个点p,则可以找到一组坐标使得p

CGAffineTransform可以使控件的产生移动、缩放、旋转效果,其坐标系统采用的是二维坐标系,坐标原点为屏幕的左上角,向右为x轴正方向,向下为y轴正方向。

Essence of linear algebra preview

  • 线性代数使用场合
![](https://upload-images.jianshu.io/upload_images/3810750-309e9f355d4c93a9.png)



讲师在课程中说道:许多学生学完了线代,会进行许多的计算,比如算行列式,算特征值,特征向量,算矩阵乘积,但是却不理解为什么矩阵的乘法这样定义,为什么cross
product会和determinant(行列式)有关系,或者特征值究竟代表的是什么东西,其实这也是我当时学线代时候的疑问,书上并没有很明确的解释,也没有这样的视频课程来给你阐述,一切都是要靠自己去想。讲师指出,很多学生对这些概念背后的几何意义含糊不清,但是实际上,会进行线性代数的数值运算和真正在几何层面理解线性代数概念,完全不是一个level。几何意义的理解可以让你知道什么时候用什么数学工具来解决实际的问题,并且可以解释其结果的意义。当实计算结果这件事,交给计算机来做就行了。课堂上应该花大力气讲解概念,而不是计算,如果真的要讲计算,也应该是教会学生用matlab这样的工具。求逆矩阵,求代数余子式,求特征值什么的,还不是分分钟的事。
  • 课程的目录
![](https://upload-images.jianshu.io/upload_images/3810750-cd7736608ad85f50.png)

引言

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

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

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

  • 拖动: drag
  • 双指缩放: pinch
  • 双指旋转: rotate
  • 单指缩放: singlePinch
  • 单指旋转: singleRotate

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

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

  • o = p1a + p2b + p3c从上面对向量和点的表达,我们可以看出为了在坐标系中表示一个点我们可以把点的位置看作是对于这个基的原点o所进行的一个位移,即一个向量p
  • o,我们在表达这个向量的同时用等价的方式表达出了点p: p = o + p1a + p2b + p3c。,是坐标系下表达一个向量和点的不同表达方式。这里可以看出,虽然都是用代数分量的形式表达向量和点,但表达一个点比一个向量需要额外的信息。如果我写一个代数分量表达,谁知道它是个向量还是一个点。我们现在把,写成矩阵的形式:

Vectors, what even are they?

  • 向量的三种理解
    讲师上来就开门见山说道:The fundamental, root-of-it-all building block for linear algebra is the vector.
    向量是线性代数的基石(国外课程往往从向量开始说起,也就是从本质入手,国内则上来先定义逆序数,计算行列式,代数余子式,很容易把学生带偏),对向量的理解可以有三种角度:物理系学生的角度、计算机系学生的角度以及数学系学生的角度。
    物理系:向量是一个矢量(arrows pointing in space), 或者说是一个在空间中有指向的箭头,定义这个向量,需要它的长度以及它指向的方向两个方面。在平面上的向量是二维的,在空间中的向量是三维的。
    计算机系:向量是ordered lists,并且在这些lists中存放的内容是numbers。
    数学系: a vector can be anything (-_-|||) 它们之间可以相加,相乘,也可以被数乘。

  • 向量的几何意义
    不同于物理,在线代的领域里,把vector放在一个坐标系中,比如xy坐标系,其出发点在原点。

![](https://upload-images.jianshu.io/upload_images/3810750-1b63f17067122e08.png)



比如这个向量,其数字的意义代表从该向量的起点(也就是原点)到终点分别在x轴和y轴上的距离,正负号代表方向。三维空间一样,只是多了一个Z轴。
  • 向量加法的几何意义
![](https://upload-images.jianshu.io/upload_images/3810750-c26fc1ef0a2ba7f1.png)



三角形法则,好比有2只蚂蚁在一张纸上,第一只蚂蚁向上走2步向右走1步,然后再向下走1步,向右走3步。第2只蚂蚁直接向上走1步,向右走4步,就能和第一只蚂蚁站在相同的位置。也就是说第一只蚂蚁两次行动叠加之后所处的位置,和第二只蚂蚁一次行动是一致的。再进一步理解,其实要达到向右4步,向上1步的那个位置,有无数种走法,第一只蚂蚁的两次行动只是其中的一种分解。它也可以走10次走到那个位置。



![](https://upload-images.jianshu.io/upload_images/3810750-178449bd1269a00e.png)
  • 向量乘法的几何意义
![](https://upload-images.jianshu.io/upload_images/3810750-440e30ba7f183600.png)

乘以大于1的数值,就是将这个向量拉伸



![](https://upload-images.jianshu.io/upload_images/3810750-d22ed3078551394d.png)

乘以小于1的数值,就是将这个向量压缩



![](https://upload-images.jianshu.io/upload_images/3810750-89ceab7a9d7325a5.png)

乘以负数,就是将这个向量翻转



拉伸,压缩,翻转向量的行为,统称为scaling,而这些数值本身,称之为scalars



![](https://upload-images.jianshu.io/upload_images/3810750-c1dd678499cb5553.png)



![](https://upload-images.jianshu.io/upload_images/3810750-9179754f5db47ded.png)

实现原理

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

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

图片 11-4图片 21-5

二、方法介绍

Linear combinations, span, and basis vectors

图片 3

把这里的3和-2都看作是一个scalar,它们对原点的单位向量i和j进行scaling

图片 4

于是,该(3,-2)向量就变成了两个scaling过的单位向量的和。

图片 5

i和j是xy坐标系中的基础向量(basis vectors)

[其实也可以选择不同的basis vectors,比如说在平面上任意的两个向量作为基,这样得到的scalars的数值是不相同的,但是同样可以通过对这一对任意选择的basis vectors进行linear combination,而得到在平面上的任意向量。详见视频]

  • 线性组合

    图片 6

**Linear
Combination**的几何意义如图所示,完整上来说,其实是向量之间的线性组合,其主体是向量,线性组合是一个操作,将各个向量scaling之后,相加在一起,就得到了参与操作的向量之间的一个Linear
Combination。
  • 线性组合的不同情况
![](https://upload-images.jianshu.io/upload_images/3810750-a0757c288f561bdd.png)

如果参与组合的一对向量不共线,那么由它们进行线性组合所得到的向量可以达到平面上的任意一个点



![](https://upload-images.jianshu.io/upload_images/3810750-d66f8d36590de0d2.png)

如果参与组合的一对向量共线,那么由它们进行线性组合所得到的向量的终点被限制在一条通过原点的直线上



![](https://upload-images.jianshu.io/upload_images/3810750-57fa819fad559b34.png)

如果参与组合的一对向量都是零向量,那么由它们进行线性组合所得到的向量永远是零向量
  • span
    span : 是一组集合,它包含两个向量之间的全部线性组合

    图片 7

如果你面对的是一组向量,那么考虑这些向量的坐标点。  
三维空间中,两个不共线的向量之间的span,也就是它们全部线性组合的集合,是一个由这两个向量所张成的平面。  
如果在三维空间中,有3个向量,其中有2个共线,那么它们3者之间的线性组合所形成的set,只是三维空间中的一个平面,其中有一个向量是多余的(redundant),因为span的set由两个向量便可以决定。而这两个共线的向量被称之为**线性相关**(Linearly
dependent)

**线性无关**(Linearly
independent)的两个向量,不能通过scaling得到对方,其在平面上的几何意义是不共线  

![](https://upload-images.jianshu.io/upload_images/3810750-0e9c92e4b6ee72c5.png)

二维空间的linearly independent



![](https://upload-images.jianshu.io/upload_images/3810750-5a7bf6e8685261ea.png)

三维空间的linearly independent
  • basis的定义

The basis of a vector space is a set of linearly independent vectors that span the full spaces.
对于任意一个向量空间而言,它的基是一组相互之间线性独立的向量的集合,这些向量之间通过线性组合,可以span整个向量空间。

基础数学知识函数

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

拖动:drag

这里是坐标基矩阵,左边的行向量分别是向量v和点p在基下的坐标。这样,向量和点再同一个基下就有了不同的表达:三维向量的第四个代数分量是0,而三维点的第四个代数分量是1。像这种用四个代数分量表示三维几何概念的方式是一种齐次坐标表示。这样,上面的如果写成,它就是个向量;如果是它就是个点。由于齐次坐标使用了4个分量来表达3D概念或者说用了3个分量来表达2D概念,从而使得放射变换可以使用矩阵进行。

下面以使用一个UIImageView图片为例,结合UIView动画、手势进行演示

Linear transformations and matrices

  • Linear transformations
    讲师说道,transformations其实无非是function的fancy说法,本质上也是input和output,输入一个vector,经过某个transformation之后,得到一个输出的vector。整个过程,可以看作是输入的vector移动到了输出的vector的位置。考虑整个平面上的vector,在经过transformation之后,得到了一个最新的位置。
![](https://upload-images.jianshu.io/upload_images/3810750-a23c52802fde0ef1.png)

input vectors



![](https://upload-images.jianshu.io/upload_images/3810750-0618e51ec143aaae.png)

output vectors

如果用arrow来考虑的话,会比较杂乱,仅仅考虑每个向量的终点(起点必在原点),那么就变成了平面上点的集合,那么其效果就是原来的点移动到了新的位置。



![](https://upload-images.jianshu.io/upload_images/3810750-3a3f94ec600e012e.png)

input vectors



![](https://upload-images.jianshu.io/upload_images/3810750-4cefd309505f528f.png)

output vectors

图片 8

  • Linear transformations的两大特点

    经过变换之后:

    1. 所有的直线还是直线
    2. 原点还在原来的位置
  • 描述Linear transformation

![](https://upload-images.jianshu.io/upload_images/3810750-32ec88c03d559a68.png)



给你一个输入的向量,如果表示????部分,从而得到你想要的输出的向量。

图片 9

在做线性变换之前的V向量

图片 10

在做线性变换之后的V向量

V向量在进行Linear Transformation之后,相当于-1倍的Transformed的i向量与2倍的Transformed的j向量之和,也就是说,在平面上,只需要记录i和j两个basis vectors的变化即可。

It started off as a certain linear combination of i-hat and j-hat and it ends up is that same linear combination of where those two vectors landed.
You can deduce where v must go based only on where i-hat and j-hat each landed.

图片 11

保留了Linear Transformation之前的网格,可以看到i向量在transformed之后,落在了(1,-2)的位置,而j向量在transformed之后,则落在了(3,0)的位置

图片 12

运算结果的几何意义

图片 13

更进一步,该线性变换就是把原来的i(1,0)变化到(1,-2),把原来的j(0,1)变换到(3,0)。那么,原来平面上的每一个点(x,y),通过该变换,可以得到在平面上新的x和y的位置,新旧点之间一一对应

图片 14

将这个变换提取成一个2*2的矩阵,第一列代表新i的位置,第二列代表新j的位置,新的i和j则是作为新的基

图片 15

这样的话,如果有一个向量v(5,7),那么它经过通过图中的2*2矩阵描述的线性变换之后的向量,可以由如图示的运算所得到。其几何意义是变换后的i,j作为新的基,保持原来的scalars不变,对新的基进行线性组合

图片 16

把它抽象化之后,则得到了矩阵乘法的运算公式,并且还可见其几何意义

图片 17

假如transformed之后的向量是线性相关的,那么所有平面上的点在变换之后就被压缩到了一条直线上

These transformations can be described using only a handful of numbers.
These numbers are the coordinates of where each basis vectors lands.
Matrices give us a language to describe these transformations where the columns represent those coordinates.
Matrix-vector multiplication is just a way to compute what that transformation does to a given vector.
Every time you see a matrix, you can interpret it as a certain transformation of space.
Matrices as transformation of space.

点(Point)

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

 

双指缩放:pinch

如果有一种法则T,对平面点集中的每个点A,都对应平面上唯一的一个点T,则T称为平面上的一个变换,T称为A的像。变换是函数概念的自然推广。平面上的图形由点组成,因而平面上的变换T会将一个图形C变到另一个图形T称为C的像。从这个意义上说,可以称T为几何变换。例如对图形作平移变换、旋转变换、缩放变换、对称变换等都是几何变换。在平面直角坐标系中,点A由坐标表示。在变换T下,点A的像为A',其中x'和y'都是x,y的函数:x' = f1, y' = f2因此,函数f1,f2能够确定一个平面上的变换T。如果能够从方程组中反解出x和y:x = g1, y = g2则由函数g1,g2确定了T的逆变换,记为T-1。设平面曲线C的参数方程为:x = x, y = y, t∈D其中D是函数x的定义域,则曲线C在变换T下的像T的参数方程为x = f1), y = f2, y, t∈D

@property(strong,nonatomic)UIImageView *imageView;
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.view addSubview:self.imageView];
}

-(UIImageView *)imageView{
    if (_imageView==nil) {
        _imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"aa"]];
        _imageView.frame = CGRectMake(0, 0, 100, 80);
    }
    return _imageView;
}

Matrix multiplication as composition

  • 组合变换概述
    组合变换,比如先进行一次rotation变换,再做一次sheer变换
![](https://upload-images.jianshu.io/upload_images/3810750-b8f8ebed51837ca4.png)

分步骤的变换矩阵



![](https://upload-images.jianshu.io/upload_images/3810750-8c8377d4cc796f4e.png)

该矩阵记录了这两次变换的总体效应



![](https://upload-images.jianshu.io/upload_images/3810750-b28a9b9006c20641.png)

两次分布变换的结果和一次组合变换的结果等效



![](https://upload-images.jianshu.io/upload_images/3810750-dca66296cf6249d6.png)



![](https://upload-images.jianshu.io/upload_images/3810750-f1a3c5096825e2f0.png)

先做的Rotation,再做的Shear,但是Rotation需要写在右边,右边的总是比左边的变换矩阵先操作
  • 组合变换示例
![](https://upload-images.jianshu.io/upload_images/3810750-3a33b0117f974b08.png)

i向量一开始在M1的第一列向量



![](https://upload-images.jianshu.io/upload_images/3810750-5a6a7086c83b6a47.png)

接下来i向量被进行M2变换



![](https://upload-images.jianshu.io/upload_images/3810750-5984fb76e6afd43a.png)

i向量在进行M2变换后落在了(2,1)位置



![](https://upload-images.jianshu.io/upload_images/3810750-03fba0832cbbddb9.png)

两次变换后i的最终位置



![](https://upload-images.jianshu.io/upload_images/3810750-c8835c7a0e4acd3b.png)

j向量的起始位置



![](https://upload-images.jianshu.io/upload_images/3810750-677b8de5eec8d564.png)

同理,j在变换后的位置
  • 组合变换概括(矩阵乘法几何意义)
![](https://upload-images.jianshu.io/upload_images/3810750-93195c1e8334c807.png)

抽象化之后



![](https://upload-images.jianshu.io/upload_images/3810750-81d701262f9b850e.png)

最开始的i(e,g)经过M2变换之后,落到了(ae+bg,ce+dg)上



![](https://upload-images.jianshu.io/upload_images/3810750-3f398e539558bdcb.png)

最开始的j(f,h)经过M2变换之后,落到了(af+bh,cf+dh)上



![](https://upload-images.jianshu.io/upload_images/3810750-54cc1b7b27ce296e.png)

3维空间中的情况



![](https://upload-images.jianshu.io/upload_images/3810750-4cd9e8886694e37d.png)

3维空间中的情况

向量(Vector)

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

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

图片 19

获取向量的函数:图片 20

 

双指旋转:rotate

平面图形几何变换

平移变换是将图形中的每一个点从一个位置移动到另一个位置的变换,tx,ty称为平移距离,则平移变换公式为:

图片 21图片 22平移变换

旋转变换是以某个参考点为圆心,将图像上的各点围绕圆心转动一个逆时针角度θ,变为新的坐标的变换。当参考点为时,旋转变换的公式为:

图片 23

由于:

图片 24

所以可化简为:

图片 25图片 26旋转变换

比例变换是使对象按比例因子放大或缩小的变换

图片 27图片 28比例变换


The determinant

  • 概述
    线性变换,有些是将原来的网格拉伸,有些是将原来的网格压缩,如果要定性的来描述变换,那么去测量拉伸或者压缩的程度不失为明智之举。
![](https://upload-images.jianshu.io/upload_images/3810750-94c48b27677a8f2b.png)
  • 实例
![](https://upload-images.jianshu.io/upload_images/3810750-440f5a95514cb6e5.png)

变化前



![](https://upload-images.jianshu.io/upload_images/3810750-4eac2f1a77743f25.png)

变化后



可以看到,该变换将i拉伸了3倍,而将j拉伸了2倍



![](https://upload-images.jianshu.io/upload_images/3810750-eb772333eb424e05.png)

变化之后i和j围成的方格的面积



![](https://upload-images.jianshu.io/upload_images/3810750-81ccbf95cbc618bb.png)

该线性变换将i和j原来围成的区域扩大了6倍



![](https://upload-images.jianshu.io/upload_images/3810750-e4e031bf9b914602.png)

shear变换之后,尽管网格形状改变,但是网格面积不变
  • 定义
![](https://upload-images.jianshu.io/upload_images/3810750-0158c05b598c1a00.png)

determinant定量的描述出,在经过一个线性变换之后,原来单位向量所围成面积变化的倍数



![](https://upload-images.jianshu.io/upload_images/3810750-89f0cc509c79ae77.png)

当determinant of a
transformation的值为0的时候情况,只要检验某个Transformation的determinant的值是否为0,就可知该transformation是否把原来的空间压缩到更小的维度上
  • determinant的正负含义——方向
![](https://upload-images.jianshu.io/upload_images/3810750-eabeb8b28f399485.png)



![](https://upload-images.jianshu.io/upload_images/3810750-9ba3b4031bd0460f.png)

如果空间翻转的话,则determinant的值为负



![](https://upload-images.jianshu.io/upload_images/3810750-137e3fadd02d17c1.png)

在三维空间中determinant of a transformation是体积的缩放



![](https://upload-images.jianshu.io/upload_images/3810750-483fb7a58898522e.png)

在三维空间中determinant的正负号通过右手法则确定
  • determinant的计算
![](https://upload-images.jianshu.io/upload_images/3810750-ec07ba454fa30e3c.png)

二维空间的情况



![](https://upload-images.jianshu.io/upload_images/3810750-e8457e58c9796599.png)

三维空间的情况



![](https://upload-images.jianshu.io/upload_images/3810750-4afca797169b0844.png)

向量模

代表 向量的长度,记为|a|,是一个标量,只有大小,没有方向;

几何意义代表的是以x,y为直角边的直角三角形的斜边,通过勾股定理进行计算;

图片 29

getLength函数:

图片 30

单指缩放:singlePinch

平面图形几何变换的矩阵表示

图片 31

从变换功能上可以把T2D分为四个子矩阵。其中

图片 32

是对图形的缩放、旋转、对称、错切等变换;

图片 33

是对图形进行平移变换;

图片 34

是对图形作投影变换,g的作用是在x轴的1/g处产生一个灭点,而h的作用是在y轴的1/h处产生一个灭点;i是对整个图形做伸缩变换。平移变换、旋转变换、比例变换、错切变换这4中基本变换都可以表示为3x3的变换矩阵和齐次坐标相乘的形式

平移变换的矩阵表示为

图片 35

tx,ty分别表示x轴方向和y轴方向的平移距离。

旋转变换的矩阵表示为

图片 36

逆时针旋转时θ取正值,顺时针旋转时θ取负值

比例变换的矩阵表示为

图片 37

  • 当b=d=0时,a和e的取值决定了缩放效果,a和e>1放大,<1缩小
  • 当b=d=0,a=-1,e=1时有x'=-x,y'=y产生与y轴对称的图形
  • 当b=d=0,a=1,e=-1时有x'=x,y'=-y产生与x轴对称的图形
  • 当b=d=0,a=e=-1时有x'=-x,y'=-y产生与原点对称的图形
  • 当b=d=1,a=e=0时有x'=y,y'=x产生与直线y=x对称的图形
  • 当b=d=-1,a=e=0时有x'=-y,y'=-x产生与直线y=-x对称的图形

错切变换的矩阵表示为

图片 38

其中当d = 0时,x' = x + by, y' = y,此时,图形的y坐标不变,x坐标随初值及变换系数b作线性变化;当b = 0时,x' = x,y' = dx + y,此时,图形的x坐标不变,y坐标随初值及变换系数d作线性变化。

一个比较复杂的变换要连续进行若干个基本变换才能完成。例如围绕任意点的旋转,需要通过3个基本变换T,R,T才能完成。这些由基本变换构成的连续变换序列称为复合变换。变换的矩阵形式使得复合变换的计算工作量大为减少。以绕任意点旋转为例,本应进行如下3次变换,分别是

  • p' = pT 将原点移动到任意点位置
  • p'' = p'R 旋转
  • p = p''T 将原点归位

合并之后为p = pTRT令Tc = TRT则有p = pTc,Tc称为复合变换矩阵。由上面推到可知在计算复合变换时,首先可将各基本变换矩阵按次序想乘,形成总的复合变换矩阵Tc然后,坐标只需与Tc想乘一次,便可同时完成一连串基本变换。因此采用复合变换矩阵可以大大节省坐标乘法所耗费的运算时间。下面我们看几个基本的复合变换:复合平移:对同一图形做两次平移相当于将两次平移相加起来,即

图片 39

复合缩放:以原点为参考点对同一图形做两次连续的缩放相当于将缩放操作相乘,即:

图片 40

复合旋转:以原点为参考点对同一图形做两次连续的旋转相当于将两次的旋转角度相加, 即:

图片 41

缩放、旋转变换都与参考点有关,上面进行的各种缩放、旋转变换都是以原点为参考点的。如果相对某个一般的参考点作缩放、旋转变换,相当于将该点移到坐标原点处,然后进行缩放、旋转变换,最后将点移回原来的位置。如关于的缩放变换为:

图片 42

各种复杂的变换无非是一些基本变换的组合,利用数学方法也就是矩阵的 乘法来解决复合变换问题,关键是将其分解为一定顺序的基本变换,然后逐一 进行这些基本变换;或者求出这些基本变换矩阵连乘积,即求出复合变换矩阵, 从而使复合变化问题得到解决。

写了这么多只是想把平面仿射变换的基本原理描述清楚,以便能对UIView.transform有更深入的理解。接下来我们进入正题

这里说的坐标系是UIView相对于其父视图的相对位置和大小

图片 43UIView外部坐标系

如上图以父视图左上角为坐标原点,x轴从原点向右递增,y轴从原点向下递增,通过改变UIView的frame和center可以调整UIView的位置和大小,当然UIView是对CALayer的封装也可以直接调整layer的frame和position达到相同的效果。基于此我们可以调整UIView的位置和大小,或者通过UIView的位置和大小进行适当的动画展示,当然也仅限于此,对于旋转、切变是无能为力的。

  • 设置View的frame和center会改变其位置和大小,同时会改变View的bounds,bounds是View相对于自身的尺寸bounds=(0,0,view.width,view.height)
  • 设置完成frame或者center之后可以通过调整bounds重新设置frame,如果frame = 重新设置bounds = (0,0,w',h')则新的frame=(x',y',w',h')

图片 44

  • 当然如果在设置完bounds之后再设置frame则bounds会被重置为(0,0,view.width,view.height)

UIView除了刚刚我们说的外部坐标系,还有一个内部坐标系。

图片 45UIView内部坐标系

跟笛卡尔坐标系稍微有点区别,以UIView视图中心为坐标原点,x轴从原点向右递增,y轴从原点向下递增,通过改变UIView的transform可以对其进行仿射变换,如上面我们提到的缩放、旋转、平移、切变等。有了这个特性UIView能做的事情就更多了,当然也可以借此做更有意思的动画。在内部坐标系中原点的位置可以通过anchorPoint调整,UIView没有开放出来,可以访问CALayer获取。

图片 46anchorPoint

参考上图通过调整anchorPoint的值可以修改内部坐标系的原点位置,设置可以把原点移动到View的左上角,设置可以把原点移动到右下角,设置可以把原点移动到View中心。当然anchorPoint的值也不限制在[0,1],可以推广到任意浮点值,相应的调整规则类似,比如设置为则可以把原点移动到左上角再向左上偏移一个View的位置。anchorPoint值的修改不只会调整原点位置,同时也会修改View的frame,修改规则如下:

图片 47

基于View的transform可以进行仿射变换,所有的变化都是基于原点位置进行的,因此anchorPoint的设置可以产生更多有意思的效果,后续我们一个个看

跟anchorPoint的设置一样,transform的设置也会引起frame的调整

图片 48Transform修改

见上图以旋转变换为例,旋转变换会让原有图形的frame从白色框变为虚线框,我们假设原有View的四个点为p0 p1 p2 p3 则旋转变换之后的点为:p0' = p0Tp1' = p1Tp2' = p2Tp3' = p3T则frame = (x',y',w',h')

图片 49

我们把上面提到的两个坐标系结合起来看一下

图片 50内外坐标系

影响View位置和形状的几个参数有:

  • frame
  • center
  • transform
  • bounds
  • anchorPoint

遵循如下规则:

  • 在设置transform之前可以通过frame和center调整View的大小和尺寸,frame的改变会影响bounds,设置bounds会重新修改frame和center,规则参考之前
  • View的transform参考内部坐标系,transform的改变会影响frame和center,但是不会修改bounds
  • 在设置了transform修改之后仍然可以通过调整bounds来修改frame和center也可以直接修改center,transform会根据新的bounds和center来计算新的frame,参考之前
  • anchorPoint的修改会影响transform的原点位置从而产生不同的变换效果,也会引起frame的重新计算

上面的理论知识已经写了很多了,接下来我们实际体验一下,看一下View的transform结构

struct CGAffineTransform { CGFloat a, b, c, d; CGFloat tx, ty;};

结合上面关于线性代数相关的知识,可以发现View的transform最终都转换成了矩阵运算

  • 移动控件

Inverse matrices, column space and null space

  • 线性方程组
![](https://upload-images.jianshu.io/upload_images/3810750-930e082bd495eab9.png)

coefficients,variables,constants



![](https://upload-images.jianshu.io/upload_images/3810750-e3ef4e408c1dff93.png)

constant matrix A, vector, constant vector
  • Ax = v的几何意义

其中constant matrix A代表的是一种linear transformation,求解的过程,就是要找到这样一个向量x,使得向量x在经过A的linear transformation之后,和v向量重合。When the determinant of this transformation is not zero, it turns out that there will be one and only one vector that lands on v. 要找到这个解向量,可以像倒带一样,对v向量进行A的逆操作。

图片 51

  • 逆矩阵
![](https://upload-images.jianshu.io/upload_images/3810750-bff2bd63cf8b2ba9.png)

假如说A矩阵对某个向量进行了一次transformation,那么如果再进行A逆矩阵的transformation,则可以还原该向量的原始状态,从而抵消掉A对它的作用

比如说90度逆时针旋转这个transformation的逆操作就是顺时针旋转90度

图片 52

图片 53

图片 54

图片 55

图片 56

determinant不为0,说明该变换不降维,A的逆矩阵存在

  • Rank
    在三维空间中,如果全部input在某个Linear Transformation之后,全部output在一条直线上,那么这个transformation拥有rank 1。如果全部output在一个平面上,那么这个transformation拥有rank 2。对于1个2*2的矩阵而言,它的rank最多为2。
![](https://upload-images.jianshu.io/upload_images/3810750-60239ce4fd047031.png)
  • 列空间
![](https://upload-images.jianshu.io/upload_images/3810750-40df2d4ca635c961.png)

image.png

This set of all possible outputs for your matrix, whether it's a line, a plane, 3d space, whatever, is called the column space of your matrix.
注意,列空间的对象是矩阵,矩阵的意义是一个Linear Transformation的表示,某个Linear Transformation的所有outputs的集合,称之为该matrix的column space。

图片 57

矩阵中的列向量,告诉你basis vectors所在的位置

图片 58

而其列空间就是其basis vectors的span

column space: The column space is the span of the columns of your matrix.
rank: The number of dimensions in the column space.

图片 59

线性变换的原点位置不会改变,故0向量永远在列空间之中

图片 60

full rank的矩阵,唯一在变换后落在原点的只有零向量自身

  • null space
![](https://upload-images.jianshu.io/upload_images/3810750-48f814de2a44d7ac.png)

某一个3维的线性变换,将空间压缩到一条直线上,那么将会有一整个平面上的向量被变换到零向量的位置

The set of vectors that lands on the origin is called the null space or the kernel of the Matrix.
如果某个向量空间在Linear Transformation之后,存在降维,那么就会有一系列原来不是零向量的向量落到了零向量的位置,所有这些向量的集合构成了null space

图片 61

对线性方程组而言,当V正好是0向量的时候,则该矩阵A的零空间便包含了该线性方程组全部可能的解

图片 62

可以通过列空间来判断对应的线性方程组是否有解

向量的数量积

向量同样也具有可以运算的属性,它可以进行加、减、乘、数量积和向量积等运算,接下来就介绍下我们使用到的数量积这个概念,也称为点积,被定义为公式:

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

单指旋转:singleRotate

UIView的复合变换

UIView *view = [UIView new];view.backgroundColor = [UIColor redColor];view.frame = CGRectMake(200, 200, 100, 100);[self.view addSubview:view];[UIView animateWithDuration:5 animations:^{ // 先平移 CGAffineTransform move = CGAffineTransformMakeTranslation; // 后旋转 CGAffineTransform rotation = CGAffineTransformMakeRotation; view.transform = CGAffineTransformConcat(rotation, move);}];

图片 63先平移后旋转

先不解释,我们接着再看一个变换

UIView *view = [UIView new];view.backgroundColor = [UIColor redColor];view.frame = CGRectMake(200, 200, 100, 100);[self.view addSubview:view];[UIView animateWithDuration:5 animations:^{ // 先旋转 CGAffineTransform rotation = CGAffineTransformMakeRotation; // 后平移 CGAffineTransform move = CGAffineTransformMakeTranslation; view.transform = CGAffineTransformConcat(move,rotation);}];

图片 64先旋转后平移

综合上面两个不同顺序的变换,由于View内部坐标系的原点在复合变换的过程中一直跟随View在移动因此平移和旋转的顺序会决定不同的结果。

  • 如果原点在整个变换过程中一直不变,则需要先旋转后平移
  • 如果原点在整个变换过程中一直跟随View,则需要先平移后旋转

目的就是保证旋转始终是围绕原点进行

1、CGAffineTransformMakeTranslation实现以初始位置为基准,在x轴方向上平移x单位,在y轴方向上平移y单位

Nonsquare matrices as transformations between dimensions

  • 概念
![](https://upload-images.jianshu.io/upload_images/3810750-c260ed9247eeaf5d.png)

非方阵体现了不同维数之间的变换



![](https://upload-images.jianshu.io/upload_images/3810750-4bf9ea8759bfbbb4.png)

此例中,i和j两个列向量的span(也就是列空间)是在三维空间中的一个平面,而这个矩阵依旧是full
rank的



![](https://upload-images.jianshu.io/upload_images/3810750-deb79011263706eb.png)

行数代表的是列向量的维数,此例中,列向量是落在三维空间中的平面上的,这是一个从三维空间到二维空间的变换

共线定理

共线,即两个向量处于 平行 的状态,当a=(x1,y1),b=(x2,y2),则存在唯一的一个实数λ,使得a=λb,代入坐标点后,可以得到 x1·y2= y1·x2;

因此当x1·y2-x2·y1>0 时,既斜率 ka > kb ,所以此时b向量相对于a向量是属于顺时针旋转,反之,则为逆时针;

Tips :

AnchorPoint

如果不修改AnchorPoint则所有的变化都是基于View的中心进行,但是可以通过修改anchorPoint改变原点的位置从而改变变换的效果

UIView *view = [UIView new];view.backgroundColor = [UIColor redColor];view.frame = CGRectMake(200, 200, 100, 100);[self.view addSubview:view];view.layer.anchorPoint = CGPointMake;[UIView animateWithDuration:5 animations:^{ view.transform = CGAffineTransformMakeRotation;}];

图片 65绕点旋转

如上图可以实现绕点旋转的效果

// 格式
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
// 使用       将图片左(100px)下(150px)方向移动
CGAffineTransform transform = CGAffineTransformMakeTranslation(-100, 150);
self.imageView.transform = transform;

Dot products and duality

  • 基本运算
![](https://upload-images.jianshu.io/upload_images/3810750-7f89311720aec68a.png)
  • 点积的几何意义
![](https://upload-images.jianshu.io/upload_images/3810750-56eab96600c1c830.png)

把w投射到v所在的直线上,将w在v上投影的长度乘以v的长度,就是其点积的值



![](https://upload-images.jianshu.io/upload_images/3810750-67962fb414ad2a23.png)

如果w的投影和v的方向相反,则点积为负
  • 点积的几种情况
![](https://upload-images.jianshu.io/upload_images/3810750-d3214b0c20dce432.png)



![](https://upload-images.jianshu.io/upload_images/3810750-8a7c77cc5a13063c.png)



![](https://upload-images.jianshu.io/upload_images/3810750-66c091bd772a4d78.png)
  • 点积的结果和顺序无关
![](https://upload-images.jianshu.io/upload_images/3810750-eddc131188bb516a.png)

v和w恰好相等的情况下



![](https://upload-images.jianshu.io/upload_images/3810750-f6c402e4d2f1fe1c.png)

如果v扩大了2倍,并不会改变w在v上投影的长度,因此等式直观成立,反之亦然
  • Duality

Duality: Natural-but-surprising correspondence
the dual of a vector is the linear transformation that it encodes
the dual of a linear transformation from some space to one dimension is a certain vector in that space

图片 66

假如说有一个线性变换,使得i落在1而j落在-2的位置

图片 67

而被变换的向量v可以拆解成如图

图片 68

基于Linearality,在变换之后,v是4倍的变换后的i,3倍变换后的j,由于在同一数轴上,合成后是-2

图片 69

两个向量的点积的效果和一个向量进行降维transfrom一样

图片 70

1*2的矩阵和2维向量之间存在关系,一个2d vector有其associated matrix,反之亦然。1*2的矩阵表示某个Linear Transformation,它能够将一个2维的vector变成1维的数字,而这个2维的vector本身是和这个矩阵所表示的Linear Transformation是相关联的

图片 71

假设有一条相对于正坐标系倾斜的数轴,u落在其1坐标的位置

图片 72

将正坐标系中的2维向量投射到这个数轴上

图片 73

其实就相当于定义了一个从2维向量到1维数字的线性变换

图片 74

u其实还是正坐标系中的一个2维向量,只是正好也落在了这个给定的倾斜数轴之上

图片 75

可以找到一个1*2的矩阵来描述这个线性变换

图片 76

要找到这个矩阵,就是要看原来的i和j,在变换后落在了哪个位置,它们最后落点的位置,便是这个1*2矩阵的列

图片 77

i和u都是单位向量,把i投射到u上,和把u投射到i上是对称的,j同理。那么,原来的i在u上投影后的落点,其实和u在正坐标系x轴上落点的数值是相同的,也就是u的横坐标

图片 78

这样就建立起u这个向量和[ux uy]这个线性变换之间的关系。So the entries of the 1*2 matrix describing the projection transformation are going to be the coordinates of u-hat。u向量的坐标因为对偶性,和描述线性变换的1*2矩阵的两列是相等的

图片 79

由于这样的关系,某一个向量和单位向量作点积运算的值,可以解释成将该向量投影到单位向量所在直线上之后所得到的长度。如果某一个向量和非单位向量作点积运算,由于线性变换的特性,可以看成是先在单位向量上进行投影,然后再乘以非单位向量扩大的倍数,也就是该非单位向量的长度

图片 80

向量也可以理解成某一个线性变换的概念性的缩写记号

旋转角度

通过数量积公式我们可以推到求出两个向量的夹角:

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

然后通过共线定理我们可以判断出旋转的方向,函数定义为:

图片 81

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

综合应用

借用一个案例来对transform做一个综合的应用,这个案例也是从实际项目中产生的。先看最终效果:

图片 82综合应用

最近在用一些零散的时间重构之前上架的一款画板应用,希望为画布增加更加灵活的操作方式,在双指拖拽画布的同时可以实现定点的缩放和旋转,可以通过双指点击完成笔迹的撤销,通过三指点击完成笔迹的重做。

把问题拆解一下,为了达到上面展示的效果,需要解决以下问题:

  • 手势的控制,双指拖拽,双指捏合,双指旋转
  • 处理各手势之间的冲突和配合
  • 处理View的平移、旋转、缩放复合变换
  • 其中旋转和缩放变换要以双指连线的中点为旋转或缩放中心

综合分析以上问题首先需要为画布增加一个容器,然后才能在容器上添加手势,通过手势控制画布的frame和transform

/// 画布var canvasView: UIView? = nil { didSet { if self.canvasView != nil { self.addSubview(self.canvasView!); self.canvasView?.backgroundColor = UIColor.white; // 移动到容器中心 self.canvasView!.center = CGPoint(x: self.bounds.size.width/2, y: self.bounds.size.height/2); // transform归零,设置为单位矩阵 self.canvasView!.transform = CGAffineTransform.identity; } }}

添加需要的手势

// 双指点击let doubleTouchesGesture = UITapGestureRecognizer(target: self, action: #selector(gestureRecognizer));doubleTouchesGesture.numberOfTapsRequired = 1;doubleTouchesGesture.numberOfTouchesRequired = 2;doubleTouchesGesture.delegate = self;self.addGestureRecognizer(doubleTouchesGesture);// 三指点击let tripleTouchesGesture = UITapGestureRecognizer(target: self, action: #selector(gestureRecognizer));tripleTouchesGesture.numberOfTapsRequired = 1;tripleTouchesGesture.numberOfTouchesRequired = 3;tripleTouchesGesture.delegate = self;self.addGestureRecognizer(tripleTouchesGesture);// 缩放let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(gestureRecognizer));pinchGesture.delegate = self;self.addGestureRecognizer(pinchGesture);// 移动let panGesture = UIPanGestureRecognizer(target: self, action: #selector(gestureRecognizer));panGesture.minimumNumberOfTouches = 2;panGesture.delegate = self;self.addGestureRecognizer(panGesture);// 旋转let rotationGesture = UIRotationGestureRecognizer(target: self, action: #selector(gestureRecognizer));rotationGesture.delegate = self;self.addGestureRecognizer(rotationGesture)

我们需要旋转、移动和缩放同时触发并且在触发旋转、移动或者缩放的时候双指点击不能被触发,但是如果用户使用三指点击时,三指手势要优先触发。因此需要对手势的delegate做一点处理

// MARK: - UIGestureRecognizerDelegateextension CanvasContentView: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { // 各手势之间要并发进行 return true; } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { if (gestureRecognizer is UIPanGestureRecognizer || gestureRecognizer is UIRotationGestureRecognizer || gestureRecognizer is UIPinchGestureRecognizer) && otherGestureRecognizer is UITapGestureRecognizer { // 移动、旋转、缩放时要避免双指点击触发 if otherGestureRecognizer.numberOfTouches == 3 { // 三指点击时用户意图明显,因此要优先触发 return false; } return true; } return false; }}

这样各种手势就可以相互配达到我们的需求

图片 83绕固定点旋转

如上图,如果是画布绕其中心旋转是很容易实现的,不需要调整View原点位置直接旋转θ角度即可。如果旋转点不在画布中心处理起来就要麻烦一点。有两种方案可以实现

  • 1、调整anchorPoint把View坐标原点移动到旋转点位置,然后通过transform设置让View旋转θ
  • 2、拆解绕点旋转变换为:先把View中心移动到目标位置,然后旋转θ角度

分析一下看一下哪种方案更合适,如果调整anchorPoint必然会引起frame的改变,也就是center位置的变化,需要在anchorPoint调整之后恢复center的位置,另外如果View在初始状态是比较容易通过旋转中心点的坐标推算出anchorPoint的新位置,但是一旦View发生了旋转就很难再计算出新的anchorPoint的位置。而方案2只需要计算出旋转过程中View中心点的位置变化即可。根据之前的理论知识坐标系中的一个点绕另一个点的旋转变换可以表示为:

图片 84

化简之后为:

图片 85

看一下部分代码实现:

private func rotateAt(center: CGPoint, rotation: CGFloat) { self.gestureParams.rotation = self.gestureParams.rotation + rotation; // x = cosθ - sinθ + x0 // y = cosθ + sinθ + y0 let x1 = self.canvasView!.center.x; let y1 = self.canvasView!.center.y; let x0 = center.x; let y0 = self.bounds.size.height - center.y; let x =  * cos -  * sin + x0 let y =  * cos +  * sin + y0; self.canvasView!.center = CGPoint(x: x, y: y); self.canvasView!.transform = CGAffineTransform.identity.rotated(by: self.gestureParams.rotation).scaledBy(x: self.gestureParams.scale, y: self.gestureParams.scale);}

图片 86以固定点为中心缩放

跟旋转类似以固定点为中心的缩放依然可以选择两种方案,我们依然以选择第二中方案,先把中心点移动到目标位置然后进行缩放变换矩阵表示为:

图片 87

化简为:

图片 88

看一下部分代码

private func scaleAt(center: CGPoint, scale: CGFloat) { // x' = Sx + x0 // y' = Sy + y0 let formerScale = self.gestureParams.scale; self.gestureParams.scale = scale * self.gestureParams.scale; self.gestureParams.scale = min(max(self.minScale, self.gestureParams.scale), self.maxScale); let currentScale = self.gestureParams.scale/formerScale; let x = self.canvasView!.center.x; let y = self.canvasView!.center.y; let x1 = currentScale * (x - center.x) + center.x; let y1 = currentScale * (y - center.y) + center.y; self.canvasView!.center = CGPoint(x: x1, y: y1); self.canvasView!.transform = CGAffineTransform.identity.rotated(by: self.gestureParams.rotation).scaledBy(x: self.gestureParams.scale, y: self.gestureParams.scale);}

最主要的问题其实都已经解决掉了,接下来就是把手势信息转换为我们需要的数据即可,这里不做过多的解释了,直接贴代码:

// MARK: - Gesturesextension CanvasContentView { @objc func gestureRecognizer(gesture: UIGestureRecognizer) { if self.canvasView != nil { switch gesture { case is UIPinchGestureRecognizer: let pinchGesture = gesture as! UIPinchGestureRecognizer; if pinchGesture.state == .began || pinchGesture.state == .changed { // 计算缩放的中心点和缩放比例,每次缩放的比例需要累计 var center = pinchGesture.location; if pinchGesture.numberOfTouches == 2 { let center0 = pinchGesture.location(ofTouch: 0, in: self); let center1 = pinchGesture.location(ofTouch: 1, in: self); center = CGPoint(x: (center0.x + center1.x)/2, y: (center0.y + center1.y)/2); } self.scaleAt(center: center, scale: pinchGesture.scale); pinchGesture.scale = 1; self.delegate?.canvasContentView(self, scale: self.gestureParams.scale); } break; case is UIPanGestureRecognizer: let panGesture = gesture as! UIPanGestureRecognizer; let location = panGesture.location; if panGesture.state == .began { // 记录开始位置 self.gestureParams.from = location; self.gestureParams.lastTouchs = gesture.numberOfTouches; }else if panGesture.state == .changed { if self.gestureParams.lastTouchs != panGesture.numberOfTouches { self.gestureParams.from = location; } // 计算偏移量 self.gestureParams.lastTouchs = panGesture.numberOfTouches; let x = location.x - self.gestureParams.from.x; let y = location.y - self.gestureParams.from.y; self.gestureParams.from = location; self.translate(x: x, y: y); self.delegate?.canvasContentView(self, x: x, y: y); } break; case is UIRotationGestureRecognizer: let rotatioGesture = gesture as! UIRotationGestureRecognizer; if rotatioGesture.state == .began || rotatioGesture.state == .changed { // 计算旋转的中心点和旋转角度,每次旋转的角度需要累计 var center = rotatioGesture.location; if rotatioGesture.numberOfTouches == 2 { let center0 = rotatioGesture.location(ofTouch: 0, in: self); let center1 = rotatioGesture.location(ofTouch: 1, in: self); center = CGPoint(x: (center0.x + center1.x)/2, y: (center0.y + center1.y)/2); } self.rotateAt(center: center, rotation: rotatioGesture.rotation); rotatioGesture.rotation = 0; self.delegate?.canvasContentView(self, rotation: self.gestureParams.rotation); } break; case is UITapGestureRecognizer: let tapGesture = gesture as! UITapGestureRecognizer; if tapGesture.numberOfTouches == 2 { self.delegate?.canvasContentView(self, tapTouches: 2); }else if tapGesture.numberOfTouches == 3 { self.delegate?.canvasContentView(self, tapTouches: 3); } break; default: break; } } }}

写了很多,总结一句,UIView在二维状态下的形变多数情况都可以转换为仿射变换或者多个仿射变换的复合变换,从而用矩阵运算的知识解决。以后再遇到比较有意思的问题我会继续补充……

2、CGAffineTransformTranslate在已有的transform基础上,增加 移动 效果

Cross products

  • 2维讨论
![](https://upload-images.jianshu.io/upload_images/3810750-bb4b4dfe1520415a.png)

v和w的叉积,就是它们所围城的这个平行四边形的面积



![](https://upload-images.jianshu.io/upload_images/3810750-0c4a9f7c1654b967.png)

v在w右侧,面积为正



![](https://upload-images.jianshu.io/upload_images/3810750-167560738048a89a.png)

v在w左侧,面积为负



![](https://upload-images.jianshu.io/upload_images/3810750-fe9124102a683713.png)



![](https://upload-images.jianshu.io/upload_images/3810750-b3419498fafeee90.png)

计算v和w的叉积,只需计算它们所构成的矩阵的determinant。Determinant本身就是度量线性变换前后的比例



![](https://upload-images.jianshu.io/upload_images/3810750-0c0286f069d67762.png)
  • 基本概念
![](https://upload-images.jianshu.io/upload_images/3810750-289e94e12fe5c99a.png)

真正叉积的结果不是一个数值,而是一个向量,两个向量的叉积,生成第三个向量,生成的向量的长度和两个向量所围成的平行四边形的面积相等,而它的方向和平行四边形所在的面相垂直



![](https://upload-images.jianshu.io/upload_images/3810750-38769c1f03d71f15.png)

其方向由右手法则所定
  • 运算
![](https://upload-images.jianshu.io/upload_images/3810750-ab4742150932347e.png)



![](https://upload-images.jianshu.io/upload_images/3810750-7d8bcee0c15f5120.png)
  • 运算公式背后的几何意义
![](https://upload-images.jianshu.io/upload_images/3810750-eea6b8fcb5d8ac35.png)

前一章对偶性中提到的一个向量有其相对应的线性变换矩阵,对任意一个向量x,y作线性变换,其结果和与这个线性变换的矩阵所关联的向量作点积是相同的



![](https://upload-images.jianshu.io/upload_images/3810750-3c603b3fce799d47.png)



![](https://upload-images.jianshu.io/upload_images/3810750-a0cf166ac01cb3cf.png)



![](https://upload-images.jianshu.io/upload_images/3810750-360558e3a38f98c7.png)

第一步,假设存在这样一个函数,输入任意一个三维向量,输出一个det的值,由v和w及输入的向量u决定。这便是一个从3d到1d的线性变换。其几何意义是该3个向量所围成的平行六面体的体积



![](https://upload-images.jianshu.io/upload_images/3810750-40c06b7b0b4a355a.png)

因为这个变换是线性的,可以用某一个矩阵来描述它



![](https://upload-images.jianshu.io/upload_images/3810750-c487bd3690bb193b.png)

由于对偶性,可以将这个矩阵立起来,作为该矩阵对应的向量,并看成其与x,y,z向量的点积



![](https://upload-images.jianshu.io/upload_images/3810750-3c670522da6877ff.png)

左侧点积的结果和P向量的坐标相同



![](https://upload-images.jianshu.io/upload_images/3810750-fedab6c6257c1f5e.png)



![](https://upload-images.jianshu.io/upload_images/3810750-e5e5124802784e63.png)

什么样的向量p才能满足,p和x,y,z向量点乘之后的值 =
x,y,z向量与v、w向量所围成的平行六面体的体积



![](https://upload-images.jianshu.io/upload_images/3810750-af668c721f5d9e00.png)

点乘的几何意义,是投影长度的乘积



![](https://upload-images.jianshu.io/upload_images/3810750-8a188a41f1646b4e.png)

假如说p没有垂直于v和w所构成的平面,那么p,w,v所构成的平行六面体的体积,是p在垂直于v,w平面上的分量去乘以v和w围成的平行四边形的面积



![](https://upload-images.jianshu.io/upload_images/3810750-15145dff43a157f5.png)

这与用x,y,z向量和垂直于v和w,且长度等于平行四边形面积的向量作点乘的结果是一致的

矩阵与变换

由于空间最本质的特征就是其可以容纳运动,因此在线性空间中,

我们用向量来刻画对象,而矩阵便是用来描述对象的运动;

实现原理

// 格式  
CGAffineTransformTranslate(CGAffineTransform t,
  CGFloat tx, CGFloat ty)
// 使用
self.imageView.transform = CGAffineTransformTranslate(self.imageView.transform, -50, 150); 

Change of basis

  • 基本定义
![](https://upload-images.jianshu.io/upload_images/3810750-f1d7b77ce998439c.png)



![](https://upload-images.jianshu.io/upload_images/3810750-93a415c5db7eaac1.png)



![](https://upload-images.jianshu.io/upload_images/3810750-042be0660610cbf2.png)

同一个向量,如果选取的basis vectors不同,则其scalars便不同



![](https://upload-images.jianshu.io/upload_images/3810750-2ea520b87863f23a.png)

在正坐标系中,b1和b2被表示成如图



![](https://upload-images.jianshu.io/upload_images/3810750-0097ee9b0dea0f52.png)

而从变换basis vectors的角度看,b1和b2还是(1,0)和(0,1)
  • 运算
![](https://upload-images.jianshu.io/upload_images/3810750-00c7e67c1df9cb87.png)

矩阵的列是在正坐标系下的b1和b2的坐标,(-1,2)是在b1,b2坐标系下的v的坐标,相乘后得到的结果,便是在正坐标系下,v的坐标



![](https://upload-images.jianshu.io/upload_images/3810750-9f8485fdea80fc0d.png)



![](https://upload-images.jianshu.io/upload_images/3810750-f70a15c6f00664c9.png)
  • 把正坐标系下的线性变换翻译成变换基的坐标系下的变换
![](https://upload-images.jianshu.io/upload_images/3810750-182f7220c272ee51.png)

此例中,在我们的正坐标系下是一个旋转90度的变换



![](https://upload-images.jianshu.io/upload_images/3810750-2baa1ec3fac65761.png)



![](https://upload-images.jianshu.io/upload_images/3810750-a35c66fce0bd4020.png)

三个矩阵乘积的结果便是在Jennifer坐标系下的旋转90度的变换



![](https://upload-images.jianshu.io/upload_images/3810750-b80979e0907b113e.png)



![](https://upload-images.jianshu.io/upload_images/3810750-69e6a7f56c39c287.png)

中间的M是在你坐标系下的变换

而矩阵是如何描述运动的呢?

我们知道,通过一个坐标系基向量便可以确定一个向量,例如 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),是基向量引起坐标系变化,然后坐标系沿用关联导致了向量的变化;

图片 89

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


Eigenvectors and eigenvalues

  • 背景
![](https://upload-images.jianshu.io/upload_images/3810750-ee15fb0a0a9f6ace.png)

在某一个向量经过某个线性变换之后,它所在的新的位置和原先所在位置经过的直接之间一般都会有所偏离



![](https://upload-images.jianshu.io/upload_images/3810750-4b5ab4cbf574d737.png)

但是有一些向量,在经过线性变换之后,它仍然在经过它原先位置的直线上,线性变换对它的作用仅仅是压缩或者拉伸了



![](https://upload-images.jianshu.io/upload_images/3810750-9f56c1df39ed9e6f.png)

对于上例矩阵所描述的线性变换,这些线上的向量还是在原来位置



![](https://upload-images.jianshu.io/upload_images/3810750-3c99ff6cb7952f0b.png)

这些待在原来位置的特殊的向量,就被称为该矩阵的特征向量



![](https://upload-images.jianshu.io/upload_images/3810750-259907528cdff597.png)

这些特征向量相对于原来向量的缩放比例,即scalar便是特征值
  • 应用
![](https://upload-images.jianshu.io/upload_images/3810750-065777c579c6fa2e.png)

一个3维的物体,其特征向量是它的旋转轴



![](https://upload-images.jianshu.io/upload_images/3810750-1d46d1c6f66d075e.png)

找到特征向量,便可以减少依赖于自己定义的坐标系,更易于理解线性变换的作用



![](https://upload-images.jianshu.io/upload_images/3810750-1e1a03f1cf7e5c29.png)



![](https://upload-images.jianshu.io/upload_images/3810750-e5f6b4bb65affbaf.png)
  • 计算
![](https://upload-images.jianshu.io/upload_images/3810750-c421b590cd75b430.png)



![](https://upload-images.jianshu.io/upload_images/3810750-7dc51e7adcfa3358.png)

如果等式成立,并且有非0的v向量,则一定存在降维,才会把原来不为0的向量,压缩到0向量上来,所以A-λI这个矩阵一定不是满秩的,也就是说其行列式的值为0
  • 对角矩阵
![](https://upload-images.jianshu.io/upload_images/3810750-0687870c3c36d4a0.png)

对角矩阵所有的基向量都是特征向量,对角线上的值便是它的特征值



![](https://upload-images.jianshu.io/upload_images/3810750-89604b9cf6697c08.png)

对于正坐标系下的变换矩阵A,算出它的两个特征向量(1,0)和(-1,1)之后,将这个A变换翻译成以A矩阵的特征向量为基下的变换

图片 90

新得到的矩阵必然是对角的,并且对角元为对应的特征值,因为以特征向量为基向量的变换中,只有缩放的变换,因此i和j在变换后,只是乘上scalar

结合代码

其实CSS的transform等变换便是通过矩阵进行的,我们平时所写的translate/rotate等语法类似于一种封装好的语法糖,便于快捷使用,而在底层都会被转换成矩阵的形式。例如transform:translate(-30px,-30px)编译后会被转换成transform : matrix(1,0,0,1,30,30);

通常在二维坐标系中,只需要 2X2 的矩阵便足以描述所有的变换了, 但由于CSS是处于3D环境中的,因此CSS中使用的是 3X3 的矩阵,表示为:

图片 91

其中第三行的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)

1
2
3
4
5
6
7
8
// 发生偏移,但基向量不变;
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);

图片 92

基础数学知识函数

  • 缩放控件

Abstract vector spaces

图片 93

函数其实也具有某种向量的性质

图片 94

图片 95

图片 96

图片 97

图片 98

图片 99

图片 100

选取basis functions,就类似于选取了basis vector

图片 101

多项式空间的基有无穷多

图片 102

图片 103

图片 104

图片 105

图片 106

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!!函数如下:

图片 107

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

1、CGAffineTransformMakeScale实现以初始位置为基准,在x轴方向上缩放x倍,在y轴方向上缩放y倍

手势原理

接下来我们将上面的函数用到实际环境中,通过图示的方式来模拟手势的操作,简要地讲解手势计算的原理。希望各位大神理解这些基础的原理后,能创造出更多炫酷的手势,像我们在mac触控板上使用的一样。

下面图例:

圆点: 代表手指的触碰点;

两个圆点之间的虚线段: 代表双指操作时组成的向量;

a向量/A点:代表在 touchstart 时获取的初始向量/初始点;

b向量/B点:代表在 touchmove 时获取的实时向量/实时点;

坐标轴底部的公式代表需要计算的值;

点(Point)

// 格式       tx,ty表示的是倍数
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
// 使用       将图片放大2倍
self.imageView.transform = CGAffineTransformMakeScale(2, 2);

Drag(拖动事件)

图片 108

上图是模拟了拖动手势,由A点移动到B点,我们要计算的便是这个过程的偏移量;

因此我们在touchstart中记录初始点A的坐标:

// 获取初始点A; let startPoint = getPoint(ev,0);

1
2
// 获取初始点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, });

1
2
3
4
5
6
7
8
9
10
11
// 实时获取初始点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事件对应的回调仓库即可;

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

2、CGAffineTransformScale在已有的transform基础上,增加 缩放 效果

Pinch(双指缩放)

图片 109

上图是双指缩放的模拟图,双指由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, });

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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,
});

向量(Vector)

// 格式  
CGAffineTransformScale(CGAffineTransform t,
  CGFloat sx, CGFloat sy)
// 使用       宽度缩小一倍,高度拉伸1.5倍
self.imageView.transform = CGAffineTransformScale(self.imageView.transform, 0.5 1.5); 

Rotate(双指旋转)

图片 110

初始时双指向量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, });

1
2
3
4
5
6
7
8
9
10
11
12
13
// a向量;
let vector1 = getVector(secondPoint, startPoint);
 
// b向量;
let vector2 = getVector(curSecPoint, curPoint);
 
// 触发事件;
this._eventFire('rotate', {
    delta: {
        rotate: getAngle(vector1, vector2),
    },
    origin: ev,
});

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


singlePinch(单指缩放)

图片 111

与上面的手势不同,单指缩放和单指旋转都需要多个特有概念:

操作元素(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, });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 计算单指操作时的基准点,获取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,
});

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

  • 旋转控件

singleRotate(单指旋转)

图片 112

结合单指缩放和双指旋转,可以很简单的知道 θ便是我们需要的旋转角度;

// 获取初始向量与实时向量 let rotateV1 = getVector(startPoint, singleBasePoint); let rotateV2 = getVector(curPoint, singleBasePoint); // 通过 getAngle 获取旋转角度并触发事件; this._eventFire('singleRotate', { delta: { rotate: getAngle(rotateV1, rotateV2), }, origin: ev, });

1
2
3
4
5
6
7
8
9
10
11
// 获取初始向量与实时向量
let rotateV1 = getVector(startPoint, singleBasePoint);
let rotateV2 = getVector(curPoint, singleBasePoint);
 
// 通过 getAngle 获取旋转角度并触发事件;
this._eventFire('singleRotate', {
    delta: {
        rotate: getAngle(rotateV1, rotateV2),
    },
    origin: ev,
});

1、CGAffineTransformMakeRotation实现以初始位置为基准,将坐标系统旋转angle弧度(弧度=π/180×角度,M_PI弧度代表180角度)

运动增量

由于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);

1
2
3
4
5
6
7
8
9
//    真实位置数据;
let dragTrans = {x = 0,y = 0};
 
// 累加上 mtouch 所传递出的增量 deltaX 与 deltaY;
dragTrans.x += ev.delta.deltaX;
dragTrans.y += ev.delta.deltaY;
 
// 通过 transform 直接操作元素;
set($drag,dragTrans);

获取向量的函数:

// 格式       angle为弧度
CGAffineTransformMakeRotation(CGFloat angle)
// 使用       
self.imageView.transform = CGAffineTransformMakeRotation(M_PI);

初始位置

维护外部的这个位置数据,如果初始值像上述那样直接取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);

1
2
3
4
5
6
7
8
9
10
// 获取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);

向量模

2、CGAffineTransformRotate在已有的transform基础上,增加 旋转 效果

结语

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

基于以上原理,我封装了几个常见的工具:(求star -.-)

Tips: 因为只针对移动端,需在移动设备中打开demo,或者pc端开启mobile调试模式!

  1. mtouch.js : 移动端的手势库,封装了上述的五种手势,精简的api设计,涵盖了常见的手势交互,基于此也可以很方便的进行扩展。
    demo
    github
  2. touchkit.js : 基于mtouch所封装的一层更贴近业务的工具包,可用于制作多种手势操作业务,一键开启,一站式服务。
    demo
    github
  3. mcanvas.js : 基于canvas 开放极简的api实现图片 一键导出等。
    demo
    github

代表向量的长度,记为|a|,是一个标量,只有大小,没有方向;

// 格式  
CGAffineTransformRotate(CGAffineTransform t,
  CGFloat angle)
// 使用       
self.imageView.transform = CGAffineTransformRotate(self.imageView.transform,
  M_PI/2.0);

致谢

  • 张鑫旭: 获取元素CSS值之getComputedStyle方法熟悉
  • 张鑫旭:理解CSS3 transform中的Matrix(矩阵)
  • AlloyTeam团队的AlloyFinger
  • hcysunyangd: 从矩阵与空间操作的关系理解CSS3的transform
  • 线性代数的理解 学完再看觉得自己弱爆了

    1 赞 6 收藏 1 评论

图片 113

几何意义代表的是以x,y为直角边的直角三角形的斜边,通过勾股定理进行计算;


  • 最初transform
    控件的transform属性默认值为CGAffineTransformIdentity,可以在形变之后设置该值以还原到最初状态

getLength函数:

// 使用
self.imageView.transform = CGAffineTransformIdentity;

向量的数量积


向量同样也具有可以运算的属性,它可以进行加、减、乘、数量积和向量积等运算,接下来就介绍下我们使用到的数量积这个概念,也称为点积,被定义为公式:

  • 反转变换效果
    CGAffineTransformInvert可以实现于transform相反的效果,比如放大3倍效果则缩小为1/3,向x轴正方向平移100px效果则为向负方向平移100px

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

共线定理

CGAffineTransform transform = CGAffineTransformMakeScale(3, 3);  
//相反  缩小至1/3                
transform = CGAffineTransformInvert(transform);
self.imageView.transform = transform;

共线,即两个向量处于平行的状态,当a=(x1,y1),b=(x2,y2),则存在唯一的一个实数λ,使得a=λb,代入坐标点后,可以得到x1·y2= y1·x2;


因此当x1·y2-x2·y1>0时,既斜率ka > kb,所以此时b向量相对于a向量是属于顺时针旋转,反之,则为逆时针;

  • 结合变换效果
    CGAffineTransformConcat结合两种变换

旋转角度

通过数量积公式我们可以推到求出两个向量的夹角:

//定义两种ransform
CGAffineTransform transform_A = CGAffineTransformMakeTranslation(0, 200);
CGAffineTransform transform_B = CGAffineTransformMakeScale(0.2, 0.2);
transform = CGAffineTransformConcat(transform_B, transform_A);

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


然后通过共线定理我们可以判断出旋转的方向,函数定义为:

  • 判断变换

1、CGAffineTransformIsIdentity可以判断view.transform当前状态是否是最初状态

矩阵与变换

bool CGAffineTransformIsIdentity(CGAffineTransform t)

由于空间最本质的特征就是其可以容纳运动,因此在线性空间中,

2、CGAffineTransformEqualToTransform可以判断两种transform是否是一样的

我们用向量来刻画对象,而矩阵便是用来描述对象的运动;

bool CGAffineTransformEqualToTransform(CGAffineTransform t1, CGAffineTransform t2) 

而矩阵是如何描述运动的呢?


我们知道,通过一个坐标系基向量便可以确定一个向量,例如a=(-1,2),我们通常约定的基向量是 i = (1,0) 与 j = (0,1); 因此:

  • 使用仿射变换转换point,size,rect

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

1、CGPointApplyAffineTransform转换point,使用一种transform来得到转换后的point

而矩阵变换的,其实便是通过矩阵转换了基向量,从而完成了向量的变换;

// transform 可以是移动、放大、旋转
CGPoint CGPointApplyAffineTransform(CGPoint point,
  CGAffineTransform t)
// 使用
CGPoint point =  CGPointMake(123, 222);
CGPoint pointNew =  CGPointApplyAffineTransform(point, CGAffineTransformMakeTranslation(77, 28));

例如上面的栗子,把a向量通过矩阵(1,2,3,0)进行变换,此时基向量i由(1,0)变换成(1,-2)与j由(0,1)变换成(3,0),沿用上面的推导,则

2、CGSizeApplyAffineTransform转换size,使用一种transform来得到转换后的size

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

//
CGSizeApplyAffineTransform(CGSize size, CGAffineTransform t)
// 使用
CGSize size = CGSizeMake(33, 44);
CGSize sizeNew = CGSizeApplyAffineTransform(size, CGAffineTransformMakeScale(2, 2));

如下图所示:

3、CGRectApplyAffineTransform转换rect,使用一种transform来得到转换后的rect

A图表示变换之前的坐标系,此时a=(-1,2),通过矩阵变换后,基向量i,j的变换引起了坐标系的变换,变成了下图B,因此a向量由(-1,2)变换成了(5,-2);

//
CGRectApplyAffineTransform(CGRect rect, CGAffineTransform t)
// 使用
CGRect rect = CGRectMake(20, 30, 50, 100);
CGRect rectNew = CGRectApplyAffineTransform(rect, CGAffineTransformMakeRotation(M_PI));

其实向量与坐标系的关联不变(a = -1i+2j),是基向量引起坐标系变化,然后坐标系沿用关联导致了向量的变化;


三、CGAffineTransform原理

结合代码

CGAffineTransform形变是通过"仿射变换矩阵"来控制的,其中平移是矩阵相加,旋转与缩放则是矩阵相乘,CGAffineTransform形变就是把二维形变使用一个三维矩阵来表示,系统提供了CGAffineTransformMake结构体来控制形变。

其实CSS的transform等变换便是通过矩阵进行的,我们平时所写的translate/rotate等语法类似于一种封装好的语法糖,便于快捷使用,而在底层都会被转换成矩阵的形式。例如transform:translate(-30px,-30px)编译后会被转换成transform : matrix(1,0,0,1,30,30);

CGAffineTransformMake(CGFloat a, CGFloat b, CGFloat c, CGFloat d, CGFloat tx, CGFloat ty)

通常在二维坐标系中,只需要 2X2 的矩阵便足以描述所有的变换了, 但由于CSS是处于3D环境中的,因此CSS中使用的是 3X3 的矩阵,表示为:

该三维变换矩阵如下:
<center>

TAG标签:
版权声明:本文由必威发布于必威-前端,转载请注明出处:这篇博文主要是解析了移动端常用手势的原理,