本来是打算自己写 save() 和 restore(),但是发现一篇博客对于 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 都包含一个保存绘画状态的栈。以下内容都属于绘画状态:
- 当前的 transformation matrix(变换矩阵)
- 当前的裁剪区域
- strokeStyle、fillStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor、globalCompositeOperation、font、textAlign、textBaseline,这些属性的当前值
**
当前路径和当前的位图并不属于绘画状态。当前路径是永久的一直存在的,要清楚或重置的话,只能通过 beginPath 方法。而当前位图是属于 canvas 的属性,而不是 context 的。 **
context.save() 会把当前的状态推入栈中。 **
context.restore() 会从栈中取出最顶部的状态,context 就还原到取出的状态。
因为一个 canvas只能有一个 2d context, 所以 save 和 restore 被广泛用于各种情况。其中最普遍的就是用于 transformation(变换矩阵)。
一个栗子告诉你 save() 和 restore() 是如何在 transformations(多次矩阵变换) 中发挥作用的。
当我们使用 transformation (矩阵变换)时,整个 context 的坐标系统都会发生变换。通常变换之后,我们下一步可能会想让坐标系统还原成变换之前正常的状态。我们要再次通过 transformation 反着让坐标系统还原吗,这显然不是一个快速可靠的方法。此时,我们就可以在变换之前,先通过 save 把正常的坐标系通过状态保存下来入栈,做完矩阵变换后在通过 restore 从栈中取出我们之前保存的状态。我们看个栗子后应该能更加清晰一些。
刚开始我们通过 canvascontext.save() 保存下当前的绘画状态,并且拷贝一份当前状态推入绘画状态栈中。 可以看到下图是一个正常坐标系统,我们保存下来当前状态。 然后我们做矩阵变换,可以看到坐标系统是发生了变化的。 接着我们画完我们想要的形状并且填充一些颜色。 现在我们想画更多的形状在画布上,但是我们不想使用当前的矩阵变换,我们就通过 restore() 把最近的状态从绘画状态栈里取出来,也就是我们之前保存的有正常坐标系统的状态。那么当前的状态就变成我们 save 之前的状态了。 可以看到背景的网格恢复到了正常的坐标系统的样子,但是我们后画的形状并没有变化,因为我们保存的状态只保存了 transformation 的状态,我们并没有设置其他想要保存的状态。
我们再来一个更直观的栗子
完整代码及注释
现在我们来看一个更加直观的栗子,来说明绘画状态栈与 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);
// 把当前状态,我们叫他状态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);
// 把当前状态,我们叫他状态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);
// 把当前状态,我们叫他状态3(翠绿色,阴影相关属性)推入栈中
ctx.save();
// 我们取出状态栈的顶部 状态3,那么当前绘制状态就是状态3了,即翠绿色和阴影相关属性
ctx.restore();
// 当前状态是状态3 画一个圆并填充,那么他是一个翠绿色带阴影的圆
ctx.beginPath();
ctx.arc(185, 75, 22, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
// 从状态栈中再取顶部状态出来,现在是状态2了,即 土黄色和阴影相关属性
ctx.restore();
// 当前状态是状态2的状态,画一个圆并填充,那么他是土黄色和阴影相关属性
ctx.beginPath();
ctx.arc(260, 75, 15, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
// 从状态栈中再取一个顶部状态出来,现在是状态1了,即 橘色和阴影相关属性
ctx.restore();
// 当前状态是状态1的状态,画一个圆并填充,那么他是橘色和阴影相关属性
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);
/* 把当前状态(fillStyle 橘色,阴影属性),推入到状态栈中,假设叫状态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);
/* 把当前状态(fillStyle 土黄色,阴影属性),推入到状态栈中,假设叫状态2 */
ctx.save();
我们看到状态栈中有两个状态了:状态1(橘色的)、状态2(土黄色的)
/* 然后我们画了一个 翠绿色的矩形,设置了阴影 */
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);
/* 把当前状态(fillStyle 翠绿色,阴影属性),推入到状态栈中,假设叫状态3 */
ctx.save();
我们看到状态栈中有三个状态了:状态1(橘色的)、状态2(土黄色的)、状态3(翠绿色的)
/*我们从状态栈顶部取出状态3,当前绘制状态就变成了状态3了(翠绿色的)*/
ctx.restore();
/*画一个翠绿色的圆*/
ctx.beginPath();
ctx.arc(185, 75, 22, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
/*我们从状态栈顶部取出状态3,当前绘制状态就变成了状态2了(土黄色的)*/
ctx.restore();
/*画一个土黄色的圆*/
ctx.beginPath();
ctx.arc(260, 75, 15, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
/*我们从状态栈顶部取出状态3,当前绘制状态就变成了状态1了(橘色色的)*/
ctx.restore();
/*画一个橘色的圆*/
ctx.beginPath();
ctx.arc(305, 75, 8, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
--------------------------------- O(∩∩)O 正文结束 O(∩∩)O--------------------------------**
这样下来是不是很简单?!希望我有翻译清楚。欢迎指正。
我是“南丘啊南丘”,希望大家在平凡的日子里好好学习,热爱生活,日日是好日!