3.1 变形
默认情况下,一个画布的坐标空间会使用画布的左上角 (0,0) 作为原点,x 值向右增加,y 值向下增加。这个坐标空间中的单位通常会被转换为像素,然后,可通过转换坐标空间在绘图过程中实现移动、缩放或旋转等操作。这些操作是通过 translate()、scale() 和 rotate()等方法来实现的,它们会对画布的变换矩阵产生影响。
3.1.1 放大与缩小
我们用scale函数来实现图形的放大和缩小。
其函数原型如下:
scale(x, y);
其中,第一个参数x表示在x轴进行缩放,即水平缩放,第二个参数 y表示在y轴上进行缩放,即在竖直方向上进行缩放。
具体实现方法如代码清单3-1所示。
代码清单 3-1
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(10,10,150,100);
ctx.scale(3,3);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(10,10,150,100);
</script>
运行后的效果如图3-1所示。
下面解释一下代码清单3-1的代码。首先,以页面上的坐标(10,10)为起点开始画了一个宽150、高100的黑色矩形,如下所示:
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(10,10,150,100);
然后,沿着x轴和y轴方向将其值放大3倍,并用同样的代码重新画一个灰色的矩形,如下所示:
ctx.scale(3,3);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(10,10,150,100);
从运行效果图可以看到,虽然用了同样的代码,但是第二次画的灰色矩形被放大了3倍,并且起始坐标值也一起被放大了3倍。
上述代码清单3-1实现了放大功能,那么现在来看看缩小功能如何实现,如代码清单3-2所示。
代码清单 3-2
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(50,50,150,100);
ctx.scale(0.5,0.5);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(50,50,150,100);
</script>
运行后的效果如图3-2所示。
在代码清单3-2中,同样先画出了一个黑色矩形,然后沿着x轴和y轴方向上分别将其值缩小到了0.5倍,并用同样的代码重新画了一个灰色的矩形。
从运行效果图可以看到,虽然用了同样的代码,但是第二次画的灰色矩形被缩小为原来的0.5倍了,并且起始坐标值也一起缩小了0.5倍。
另外还需要了解的是,使用scale函数时,如果将参数设置为负数,还可以使图形翻转,比如代码清单3-3就实现了竖直方向的翻转。
代码清单 3-3
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<canvas id="myCanvas" width="500" height="350" style="background-color: #cccccc;">
你的浏览器不支持HTML5
</canvas>
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
var image = new Image();
image.src = "face.jpg";
image.onload = function(){
ctx.drawImage(image,10,10);
ctx.scale(1,-1);
ctx.drawImage(image,250,-250);
};
</script>
</body>
</html>
运行效果如图3-3所示。
3.1.2 平移
使用translate函数可以实现对图形进行平移的功能。
其函数原型如下:
translate (x, y);
其中,第一个参数x表示在x轴进行平移,即在水平方向上平移,第二个参数 y表示在y轴上进行缩放,即在竖直方向上平移。
具体的实现方法如代码清单3-4所示。
代码清单 3-4
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(10,10,150,100);
ctx.translate(50,100);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(10,10,150,100);
</script>
运行效果如图3-4所示。
在代码清单3-4中的如下代码表示在水平方向上向右平移50,在竖直方向上向下平移100,图3-4中灰色矩形是平移之后的效果。
ctx.translate(50,100);
3.1.3 旋转
利用rotate函数可以实现图形的旋转功能。
其函数原型如下:
rotate (angle);
这里需要注意的是,传入rotate里的参数angle是弧度而不是角度。如果角度为angle,那么换算为弧度就是angle*Math.PI/180,具体实现方法如代码清单3-5所示。
代码清单 3-5
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(200,50,100,50);
ctx.rotate(45*Math.PI/180);
ctx.beginPath();
ctx.strokeStyle = "#ff0000";
ctx.strokeRect(200,50,100,50);
</script>
运行效果如图3-5所示。
在图3-5中灰色矩形是旋转45度后的图形,可以看到用rotate来实现旋转时,是以Canvas的起始坐标(0,0)为中心进行旋转的。如果要想让图形以自己为中心旋转,则需要使用3.1.2节中的translate函数,如代码清单3-6所示。
代码清单 3-6
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(200,50,100,50);
ctx.translate(250,75);
ctx.rotate(45*Math.PI/180);
ctx.translate(-250, -75);
ctx.beginPath();
ctx.strokeStyle = "#ff0000";
ctx.strokeRect(200,50,100,50);
</script>
运行效果如图3-6所示。
在代码清单3-6中,下面的代码表示先将Canvas的起始坐标向右移250,向下移75,即移至所画矩形的中心处;然后开始旋转45度;接着再将Canvas的起始坐标向左移250,向上移75,即移回到原来位置,这样就完成了图形以自己为中心的旋转。
ctx.translate(250,75);
ctx.rotate(45*Math.PI/180);
ctx.translate(-250, -75);
3.1.4 利用transform矩阵实现多样化的变形
上面分别讲了缩放、平移以及旋转的实现方法。其实所有这些变形都是可以通过变形矩阵transform来实现的。先来看看transform函数的定义。
transform函数的原型如下:
transform (a,b,c,d,e,f);
该函数的各个变量对应以下变换矩阵中相应位置的参数。
a c e
b d f
0 0 1
下面来说明如何使用这个变换矩阵来实现上面的各种变形。
1 . 缩放
假设原始坐标为(x,y),缩放后的坐标为(x1,y1),缩放的倍数分别为a和d,那么就有下列公式:
x1=a*x
y1=d*y
因此,我们得到了如下矩阵公式。
x1 a 0 0 x
y1 = 0 d 0 y
1 0 0 1 1
这样就可以用transform (a,0,0,d,0,0)来替换scale(a,d)了,如代码清单3-7所示。
代码清单 3-7
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(10,10,150,100);
ctx.transform(3,0,0,3,0,0);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(10,10,150,100);
</script>
运行效果如图3-7所示。
可以看到,这里用代码清单3-7实现了和代码清单3-1一样的功能。
- 平移
同样,假设原始坐标为(x,y),平移后的坐标为(x1,y1),在x轴和y轴的平移量分别为e和f,那么就有下列公式:
x1=x+e
y1=y+f
因此,我们得到如下矩阵公式。
x1 1 0 e x
y1 = 0 1 f y
1 0 0 1 1
这样就可以用transform (1,0,0,1,e,f)来替换translate (e,f)了,如代码清单3-8所示。
代码清单 3-8
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(10,10,150,100);
ctx.transform (1,0,0,1,50,100);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(10,10,150,100);
</script>
运行效果如图3-8所示。
可以看到,这里用代码清单3-8实现了和代码清单3-4一样的功能。
3 . 旋转
旋转对应的矩阵公式如下。
x1 cos –sin 0 x
y1 = sin cos 0 y
1 0 0 1 1
现在可以用transform (cos , sin , –sin , cos ,0,0)来替换rotate ()了,如代码清单3-9所示。
代码清单 3-9
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(200,50,100,50);
ctx.transform (Math.cos(45*Math.PI/180),
Math.sin(45*Math.PI/180),
-Math.sin(45*Math.PI/180),
Math.cos(45*Math.PI/180),0,0);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(200,50,100,50);
</script>
</body>
运行效果如图3-9所示。
可以看到,这里用代码清单3-9实现了和代码清单3-5一样的功能。
这样,我们就用transform完成了所有变形。另外,变换矩阵也可以通过setTransform函数来实现,setTransform的参数与transform一样,不同的是,setTransform函数是先消去之前的transform变换,然后重新进行变换的。为了区分transform和setTransform,我们看看下面的例子,如代码清单3-10所示。
代码清单 3-10
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(200,50,100,50);
ctx.transform (Math.cos(5*Math.PI/180),
Math.sin(5*Math.PI/180),
-Math.sin(5*Math.PI/180),
Math.cos(5*Math.PI/180),0,0);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(200,50,100,50);
ctx.transform (Math.cos(10*Math.PI/180),
Math.sin(10*Math.PI/180),
-Math.sin(10*Math.PI/180),
Math.cos(10*Math.PI/180),0,0);
ctx.beginPath();
ctx.strokeStyle = "#999999";
ctx.strokeRect(200,50,100,50);
</script>
运行效果如图3-10所示。
在代码清单3-10中,首先旋转5度,然后再旋转10度,所以第二次旋转相当于旋转了15度。
下面再看看代码清单3-11中setTransform的使用方法。
代码清单 3-11
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000000";
ctx.strokeRect(200,50,100,50);
ctx.transform (Math.cos(5*Math.PI/180),
Math.sin(5*Math.PI/180),
-Math.sin(5*Math.PI/180),
Math.cos(5*Math.PI/180),0,0);
ctx.beginPath();
ctx.strokeStyle = "#cccccc";
ctx.strokeRect(200,50,100,50);
ctx.setTransform (Math.cos(10*Math.PI/180),
Math.sin(10*Math.PI/180),
-Math.sin(10*Math.PI/180),
Math.cos(10*Math.PI/180),0,0);
ctx.beginPath();
ctx.strokeStyle = "#999999";
ctx.strokeRect(200,50,100,50);
</script>
运行效果如图3-11所示。
可以看到,第二次的旋转没有在第一次旋转的基础上进行,因为用setTransform实现旋转效果的时候,已经清除了上一个transform,所以只旋转了10度。
4 . 倾斜
接着来看一下如何实现图像的倾斜效果。首先看图3-12。
图中的3个点p0、p1、p2遵循以下矩形公式。
(p1.x–p0.x)/width (p2.x–p0.x)/height p0.x
(p1.y–p0.y)/width (p2.y–p0.y)/height p0.y
0 0 1
将上面的矩形公式带入setTransform函数中,如代码清单3-12所示。
代码清单 3-12
<script type="text/javascript">
var c=document.getElementById('myCanvas');
var ctx=c.getContext('2d');
ctx.setTransform(1,10/150,-40/100,1,40,10);
ctx.rect(50,50,150,100);
ctx.stroke();
</script>
运行效果如图3-13所示。
上述倾斜效果也可以根据另外3个点来实现,看图3-14。
图中的3个点p1、p2、p3遵循以下矩形公式。
(p3.x–p2.x)/width (p3.x–p1.x)/height p2.x
(p3.y–p2.y)/width (p3.y–p1.y)/height p2.y
0 0 1
将上面的矩形公式代入setTransform函数中,如代码清单3-13所示。
代码清单 3-13
<script type="text/javascript">
var c=document.getElementById('myCanvas');
var ctx=c.getContext('2d');
ctx.setTransform(130/150,-20/150,-20/100,80/100,0,0);
ctx.rect(50,50,150,100);
ctx.stroke();
</script>
运行效果如图3-15所示。
5 . 图片的扭曲效果
根据以上所介绍的内容,已经可以做到倾斜、旋转、平移和缩放4种基本变形,如图3-16所示。
但是,对于非上述4种基本变形的特殊变形,就无法直接实现了。比如图3-17中的扭曲变形效果。
这时候,就需要通过多种变形组合来实现。先对图3-17进行分解,如图3-18所示。
此时分解出的两个图形可分别看作是两个基本变形的一部分,如图3-19所示。
这样一来就简单了,只需要实现两个倾斜的变形,然后利用clip函数将图片的一部分绘制出来,就可以完成图3-17中的效果了,如代码清单3-14所示。
代码清单 3-14
<script type="text/javascript">
var c=document.getElementById('myCanvas');
var ctx=c.getContext('2d');
var img = new Image();
img.src="face.jpg";
img.onload = function(){
ctx.save();
ctx.beginPath();
ctx.moveTo(80,0);
ctx.lineTo(320,40);
ctx.lineTo(0,200);
ctx.closePath();
ctx.clip();
ctx.setTransform((320-80)/240,40/240,-80/240,200/240,80,0);
ctx.drawImage(img,0,0);
ctx.restore();
ctx.save();
ctx.beginPath();
ctx.moveTo(320,40);
ctx.lineTo(0,200);
ctx.lineTo(200,150);
ctx.closePath();
ctx.clip();
ctx.setTransform(200/240,(150-200)/240,(200-320)/240,(150-40)/240,0,200);
ctx.drawImage(img,0,0-240);
ctx.restore();
};
</script>
运行上面的代码,就可以得到图3-17的效果了。
解释一下代码清单3-14。首先看一下左半边图形的绘制。
ctx.beginPath();
ctx.moveTo(80,0);
ctx.lineTo(320,40);
ctx.lineTo(0,200);
ctx.closePath();
ctx.clip();
上面这段代码是以(80,0)、(320,40)和(0,200) 3个点为顶点绘制一个三角形,然后利用clip函数将这个三角形作为绘图的可视区域。
ctx.setTransform(200/240,(150-200)/240,(200-320)/240,(150-40)/240,0,200);
上面的代码是以刚才的三角形的3个顶点来进行倾斜变形的,这个变形的原理已经在代码清单3-12中讲过了。
ctx.drawImage(img,0,0);
上面的代码是绘制图片,因为绘图的可视区域只是一个三角形,所以绘制完的图片只有一部分。
右半边图形的绘制思路和左半边是一样的。同时,两次绘图都加上了save()函数和restore()函数,这是为了让两次变形和绘图互不干涉。