理解 canvas 的 save() 和 restore()

本來是打算自己寫 save() 和 restore(),但是發(fā)現(xiàn)一篇博客對于 save() 和 restore() 的理解很清晰易懂,就翻譯過來了。 原文鏈接:https://html5.litten.com/understanding-save-and-restore-for-the-canvas-context/

--------------------------------- O(∩∩)O 以下是正文 O(∩∩)O-------------------------------**

首先,曾幾何時我也無法理解 save() 和 restore() 的真正用意。其實呢,非常簡單,這里有幾個栗子能幫助大家更好的理解這兩個方法。 <a name="PFcpq"></a>

我們先看下官方文檔對 save 和 restore 的定義:

每個 canvas 的 context 都包含一個保存繪畫狀態(tài)的棧。以下內容都屬于繪畫狀態(tài):

  • 當前的 transformation matrix(變換矩陣)
  • 當前的裁剪區(qū)域
  • strokeStyle、fillStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、globalCompositeOperation、font、textAlign、textBaseline,這些屬性的當前值

**

當前路徑和當前的位圖并不屬于繪畫狀態(tài)。當前路徑是永久的一直存在的,要清楚或重置的話,只能通過 beginPath 方法。而當前位圖是屬于 canvas 的屬性,而不是 context 的。 **

context.save() 會把當前的狀態(tài)推入棧中。 **

context.restore() 會從棧中取出最頂部的狀態(tài),context 就還原到取出的狀態(tài)。

因為一個 canvas只能有一個 2d context, 所以 save 和 restore 被廣泛用于各種情況。其中最普遍的就是用于 transformation(變換矩陣)。

<a name="mhgsv"></a>

一個栗子告訴你 save() 和 restore() 是如何在 transformations(多次矩陣變換) 中發(fā)揮作用的。

當我們使用 transformation (矩陣變換)時,整個 context 的坐標系統(tǒng)都會發(fā)生變換。通常變換之后,我們下一步可能會想讓坐標系統(tǒng)還原成變換之前正常的狀態(tài)。我們要再次通過 transformation 反著讓坐標系統(tǒng)還原嗎,這顯然不是一個快速可靠的方法。此時,我們就可以在變換之前,先通過 save 把正常的坐標系通過狀態(tài)保存下來入棧,做完矩陣變換后在通過 restore 從棧中取出我們之前保存的狀態(tài)。我們看個栗子后應該能更加清晰一些。

剛開始我們通過 canvascontext.save() 保存下當前的繪畫狀態(tài),并且拷貝一份當前狀態(tài)推入繪畫狀態(tài)棧中。 可以看到下圖是一個正常坐標系統(tǒng),我們保存下來當前狀態(tài)。 然后我們做矩陣變換,可以看到坐標系統(tǒng)是發(fā)生了變化的。 接著我們畫完我們想要的形狀并且填充一些顏色。 現(xiàn)在我們想畫更多的形狀在畫布上,但是我們不想使用當前的矩陣變換,我們就通過 restore() 把最近的狀態(tài)從繪畫狀態(tài)棧里取出來,也就是我們之前保存的有正常坐標系統(tǒng)的狀態(tài)。那么當前的狀態(tài)就變成我們 save 之前的狀態(tài)了。 可以看到背景的網格恢復到了正常的坐標系統(tǒng)的樣子,但是我們后畫的形狀并沒有變化,因為我們保存的狀態(tài)只保存了 transformation 的狀態(tài),我們并沒有設置其他想要保存的狀態(tài)。

<a name="RJDVz"></a>

我們再來一個更直觀的栗子

完整代碼及注釋

現(xiàn)在我們來看一個更加直觀的栗子,來說明繪畫狀態(tài)棧與 save() 和 restore() 是如何配合運作的。

<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Canvas Test</title>
</head>
<body>
<header> </header>
<nav> </nav>
<section>

<div>
<canvas id="canvas" width="320" height="200">
This text is displayed if your browser does not support HTML5 Canvas.
</canvas>
</div>

<script type="text/javascript">
var canvas;
var ctx;

function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
draw();
}


function draw() {
// 畫了一個 橘色的矩形,設置陰影
ctx.fillStyle = '#FA6900';
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur    = 4;
ctx.shadowColor   = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(0,0,15,150);
  
// 把當前狀態(tài),我們叫他狀態(tài)1(橘色、陰影相關屬性)推入棧中
ctx.save();
  
// 畫了一個 土黃色的矩形,設置陰影
ctx.fillStyle = '#E0E4CD';
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur    = 4;
ctx.shadowColor   = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(30,0,30,150);
  
// 把當前狀態(tài),我們叫他狀態(tài)2(土黃色,陰影相關屬性)推入棧中
ctx.save();
  
// 畫一個 翠綠色的矩形,設置陰影
ctx.fillStyle = '#A7DBD7';
ctx.shadowOffsetX = 15;
ctx.shadowOffsetY = 15;
ctx.shadowBlur    = 4;
ctx.shadowColor   = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(90,0,45,150);
  
// 把當前狀態(tài),我們叫他狀態(tài)3(翠綠色,陰影相關屬性)推入棧中
ctx.save();
  
// 我們取出狀態(tài)棧的頂部 狀態(tài)3,那么當前繪制狀態(tài)就是狀態(tài)3了,即翠綠色和陰影相關屬性
ctx.restore();
  
// 當前狀態(tài)是狀態(tài)3 畫一個圓并填充,那么他是一個翠綠色帶陰影的圓
ctx.beginPath();
ctx.arc(185, 75, 22, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();

// 從狀態(tài)棧中再取頂部狀態(tài)出來,現(xiàn)在是狀態(tài)2了,即 土黃色和陰影相關屬性
ctx.restore();
  
// 當前狀態(tài)是狀態(tài)2的狀態(tài),畫一個圓并填充,那么他是土黃色和陰影相關屬性
ctx.beginPath();
ctx.arc(260, 75, 15, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();

// 從狀態(tài)棧中再取一個頂部狀態(tài)出來,現(xiàn)在是狀態(tài)1了,即 橘色和陰影相關屬性
ctx.restore();
  
// 當前狀態(tài)是狀態(tài)1的狀態(tài),畫一個圓并填充,那么他是橘色和陰影相關屬性
ctx.beginPath();
ctx.arc(305, 75, 8, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
}

init();
</script>
</section>
<aside> </aside>
<footer> </footer>
</body>
</html>

分段代碼、效果圖、注釋具體說明

接下來我們分段代碼并結合效果圖來說明,會更加清晰。

/* 我們畫了一個橘色的矩形,設置了陰影 */
ctx.fillStyle = '#FA6900'; 
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur    = 4;
ctx.shadowColor   = 'rgba(204, 204, 204, 0.5)';      
ctx.fillRect(0,0,15,150);
/* 把當前狀態(tài)(fillStyle 橘色,陰影屬性),推入到狀態(tài)棧中,假設叫狀態(tài)1 */
ctx.save();  

可以看到下圖狀態(tài)棧中多了一個狀態(tài),并且很貼心的顏色也與矩形一直,讓人更好理解。

can1.gif
state1.gif
/* 然后我們畫了一個 土黃色的矩形,設置了陰影 */
ctx.fillStyle = '#E0E4CD'; 
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur    = 4;
ctx.shadowColor   = 'rgba(204, 204, 204, 0.5)';      
ctx.fillRect(30,0,30,150);
/* 把當前狀態(tài)(fillStyle 土黃色,陰影屬性),推入到狀態(tài)棧中,假設叫狀態(tài)2 */
ctx.save(); 

我們看到狀態(tài)棧中有兩個狀態(tài)了:狀態(tài)1(橘色的)、狀態(tài)2(土黃色的)

can2.gif
state2 (1).gif
/* 然后我們畫了一個 翠綠色的矩形,設置了陰影 */
ctx.fillStyle = '#A7DBD7'; 
ctx.shadowOffsetX = 15;
ctx.shadowOffsetY = 15;
ctx.shadowBlur    = 4;
ctx.shadowColor   = 'rgba(204, 204, 204, 0.5)';      
ctx.fillRect(90,0,45,150);
/* 把當前狀態(tài)(fillStyle 翠綠色,陰影屬性),推入到狀態(tài)棧中,假設叫狀態(tài)3 */
ctx.save();

我們看到狀態(tài)棧中有三個狀態(tài)了:狀態(tài)1(橘色的)、狀態(tài)2(土黃色的)、狀態(tài)3(翠綠色的)

can3.gif
state3 (1).gif
/*我們從狀態(tài)棧頂部取出狀態(tài)3,當前繪制狀態(tài)就變成了狀態(tài)3了(翠綠色的)*/
ctx.restore();
/*畫一個翠綠色的圓*/
ctx.beginPath();
ctx.arc(185, 75, 22, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill(); 
can4.gif
state3 (1).gif
/*我們從狀態(tài)棧頂部取出狀態(tài)3,當前繪制狀態(tài)就變成了狀態(tài)2了(土黃色的)*/
ctx.restore();
/*畫一個土黃色的圓*/
ctx.beginPath();
ctx.arc(260, 75, 15, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
can5.gif
state2.gif
/*我們從狀態(tài)棧頂部取出狀態(tài)3,當前繪制狀態(tài)就變成了狀態(tài)1了(橘色色的)*/
ctx.restore();
/*畫一個橘色的圓*/
ctx.beginPath();
ctx.arc(305, 75, 8, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
can6.gif
state1.gif

--------------------------------- O(∩∩)O 正文結束 O(∩∩)O--------------------------------**

這樣下來是不是很簡單?!希望我有翻譯清楚。歡迎指正。

我是“南丘啊南丘”,希望大家在平凡的日子里好好學習,熱愛生活,日日是好日!

點贊.gif
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容