很多时候,我们绘制出一个图形之后,并不能达到我们预期的效果,这个时候,适当地运用图形的变换(transformations,如旋转和缩放等),可以创建出大量复杂多变的图形。
1、保存和恢复Canvas状态
Canvas指的是当前画面的所有样式、变形和裁切的一个快照,以堆的方式保存。save和restore方法用于保存和恢复Canvas状态,这两个方法都不需要任何参数,用法如下:
context.save();
context.restore();
sava方法可以暂时将当前状态保存在堆中,这些状态可以是各种属性(如strokeStyle、fillStyle和globalCompositeOperation等)的值。当前应用的变形、当前裁切的路径等。restore方法用于将上一个保存状态从堆中再次取出,恢复该状态的所有设置。
举一个简单的示例:
<body>
<canvas id="myCanvas" style="border: 1px solid #000" width="300" height="200"></canvas>
<script>
var c = document.getElementById("myCanvas");
var context = c.getContext("2d");
// 开始绘制矩形
context.fillStyle = "red";
context.strokeStyle = "blue";
context.fillRect(20,20,100,100);
context.strokeRect(10,10,120,120);
context.fill();
context.stroke();
// 保存当前Canvas状态
context.save();
// 绘制另一个矩形
context.fillStyle = "orange";
context.strokeStyle = "green";
context.fillRect(150,20,100,100);
context.strokeRect(140,10,120,120);
context.fill();
context.stroke();
// 恢复第一个矩形的状态
context.restore();
// 绘制两个矩形
context.fillRect(20,140,50,50);
context.strokeRect(140,140,50,50);
</script>
</body>
先来看看最后的效果图:
我们保存的Canvas状态属性值是fillStyle填充为红色,strokeStyle轮廓为蓝色,当我们没有调用restore方法恢复时,依旧使用的是我们自己定义的属性值。但是,我们可以看到当我们调用restore后,绘制出来的矩形填充以及轮廓就是我们之前保存的值。
2、移动坐标空间
在上一篇Canvas元素中我们提到过,画布的坐标空间默认的是以画布左上角(0,0)为原点,x轴水平向右为正向,y轴垂直向下为正向。而在绘制图形时,我们可以使用translate方法移动坐标空间,使画布的变换矩阵发生水平和垂直方向的偏移,其用法如下:
context.translate(dx,dy);
其中dx和dy分别为坐标原点沿水平和垂直两个方向的偏移量。如下图所示:
当然了,在进行图形变换之前,最好先要养成使用save方法保存当前状态的好习惯。在许多情况下,使用restore方法来自动恢复原来的状态要比手动恢复更高效,特别是当重复某种运算时。接下来,我们就通过绘制一个伞状图形的示例加深对它的理解。
<body>
<canvas id="myCanvas" style="border: 1px solid red;" width="700" height="200"></canvas>
</body>
<script>
function drawTop(ctx,fillStyle){
ctx.fillStyle = fillStyle;
ctx.beginPath();
// 以坐标原点(0,0)为圆心,半径为30px,0为开始的角度,Math.PI为结束的角度,即180度,最后一个参数表示顺时针方向绘制
ctx.arc(0,0,30,0,Math.PI,true);
ctx.closePath();
ctx.fill();
}
function drawGrip(ctx){
// 保存了之前定义的属性值
ctx.save();
ctx.fillStyle = "blue";
// 以(-1.5,0)为起始坐标,绘制宽为1.5px,高为40px的矩形
// 这里解释一下为什么以(-1.5,0)为坐标,是因为后面需要绘制的宽为1.5px,所以提前移动过去,绘制的时候右移就到了中心的位置
ctx.fillRect(-1.5,0,1.5,40);
ctx.beginPath();
ctx.strokeStyle = "blue";
// 这就是我们看到的伞的下部分伞钩
ctx.arc(-5,40,4,Math.PI,Math.PI*2,true);
ctx.stroke();
ctx.closePath();
// 恢复了之前定义的属性值
ctx.restore();
}
function draw(){
var ctx = document.getElementById("myCanvas").getContext("2d");
// 垂直方向和水平方向发生了偏移
ctx.translate(80,80);
// 循环10次,表明有10个伞状图形
for(var i=0;i<10;i++){
ctx.save();
// 水平方向发生偏移
ctx.translate(60*i,0);
// 绘制伞状图形的上半部分,也就是半圆部分,第二个参数表示填充颜色
drawTop(ctx,"rgb("+(30*i)+","+(255-30*i)+",255)");
// 绘制伞状图形的下半部分
drawGrip(ctx);
ctx.restore();
}
}
// 在页面或图像加载完后触发
window.onload = function(){
draw();
}
</script>
最后的效果图:
在这里,我只想说的一点是,关于
ctx.fillRect(-1.5,0,1.5,40);和ctx.arc(-5,40,4,Math.PI,Math.PI*2,true);
这两个语句的坐标问题,主要是取决于你后面绘制的图形的宽度。
Canvas中图形移动的实现,实际上是通过改变画布的坐标原点实现的,所谓的“移动图形”,只是看上去被移动的样子,移动的是其实是坐标空间。
3、旋转坐标空间
rotate方法用于以原点为中心旋转Canvas,实质上旋转的是Canvas上下文对象的坐标空间,具体用法是:
context.rotate(angle);
rotate方法只有一个参数,即旋转角度angle,其顺时针方向为正方向,以弧度为单位,旋转中心是Canvas的原点。
拿上一个伞状例子来说,我现在的状态是一行排列的,那么我现在想要将它旋转成一个圆形队列,又该怎么实现呢?
function draw(){
var ctx = document.getElementById("myCanvas").getContext("2d");
// 垂直方向和水平方向发生了偏移
ctx.translate(150,150);
for(var i=1;i<9;i++){
ctx.save();
// 图片旋转角度
ctx.rotate(Math.PI*(2/4+i/4));
// 水平方向发生偏移
ctx.translate(0,-100);
// 绘制伞状图形的上半部分,也就是半圆部分,第二个参数表示填充颜色
drawTop(ctx,"rgb("+(30*i)+","+(255-30*i)+",255)");
// 绘制伞状图形的下半部分
drawGrip(ctx);
ctx.restore();
}
}
其实,到最后,我们会发现,只是draw方法发生了变化。具体是什么变化呢?无非就是多了ctx.rotate(Math.PI*(2/4+i/4));和ctx.translate(0,-100);语句。那么接下来,我们一起思考为什么要使用这两个方法里的数据。
首先,我们首先明确Math.PI是180度,那么很显然旋转一圈是360度,也就是说,后面的参数(2/4+i/4)中,i=1时表示初始旋转的位置,后面的则依次递加。
其次,为什么每次都要将坐标空间沿y轴负方向移动100px呢?那是因为,如果我们不移动,最后所有的图形都会重合成一个圆,就像下面这样:
而我们实际想要的效果是什么呢?
4、缩放图形
scale方法用于增减Canvas上下文对象中的像素数目,从而实现图形或位图的放大或缩小,其用法是:
context.scale(x,y);
其中x,y为必须接受的参数,x为横轴的缩放因子,y轴为纵轴的缩放因子,它们的值必须是正值。
若放大图形,参数值需大于1;
若缩小图形,参数值需小于1;
若参数值等于1,无任何效果。
我们可以用一个螺旋状由大到小的例子来深入理解缩放的使用:
<body>
<canvas id="myCanvas" style="border: 1px solid red;" width="700" height="300"></canvas>
</body>
<script>
function draw(){
var ctx = document.getElementById("myCanvas").getContext("2d");
// 垂直方向和水平方向发生了偏移
ctx.translate(200,20);
for(var i=1;i<80;i++){
ctx.save();
ctx.translate(30,30);
ctx.scale(0.95,0.95);
ctx.rotate(Math.PI/12);
ctx.beginPath();
ctx.fillStyle = "red";
ctx.globalAlpha = "0.4";
ctx.arc(0,0,50,0,Math.PI*2,true);
ctx.closePath();
ctx.fill();
}
}
// 在页面或图像加载完后触发
window.onload = function(){
draw();
}
</script>
其实,这也不难理解,我们反反复复使用的都是提到过的方法,唯一的不同是,我们现在把所有的方法整合在了一起,我们传递的参数不再是一个确定的值,所以,我们实现的效果也是千变万化的。
关于Canvas元素其实还有其他的很多方法,后续将会继续补充,希望大家能够共同进步!