動畫就是不間斷地、基于時間的更新與重繪
- 不間斷的: 是指動畫需要一個不停地重複循環機制
是以一般有兩種選擇
- 1、類似while ( true ) { }之類的死循環,除非滿足退出死循環的條件,否則就一直不停的重複相同行為。(在Windows下的D3D / OpenGL開發中,經常使用這種模式來驅動動畫不斷運作)
- 2、另一種就是類似定時器的回調。例如使用HTML DOM中的window對象的setTimeout、setInterval、 requestAnimationFrame .
關于這個可以讀我的這篇文章, 裡面有使用到:
動畫類型
- 逐幀動畫
- 也叫定格動畫,其原理即将準備好的素材以固定頻率連續播放,進而産生動畫效果。如GIF,視訊、css3中的animation-timing-function的階梯函數steps(number_of_steps, direction)實作逐幀動畫連續播放。
- 關鍵幀動畫
- 需要動畫效果的屬性,準備一組與時間相關的值,這些值都是在動畫序列中比較關鍵的幀中提取出來的,而其他時間幀中的值,可以用這些關鍵值,采用特定插值方法計算獲得,進而得到比較流暢的動畫效果。
- 關鍵幀最早出現在動畫制作中,畫師先完成關鍵畫面,再補全中間幀生成動畫,這樣就能解放人力,大幅度提升效率。後來随着計算機技術發展。中間幀由計算機完成,插值代替了設計中間幀的動畫師。
- 插值: 一種通過已知的、離散的資料點,在範圍内推求新資料點的工程或方法。
- 生成插值的方法很多,常見的如 貝塞爾曲線
- 貝塞爾曲線最大的特點是平滑,時間曲線平滑意味着有較少的突兀變化,這是一般動畫設計所追求的。
- 理論上貝塞爾曲線能夠拟合任意曲線,如抛物線甚至能夠做到完全拟合
function generateCubicBezier (v, g, t){
var a = v / g;
var b = t + v / g;
return [[(a / 3 + (a + b) / 3 - a) / (b - a), (a * a / 3 + a * b * 2 / 3 - a * a) / (b * b - a * a)],
[(b / 3 + (a + b) / 3 - a) / (b - a), (b * b / 3 + a * b * 2 / 3 - a * a) / (b * b - a * a)]];
}
requestAnimationFrame()
window.requestAnimationFrame() 告訴浏覽器——你希望執行一個動畫,并且要求浏覽器在下次重繪之前調用指定的回調函數更新動畫。該方法需要傳入一個回調函數作為參數,該回調函數會在浏覽器下一次重繪之前執行
注意:若你想在浏覽器下次重繪之前繼續更新下一幀動畫,那麼回調函數自身必須再次調用window.requestAnimationFrame()
當你準備更新動畫時你應該調用此方法。這将使浏覽器在下一次重繪之前調用你傳入給該方法的動畫函數(即你的回調函數)。回調函數執行次數通常是每秒60次,但在大多數遵循W3C建議的浏覽器中,回調函數執行次數通常與浏覽器螢幕重新整理次數相比對。為了提高性能和電池壽命,是以在大多數浏覽器裡,當requestAnimationFrame() 運作在背景标簽頁或者隐藏的<iframe> 裡時,requestAnimationFrame() 會被暫停調用以提升性能和電池壽命。
回調函數會被傳入DOMHighResTimeStamp參數,訓示目前被 requestAnimationFrame() 排序的回調函數被觸發的時間。在同一個幀中的多個回調函數,它們每一個都會接受到一個相同的時間戳,即使在計算上一個回調函數的工作負載期間已經消耗了一些時間。該時間戳是一個十進制數,機關毫秒,最小精度為1ms(1000μs)。
請確定總是使用第一個參數(或其它獲得目前時間的方法)計算每次調用之間的時間間隔,否則動畫在高重新整理率的螢幕中會運作得更快。請參考下面例子的做法。
// start 記錄的是第一次調用step函數的時間點,用于計算與第一次調用step函數的時間差,以毫秒為機關
let start: number = 0;
// lastTime 記錄的是上一次調用step函數的時間點,用于計算兩幀之間的時間差,以毫秒為機關
let lastTime: number = 0;
// count用于記錄step函數運作次數
let count: number =0;
/**
* step 函數用于計算:
* 1、擷取目前時間點與HTML程式啟動時的時間差: timestamp
* 2、 擷取目前時間點與第一次調用step時的時間差: elapedMsec
* 3、 擷取目前時間點與上一次調用step時的時間差: intervalMsec
* step函數是作為requestAnimationFrame方法的回調函數使用的
* 是以step函數的簽名必須是(timestamp: number) => void
*
*/
function step (timestamp: number) : void {
// 第一次調用本函數時,設定start 和latsTime為timestamp;
if(!start) start = timestamp;
if(!lastTime) lastTime = timestamp
// 計算目前時間點與第一次調用step時間點的差
let elapsedMsec: number = timestamp - start;
// 計算目前時間點與上次調用step時間點的差(可以了解為兩幀之間的時間差)
let intervalMsec: number = timestamp -lastTime;
// 記錄上一次時間戳
lastTime = timestamp;
// 計數器,用于記錄step函數被調用的次數
count ++ ;
console.log(" " + count + "timestamp = " + timestamp)
console.log(" " + count + "elapsedMsec = " + elapsedMsec)
console.log(" " + count + "intervalMsec= " + intervalMsec)
// 使用requestAnimationFrame調用step函數
window.requestAnimationFrame(step);
}
// 使用requestAnimationFrame啟動step
// 而step函數中又會調用requestAnimationFrame來回調step函數
// 進而形成不間斷地遞歸調用,驅動動畫不停運作
window.requestAnimationFrame(step)
戳這裡檢視示範 -> https://codepen.io/AlexZ33/pen/gOMwped?editors=0011
requestAnimationFrame與螢幕重新整理頻率
通過requestAnimationFrame方法啟動step回調函數後,每次調用step函數的時間間隔固定在16.66毫秒左右,基本上每秒調用60次step函數(1000 / 60約等于16.66毫秒,其中1秒等于1000毫秒)。每秒step函數調用的次數(頻率)實際上是和螢幕螢幕重新整理次數(頻率)保持一緻
Windows系統電腦的螢幕螢幕重新整理頻率是60赫茲,可以人為認定螢幕每秒重新整理重繪60次,将螢幕重新整理頻率60赫茲更改為48赫茲,來看看結果會怎樣。
每次調用step函數的時間間隔固定在20.83毫秒左右,符合目前螢幕螢幕重新整理頻率48赫茲的設定(1000 / 48約等于20.83毫秒),由此可見request Animation Frame是一個與硬體相關的方法,該方法會保持與螢幕重新整理頻率一緻的狀态。
看上面的step函數,會發現它很簡單,隻是輸出3個時間差,這種操作本身花不了多少時間,是以能保持16毫秒的頻率一直穩定運作是很正常的。那麼,如果在step中進行大量耗時操作(恢複到螢幕螢幕重新整理頻率60赫茲的情況下),結果會如何呢?
在step函數的count ++代碼後面添加如下代碼:
兩次step調用間隔所耗時間有很明顯的波動,但是通過統計,會發現一個很明顯的規律
兩幀之間的時間間隔(intervalMsec)總是16.66毫秒的倍數(目前螢幕螢幕重新整理頻率60赫茲,折算成每幀需要16.66毫秒重新整理一次)。
由此可見,requestAnimationFrame方法會穩定間隔時間:
● 如果目前的回調操作(step函數)在16.66毫米内能完成,那麼requestAnimationFrame會等到16.66毫秒時繼續下一次step回調函數的調用。
● 如果目前的回調操作(step函數)大于16.66毫秒,則會以16.66毫秒為倍數的時間間隔進行下一次step回調函數的調用。
● 當将螢幕螢幕重新整理頻率60赫茲設定成48赫茲時,結果也類似,兩幀之間間隔時間總是20.83毫秒的倍數。
基于時間的更新與重繪
/**
* 需求,假設想讓一個物體(例如一個矩形或圓球)沿着水準軸(x軸),以每秒10個像素的速度進行移動,那麼應該怎麼做呢?
*/
/**
* 涉及兩個基本步驟,讓物體進行基于時間(每秒10個像素)的* * 更新,以及更新完後進行顯示。
*
* 搭建一個最簡單的更新與重繪架構。上一節中,在step回調函* * 數中已經計算出了3個以毫秒表示的時間間隔(時間差),即:
● 目前時間點與目前HTML應用啟動時的時間差timestamp。
● 目前時間點與第一次調用step回調函數時的時間差elapsedMsec。
● 目前時間點與上一次調用step回調函數時的時間差intervalMsec。
以上3個時間間隔或時間差中最有用的是第3個時間差,特别适合做基于時間的更新。
*/
let posX: number = 0;
let speedX: number = 10; //機關為秒,以每秒10個個像素的速度進行位移
// 聲明一個函數用于更新操作
function update(timestamp: number, elapsedMsec: number, intervalMsec: number): void {
// 參數都是使用毫秒為機關, 而現在的速度都是以秒為機關
// 是以需要将毫秒轉換為秒來表示
let t : number = intervalMsec / 1000.0;
// 線性速度公式: posX = posX + speedX * t;
posX += speedX * t;
console.log("current posX: " + posX);
}
// 渲染
// 使用CanvasRenderingContext2D繪圖上下文渲染對象進行物體的繪制
function render (ctx: CanvasRenderingContext2D | null) : void {
// 簡單起見,僅僅輸出render字元串
console.log("render")
}
// 将這兩個函數由step函數進行調用,就可以形成一個基本的架構
// start 記錄的是第一次調用step函數的時間點,用于計算與第一次調用step函數的時間差, 以毫秒為機關
let start : number = 0;
// lastTime 記錄的是上一次調用step函數的時間點,用于計算兩幀之間的時間差, 以毫秒為機關
let lastTime : number = 0;
// count 用于記錄step函數運作的次數
let count : number = 0;
/**
* step函數用于計算
* 1. 擷取目前時間點 與 html程式啟動時間差: timestamp
* 2. 擷取目前時間點與第一次調用step時的時間差: elapsedMsec
* 3. 擷取目前時間點與上一次調用step時的時間差:intervalMsec
*/
function step (timestamp: number) {
// 第一次調用本函數時, 設定start和lastTime為timestamp
if(!start) start = timestamp;
if(!lastTime) lastTime = timestamp;
// 計算目前時間點與第一次調用step時間點的差
let elapsedMsec = timestamp - start;
// 計算目前時間點與上一次調用step時間點的差(可以了解兩幀之間的時間差)
let intervalMsec = timestamp - lastTime;
// 記錄上一次的時間戳
lastTime = timestamp;
// 進行基于時間的更新
update(timestamp, elapsedMsec, intervalMsec);
//調用渲染函數, 目前并沒有使用CanvasRenderingContext2D類, 是以設定null
render(null)
// 使用requestAnimationFrame調用step函數
window.requestAnimationFrame(step);
};
// 使用requestAnimationFrame啟動step
// 而step函數中通過調用requestAnimtionFrame來回調step函數
// 進而形成不間斷的遞歸調用,驅動動畫不停地運作
window.requestAnimationFrame(step);
https://codepen.io/AlexZ33/pen/NWrbyMO?editors=1011codepen.io/AlexZ33/pen/NWrbyMO?editors=1011
點選上面的連結檢視結果:
會發現每次調用step回調函數後,posX總是根據傳入的時間差進行更新,這樣的好處不管在哪個浏覽器中或是不同運作速度的計算機上,每秒鐘的運動距離都是恒定的(每秒10像素)。如果不使用基于時間的更新,那麼在不同浏覽器中不同的CPU計算機上,會有明顯的快慢差别。實際上這是一個很固定的流程,将這個動畫流程封裝起來形成一個類,以後所有的程式都可以使用該類的子類。下一節就來封裝這個流程。
Application的類
可以将動畫的相關功能都封裝到一個名為Application的類中,該類主要是作為應用程式的入口類。
- 它能啟動或關閉動畫循環
- 進行基于時間的更新與重繪
- 可以對輸入事件(如滑鼠事件或者鍵盤事件)提供事件分發和處理功能,
- 可以被繼承擴充,用于Canvas2D和WebGL渲染。并且具有一個允許以不同幀率運作的計時器。
CanvasInputEvent基類
// CanvasKeyboardEvent和CanvasMouseEvent都繼承自本類
// 基類定義了共同的屬性,keyboard或者mouse事件都能使用組合鍵
// 例如可以按住Ctrl鍵的同時單擊滑鼠左鍵做某事
// 當然也可以按住Alt + A 鍵做另外一些事情
class CanvasInputEvent {
// 3 個 boolean變量,用來訓示Alt、Ctrl、Shift鍵是否被按下
public altKey: boolean;
public ctrlKey: boolean;
public shiftKey: boolean;
// type 是一個枚舉對象,用來表示目前的事件類型, 枚舉類型定義在下面的代碼中
public type : EInputEventType;
// 構造函數, 使用default參數,初始化時3個組合鍵都是false狀态
public constructor (altKey: boolean = false, ctrlKey: boolean = false, shiftKey: boolean =false, type: EInputEventType = EInputEventType.MOUSEEVENT){
this.altKey = altKey;
this.ctrlKey = altKey;
this.shiftKey = shiftKey;
this.type = type;
}
}
// 看一下EInputEventType這個枚舉,該枚舉羅列出目前支援的各個輸入事件,包括滑鼠和鍵盤事件。
enum EInputEventType{
MOUSEEVENT, // 總類,表示滑鼠事件
MOUSEDOWN, // 滑鼠按下事件
MOUSEUP, // 滑鼠彈起事件
MOUSEMOVE, // 滑鼠移動事件
MOUSEDRAG, // 滑鼠拖動事件
KEYBOARDEVENT, // 總類,表示鍵盤事件
KEYUP,// 鍵按下事件
KEYDOWN, //鍵彈起事件
KEYPRESS, // 按鍵事件
}
class CanvasMouseEvent extends CanvasInputEvent {
// button 表示目前按下滑鼠哪個鍵
// [ 0: 滑鼠左鍵, 1 : 滑鼠中鍵, 2: 滑鼠右鍵]
public buttton : number;
// 基于canvas坐标系的表示
// vec2,表示2D向量
public canvasPosition: vec2;
public localPosition: vec2;
public constructor (canvasPos: vec2, button: number, altKey: boolean=false, ctrlKey: boolean = false, shiftkey: boolean=false) {
super (altKey, ctrlKey, shiftKey);
this.canvasPosition = canvasPos;
this.button = button;
// 暫時建立一個vec2對象
this.localPosition = vec2.create();
}
}
class CanvasKeyBoardEvent extends CanvasInputEvent {
// 目前按下的鍵的ASCII字元
public key : string;
// 目前按下的鍵的ASCII(數字)
public keyCode: number;
// 目前按下的鍵是否不停地觸發事件
public repeat: boolean;
public constructor (key: string, keyCode: number, repeat: boolean, altKey: boolean = false, ctrlKey: boolean = false, shiftKey : boolean = false) {
super (altKey, ctrlKey, shiftKey, EInputEventType.KEYBOARDEVENT);
this.key = key;
this.keyCode = keyCode;
this.repeat = repeat;
}
}
這部分内容可以看這位同學的總結:
https://zhuanlan.zhihu.com/p/65556977