2D矩阵对旋转的局限性在之前的文章中咱们探讨过旋转这件事,也说过用2阶矩阵形容的旋转:
教练我想学矩阵
很多成熟的框架/库也是基于矩阵这套逻辑来写的,比如说我膜拜的pixi.js
{ // get the matrix values of the displayobject based on its transform properties.. lt.a = this._cx * this.scale.x; lt.b = this._sx * this.scale.x; lt.c = this._cy * this.scale.y; lt.d = this._sy * this.scale.y; lt.tx = this.position.x - ((this.pivot.x * lt.a) + (this.pivot.y * lt.c)); lt.ty = this.position.y - ((this.pivot.x * lt.b) + (this.pivot.y * lt.d)); this._currentLocalID = this._localID; // force an update.. this._parentID = -1;}
然而很显著应用二阶矩阵有两个问题,第一旋转操作只能利用于原点,咱们无奈应用二阶矩阵实现绕任意点转动的计算,第二遇到平移操作时咱们须要拓展成齐次矩阵(这部分能够回看之前的文章)第一个问题其实在引入奇次矩阵后就能够解决,只有先把旋转点A平移到0,绕原点旋转,再把0平移回旋转点A。
这套逻辑尽管也够用,但更多状况下咱们只须要解决绕某个原点外的点旋转的逻辑即可。
比方下图模仿一个地月日零碎时解决月球绕地球的旋转。
有没有更优雅的模式呢?还真有那用复数。复数与复立体复数是什么?
复数与复立体
复数就是一个定义在复立体上的数,复立体相似咱们的直角坐标系,只不过x轴上是实数单位,y轴上是虚数单位(就是高中数学外面那个√-1)
这个复立体上的一个点示意办法又很多比方能够用x轴y轴对应的长度示意成数系:(a,b)
能够分解成xy轴(相似物理力的合成\( ):a cos\theta + b\bar{i}sin\theta \)
留神这里的加号并不示意两者能相加只是个记号
还能够示意成相似极坐标的模式:\( re^{i\theta} \)
同时因为复数是一个数,它还能够进行四则运算。
加法:
\( z_{A}+z_{B} = (x_{A},y_{A})+(x_{B},y_{B}) = (x_{A}+x_{B},y_{A}+y_{B}) \)
这不就是平移操作?就是A点向x方向平移了\( x_{B} \)向y方向平移了\( y_{B} \)
乘法:
\( z_{A}z_{B}=(Ae^{i\alpha})(Be^{i\beta}) = ABe^{i(\alpha+\beta)} \)
他的几何意义:
A绕原点逆时针旋转了\(\beta\)而长度减少了B倍。看到这里是不是DNA动了?
复数体系内缩放/旋转/平移是闭合的。不过咱们明天不聊缩放只着墨于旋转+平移。
复数形容平移与旋转
平移操作
咱们应用记号\( T_{v}(z) \)示意将立体上的一个复数z挪动v,即:\( T_{v}(z) = z + v \)。\( T_{v} \)的逆向操作就是\( T_{v}^{-1} \)。
因为\( T_{v}(z)T_{v}^{-1} (z)= T_{v}^{-1}(z)T_{v}(z)=z \)所以\( T_{v}^{-1}= T_{-v} \)
而复合的平移能够写成:
\( T_{v}\circ T_{w}(z) = T_{v}(z+w)=z+(w+v) \)
旋转操作
对于旋转操作咱们能够定义绕a旋转为\( R_{a}^{\theta} \)而显然\( R_{a}^{\theta}R_{a}^{\vartheta}=R_{a}^{\theta+\vartheta} \),\( (R_{a}^{\theta})^{-1} = R_{a}^{-\theta} \)。而绕原点的旋转则能够写成
\( R_{0}^{\theta}(z)=e^{i\theta}z \)
那么怎么求出这个\( R_{a}^{\theta} \),咱们能够套用矩阵时的思路,把先把a平移到0, 绕原点旋转, 再把0平移回a:
\( R_{a}^{\theta}(z)=T_{a}\circ R_{a}^{\theta}\circ T_{a}^{-1}(z)=e^{i\theta}(z-a)+a=e^{i\theta}z+k \)
其中\( k=a(1-e^{i\theta}) \)
这就意味着,绕任何点的旋转都能够写成绕原点的旋转再加上一个平移量。而后这个操作还是关闭的。
那么间断绕两个不同的点旋转?
\( (R_{a}^{\alpha}\circ R_{b}^{\beta}) (z)= e^{i(\alpha+\beta)}z+v 其中 v=be^{i\alpha}(1-e^{i\beta})+a(1-e^{\alpha}) \)
\( 当(\alpha+\beta) \ne 2k\pi 时:\)
\( (R_{a}^{\alpha}\circ R_{b}^{\beta}) (z)=R_{c}^{\alpha+\beta} 其中 c= \frac{v}{1-e^{i(\alpha+\beta)}}=\frac{be^{i\alpha}(1-e^{i\beta})+a(1-e^{\alpha})}{1-e^{i(\alpha+\beta)}} \)
\( 当(\alpha+\beta) = 2k\pi 时 e^{i(\alpha+\beta)}=1\)
\( (R_{a}^{\alpha}\circ R_{b}^{\beta}) =T_{c} 其中 c=(1-e^{i\alpha})(b-a) \)
几何解释
最初咱们再从几何直观上了解下这两个过程,绕点a旋转后再绕点b旋转,在数学上能够证实总能找到另外一点c使得绕c点旋转\( (\alpha+\beta) \)与绕点a旋转后再绕点b旋转等价:
而 时,就是按连贯第一个旋转核心到第二个旋转核心的复数的2倍作平移:
代码层面
代码层面反而是最简略的,乞丐版只须要实现一个类complexNumber即可,在外部有一个办法add和multi,对外只须要裸露一个rotate办法即可:
class complexNumber { _real: number; _imaginary: number; constructor(real: number, imaginary: number) { this._real = real; this._imaginary = imaginary; } private _add(a:complexNumber,b:complexNumber):complexNumber{ return new complexNumber(a._real+b._real,a._imaginary+b._imaginary); } private _mul(a:complexNumber,b:complexNumber):complexNumber{ const real = a._real*b._real-a._imaginary*b._imaginary; const imaginary = a._real*b._imaginary+a._imaginary*b._real; return new complexNumber(real,imaginary); } private sub(a:complexNumber,b:complexNumber):complexNumber{ return new complexNumber(a._real-b._real,a._imaginary-b._imaginary); } public rotate(angle:number,z:complexNumber):[number,number]{ const p = new complexNumber(Math.cos(angle),Math.sin(angle)); const v:complexNumber = this._mul(z,this.sub(new complexNumber(1,0),p)); const result:complexNumber = this._add(this._mul(p,this),v); return [result._real,result._imaginary] }}