一、基本概念
什么是Canvas
<canvas>是一个新的HTML元素,这个元素在HTML5 中被定义。这个元素通常可以被用来在HTML页面中通过JavaScript进行绘制图形、合成图像等等操作,也可以用来做一些动画。当然,目前HTML5规范还在草稿阶段,正式发布也许要等到2010年,不过现在已经有不少浏览器已经支持了部分HTML5规范。目前支持canvas元素的浏览器有Firefox3+、Safari4、Chrome2.0+等,因此,在运行本页中的例子时,请确保你使用的是上述浏览器之一。
尽管在Mozilla 已经有不少关于Canvas的教程,我还是决定把自己的学习过程记录下来。如果觉得我写的不够明白,那么你可以在参考资料中找到Mozilla网站上Canvas教程的链接。
另外,可以在这里 找到一些有趣的Canvas示例
开始使用Canvas
使用Canvas很简单,与使用其他HTML元素一样,只需要在页面中添加一个<canvas>标签即可:
<canvas id="screen" width="400" height="400"></canvas>
当然,这样只是简单的创建了一个Canvas对象而已,并没有对它进行任何操作,这个时候的canvas元素看上去与div元素是没什么区别的,在页面上什么都看不出来:)
另外,canvas元素的大小可以通过width与height属性来指定,这与img元素有点相似。
Canvas的核心:Context
前面说到可以通过JavaScript来操作Canvas对象来进行绘制图形、合成图像等操作,这些操作并不是通过Canvas对象本身来进行的,而是通过Canvas对象的一个方法getContext获取Canvas操作上下文来进行。也就是说,在后面我们使用Canvas对象的过程中,都是与Canvas对象的Context打交道,而Canvas对象本身可以用来获取Canvas对象的大小等信息。
要获取Canvas对象的Context很简单,直接调用canvas元素的getContext方法即可,在调用的时候需要传递一个Context类型参数,目前可以用的并且是唯一可以用的类型值就是2d:
<canvas id="screen" width="400" height="400"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("screen");
var ctx = canvas.getContext("2d");
</script>
Firefox3.0.x的尴尬
Firefox3.0.x虽然支持了canvas元素,但是并没有完全按照规范来实现,规范中的fillText、 measureText两个方法在Firefox3.0.x中被几个Firefox特有的方法代替,因此在Firefox3.0.x中使用Canvas时需要先fix这个几个方法在不同浏览器中的差别。
下面这代码取自MozillaBespin 项目,它修正了Firefox3.0.x中Canvas的Context对象与HTML5规范不一致的地方:
function fixContext(ctx) {
// * upgrade Firefox 3.0.x text rendering to HTML 5 standard
if (!ctx.fillText && ctx.mozDrawText) {
ctx.fillText = function(textToDraw, x, y, maxWidth) {
ctx.translate(x, y);
ctx.mozTextStyle = ctx.font;
ctx.mozDrawText(textToDraw);
ctx.translate(-x, -y);
};
}
// * Setup measureText
if (!ctx.measureText && ctx.mozMeasureText) {
ctx.measureText = function(text) {
if (ctx.font) ctx.mozTextStyle = ctx.font;
var width = ctx.mozMeasureText(text);
return { width: width };
};
}
// * Setup html5MeasureText
if (ctx.measureText && !ctx.html5MeasureText) {
ctx.html5MeasureText = ctx.measureText;
ctx.measureText = function(text) {
var textMetrics = ctx.html5MeasureText(text);
// fake it 'til you make it
textMetrics.ascent = ctx.html5MeasureText("m").width;
return textMetrics;
};
}
// * for other browsers, no-op away
if (!ctx.fillText) {
ctx.fillText = function() {};
}
if (!ctx.measureText) {
ctx.measureText = function() { return 10; };
}
return ctx;
}
注意:到Opera9.5为止,Opera还不支持HTML5规范中Canvas对象的fillText以及其相关方法和属性。
Hello,Canvas!
在对Canvas进行了一些初步了解后,开始来写我们的第一个Canvas程序,闻名的HelloWorld的又一个分支“Hello,Canvas”:
<canvas id="screen" width="400" height="400"></canvas>
<script type="text/javascript">
(function() {
var canvas = document.getElementById("screen");
var ctx = fixContext(canvas.getContext("2d"));
ctx.font = "20pt Arial";
ctx.fillText("Hello, Canvas!", 20, 20);
ctx.fillText("www.xujiwei.com", 20, 50);
function fixContext(ctx) {
// * upgrade Firefox 3.0.x text rendering to HTML 5 standard
if (!ctx.fillText && ctx.mozDrawText) {
ctx.fillText = function(textToDraw, x, y, maxWidth) {
ctx.translate(x, y);
ctx.mozTextStyle = ctx.font;
ctx.mozDrawText(textToDraw);
ctx.translate(-x, -y);
};
}
// * Setup measureText
if (!ctx.measureText && ctx.mozMeasureText) {
ctx.measureText = function(text) {
if (ctx.font) ctx.mozTextStyle = ctx.font;
var width = ctx.mozMeasureText(text);
return { width: width };
};
}
// * Setup html5MeasureText
if (ctx.measureText && !ctx.html5MeasureText) {
ctx.html5MeasureText = ctx.measureText;
ctx.measureText = function(text) {
var textMetrics = ctx.html5MeasureText(text);
// fake it 'til you make it
textMetrics.ascent = ctx.html5MeasureText("m").width;
return textMetrics;
};
}
// * for other browsers, no-op away
if (!ctx.fillText) {
ctx.fillText = function() {};
}
if (!ctx.measureText) {
ctx.measureText = function() { return 10; };
}
return ctx;
}
})();
</script>
运行示例,Canvas对象所在区域显示出“Hello,World!”,这正是代码中ctx.fillText("Hello,World!",20,20);的作用。
fillText以及相关属性
fillText方法用来在Canvas中显示文字,它可以接受四个参数,其中最后一个是可选的:
voidfillText(inDOMStringtext,infloatx,infloaty,[Optional]infloatmaxWidth);
其中maxWidth表示显示文字时最大的宽度,可以防止文字溢出,不过我在测试中发现在Firefox与Chomre中指定了maxWidth时也没有任何效果。
在使用fillText方法之前,可以通过设置Context的font属性来调整显示文字的字体,在上面的示例中我使用了“20ptArial”来作为显示文字的字体,你可以自己设置不同的值来看具体的效果。
二、路径
图形的基础-路径
在Canvas中,所有基本图形都是以路径为基础的,也就是说,我们在调用2dContext的lineTo、rect等方法时,其实就是往已经的context路径集合中再添加一些路径点,在最后使用fill或stroke方法进行绘制时,都是依据这些路径点来进行填充或画线。
在每次开始绘制路径前,都应该使用context.beginPath()方法来告诉Context对象开始绘制一个新的路径,否则接下来绘制的路径会与之前绘制的路径叠加,在填充或画边框时就会出现问题。在绘制完成路径后,可以直接使用context.closePath()方法来关闭路径,或者手动关闭路径。另外,如果在填充时路径没有关闭,那么Context会自动调用closePath方法将路径关闭。
基本路径方法
1.beginPath,closePath
这两个方法在前面已经介绍过,分别用来通知Context开始一个新的路径和关闭当前的路径。
在Canvas中使用路径时,应该要保持一个良好的习惯,每次开始绘制路径前都要调用一次beginPath方法,否则画出来的效果难看不说,还会严重影响性能。
在下面这张图中,左边的图形在每次绘制矩形前都调用了一次beginPath来清除之前的路径并重新开始绘制新的路径,而后面的图形则就只在绘制所有图形前调用了一次beginPath来清除路径,因此,虽然这里是使用的边框色是#666,但是右边的图形颜色比左边的深一些,因为每次使用stroke绘制边框时,会把之前的路径再次绘制一遍,叠加起来颜色就比原来深一些。
<canvas id="canvas" width="500" height="500"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.strokeStyle = "#666";
function useBeginPath() {
for (var i = 0; i < 5; ++i) {
ctx.beginPath();
ctx.rect(10 + i*20, 10 + i*20, 210 - i*40, 210 - i*40);
ctx.stroke();
}
}
function notUseBeginPath() {
ctx.beginPath();
for (var i = 0; i < 5; ++i) {
ctx.rect(240 + i*20, 10 + i*20, 210 - i*40, 210 - i*40);
ctx.stroke();
}
}
useBeginPath();
notUseBeginPath();
</script>
在Context中路径数较少时,如果不考虑显示效果,性能上还可以接受,但是如果Context中的路径数很多时,在开始绘制新路径前不使用beginPath的话,因为每次绘制都要将之前的路径重新绘制一遍,这时性能会以指数下降。
因此,除非有特殊需要,每次开始绘制路径前都要调用beginPath来开始新路径。
2.移动与直线moveTo,lineTo,rect
<canvas id="canvas" width="500" height="500"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(110,110);
ctx.lineTo(10, 110);
ctx.lineTo(10, 10);
ctx.stroke();
ctx.beginPath();
ctx.rect(120, 10, 100, 100);
ctx.stroke();
</script>
voidmoveTo(infloatx,infloaty);
在Canvas中绘制路径,一般是不需要指定起点的,默认的起点就是上一次绘制路径的终点,因此,如果需要指定起点的话,就需要使用moveTo方法来指定要移动到的位置。
voidlineTo(infloatx,infloaty);
lineTo方法则是绘制一条直接路径到指定的位置。在调用完lineTo方法后,Context内部的绘制起点会移动到直线的终点。
voidrect(infloatx,infloaty,infloatw,infloath);
rect方法用来绘制一个矩形路径,通过参数指定左上角位置以及宽和高。在调用rect后,Context的绘制起点会移动到rect绘制的矩形的左上角。
rect方法与后面要介绍的arc方法与其他路径方法有一点不同,它们是使用参数指定起点的,而不是使用Context内部维护的起点。
3.曲线arcTo,arc,quadraticCurveTo,bezierCurveTo
voidarcTo(infloatx1,infloaty1,infloatx2,infloaty2,infloatradius);
按照WHATWG文档的说明,这个方法是画一个与两条射线相切的的圆弧,两条射线其中一条为穿过Context绘制起点,终点为(x1,y1),另外一条为穿过(x2,y2),终点为(x1,y1),这条圆弧为最小的与这两条射线相切的圆弧。在调用完arcTo方法后,将圆弧与射线(x1,y1)-(x2,y2)的切点添加到当前路径中,做为下次绘制的起点。
在测试中发现,Firefox和Opera目前对这个方法的支持并不好,只有Chrome和Safari4能绘制出正确的路径。
<canvas id="canvas" width="500" height="500"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.translate(200, 200);
ctx.moveTo(10, 10);
ctx.arcTo(110, 60, 10, 110, 30);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "#999";
ctx.moveTo(10, 6);
ctx.lineTo(114, 60);
ctx.lineTo(10, 114);
ctx.stroke();
</script>
voidarc(infloatx,infloaty,infloatradius,infloatstartAngle,infloatendAngle,inbooleananticlockwise);
arc方法用来绘制一段圆弧路径,通过圆心位置、起始弧度、终止弧度来指定圆弧的位置和大小,这个方法也不 依赖于Context维护的绘制起点。而在画圆弧时的旋转方向则由最后一个参数anticlockwise来指定,如果为true就是逆时针,false则为顺时针。
voidquadraticCurveTo(infloatcpx,infloatcpy,infloatx,infloaty);
quadraticCurveTo方法用来绘制二次样条曲线路径,参数中cpx与cpy指定控制点的位置,x和y指定终点的位置,起点则是由Context维护的绘制起点。
voidbezierCurveTo(infloatcp1x,infloatcp1y,infloatcp2x,infloatcp2y,infloatx,infloaty);
bezierCurveTo方法用来绘制贝塞尔曲线路径,它与quadraticCurveTo相似,不过贝塞尔曲线有两个控制点,因此参数中的cp1x,cp1y,cp2x,cp2y用来指定两个控制点的位置,而x和y指定绺的位置。
<canvas id="canvas" width="500" height="500"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.translate(10, 10);
ctx.beginPath();
ctx.arc(50, 50, 50, 0, Math.PI, true);
ctx.stroke();
// quadraticCurveTo
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.moveTo(110, 50);
ctx.quadraticCurveTo(160, 0, 210, 50);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.moveTo(110, 50);
ctx.lineTo(160, 0);
ctx.lineTo(210, 50);
ctx.stroke();
// bezierCurveTo
ctx.beginPath();
ctx.strokeStyle = "#000";
ctx.moveTo(220, 50);
ctx.bezierCurveTo(250, 0, 280, 10, 320, 50);
ctx.stroke();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.moveTo(220, 50);
ctx.lineTo(250, 0);
ctx.lineTo(280, 10);
ctx.lineTo(320, 50);
ctx.stroke();
</script>
4.fill,stroke,clip
fill与stroke这两个方法很好理解,分别用来填充路径与绘制路径线条。
clip方法用来给Canvas设置一个剪辑区域,在调用clip方法之后的代码只对这个设定的剪辑区域有效,不会影响其他地方,这个方法在要进行局部更新时很有用。默认情况下,剪辑区域是一个左上角在(0,0),宽和高分别等于Canvas元素的宽和高的矩形。
在画这个图时,虽然两次都是使用fillRect(0,0,100,100)填充了一个100x100大小矩形,但是显示的结果却是第二次填充的只是中间的一小块,这是因为在两次填充之间使用clip方法设定了剪辑区域,这样第二次填充时只会影响到所设定的中间那一小部分区域。
<canvas id="canvas" width="500" height="500"></canvas>
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.translate(10, 10);
// fill a green rectangle
ctx.fillStyle = "green";
ctx.fillRect(0, 0, 100, 100);
// set the clipping region
ctx.beginPath();
ctx.rect(30, 30, 40, 40);
ctx.clip();
ctx.stroke();
// fill a yellow rectangle
ctx.fillStyle = "yellow";
ctx.fillRect(0, 0, 100, 100);
</script>
5.clearRect,fillRect,strokeRect
这三个方法并不是路径方法,而是用来直接处理Canvas上的内容,相当于Canvas的背景,调用这三个方法也不会影响Context绘图的起点。
要清除Canvas上的所有内容时,可以直接调用context.clearRect(0,0,width,height)来直接清除,而不需要使用路径方法绘制一个与Canvas同等大小的矩形路径再使用fill方法去清除。
结语
通过Canvas的路径方法,可以使用Canvas处理一些简单的矢量图形,这样在缩放时也不会失真。不过Canvas的路径方法也不是很强大,至少连个椭圆的路径都没有……
参考资料
1.TheCanvasElement,WHATWG