canvas核心技術-如何實現簡單動畫

這篇是學習和回顧canvas系列筆記的第四篇,完整筆記詳見:canvas核心技術javascript

在前面幾篇中,咱們回顧了在canvas中繪製線段,圖形,圖片等基本功能,當在製做2d遊戲或者更爲豐富的圖表庫時,必須提供強大的動畫功能。canvas自己不提供像css中animation屬性專門來實現動畫,可是canvas提供了translatescalerotate等基本功能,咱們能夠經過組合使用這些功能來實現動畫。css

跟動畫有關的概念中,咱們還要理解幀速率。咱們一般說一幀,就是瀏覽器完整繪製一次所通過的時間。現代瀏覽器的幀速率通常是60fps,就是在1s內能夠繪製60次。若是幀速率太低,就會以爲明顯的卡頓了。通常是幀速率越高,動畫越流暢。在JavaScript中,咱們要在1s內繪製60次,之前的作法是使用setTimeout或者setInterval來定時執行。java

setInterval(() => {
   // 執行繪製操做
}, 1000 / 60);
複製代碼

這種經過定時器的方式,雖然能夠實現,但不是最好的方式,它只是以固定的時間間隔向執行隊列中添加繪製代碼,並不必定能跟瀏覽器的更新頻率同步,而且嚴重依賴當前執行棧的狀況,若是某一次執行棧裏執行了複雜大量的運算,那麼咱們添加的繪製代碼可能就不會在咱們設置的時間間隔內執行了。在H5中,現代瀏覽器都提供了requestAnimationFrame這個方法來執行動畫更新邏輯,它會在瀏覽器的下一次更新時執行傳遞給它的函數,咱們徹底沒必要考慮瀏覽器的幀速率了,能夠更加專一於動畫更新的邏輯上。git

const animate = () => {
  // 執行繪製操做
  requestAnimationFrame(animate);
};
animate();
複製代碼

固然,若是要兼容之前的瀏覽器,咱們通常須要結合requestAnimationFramesetTimeout或者setInterval來實現polyfill,簡單的處理方式大體以下,更好的實現方式能夠查看rAF.jsgithub

function myRequestAnimationFrame(callback) {
  if (requestAnimationFrame) {
    return requestAnimationFrame(callback);
  } else {
    return setTimeout(() => {
      if (performance && performance.now) {
        return callback(performance.now());
      } else {
        return callback(Date.now());
      }
    }, 1000 / 60);
  }
}

function cancelMyRequestAnimationFrame(id) {
  if (cancelAnimationFrame) {
    cancelAnimationFrame(id);
  } else {
    clearTimeout(id);
  }
}

複製代碼

平移

在動畫處理中,css能夠針對某一個具體的元素來執行平移操做,在canvas中,只能平移座標系,從而間接的改變了canvas中元素的位置。在canvas核心技術-如何繪製線段中,詳細講解了canvas座標系相關知識,有興趣的同窗能夠先去看看。canvas座標系默認原點是在左上角,水平向右爲X正方向,垂直向下爲Y正方向。能夠經過平移canvas座標系,能夠把座標原點移動到canvas中某一塊區域,或者canvas可見區域外。canvas

//平移座標系以前
ctx.strokeStyle = 'grey'; 
ctx.setLineDash([2, 2]);
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke(); 
//平移座標系
ctx.translate(120,20); //平移座標系,往右平移120px,往下平移20px
ctx.beginPath(); //開始新的路徑
ctx.strokeStyle='blue'; 
ctx.setLineDash([]); 
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); 
複製代碼

咱們在平移以前,在座標(10,10)處繪製了一個邊長都爲100的矩形,如圖灰色虛線矩形,接着,咱們調用ctx.translate(120,20)把座標系向左平移120個像素,向下平移了20個像素,以後,咱們有一樣的在座標(10,10)處繪製了一個邊長爲100的矩形,如圖藍色實線矩形。這兩個矩形,咱們繪製的座標和邊長都沒有改變,可是座標系被平移了,因此繪製出來的位置也發生了變化。瀏覽器

座標系平移示意圖以下,函數

縮放

座標系不只能夠平移,還能夠被縮放,canvas提供了ctx.scale(x,y) 來縮放X軸和Y軸。在默認狀況下,canvas的縮放因子都是1.0,表示在canvas座標系中,1個單位就表示繪製的1px長度,若是經過scale函數改變縮放因子爲0.5,則在canvas座標系中,1個單位就表示繪製0.5px長度了,原來的圖形被繪製出來就只有一半大小了。post

ctx.strokeStyle = 'grey'; 
ctx.fillStyle = 'yellow'; 
ctx.globalAlpha = 0.5; 
ctx.fillRect(0, 0, width, height); //填充當前canvas整個區域
ctx.globalAlpha = 1;
ctx.setLineDash([2, 2]); 
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke(); 
ctx.scale(0.5, 0.5); //縮放座標系,X軸和Y軸都同時縮放爲0.5
ctx.beginPath(); //開始新的路徑
ctx.fillStyle = 'green';
ctx.strokeStyle = 'red';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height); //填充縮放以後的canvas整個區域
ctx.globalAlpha = 1;
ctx.setLineDash([]); 
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); 
複製代碼

能夠看到,咱們將X軸和Y軸同時都縮小爲原來的一半,新繪製出來的矩形(紅色實線)不只寬高都縮小爲原來的一半了,且左上角座標位置也發生了變化。這裏要理解的是,咱們在縮放,是針對座標系縮放的,黃色區域爲縮放以前的canvas座標系區域,綠色區域爲縮放0.5以後的canvas座標系區域。學習

ctx.scale(0.5, 1); //縮放座標系,X軸縮放爲0.5,Y軸不變
複製代碼

能夠對X軸和Y軸的縮放因子設置爲不同,如上面示例,對X軸縮小爲0.5,而Y軸不變,縮放以後的Canvas區域在X軸上就變爲原來的一半了。

還有一些其餘的技巧,好比製做鏡像,設置縮放ctx.scale(-1,1)就能夠繪製出Y軸的對稱鏡像了。同理,設置縮放ctx.scale(1,-1)就能夠繪製出X軸的對稱鏡像了。

ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.translate(width / 2, 0); //先將座標系向X軸平移到中間
ctx.strokeStyle = 'grey'; //設置描邊樣式
ctx.fillStyle = 'yellow';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([2, 2]); //設置虛線
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke(); //描邊
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
ctx.scale(-1, 1); //縮放座標系,X軸和Y軸都同時縮放爲0.5
ctx.beginPath(); //開始新的路徑
ctx.fillStyle = 'green';
ctx.strokeStyle = 'red'; //設置描邊樣式
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); //描邊
複製代碼

如圖,咱們實現了在Y軸對稱的鏡像,在設置縮放以前先平移了座標系到X軸的中間,由於不這樣的話,咱們縮放以後,繪製出來的部分就在canvas可見區域外面了,就看不到了。

旋轉

在canvas中能夠經過ctx.rotate(angle)來實現座標系的旋轉,參數angle是弧度值,而不是角度值。1角度等於\frac\pi{180},在調用以前須要先進行角度轉弧度,計算公式以下,

//角度轉換爲弧度
function toAngle(degree) {
  return (degree * Math.PI) / 180;
}
複製代碼

咱們來看一個將座標系旋轉15角度的示例,以下,

ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.strokeStyle = 'grey'; //設置描邊樣式
ctx.fillStyle = 'yellow';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([2, 2]); //設置虛線
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke(); //描邊
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
ctx.rotate(15* Math.PI/180); //將座標系旋轉15角度
ctx.beginPath(); //開始新的路徑
ctx.fillStyle = 'green';
ctx.strokeStyle = 'red'; //設置描邊樣式
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); //描邊
複製代碼

黃色區域是旋轉原默認canvas座標系區域,綠色區域就是旋轉以後的座標系區域了,能夠看到,旋轉操做的實際也是把整個canvas座標都旋轉了,canvas裏面的內容都會跟着被旋轉。傳入的參數angle不只能夠是正數,也能夠是負數,正數是順時針旋轉,負數表示逆時針旋轉。

ctx.rotate(-15* Math.PI/180);  //逆時針旋轉15角度
複製代碼

這些使用都比較簡單,也好理解。在實際中,能夠須要同時對canvas座標系進行平移,縮放和旋轉。在這種狀況下,咱們能夠分別單獨的使用上面這些方法進行對應的操做,他們的效果是疊加的。在canvas中,實際還提供了一個方法,能夠同時實現平移,縮放,旋轉。下面,咱們就來看看這個方法的神奇之處。

transform

在進行座標系數據變換時,最經常使用的手段就是先建模成單位矩陣,而後對單位矩陣作變換。實際上,上面說的平移,縮放,旋轉都是做用到矩陣上的。canvas中ctx.transform(a,b,c,d,e,f)提供了6個參數,在canvas中矩陣是縱向存儲的,表明的矩陣爲,

\begin{pmatrix}
a&c&e\\
b&d&f\\
0&0&1\\
\end{pmatrix}*\begin{pmatrix}
x\\
y\\
w\\
\end{pmatrix} = \begin{pmatrix}
x^{\prime}\\
y^{\prime}\\
w^{\prime}\\
\end{pmatrix}

在2維座標系中,表示一個點爲(x,y),爲了作矩陣變換,咱們須要將標準的2維座標擴展到3維,須要增長一維w,這就是2維齊次座標系(x,y,w)。齊次座標上一點(x,y,w)映射到實際的2維座標系中就是(x/w,y/w)。若是想要點(x,y,w)映射在實際2維座標系是(x,y),因此咱們只須要 設置w=1就能夠了,更多可查看齊次座標。而後根據矩陣相乘獲得的公式以下,

x^{\prime} = ax + cy + e
\\
y^{\prime} = bx + dy + f

先來看看平移,咱們看看把一個點(x,y)平移到另一個點(x',y')。公式以下,

x^{\prime} = x + d_{x}
\\
y^{\prime} = y + d_{y}

將平移公式代入到上面咱們推到出來的矩陣變換公式中能夠獲得,a=1c=0e=d_{x}b=0d=1f=d_{y}。咱們用transform實現平移,只須要調用ctx.transform(1,0,0,1,dx,dy),效果跟調用ctx.translate(dx,dy)同樣的。

//平移座標系以前
ctx.strokeStyle = 'grey';
ctx.setLineDash([2, 2]);
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke();
//平移座標系
// ctx.translate(120,20); //平移座標系,往右平移120px,往下平移20px
ctx.transform(1, 0, 0, 1, 120, 20); //使用transform來平移
ctx.beginPath(); //開始新的路徑
ctx.strokeStyle = 'blue';
ctx.setLineDash([]);
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke();
複製代碼

能夠看到,ctx.translate(120,20);ctx.transform(1, 0, 0, 1, 120, 20);獲得的效果是同樣的。

再來看看縮放,咱們把一個點(x,y) 經過縮放座標系k以後,獲得的新的點的座標爲(x',y')。公式以下,

x^{\prime} = k * x \\
y^{\prime} = k * y

咱們也將縮放公式代入到矩陣變換公式中,能夠獲得a = kc = 0e = 0b = 0d = kf = 0。咱們用transform來實現縮放,只須要調用ctx.transform(k,0,0,k,0,0),效果跟調用ctx.scale(k,k)同樣的。

ctx.strokeStyle = 'grey';
ctx.fillStyle = 'yellow';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height); //填充當前canvas整個區域
ctx.globalAlpha = 1;
ctx.setLineDash([2, 2]);
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke();
// ctx.scale(0.5, 0.5); //縮放座標系,X軸和Y軸都同時縮放爲0.5
ctx.transform(0.5, 0, 0, 0.5, 0, 0); //使用transform來縮放
ctx.beginPath(); //開始新的路徑
ctx.fillStyle = 'green';
ctx.strokeStyle = 'red';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height); //填充縮放以後的canvas整個區域
ctx.globalAlpha = 1;
ctx.setLineDash([]);
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); 
複製代碼

能夠看到,調用ctx.transform(0.5,0,0,0.5,0,0)ctx.scale(0.5,0.5)效果是同樣的。

最後來看看旋轉,咱們把一個座標(x,y)在旋轉座標角度\beta以後獲得新的座標(x',y'),公式以下,

x^{\prime} = \cos(\beta) * x-\sin(\beta)*y
\\
y^{\prime} =   \sin(\beta)*x + \cos(\beta) *y

上面的公式,是根據三角形兩角和差公式計算出來的,推導詳見2D Rotation。同理,咱們將旋轉公式代入到矩陣變換公式能夠獲得a=\cos(\beta)c=-\sin(\beta)e=0b=\sin(\beta)d=\cos(\beta)f=0。咱們調用ctx.transform(\cos(\beta),\sin(\beta),-\sin(\beta),\cos(\beta),0,0)ctx.rotate(\beta)是同樣的。注意,咱們這裏的\beta是弧度值。

ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.strokeStyle = 'grey'; //設置描邊樣式
ctx.fillStyle = 'yellow';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([2, 2]); //設置虛線
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke(); //描邊
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
// ctx.rotate(15* Math.PI/180); //將座標系旋轉15角度
let angle = (15 * Math.PI) / 180; //計算獲得弧度值
let cosAngle = Math.cos(angle); //計算餘弦 
let sinAngle = Math.sin(angle); //計算正弦
ctx.transform(cosAngle, sinAngle, -sinAngle, cosAngle, 0, 0); //使用transform旋轉
ctx.beginPath(); //開始新的路徑
ctx.fillStyle = 'green';
ctx.strokeStyle = 'red'; //設置描邊樣式
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); //描邊
複製代碼

能夠看到調用ctx.transform(cosAngle,sinAngle,-sinAngle,cosAngle,0,0)ctx.rotate(angle)是同樣的效果。

上面三種基本的操做座標系的方式,咱們均可以經過transform實現,經過組合,咱們能夠一次性設置座標系的平移,旋轉,縮放,只須要計算出正確的a,b,c,d,e,f。例如,咱們將上面三種操做同時實現,先平移,再縮放,最後再旋轉,分別給出translate+scale+rotate來實現,和transform來實現,

  • translate+scale+rotate組合實現
ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.strokeStyle = 'grey'; //設置描邊樣式
ctx.fillStyle = 'yellow';
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([2, 2]); //設置虛線
ctx.rect(10, 10, 100, 100); //繪製矩形
ctx.stroke(); //描邊
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
let angle = (15 * Math.PI) / 180;
ctx.translate(120, 20); //先平移
ctx.scale(0.5, 0.5); //再縮放
ctx.rotate(angle);//最後再旋轉
ctx.beginPath(); //開始新的路徑
ctx.fillStyle = 'green';
ctx.strokeStyle = 'red'; //設置描邊樣式
ctx.globalAlpha = 0.5;
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1;
ctx.setLineDash([]); //設置實線
ctx.strokeText('我是文字', 60, 60);
ctx.rect(10, 10, 100, 100); //繪製一樣的矩形
ctx.stroke(); //描邊
複製代碼

  • transform一次性實現
let angle = (15 * Math.PI) / 180;
// ctx.translate(120, 20);
// ctx.scale(0.5, 0.5);
// ctx.rotate(angle);
let cosAngle = Math.cos(angle);
let sinAngle = Math.sin(angle);
ctx.transform(0.5 * cosAngle, 0.5 * sinAngle, -0.5 * sinAngle, 0.5 * cosAngle, 120, 20);
複製代碼

這兩種方式最終獲得的效果是同樣的,其實在將translate+scale+rotate組合用transform一次性實現時,就是在作矩陣的變換計算,

\begin{pmatrix}
1&0&120\\
0&1&20\\
0&0&1\\
\end{pmatrix}*\begin{pmatrix}
0.5&0&0\\
0&0.5&0\\
0&0&1\\
\end{pmatrix}*\begin{pmatrix}
\cos(\beta)&-\sin(\beta)&0\\
\sin(\beta)&\cos(\beta)&0\\
0&0&1\\
\end{pmatrix} = \begin{pmatrix}
0.5*\cos(\beta)&-0.5*\sin(\beta)&120\\
0.5*\sin(\beta)&0.5*\cos(\beta)&20\\
0&0&1\\
\end{pmatrix}

三個矩陣相乘,分別是平移矩陣*縮放矩陣*旋轉矩陣,根據計算出來的矩陣,最後代入到公式中,能夠獲得a=0.5*\cos(\beta)b=0.5*\sin(\beta)c=-0.5*\sin(\beta)d=0.5*\cos(\beta)e=120f=20

transform若是屢次調用,它的效果也是疊加的,例如,咱們也能夠分開用transform來實現上面的平移,縮放,旋轉,

ctx.transform(1, 0, 0, 1, 120, 20); //使用transform來平移
ctx.transform(0.5, 0, 0, 0.5, 0, 0); //使用transform來縮放
ctx.transform(cosAngle, sinAngle, -sinAngle, cosAngle, 0, 0); //使用transform旋轉
複製代碼

第二次調用transform來縮放,是在第一次平移以後的座標系上進行的,第三次調用transform來旋轉,是在第一次和第二次結果上來進行的。canvas中提供了setTransform函數,它相似於transform函數,一樣接受a,b,c,d,e,f6個參數,且參數含義與transform中一摸同樣,跟transform不一樣之處在於,它不會疊加矩陣變換的效果,它會先重置當前座標系矩陣爲默認的單元矩陣,以後再執行跟transform同樣的矩陣變換。因此,若是咱們在調用transform變換矩陣時,不想屢次調用疊加,那麼能夠替換使用setTransform。實際上還有一個實驗性的函數resetTransform,它的做用就是重置當前座標系矩陣爲默認的單元矩陣,去掉了做用在默認座標系上的變換效果,注意它是一個實驗性的函數,還有不少瀏覽器都沒有提供支持,不建議使用。經過分析,咱們能夠獲得,

setTransform(a,b,c,d,e,f)=resetTransform() + transform(a,b,c,d,e,f)

小結

這篇文章主要是學習和回顧了canvas中座標系的變換,咱們是經過矩陣變換來實現canvas座標系的變化,包括translatescalerotatetransformsetTransform,經過組合使用,能夠實現強大的動畫效果。實際上,動畫效果應該在一段時間內持續變化,這篇文章,只學習了單一的變化,尚未涉及時間等動畫因素,下一篇準備學習和回顧動畫的高級知識,包括時間因素,物理因素,時間扭曲變化函數等。

相關文章
相關標籤/搜索