5000字前端動畫交互實現小談

author:山鬼 有點爛,因此先看掘金的吧javascript

5000字,帶你瞭解動畫與交互的基本實現 不少內容寫的比較粗略,因此還望你們不要太過吐槽,後續我會給完善的。css

1. 空間與轉換

當圖形被繪製在屏幕上的時候,不管是2D仍是3D,都會有其本身的空間,也會有其本身的轉換數據。java

空間座標

  1. 齊次座標和轉換矩陣: 在計算機圖形學中,一般是才用齊次座標來表示空間內的點,在三維空間內,會使用四元向量來表示。
[\frac{x}{w},\frac{y}{w},\frac{z}{w}]=[x,y,z,w]

通常w的默認值爲1,較爲基本的旋轉,平移,縮放多采用的是4維矩陣,當咱們須要一些複雜的操做時,還能夠經過矩陣得到複合矩陣。git

基本的轉換操做

不管是css仍是canvas等圖形的轉換操做,採用的操做都是相同的。github

平移web

\begin{matrix}x\\y\\z\\1\end{matrix}=\begin{matrix}x\\y\\z\\1\end{matrix}*\begin{matrix}1&0&0&t\\0&1&0&t\\0&0&1&t\\0&0&0&1\end{matrix}

旋轉xcanvas

\begin{matrix}x\\y\\z\\1\end{matrix}=\begin{matrix}x\\y\\z\\1\end{matrix}*\begin{matrix}1&0&0&0\\0&cosX&-sinX&0\\0&sinX&cosX&0\\0&0&0&1\end{matrix}

旋轉yapi

\begin{matrix}x\\y\\z\\1\end{matrix}=\begin{matrix}x\\y\\z\\1\end{matrix}*\begin{matrix}cosX&0&sinX&0\\0&1&0&0\\-sinX&0&cosX&0\\0&0&0&1\end{matrix}

旋轉zpromise

\begin{matrix}x\\y\\z\\1\end{matrix}=\begin{matrix}x\\y\\z\\1\end{matrix}*\begin{matrix}cosX&-sinX&0&0\\sinX&cosX&0&0\\0&0&1&0\\0&0&0&1\end{matrix}

放縮ide

\begin{matrix}S1x\\S1y\\S1z\\1\end{matrix}=\begin{matrix}x\\y\\z\\1\end{matrix}*\begin{matrix}S1&0&0&0\\0&S1&0&0\\0&0&S1&0\\0&0&0&1\end{matrix}

也許在看3D的圖形轉換的時候,會感受好複雜,可是當咱們去看2D的時候,砍去了一個維度,公式也就固定了。

\begin{matrix}x\\y\\1\end{matrix}=\begin{matrix}x\\y\\1\end{matrix}*\begin{matrix}
cosX&-sinX&0\\sinX&cosX&0\\0&0&1\end{matrix}

進一步簡化

\begin{matrix}x\\y\\\end{matrix}=\begin{matrix}x\\y\\\end{matrix}*\begin{matrix}cosX&-sinX\\sinX&cosX\\\end{matrix}

這個時候,咱們會獲得一段較爲常見的旋轉代碼

/** 向量定義 var Vector2={ x:0, y:0 } **/
function rotate(site,angle=0){
    	var _angle=angle/180*Math.PI;//將弧度轉換爲角度
    	//進行計算
    	var x1=site.x*Math.cos(_angle)-site.y*Math.sin(_angle);
    	var y1=site.x*Math.sin(_angle)+site.y*Math.cos(_angle);
    	//返回新的向量
    	return {
    	    x:x1,
    	    y:y1
    	}
};
複製代碼

侷限性: 矩陣的數據轉換由於數據格式化,因此並不適用於如非線性動畫的轉換。

一週是360度,也是2π弧度。弧度是這樣定義的,一個角對應的弧長與半徑的比值就是弧度。半徑爲1的圓周長是2π,因此360度=2π弧度,之後的類推就好了。幾個重要的角度還有:30度=π/6弧度,60度=π/3弧度,90度=π/2弧度,180度=π弧度等。

向量之說

在空間之中,能夠被劃分爲空間座標與對象座標 用CSS來表示的話,空間座標有些相似position:absolute以整個視圖的原點爲基準。而對象座標的說法更貼切的應該是相對座標,相似position:relative 爲了方便對於座標進行計算以及數據轉換,空間中的任何點信息均可以使用向量來做爲信息載體。

Example: (1,1)能夠表示爲空間中x=1,y=1的座標點,也能夠表示爲從(0,0)到(1,1)的距離。 從新定義一個Vector2的類

function Vector2(x=0,y=0){
	if(!(this instanceof Vector2)){
		return new Vector2(x,y);
	}
	this.x=x;
	this.y=y;
}
Vector2.prototype = {
    copy: function() {//返回新的向量
    	 return new Vector2(this.x, this.y); },

    length: function() {//當前向量的長度
    	 return Math.sqrt(this.x * this.x + this.y * this.y); },

    normalize: function() {//單位向量
     var inv = 1 / this.length(); 
     return new Vector2(this.x * inv, this.y * inv); },

    negate: function() {//反向向量 
    	return new Vector2(-this.x, -this.y); },

    add: function(v) {//向量和
    	return new Vector2(this.x + v.x, this.y + v.y); },

    subtract: function(v) {//向量差
     return new Vector2(this.x - v.x, this.y - v.y); },

    multiply: function(f) {//向量積 
    	return new Vector2(this.x * f, this.y * f); },

    divide: function(f) { //向量方向化
    	var invf = 1 / f; 
    	return new Vector2(this.x * invf, this.y * invf); },

    dot: function(v) {//點積 
    	return this.x * v.x + this.y * v.y; },
    move:function(v){
        this.x=v.x;
        this.y=v.y;
        return this;
    },
    prependicular:function() {//法向量
    	return new Vector(this.y, -this.x);
	},
    rotate:function(angle=0){
    	var _angle=angle/180*Math.PI;
    	this.x1=this.x*Math.cos(_angle)-this.y*Math.sin(_angle);
    	this.y1=this.x*Math.sin(_angle)+this.y*Math.cos(_angle);
    },
};
複製代碼

向量的運用:速度(v),力(f),方向(d),顏色(rgb)等...

當咱們把信息使用向量存儲值後,就會發現不少功能都是清晰明瞭,好比屬性的插值運算

角度

角度的計算,在計算機動畫實現中,有定角表達 歐拉角表達 軸角表達這三種說法,不過這些都不須要去了解,由於在插值計算的過程當中,這些技術並不合適,若是想深刻了解緣由的,能夠去了解一下什麼是萬向節死鎖(gimbal lock )

歐拉角

歐拉角是表達旋轉最簡單的一種方式,表達了物體繞座標系的軸的旋轉角度,2D平面內提供了大量的旋轉api ,css裏的transform:rotate(90deg),canvas裏的ctx.rotate(angle),對於3D方面,css也是提供了在各個軸向上的Rotate,canvas則更可能是在webgl中使用的矩陣變換。

對於歐拉角的定義,有人歸納了一下幾點。

  1. 旋轉角的組合方式:以(x,y,z)來講明就是角度的執行順序,如X-Y-Z或者Z-X-Y,用css來講就是X-Y-Z== rotateX()-rotateY()-rotateZ()
  2. 旋轉角度的參考座標系統(旋轉是相對於固定的座標系仍是相對於自身的座標系)
  3. 使用旋轉角度是左手系仍是右手系

萬向節死鎖

在歐拉角中,咱們能夠發現,在軸轉向的時候,會有一個順序,若是當角度不恰當,會致使軸旋轉的過程當中,有兩個軸會發生重合,致使維度下降。

固然,咱們也可使用代碼來對萬向節死鎖進行復現。

Point.Rotate(new Vector3(0, 0, 10));  
Point.Rotate(new Vector3(0, 90, 0));  
Point.Rotate(new Vector3(20, 0, 0));  
複製代碼

只須要固定住某一個軸的轉角爲90°,不管怎麼去調整其餘的軸,都會發現,他們只會在平面上運動。

咱們所要了解的是 四元數,這個詞的概念在遊戲開發中很常見。那麼選擇四元數來處理自由度旋轉的優點在哪裏呢。 優點

  1. 不存在萬向節死鎖
  2. 計算效率高(矩陣旋轉效率較低)
  3. 能夠以物體的中心點爲軸來作旋轉

弱點

  1. 旋轉軸限制(矩陣旋轉能夠任意軸)
  2. 不能夠超過180°(矩陣旋轉無限制)

在瞭解四元數以前,咱們要了解一個知識點複數,若是已有基礎,能夠跳過。

複數

定義: 任意一個複數 z ∈ C 均可以表示爲 z = a +bi的形式,其中 a, b ∈ R 並且 2i^2=−1.咱們將 a 稱之爲這個複數的實部(Real Part),b稱之爲這個複數的虛部(Imaginary Part). 若是將複數使用座標系來表示。

四元數

四元數是一個恐怖的東西,由於當把他放在圖形中去理解,你會發現比矩陣的還要難理解不少,在正常的座標系中,每一個軸都會是一個直線,而在四元數中,多出一個軸向,並且這個軸會垂直於任何一個軸,相對於複數的二維空間,四元數則是三維的複數形式,是一種高階複數,感受像就是四維空間。

四元數的數學表達仍是比較好理解的Q=w+xi+yj+zk,Q是一個四元數,w是一個實部,x,y,z則是虛部,且i^2+j^2+k^2=-1

當四元數應用到旋轉中的時候,咱們一般能夠這麼表示一個Q=(w,(x,y,z))=(w,v),w是實數,v是向量,每一次的旋轉都會須要兩個四元數來配合,四元數的的範圍在[-1,1]之間。

接下來咱們試着實現一個四元數

/* 四元數 */
 
class Quaternion{
    constructor(x=0,y=0,z=0,w=0){
        this.x=x;
        this.y=y;
        this.z=z;
        this.w=w;
    }
    fromAxisVector(axisVector,angle){// 由 旋轉軸向量,旋轉角 獲得
        var t = sin(0.5*angle);
 		this.w = cos(0.5*angle);
		this.x = axisVector.x * t;
		this.y = axisVector.y * t;
		this.z = axisVector.z * t;       
    }
    add(q){
  		this.w += q.w;
		this.x += q.x;
		this.y += q.y;
		this.z += q.z;     
    }
    subtract(q){
   		this.w -= q.w;
		this.x -= q.x;
		this.y -= q.y;
		this.z -= q.z;          
    }
    multiply(q){
        var {x,y,z,w}=q;
 		this.w = w*q.w - x*q.x - y*q.y - z*q.z;
		this.x = w*q.x + x*q.w + y*q.z - z*q.y;
		this.y = w*q.y + y*q.w + z*q.x - x*q.z;
		this.z = w*q.z + z*q.w + x*q.y - y*q.x;       
    }
    normalize(){
        var {x,y,z,w}=this;
 		var magnitude = Math.sqrt(x*x + y*y + z*z + w*w);
		if (magnitude != 0)
		{
			x /= magnitude;
			y /= magnitude;
			z /= magnitude;
			w /= magnitude;
		}       
    }
    convertToMatrix4(){//轉換爲矩陣
 		// 四元數與矩陣的轉換
		// [ 1-2y2-2z2 , 2xy-2wz , 2xz+2wy ]
		// [ 2xy+2wz , 1-2x2-2z2 , 2yz-2wx ]
		// [ 2xz-2wy , 2yz+2wx , 1-2x2-2y2 ]
 		var {x,y,z,w}=this;
		var xx = x*x;  var xy = x*y; 
        var xz = x*z;  var xw = x*w; 
		var yy = y*y;  var yz = y*z;  
        var yw = y*w;  var zz = z*z;  var zw = z*w;
 
		return Matrix4(  1-2*(yy+zz),  2*(xy-zw),    2*(xz+yw),    0,
							2*(xy+zw),    1-2*(xx+zz),  2*(yz-xw),    0,
							2*(xz-yw),    2*(yz+xw),    1-2*(xx+yy),  0,
							0,            0,            0,            1  );
       
    }
}
 

複製代碼

2. 插值計算

插值運動是指經過一些離散的數據進行數據的擬合,從而推斷出新的未知數據點,使用簡單函數來模擬複雜函數,從而提高數據的精度。

插值計算在運動之中,最多見的就是屬性插值,如顏色漸變,寬高過分,緩動動畫等,主要是經過計算機自行去計算,實現自動補幀。Flash中的補間動畫採用的就是插值補間補幀。

假設給定n個離散數據,定義了其座標爲(x_k,y_k),k=1,2,3... 在區間 [a,b] 上有函數g(x), 能夠知足g(x_i)=f(x_i),那麼g(x)則能夠被稱爲是f(x)在的 [a,b] 上插值函數,這也就是使用簡單函數來模擬複雜函數。

屬性 插值類型 效果
color/alpha 線性 (顏色/透明度)漸變過分
加速度 線性 勻變速
歐拉角 線性 旋轉
速度 非線性 變加速

線性插值

線性插是一種很常見的插值方法,在動畫計算中很常見,能夠用來實現自動補幀,其基本的實現也較爲簡單。

線性插值通常是採用兩點數據進行計算,最多見的就是直線插值,tween.js的Linear就是線性插值的一個實例。

/* * t: current time(當前時間); * b: beginning value(初始值); * c: change in value(變化量); * d: duration(持續時間)。 */    
Linear: function(t, b, c, d) { 
        return c * t / d + b; 
    }
複製代碼

多項式插值

多項式插值是線性插值的一個延伸,在線性插值的原公式上,支持了高階多項式計算。

Quad: {
        easeIn: function(t, b, c, d) {
            return c * (t /= d) * t + b;
        },
        easeOut: function(t, b, c, d) {
            return -c *(t /= d)*(t-2) + b;
        },
        easeInOut: function(t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t + b;
            return -c / 2 * ((--t) * (t-2) - 1) + b;
        }
    }
複製代碼

這是Tween.js中的二次方插值,同時,還包含了三次方插值,甚至五次方插值。

三角插值

三角插值這裏指的就是三角函數COS TAN SIN,以x軸與y軸造成關係.如:

  • v=_v*Sin(t) 速度隨着時間的增加而產生變化

3. 基本動畫

有了以前的基礎知識與插值的基礎,就有了足夠的而基礎去進行動畫的嘗試。

因而咱們能夠從一個點開始構建

class Point{
    constructor(x,y){
        this.pos=new Vector2(x,y);
    }
    draw(){
        //圖形繪製
    }
    updata(){
        //邏輯處理,數據更新
    }
}
複製代碼

這裏的點已經具備了Vector2的方法,從而使得這個點在二維空間中具備了必定的能力,包括平移,旋轉。

以前有說,幾乎全部的屬性均可以使用向量做爲載體,因而這裏,可使用Vector2Point賦予不少的屬性,即可以獲得

class Point{
    constructor(x,y){
        this.pos=new Vector2(x,y);
        this.f=new Vector(0,0);
        this.m=10;
        this.a=this.f.length()/this.m;
    }
}
複製代碼

很簡單的一個F=m*a 公式,就給Point賦予了接受外界力的能力,以及運動的能力。

F=m*a  (F爲協力,F是個矢量)
v= v_0+at
S=v_0t+\frac{at^2}{2}

這幾個公式是力與運動學之中最經常使用也是最關鍵的幾個公式,也是運動學中很關鍵的一步,那麼如何正確的去計算一個物體的運動狀態呢。

  1. 判斷物體當前狀態,是單體,仍是有連接狀態
  2. 對物體所受力進行求和,對單個Point進行updata
  3. 對物體進行重繪

這樣,就能夠將基本運動的動畫利用物理公式從而實現,如勻加速,變加速,圓周運動等。

4.動畫中的狀態機

鏈式動畫

狀態機在遊戲開發中是一個很常見的詞彙,那麼狀態機的存在是爲了什麼,在哪些地方有運用呢,

首先以Point爲基礎,添加一個狀態量

const PEDDING='PEDDING';//靜止狀態
const MOVING ='MOVING';//運動狀態
const SHOW   ='SHOW';//顯示
const OUT    ='OUT';//屏幕以外

//狀態判斷
if(Point.status=='PEDDING'){
    cb();
}
複製代碼

這麼看起來是否是有些熟悉,對比發現,promise其實也是一個狀態機,不斷判斷當前的執行狀態,來肯定什麼時候進行下一個事件的執行,對比着promise的鏈式調用,也就能夠輕易的去明白一些動畫庫中的鏈式調用原理。

資源管理器

在視圖中進行動畫的物體,總會有一部分會消失在視圖以外,爲了下降了內存佔有,也許能夠直接使obj=nul,可是當咱們仍須要其後續的出現,再去使用申請一個新的對象?顯然有不少不合理的地方,因而便有了資源管理器。

var p1=new Point(0,0);
var p2=new Point(1,1)
var resource=[p1,p2];
//狀態判斷
resource.forEach(p=>{
    if(p.status=='SHOW'){
        p.updata();
        p.draw();
    }
    if(p.status=='OUT'){
       	//對p進行移除或者重置設置
    }
})
複製代碼

這樣的優點是能夠下降大量的計算以及渲染工做,若是打算完全移除某個物體,則可使用Array.splice

用戶交互

用戶交互也是很經常使用的一個狀態機,以canvas爲例,用戶的事件監聽是針對canvas總體的,若是咱們想實現一個拖拽的功能。

狀態分析:

  1. 正常狀況,鼠標釋放,status·爲UP
  2. 按下的狀態,status爲DOWN
  3. 按下後移動鼠標,status爲DROP

狀態機的存在是以鼠標事件爲本體。

5.碰撞檢測

實現了物體基本的運動與交互,那麼接下來須要實現的就是物體與物體的交互,如今在咱們所瞭解到的碰撞檢測方法。

  1. 包圍盒
  2. 包圍球

這兩個也是最爲簡單計算,也是最適合作粗計算階段的碰撞檢測,能夠將一些沒必要要進行進行精密計算的物體圖形排除在外,減小計算量

包圍盒

以物體中心爲基礎,生成最小的包圍矩形

rectB.x > rectA.x - rectB.width &&
rectB.x < rectA.x + rectA.width + rectB.width &&
rectB.y > rectA.y - rectB.height &&
rectB.y < rectA.y + rectA.height + rectB.height
複製代碼

包圍球

以物體爲基礎,生成最小的包圍球形

Math.sqrt(Math.pow(circleA.x - circleB.x, 2) + Math.pow(circleA.y - circleB.y, 2)) < circleA.radius + circleB.radius
複製代碼

分離軸

分離軸也許聽起來暈,甚至看網上的一些講解也很暈,那麼能夠考慮在這個時候打開網易雲音樂,點一首你最愛的歌,而後開始閱讀。

分離軸,顧名思義是將軸分離開,那麼在咱們所瞭解的領域中,最長出現的就是x軸與y軸,這也是座標系的基礎,那麼軸的特色是什麼,垂直,這也是分離軸的依據所在。

分離軸的實現有些像模擬燈光投影,當光線穿過兩個空間中的物體,爲了防止影子變形,設置一個垂直光線的擋板,想像一下,若是光線能夠從兩個物體中穿出,那麼兩個物體之間就不存在接觸,那麼投射的影子也就不會出現重疊,當足夠多的光線進行穿透,若是出現垂直光線的擋板沒有出現陰影重疊,那麼咱們就能夠認定這兩個物體沒有發生碰撞。

碰撞的檢測,是隻須要一組軸的檢測未重合,那麼能夠斷定爲分離,若是全部軸的檢測都重合,則物體發生碰撞

因而這裏咱們就有了兩個軸,光軸與投影軸。

因而咱們有了第一縷陽光

var Light=new Vector2(0,0);
複製代碼

讓陽光來穿過物體

var Point1 =new Point(0,0);
var Point2 =new Point(0,1);
var Light =Point1.pos.subtract(Point2.pos);//光線向量
var Panel =Light.prependicular();			//獲取投影軸的向量
var axis  =Panel.normalize();				//軸的單位向量,爲投影點作準備

複製代碼

求出咱們的投影點,這裏所須要的公式 v_1*v_2=|v_1|*|v_2|*conθ

Light.dot(axis);
複製代碼

獲得了投影點後,一個物體在一個軸面上的投影點的最大值與最小值的差值,就是陰影面的範圍。

像素檢測

像素檢測的方法就是將每一個物體當前的像素位置都存儲起來,再比較物體之間的像素是否有重複,可是計算量龐大。

檢測優化

柵格化

柵格化的意思就是將屏幕劃分爲數個小塊,對不一樣區域內的物體進行單獨處理,對於處於分界線上的物體,則能夠進行屢次判斷。最經常使用的柵格法就是四叉樹

未完待續!!

後續還有更精彩的如IK/FK動畫,2.5D的效果實現等... 若是發現有哪些錯誤,歡迎指出。

相關文章
相關標籤/搜索