5000字前端動畫互動實現小談
author:山鬼 ofollow,noindex">有點爛,所以先看掘金的吧
5000字,帶你瞭解動畫與互動的基本實現 很多內容寫的比較粗略,所以還望大家不要太過吐槽,後續我會給完善的。
1. 空間與轉換
當圖形被繪製在螢幕上的時候,無論是2D還是3D,都會有其自己的空間,也會有其自己的轉換資料。
空間座標
- 齊次座標和轉換矩陣: 在計算機圖形學中,通常是才用齊次座標來表示空間內的點,在三維空間內,會使用四元向量來表示。
一般w的預設值為1,較為基本的旋轉,平移,縮放多采用的是4維矩陣,當我們需要一些複雜的操作時,還可以通過矩陣獲得複合矩陣。
基本的轉換操作
無論是css還是canvas等圖形的轉換操作,採用的操作都是相同的。
平移
旋轉x
旋轉y
旋轉z
放縮
也許在看3D的圖形轉換的時候,會感覺好複雜,但是當我們去看2D的時候,砍去了一個維度,公式也就固定了。
進一步簡化
這個時候,我們會得到一段較為常見的旋轉程式碼
/** 向量定義 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中使用的矩陣變換。
對於尤拉角的定義,有人概括了一下幾點。
X-Y-Z== rotateX()-rotateY()-rotateZ()
萬向節死鎖
在尤拉角中,我們可以發現,在軸轉向的時候,會有一個順序,如果當角度不恰當,會導致軸旋轉的過程中,有兩個軸會發生重合,導致維度降低。

當然,我們也可以使用程式碼來對萬向節死鎖進行復現。
Point.Rotate(new Vector3(0, 0, 10)); Point.Rotate(new Vector3(0, 90, 0)); Point.Rotate(new Vector3(20, 0, 0)); 複製程式碼
只需要固定住某一個軸的轉角為90°,無論怎麼去調整其他的軸,都會發現,他們只會在平面上運動。
我們所要了解的是 四元數 ,這個詞的概念在遊戲開發中很常見。那麼選擇四元數來處理自由度旋轉的優勢在哪裡呢。 優勢
- 不存在萬向節死鎖
- 計算效率高(矩陣旋轉效率較低)
- 可以以物體的中心點為軸來做旋轉
弱點
- 旋轉軸限制(矩陣旋轉可以任意軸)
- 不可以超過180°(矩陣旋轉無限制)
在瞭解四元數之前,我們要了解一個知識點 複數 ,如果已有基礎,可以跳過。
複數
定義:任意一個複數 z ∈ C 都可以表示為 z = a +bi的形式,其中 a, b ∈ R 而且 .我們將 a 稱之為這個複數的實部(Real Part),b稱之為這個複數的虛部(Imaginary Part). 如果將複數使用座標系來表示。

四元數
四元數是一個恐怖的東西,因為當把他放在圖形中去理解,你會發現比矩陣的還要難理解很多,在正常的座標系中,每個軸都會是一個直線,而在四元數中,多出一個軸向,而且這個軸會垂直於任何一個軸,相對於複數的二維空間,四元數則是三維的複數形式,是一種高階複數,感覺像就是四維空間。
四元數的數學表達還是比較好理解的 ,Q是一個四元數,w是一個實部,x,y,z則是虛部,且 。
當四元數應用到旋轉中的時候,我們通常可以這麼表示一個 ,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個離散資料,定義了其座標為 在區間 上有函式g(x), 可以滿足 ,那麼g(x)則可以被稱為是f(x)在的 上插值函式,這也就是使用簡單函式來模擬複雜函式。
屬性 | 插值型別 | 效果 |
---|---|---|
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
的方法,從而使得這個點在二維空間中具有了一定的能力,包括平移,旋轉。
之前有說,幾乎所有的屬性都可以使用向量作為載體,於是這裡,可以使用 Vector2
給 Point
賦予很多的屬性,便可以得到
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; } } 複製程式碼
很簡單的一個 公式,就給 Point
賦予了接受外界力的能力,以及運動的能力。
這幾個公式是力與運動學之中最常用也是最關鍵的幾個公式,也是運動學中很關鍵的一步,那麼如何正確的去計算一個物體的運動狀態呢。
- 判斷物體當前狀態,是單體,還是有連結狀態
- 對物體所受力進行求和,對單個
Point
進行updata
- 對物體進行重繪
這樣,就可以將基本運動的動畫利用物理公式從而實現,如勻加速,變加速,圓周運動等。
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整體的,如果我們想實現一個拖拽的功能。
狀態分析:
- 正常情況,滑鼠釋放,status·為UP
- 按下的狀態,status為DOWN
- 按下後移動滑鼠,status為DROP
狀態機的存在是以滑鼠事件為本體。
5.碰撞檢測
實現了物體基本的運動與互動,那麼接下來需要實現的就是物體與物體的互動,現在在我們所瞭解到的碰撞檢測方法。
- 包圍盒
- 包圍球
這兩個也是最為簡單計算,也是最適合做粗計算階段的碰撞檢測,可以將一些不必要進行進行精密計算的物體圖形排除在外,減少計算量
包圍盒
以物體中心為基礎,生成最小的包圍矩形
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();//軸的單位向量,為投影點做準備 複製程式碼
求出我們的投影點,這裡所需要的公式
Light.dot(axis); 複製程式碼
得到了投影點後,一個物體在一個軸面上的投影點的最大值與最小值的差值,就是陰影面的範圍。
畫素檢測
畫素檢測的方法就是將每個物體當前的畫素位置都儲存起來,再比較物體之間的畫素是否有重複,但是計算量龐大。