本节课,我们学习Canvas 变形(Transformations)。
在本教程前面的部分中,我们已经了解了 Canvas 网格和坐标空间。到目前为止,我们只是根据我们的需要使用默认的网格,改变整个画布的大小。变形是一种更强大的方法,可以将原点移动到另一点、对网格进行旋转和缩放。
状态的保存和恢复 Saving and restoring state
在了解变形之前,我先介绍两个在你开始绘制复杂图形时必不可少的方法。
save()
保存画布 (canvas) 的所有状态
restore()
save 和 restore 方法是用来保存和恢复 canvas 状态的,都没有参数。Canvas 的状态就是当前画面应用的所有样式和变形的一个快照。
Canvas 状态存储在栈中,每当save()
方法被调用后,当前的状态就被推送到栈中保存。一个绘画状态包括:
- 当前应用的变形(即移动,旋转和缩放,见下)
- 以及下面这些属性:
strokeStyle
,fillStyle
,globalAlpha
,lineWidth
,lineCap
,lineJoin
,miterLimit
,lineDashOffset
,shadowOffsetX
,shadowOffsetY
,shadowBlur
,shadowColor
,globalCompositeOperation
,font
,textAlign
,textBaseline
,direction
,imageSmoothingEnabled
- 当前的裁切路径(clipping path),会在下一节介绍
你可以调用任意多次 save
方法。每一次调用 restore
方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
save 和 restore 的应用例子
我们尝试用这个连续矩形的例子来描述 canvas 的状态栈是如何工作的。
到目前为止所做的动作和前面章节的都很类似。不过一旦我们调用 restore
,状态栈中最后的状态会弹出,并恢复所有设置。如果不是之前用 save
保存了状态,那么我们就需要手动改变设置来回到前一个状态,这个对于两三个属性的时候还是适用的,一旦多了,我们的代码将会猛涨。
当第二次调用 restore
时,已经恢复到最初的状态,因此最后是再一次绘制出一个黑色的四方形。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,150,150); // 使用默认设置绘制一个矩形
ctx.save(); // 保存默认状态
ctx.fillStyle = '#09F' // 在原有配置基础上对颜色做改变
ctx.fillRect(15,15,120,120); // 使用新的设置绘制一个矩形
ctx.save(); // 保存当前状态
ctx.fillStyle = '#FFF' // 再次改变颜色配置
ctx.globalAlpha = 0.5;
ctx.fillRect(30,30,90,90); // 使用新的配置绘制一个矩形
ctx.restore(); // 重新加载之前的颜色状态
ctx.fillRect(45,45,60,60); // 使用上一次的配置绘制一个矩形
ctx.restore(); // 加载默认颜色配置
ctx.fillRect(60,60,30,30); // 使用加载的配置绘制一个矩形
}
移动 Translating
我们先介绍 translate
方法,它用来移动 canvas 和它的原点到一个不同的位置。translate(x, y)
translate
方法接受两个参数。*x *是左右偏移量,y 是上下偏移量,如右图所示。
在做变形之前先保存状态是一个良好的习惯。大多数情况下,调用 restore 方法比手动恢复原先的状态要简单得多。又,如果你是在一个循环中做位移但没有保存和恢复 canvas 的状态,很可能到最后会发现怎么有些东西不见了,那是因为它很可能已经超出 canvas 范围以外了。
translate
的例子
这个例子显示了一些移动 canvas 原点的好处。如果不使用 translate
方法,那么所有矩形都将被绘制在相同的位置(0,0)。translate
方法同时让我们可以任意放置这些图案,而不需要在 fillRect()
方法中手工调整坐标值,既好理解也方便使用。
我在 draw
方法中调用 fillRect()
方法 9 次,用了 2 层循环。每一次循环,先移动 canvas,画螺旋图案,然后恢复到原始状态。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
ctx.save();
ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)';
ctx.translate(10 + j * 50, 10 + i * 50);
ctx.fillRect(0, 0, 25, 25);
ctx.restore();
}
}
}
DEMO:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>变形 Transformations | Web176教程web176.com</title> <meta name="robots" content="noindex, nofollow"> <style> body { padding: 0; margin: 0; } svg:not(:root) { display: block; } .playable-code { background-color: #f4f7f8; border: none; border-left: 6px solid #558abb; border-width: medium medium medium 6px; color: #4d4e53; height: 100px; width: 90%; padding: 10px 10px 0; } .playable-canvas { border: 1px solid #4d4e53; border-radius: 2px; } .playable-buttons { text-align: right; width: 90%; padding: 5px 10px 5px 26px; } </style> </head> <body> <canvas id="canvas" width="150" height="150"></canvas> <script> function draw() { var ctx = document.getElementById('canvas').getContext('2d'); for (var i = 0; i < 3; i++) { for (var j = 0; j < 3; j++) { ctx.save(); ctx.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)'; ctx.translate(10 + j * 50, 10 + i * 50); ctx.fillRect(0, 0, 25, 25); ctx.restore(); } } } draw(); </script> </body> </html>
旋转 Rotating
第二个介绍 rotate
方法,它用于以原点为中心旋转 canvas。rotate(angle)
这个方法只接受一个参数:旋转的角度 (angle),它是顺时针方向的,以弧度为单位的值。
旋转的中心点始终是 canvas 的原点,如果要改变它,我们需要用到 translate
方法。
rotate
的例子
在这个例子里,见右图,我用 rotate
方法来画圆并构成圆形图案。当然你也可以分别计算出 x 和 y 坐标(x = r*Math.cos(a); y = r*Math.sin(a)
)。这里无论用什么方法都无所谓的,因为我们画的是圆。计算坐标的结果只是旋转圆心位置,而不是圆本身。即使用 rotate
旋转两者,那些圆看上去还是一样的,不管它们绕中心旋转有多远。
这里我们又用到了两层循环。第一层循环决定环的数量,第二层循环决定每环有多少个点。每环开始之前,我都保存一下 canvas 的状态,这样恢复起来方便。每次画圆点,我都以一定夹角来旋转 canvas,而这个夹角则是由环上的圆点数目的决定的。最里层的环有 6 个圆点,这样,每次旋转的夹角就是 360/6 = 60 度。往外每一环的圆点数目是里面一环的 2 倍,那么每次旋转的夹角随之减半。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>变形 Transformations - a_rotate_example - code sample | Web176教程web176.com</title> <meta name="robots" content="noindex, nofollow"> <style> body { padding: 0; margin: 0; } svg:not(:root) { display: block; } .playable-code { background-color: #f4f7f8; border: none; border-left: 6px solid #558abb; border-width: medium medium medium 6px; color: #4d4e53; height: 100px; width: 90%; padding: 10px 10px 0; } .playable-canvas { border: 1px solid #4d4e53; border-radius: 2px; } .playable-buttons { text-align: right; width: 90%; padding: 5px 10px 5px 26px; } </style> </head> <body> <canvas id="canvas" width="300" height="200"></canvas> <script> draw(); function draw() { var ctx = document.getElementById('canvas').getContext('2d'); ctx.translate(75,75); for (var i=1;i<6;i++){ // Loop through rings (from inside to out) ctx.save(); ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)'; for (var j=0;j<i*6;j++){ // draw individual dots ctx.rotate(Math.PI*2/(i*6)); ctx.beginPath(); ctx.arc(0,i*12.5,5,0,Math.PI*2,true); ctx.fill(); } ctx.restore(); } } </script> </body> </html>
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.translate(75,75);
for (var i=1;i<6;i++){ // Loop through rings (from inside to out)
ctx.save();
ctx.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
for (var j=0;j<i*6;j++){ // draw individual dots
ctx.rotate(Math.PI*2/(i*6));
ctx.beginPath();
ctx.arc(0,i*12.5,5,0,Math.PI*2,true);
ctx.fill();
}
ctx.restore();
}
}
缩放 Scaling
接着是缩放。我们用它来增减图形在 canvas 中的像素数目,对形状,位图进行缩小或者放大。scale(x, y)
scale
方法可以缩放画布的水平和垂直的单位。两个参数都是实数,可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比 1 小,会缩小图形,如果比 1 大会放大图形。默认值为 1,为实际大小。
画布初始情况下,是以左上角坐标为原点的第一象限。如果参数为负实数,相当于以 x 或 y 轴作为对称轴镜像反转(例如,使用translate(0,canvas.height); scale(1,-1);
以 y 轴作为对称轴镜像反转,就可得到著名的笛卡尔坐标系,左下角为原点)。
默认情况下,canvas 的 1 个单位为 1 个像素。举例说,如果我们设置缩放因子是 0.5,1 个单位就变成对应 0.5 个像素,这样绘制出来的形状就会是原先的一半。同理,设置为 2.0 时,1 个单位就对应变成了 2 像素,绘制的结果就是图形放大了 2 倍。
scale
的例子
这最后的例子里,我们用不同的缩放方式来画两个图形。
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
// draw a simple rectangle, but scale it.
ctx.save();
ctx.scale(10, 3);
ctx.fillRect(1, 10, 10, 10);
ctx.restore();
// mirror horizontally
ctx.scale(-1, 1);
ctx.font = '48px serif';
ctx.fillText('MDN', -135, 120);
}
DEMO:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>变形 Transformations - a_scale_example - code sample | Web176教程web176.com</title> <meta name="robots" content="noindex, nofollow"> <style> body { padding: 0; margin: 0; } svg:not(:root) { display: block; } .playable-code { background-color: #f4f7f8; border: none; border-left: 6px solid #558abb; border-width: medium medium medium 6px; color: #4d4e53; height: 100px; width: 90%; padding: 10px 10px 0; } .playable-canvas { border: 1px solid #4d4e53; border-radius: 2px; } .playable-buttons { text-align: right; width: 90%; padding: 5px 10px 5px 26px; } </style> </head> <body> <canvas id="canvas" width="150" height="150"></canvas> <script> function draw() { var ctx = document.getElementById('canvas').getContext('2d'); // draw a simple rectangle, but scale it. ctx.save(); ctx.scale(10, 3); ctx.fillRect(1, 10, 10, 10); ctx.restore(); // mirror horizontally ctx.scale(-1, 1); ctx.font = '48px serif'; ctx.fillText('MDN', -135, 120); } draw(); </script> </body> </html>
变形 Transforms
最后一个方法允许对变形矩阵直接修改。
transform(a, b, c, d, e, f)
这个方法是将当前的变形矩阵乘上一个基于自身参数的矩阵,如下面的矩阵所示:
[ a c e b d f 0 0 1 ]
如果任意一个参数是Infinity
,变形矩阵也必须被标记为无限大,否则会抛出异常。
这个函数的参数各自代表如下:
a (m11)
水平方向的缩放
b(m12)
竖直方向的倾斜偏移
c(m21)
水平方向的倾斜偏移
d(m22)
竖直方向的缩放
e(dx)
水平方向的移动
f(dy)
竖直方向的移动
setTransform(a, b, c, d, e, f)
这个方法会将当前的变形矩阵重置为单位矩阵,然后用相同的参数调用 transform
方法。如果任意一个参数是无限大,那么变形矩阵也必须被标记为无限大,否则会抛出异常。从根本上来说,该方法是取消了当前变形,然后设置为指定的变形,一步完成。
resetTransform()
重置当前变形为单位矩阵,它和调用以下语句是一样的:ctx.setTransform(1, 0, 0, 1, 0, 0);
transform
/ setTransform
的例子
function draw() {
var ctx = document.getElementById('canvas').getContext('2d');
var sin = Math.sin(Math.PI/6);
var cos = Math.cos(Math.PI/6);
ctx.translate(100, 100);
var c = 0;
for (var i=0; i <= 12; i++) {
c = Math.floor(255 / 12 * i);
ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
ctx.fillRect(0, 0, 100, 10);
ctx.transform(cos, sin, -sin, cos, 0, 0);
}
ctx.setTransform(-1, 0, 0, 1, 100, 100);
ctx.fillStyle = "rgba(255, 128, 255, 0.5)";
ctx.fillRect(0, 50, 100, 100);
}
DEMO:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>变形 Transformations - transform_settransform_的例子 | Web176教程web176.com</title> <meta name="robots" content="noindex, nofollow"> <style> body { padding: 0; margin: 0; } svg:not(:root) { display: block; } .playable-code { background-color: #f4f7f8; border: none; border-left: 6px solid #558abb; border-width: medium medium medium 6px; color: #4d4e53; height: 100px; width: 90%; padding: 10px 10px 0; } .playable-canvas { border: 1px solid #4d4e53; border-radius: 2px; } .playable-buttons { text-align: right; width: 90%; padding: 5px 10px 5px 26px; } </style> </head> <body> <canvas id="canvas" width="200" height="250"></canvas> <script> function draw() { var ctx = document.getElementById('canvas').getContext('2d'); var sin = Math.sin(Math.PI/6); var cos = Math.cos(Math.PI/6); ctx.translate(100, 100); var c = 0; for (var i=0; i <= 12; i++) { c = Math.floor(255 / 12 * i); ctx.fillStyle = "rgb(" + c + "," + c + "," + c + ")"; ctx.fillRect(0, 0, 100, 10); ctx.transform(cos, sin, -sin, cos, 0, 0); } ctx.setTransform(-1, 0, 0, 1, 100, 100); ctx.fillStyle = "rgba(255, 128, 255, 0.5)"; ctx.fillRect(0, 50, 100, 100); } draw(); </script> </body> </html>
作者:terry,如若转载,请注明出处:https://www.web176.com/canvas/7625.html